@kungfu-tech/buildchain 3.0.2-alpha.6 → 3.0.2-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/contracts/auditable-demo-media-profiles-v1.json +168 -0
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +103 -0
- package/contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt +2 -0
- package/contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json +16 -0
- package/contracts/fixtures/auditable-demo-web-delivery-v1/scene.json +12 -0
- package/dist/site/buildchain-contract.json +45 -27
- package/dist/site/buildchain-site.json +42 -32
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +11 -3
- package/dist/site/kfd-claims.json +74 -8
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -7
- package/dist/site/node-api-registry.json +44 -5
- package/dist/site/page-registry.json +30 -20
- package/dist/site/public-surface-audit.json +38 -10
- package/dist/site/publication-authority-registry.json +26 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +41 -7
- package/docs/MAP.md +2 -0
- package/docs/auditable-demo.md +55 -3
- package/docs/dev-alpha-candidate-patrol.md +9 -0
- package/docs/github-artifact-attestation.md +1 -1
- package/docs/release-governance.md +32 -0
- package/docs/reusable-build-surface.md +73 -58
- package/docs/runtime-train-validation.md +21 -0
- package/docs/versioning.md +1 -0
- package/package.json +5 -1
- package/packages/core/artifact-signing-result.js +228 -0
- package/packages/core/artifact-signing.js +412 -0
- package/packages/core/buildchain-config.js +58 -0
- package/packages/core/buildchain-contract.js +8 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/detached-artifact-signature.js +121 -0
- package/packages/core/github-governance-authority.js +6 -0
- package/packages/core/index.js +27 -0
- package/scripts/auditable-demo.mjs +491 -22
- package/scripts/buildchain-channel-router.mjs +8 -2
- package/scripts/check-inventory.mjs +5 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +18 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
- package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
- package/scripts/generate-site-bundle.mjs +3 -0
- package/scripts/import-artifact-signing-results.mjs +76 -0
- package/scripts/inspect-artifact-signing-requests.mjs +101 -0
- package/scripts/materialize-artifact-signing-request.mjs +66 -0
- package/scripts/merge-artifact-signing-results.mjs +76 -0
- package/scripts/release-line-policy.mjs +27 -0
- package/scripts/runtime-ref-core.mjs +10 -6
- package/scripts/seal-artifact-signing-requests.mjs +368 -0
- package/scripts/sign-detached-artifact-requests.mjs +237 -0
- package/scripts/verify-artifact-signing-results.mjs +99 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: draft
|
|
3
|
+
period: ongoing
|
|
4
|
+
theme: buildchain-reusable-build
|
|
5
|
+
doc_type: technical-reference
|
|
6
|
+
source_level: local-files
|
|
7
|
+
confidence: high
|
|
8
|
+
sensitivity: public
|
|
9
|
+
evidence_grade: A
|
|
10
|
+
review_state: unreviewed
|
|
11
|
+
last_reviewed: 2026-07-27
|
|
12
|
+
ai_provenance:
|
|
13
|
+
model_family: GPT-5
|
|
14
|
+
product: Codex
|
|
15
|
+
generated_at: 2026-07-27
|
|
16
|
+
invisible_context: not asserted
|
|
17
|
+
---
|
|
18
|
+
|
|
1
19
|
# Reusable Build Surface
|
|
2
20
|
|
|
3
21
|
Buildchain v2 provides a reusable build workflow for repositories that need
|
|
@@ -35,7 +53,7 @@ jobs:
|
|
|
35
53
|
|
|
36
54
|
`buildchain-channel` defaults to `auto`. Selection uses this precedence:
|
|
37
55
|
|
|
38
|
-
1. an explicit `buildchain-ref` train, SHA, or official channel;
|
|
56
|
+
1. an explicit `buildchain-ref` protected authority, train, SHA, or official channel;
|
|
39
57
|
2. an explicit `buildchain-channel: alpha|stable`;
|
|
40
58
|
3. `publish-channel: alpha|release|major`;
|
|
41
59
|
4. GitHub release prerelease metadata;
|
|
@@ -482,8 +500,8 @@ The reusable workflow exposes the resolved contract:
|
|
|
482
500
|
| `trusted-event` | `true` when the event is trusted enough to reach build runners |
|
|
483
501
|
| `buildchain-runtime-ref` | Runtime ref selected after applying the empty-default or override policy |
|
|
484
502
|
| `buildchain-runtime-sha` | Immutable Buildchain runtime commit used by all runtime checkouts |
|
|
485
|
-
| `buildchain-runtime-class` | `stable`, `alpha`, `train`, `exact-sha`, or `development`
|
|
486
|
-
| `buildchain-runtime-override` | `true` when
|
|
503
|
+
| `buildchain-runtime-class` | `stable`, `alpha`, `authority`, `train`, `exact-sha`, or `development` |
|
|
504
|
+
| `buildchain-runtime-override` | `true` when an authority, train, or exact-SHA `buildchain-ref` override was accepted |
|
|
487
505
|
| `buildchain-runtime-trust-decision` | Runtime override trust decision |
|
|
488
506
|
| `buildchain-contract-lock-status` | `unchanged`, `compatible-drift`, `breaking-drift`, `missing-lock`, `non-floating-runtime`, or first-release `runtime-contract-unavailable` |
|
|
489
507
|
| `buildchain-contract-lock-drift` | `true` when the floating runtime SHA or contract digest changed |
|
|
@@ -523,65 +541,62 @@ release jobs can detect drifting diagnostics JSON contracts and missing or
|
|
|
523
541
|
drifting diagnostics sidecar manifests without downloading the per-platform
|
|
524
542
|
diagnostics artifacts first.
|
|
525
543
|
|
|
526
|
-
##
|
|
527
|
-
|
|
528
|
-
Alpha consumers can ask the build controller to seal one exact macOS app and
|
|
529
|
-
hand it to a separate protected signing job. The built-in mode is suitable
|
|
530
|
-
when the protected environment belongs to the repository that defines the
|
|
531
|
-
reusable workflow:
|
|
544
|
+
## Artifact Signing Authority
|
|
532
545
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
credential-island-macos-platform-id: macos-arm64
|
|
538
|
-
```
|
|
546
|
+
Artifact signing is a Buildchain capability, not a macOS application workflow.
|
|
547
|
+
Consumers declare desired signature state next to their artifact facts; they do
|
|
548
|
+
not configure certificates, Team IDs, notary credentials, protected
|
|
549
|
+
environments, authority roles, or signing jobs:
|
|
539
550
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
credential-island-macos-platform-id: macos-arm64
|
|
551
|
+
```toml
|
|
552
|
+
[[signing.artifacts]]
|
|
553
|
+
id = "native-engine"
|
|
554
|
+
path = "dist/kungfu-engine"
|
|
555
|
+
profile = "auto"
|
|
556
|
+
kind = "mach-o"
|
|
557
|
+
platforms = ["macos-arm64", "macos-x64"]
|
|
548
558
|
```
|
|
549
559
|
|
|
550
|
-
|
|
551
|
-
binds the
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
workflow
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
560
|
+
Every native and container build lane reads this declaration after the build
|
|
561
|
+
lifecycle and before verification. Buildchain binds the exact artifact bytes or directory tree to
|
|
562
|
+
the caller repository, source commit, source tree, immutable runtime, platform,
|
|
563
|
+
and requested signature semantics, then publishes a deterministic
|
|
564
|
+
`<artifact>-signing-request-<platform>-<source-sha>` request. No consumer
|
|
565
|
+
workflow step is required.
|
|
566
|
+
|
|
567
|
+
`profile = "auto"` resolves signable Apple artifacts such as Mach-O files,
|
|
568
|
+
`.dylib`, `.framework`, `.app`, `.xpc`, `.plugin`, `.pkg`, and `.dmg` to the
|
|
569
|
+
native `apple-developer-id` provider. Windows `pe` and `binary` artifacts
|
|
570
|
+
resolve to timestamped native `windows-authenticode`; Windows PE never falls
|
|
571
|
+
back to a detached signature. Linux and other non-native binary files,
|
|
572
|
+
archives, blobs, and directories resolve to `detached-signature-v1`. Buildchain records that as a
|
|
573
|
+
detached cryptographic signature and never misrepresents it as an operating
|
|
574
|
+
system code signature. Explicit incompatible provider/kind/platform
|
|
575
|
+
combinations fail closed.
|
|
576
|
+
|
|
577
|
+
The request schema rejects credential and authority-infrastructure fields. The
|
|
578
|
+
Buildchain-owned signing authority is responsible for credential selection,
|
|
579
|
+
native signing, notarization where applicable, immutable result delivery, and a
|
|
580
|
+
receipt bound to the request digest, runtime SHA, output digest, and signature
|
|
581
|
+
evidence. Consumer repositories neither receive nor duplicate credential-island
|
|
582
|
+
material. The reusable workflow dispatches the sealed request to the
|
|
583
|
+
Buildchain repository, waits for its protected authority workflow, verifies the
|
|
584
|
+
immutable result, replaces only the declared artifact with the returned final
|
|
585
|
+
bytes, and then runs the consumer's normal verification. Platform manifests,
|
|
586
|
+
KFD evidence, checksums, and Release Passport inputs therefore observe the
|
|
587
|
+
final signed artifact rather than the pre-signing build output.
|
|
588
|
+
|
|
589
|
+
The durable v3 authority runtime is
|
|
590
|
+
`authority/v3/v3.0/artifact-signing`. It is channel-neutral: alpha and stable
|
|
591
|
+
release work use the same protected `buildchain-artifact-signing` environment
|
|
592
|
+
and provider identities. The authority ref is protected independently from
|
|
593
|
+
release channels and can advance only through reviewed, checked changes; the
|
|
594
|
+
temporary `train/v3/v3.0/artifact-signing-authority` ref is retained only as a
|
|
595
|
+
bounded migration rollback.
|
|
596
|
+
|
|
597
|
+
The older `credential-island-macos-*` reusable-workflow inputs remain a
|
|
598
|
+
compatibility surface while existing callers migrate. They are not the target
|
|
599
|
+
consumer contract and must not be used to design new integrations.
|
|
585
600
|
|
|
586
601
|
## Artifact Transfer Relay
|
|
587
602
|
|
|
@@ -64,6 +64,27 @@ item that still needs to be merged; it is only a temporary fast-use,
|
|
|
64
64
|
diagnostic, and rollback channel for initiating repositories. Retained trains
|
|
65
65
|
are cleaned up by a separate periodic Buildchain cleanup task.
|
|
66
66
|
|
|
67
|
+
## Formal artifact-signing authority ref
|
|
68
|
+
|
|
69
|
+
Artifact signing uses a durable, channel-neutral authority ref after its
|
|
70
|
+
runtime has passed downstream validation:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
authority/v3/v3.0/artifact-signing
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Unlike a train, this ref is a protected execution boundary. Alpha and stable
|
|
77
|
+
release intent use the same authority ref and the same
|
|
78
|
+
`buildchain-artifact-signing` environment; channel promotion never selects a
|
|
79
|
+
different certificate environment. Updates to the authority ref require a
|
|
80
|
+
reviewed pull request, the normal `check` and `verify` status contexts, and a
|
|
81
|
+
fast-forward-safe protected branch policy. Deletion and non-fast-forward
|
|
82
|
+
updates are forbidden.
|
|
83
|
+
|
|
84
|
+
The temporary `train/v3/v3.0/artifact-signing-authority` ref remains a bounded
|
|
85
|
+
rollback and diagnostic pointer during migration. It is not the production
|
|
86
|
+
identity and must not regain credential ownership.
|
|
87
|
+
|
|
67
88
|
## Consumer workflow requirement
|
|
68
89
|
|
|
69
90
|
Consumers keep their reusable workflow pinned to the stable shell:
|
package/docs/versioning.md
CHANGED
|
@@ -61,6 +61,7 @@ repository changes are patch".
|
|
|
61
61
|
|
|
62
62
|
| Date | Action | Line | Faces | Class | Rationale | PR |
|
|
63
63
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
64
|
+
| 2026-07-28 | extend-minor | `v3.0` | auditable-demo-workflow, auditable-demo-media-profile, auditable-demo-media-receipt | additive | Add opt-in archive, web-delivery, and site-hero profiles; independently bind codec, container, audio, layout, byte-budget, role, and fast-start facts into a v2 media receipt while preserving the existing archive default. | |
|
|
64
65
|
| 2026-07-26 | extend-minor | `v3.0` | auditable-demo-workflow, auditable-demo-evidence | additive | Forward-port the consumer-neutral reusable Gate that binds exact same-run GitHub Artifacts to checked-in adapters and immutable renderer evidence, with optional media rendering only from the exact passing Gate bundle. | #1862 |
|
|
65
66
|
| 2026-07-23 | extend-minor | `v2.14` | credential-island-macos-input, protected-signer-job, macos-signing-evidence, action-subpaths | additive | The reusable build surface can seal an exact source-bound macOS app and hand it to a protected caller environment, where an immutable Buildchain action signs, notarizes, staples, Gatekeeper-assesses, and returns an auditable additional release-candidate platform without exposing credentials to consumer lifecycle jobs. | |
|
|
66
67
|
| 2026-07-20 | extend-minor | `v2.14` | anchored-derived-version-material, build-controller-evidence, release-passport, package-subpaths, release-propagation-controller | additive | Anchored/manual consumers can declare derived version witnesses that Buildchain regenerates and verifies before heavy builds, binds to exact alpha/release trees and passports, and admits during protected promotion; propagation receipts now model their existing optional consumer stages. | |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "3.0.2-alpha.
|
|
3
|
+
"version": "3.0.2-alpha.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"./artifact-passport": "./packages/core/artifact-passport.js",
|
|
19
19
|
"./artifact-verification-envelope": "./packages/core/artifact-verification-envelope.js",
|
|
20
20
|
"./github-artifact-attestation": "./packages/core/github-artifact-attestation.js",
|
|
21
|
+
"./artifact-signing": "./packages/core/artifact-signing.js",
|
|
22
|
+
"./artifact-signing-result": "./packages/core/artifact-signing-result.js",
|
|
23
|
+
"./detached-artifact-signature": "./packages/core/detached-artifact-signature.js",
|
|
21
24
|
"./anchored-version-material": "./packages/core/anchored-version-material.js",
|
|
22
25
|
"./buildchain-contract": "./packages/core/buildchain-contract.js",
|
|
23
26
|
"./candidate-timeline": "./packages/core/candidate-timeline.js",
|
|
@@ -82,6 +85,7 @@
|
|
|
82
85
|
"AGENTS.md",
|
|
83
86
|
"bin/",
|
|
84
87
|
"CONTRIBUTING.md",
|
|
88
|
+
"contracts/",
|
|
85
89
|
"LICENSE-POLICY.md",
|
|
86
90
|
"scripts/*.mjs",
|
|
87
91
|
"packages/core/",
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ARTIFACT_SIGNING_AUTHORITY_CONTRACT,
|
|
7
|
+
artifactSigningDigest,
|
|
8
|
+
validateArtifactSigningReceipt,
|
|
9
|
+
validateArtifactSigningRequest,
|
|
10
|
+
} from "./artifact-signing.js";
|
|
11
|
+
|
|
12
|
+
export const ARTIFACT_SIGNING_RESULT_CONTRACT =
|
|
13
|
+
"kungfu-buildchain-artifact-signing-result/v1";
|
|
14
|
+
|
|
15
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
16
|
+
|
|
17
|
+
function required(value, label) {
|
|
18
|
+
const normalized = String(value || "").trim();
|
|
19
|
+
if (!normalized) throw new Error(`${label} must be a non-empty string`);
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function digest(value, label) {
|
|
24
|
+
const normalized = required(value, label).toLowerCase();
|
|
25
|
+
if (!SHA256_PATTERN.test(normalized)) {
|
|
26
|
+
throw new Error(`${label} must be a sha256 digest`);
|
|
27
|
+
}
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stableJson(value) {
|
|
32
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
33
|
+
if (value && typeof value === "object") {
|
|
34
|
+
return `{${Object.keys(value)
|
|
35
|
+
.sort()
|
|
36
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
37
|
+
.join(",")}}`;
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function documentDigest(value) {
|
|
43
|
+
const { digest: _digest, ...basis } = value;
|
|
44
|
+
return artifactSigningDigest(basis);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeEvidence(evidence = []) {
|
|
48
|
+
if (!Array.isArray(evidence) || evidence.length === 0) {
|
|
49
|
+
throw new Error("artifact signing result requires verification evidence");
|
|
50
|
+
}
|
|
51
|
+
return evidence.map((entry, index) => ({
|
|
52
|
+
kind: required(entry?.kind, `evidence[${index}].kind`),
|
|
53
|
+
path: required(entry?.path, `evidence[${index}].path`),
|
|
54
|
+
digest: digest(entry?.digest, `evidence[${index}].digest`),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function artifactSigningEvidenceDigest(evidence = []) {
|
|
59
|
+
const normalized = normalizeEvidence(evidence);
|
|
60
|
+
return artifactSigningDigest(
|
|
61
|
+
normalized.map(({ kind, digest: entryDigest }) => ({
|
|
62
|
+
kind,
|
|
63
|
+
digest: entryDigest,
|
|
64
|
+
})),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createArtifactSigningResult({
|
|
69
|
+
request,
|
|
70
|
+
receipt,
|
|
71
|
+
receiptPath = "receipt.json",
|
|
72
|
+
payload = {},
|
|
73
|
+
evidence = [],
|
|
74
|
+
verification = {},
|
|
75
|
+
} = {}) {
|
|
76
|
+
const requestCheck = validateArtifactSigningRequest(request);
|
|
77
|
+
if (!requestCheck.ok) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`invalid artifact signing request: ${requestCheck.issues.join(", ")}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const receiptCheck = validateArtifactSigningReceipt(receipt, { request });
|
|
83
|
+
if (!receiptCheck.ok || receipt?.status !== "passed") {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`invalid passed artifact signing receipt: ${receiptCheck.issues.join(", ")}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const normalizedEvidence = normalizeEvidence(evidence);
|
|
89
|
+
const evidenceDigest = artifactSigningEvidenceDigest(normalizedEvidence);
|
|
90
|
+
const payloadDigest = digest(payload.digest, "payload digest");
|
|
91
|
+
const payloadBytes = Number(payload.bytes);
|
|
92
|
+
if (!Number.isSafeInteger(payloadBytes) || payloadBytes < 0) {
|
|
93
|
+
throw new Error("payload bytes must be a non-negative safe integer");
|
|
94
|
+
}
|
|
95
|
+
if (receipt.result?.artifactDigest !== payloadDigest) {
|
|
96
|
+
throw new Error("receipt artifact digest does not bind the result payload");
|
|
97
|
+
}
|
|
98
|
+
if (receipt.result?.evidenceDigest !== evidenceDigest) {
|
|
99
|
+
throw new Error("receipt evidence digest does not bind the result evidence");
|
|
100
|
+
}
|
|
101
|
+
const provider = required(verification.provider, "verification provider");
|
|
102
|
+
if (provider !== request.signature.provider) {
|
|
103
|
+
throw new Error("verification provider does not match the requested provider");
|
|
104
|
+
}
|
|
105
|
+
if (verification.status !== "passed") {
|
|
106
|
+
throw new Error("artifact signing result verification must pass");
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(verification.checks) || verification.checks.length === 0) {
|
|
109
|
+
throw new Error("artifact signing result requires provider verification checks");
|
|
110
|
+
}
|
|
111
|
+
const result = {
|
|
112
|
+
schemaVersion: 1,
|
|
113
|
+
contract: ARTIFACT_SIGNING_RESULT_CONTRACT,
|
|
114
|
+
requestDigest: request.digest,
|
|
115
|
+
authority: {
|
|
116
|
+
contract: ARTIFACT_SIGNING_AUTHORITY_CONTRACT,
|
|
117
|
+
id: receipt.authority.id,
|
|
118
|
+
runtimeSha: receipt.authority.runtimeSha,
|
|
119
|
+
},
|
|
120
|
+
source: { ...request.source },
|
|
121
|
+
signature: { ...request.signature },
|
|
122
|
+
artifact: {
|
|
123
|
+
id: request.artifact.id,
|
|
124
|
+
inputDigest: request.artifact.digest,
|
|
125
|
+
path: required(payload.path, "payload path"),
|
|
126
|
+
bytes: payloadBytes,
|
|
127
|
+
digest: payloadDigest,
|
|
128
|
+
},
|
|
129
|
+
evidence: normalizedEvidence,
|
|
130
|
+
evidenceDigest,
|
|
131
|
+
verification: {
|
|
132
|
+
status: "passed",
|
|
133
|
+
provider,
|
|
134
|
+
checks: verification.checks.map((check, index) =>
|
|
135
|
+
required(check, `verification.checks[${index}]`),
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
receipt: {
|
|
139
|
+
path: required(receiptPath, "receipt path"),
|
|
140
|
+
digest: digest(receipt.digest, "receipt digest"),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
result.digest = documentDigest(result);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function validateArtifactSigningResult(result, { request, receipt } = {}) {
|
|
148
|
+
const issues = [];
|
|
149
|
+
try {
|
|
150
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
151
|
+
throw new Error("artifact signing result must be an object");
|
|
152
|
+
}
|
|
153
|
+
if (result.contract !== ARTIFACT_SIGNING_RESULT_CONTRACT) {
|
|
154
|
+
issues.push("artifact signing result contract mismatch");
|
|
155
|
+
}
|
|
156
|
+
const rebuilt = createArtifactSigningResult({
|
|
157
|
+
request,
|
|
158
|
+
receipt,
|
|
159
|
+
receiptPath: result.receipt?.path,
|
|
160
|
+
payload: result.artifact,
|
|
161
|
+
evidence: result.evidence,
|
|
162
|
+
verification: result.verification,
|
|
163
|
+
});
|
|
164
|
+
if (result.digest !== rebuilt.digest) {
|
|
165
|
+
issues.push("artifact signing result digest mismatch");
|
|
166
|
+
}
|
|
167
|
+
if (stableJson(result) !== stableJson(rebuilt)) {
|
|
168
|
+
issues.push("artifact signing result contains unsupported or stale fields");
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
issues.push(String(error?.message || error));
|
|
172
|
+
}
|
|
173
|
+
return { ok: issues.length === 0, issues };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveFile(root, relative, label) {
|
|
177
|
+
const resolvedRoot = path.resolve(root);
|
|
178
|
+
const resolved = path.resolve(resolvedRoot, required(relative, label));
|
|
179
|
+
const rel = path.relative(resolvedRoot, resolved);
|
|
180
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
181
|
+
throw new Error(`${label} must resolve below the result root`);
|
|
182
|
+
}
|
|
183
|
+
return resolved;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sha256File(filePath) {
|
|
187
|
+
return `sha256:${crypto
|
|
188
|
+
.createHash("sha256")
|
|
189
|
+
.update(fs.readFileSync(filePath))
|
|
190
|
+
.digest("hex")}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function verifyArtifactSigningResultFiles({
|
|
194
|
+
root,
|
|
195
|
+
request,
|
|
196
|
+
receipt,
|
|
197
|
+
result,
|
|
198
|
+
} = {}) {
|
|
199
|
+
const issues = [];
|
|
200
|
+
const resultCheck = validateArtifactSigningResult(result, { request, receipt });
|
|
201
|
+
issues.push(...resultCheck.issues);
|
|
202
|
+
try {
|
|
203
|
+
const payloadPath = resolveFile(root, result.artifact.path, "payload path");
|
|
204
|
+
const stat = fs.statSync(payloadPath);
|
|
205
|
+
if (!stat.isFile()) issues.push("result payload is not a regular file");
|
|
206
|
+
if (stat.size !== result.artifact.bytes) issues.push("result payload byte count mismatch");
|
|
207
|
+
if (sha256File(payloadPath) !== result.artifact.digest) {
|
|
208
|
+
issues.push("result payload digest mismatch");
|
|
209
|
+
}
|
|
210
|
+
const receiptPath = resolveFile(root, result.receipt.path, "receipt path");
|
|
211
|
+
const receiptFromFile = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
|
|
212
|
+
if (
|
|
213
|
+
receiptFromFile.digest !== result.receipt.digest ||
|
|
214
|
+
stableJson(receiptFromFile) !== stableJson(receipt)
|
|
215
|
+
) {
|
|
216
|
+
issues.push("result receipt file does not match the bound receipt");
|
|
217
|
+
}
|
|
218
|
+
for (const entry of result.evidence || []) {
|
|
219
|
+
const evidencePath = resolveFile(root, entry.path, "evidence path");
|
|
220
|
+
if (sha256File(evidencePath) !== entry.digest) {
|
|
221
|
+
issues.push(`result evidence digest mismatch: ${entry.kind}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
issues.push(String(error?.message || error));
|
|
226
|
+
}
|
|
227
|
+
return { ok: issues.length === 0, issues };
|
|
228
|
+
}
|