@kungfu-tech/buildchain 3.0.2-alpha.0 → 3.0.2-alpha.10
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 +4 -2
- package/actions/github-artifact-attestation/README.md +10 -0
- package/actions/promote-buildchain-ref/README.md +7 -0
- package/bin/buildchain.mjs +5 -0
- package/bin/internal/trust-release-cli.mjs +74 -3
- 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/artifact-schemas.json +5 -1
- package/dist/site/buildchain-contract.json +133 -34
- package/dist/site/buildchain-site.json +159 -40
- package/dist/site/capability-registry.json +14 -13
- package/dist/site/cli-registry.json +12 -0
- package/dist/site/controller-registry.json +72 -4
- package/dist/site/kfd-claims.json +338 -20
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +24 -9
- package/dist/site/node-api-registry.json +89 -11
- package/dist/site/page-registry.json +135 -25
- package/dist/site/public-surface-audit.json +203 -21
- package/dist/site/publication-authority-registry.json +72 -2
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-model.json +2 -1
- package/dist/site/release-passport-check-manifest.json +1 -0
- package/dist/site/release-provenance.json +6 -0
- package/dist/site/schemas/release-passport-v1.schema.json +6 -0
- package/dist/site/site-manifest.json +21 -13
- package/dist/site/workflow-registry.json +167 -13
- package/docs/MAP.md +5 -1
- package/docs/auditable-demo.md +55 -3
- package/docs/binary-distribution.md +7 -0
- package/docs/cli.md +9 -0
- package/docs/dev-alpha-candidate-patrol.md +111 -0
- package/docs/github-artifact-attestation.md +219 -0
- package/docs/release-governance.md +32 -0
- package/docs/release-passport.md +13 -0
- package/docs/reusable-build-surface.md +83 -61
- package/docs/runtime-train-validation.md +21 -0
- package/docs/versioning.md +1 -0
- package/package.json +8 -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 +14 -0
- package/packages/core/buildchain-kfd-claims.js +5 -0
- package/packages/core/buildchain-publication-authority.js +3 -0
- package/packages/core/cache-evidence.js +288 -0
- package/packages/core/channel-candidate.js +186 -0
- package/packages/core/detached-artifact-signature.js +121 -0
- package/packages/core/diagnostics.js +276 -10
- package/packages/core/github-artifact-attestation.js +642 -0
- package/packages/core/github-governance-authority.js +77 -16
- package/packages/core/index.js +62 -0
- package/packages/core/publication-authority.js +1 -1
- package/packages/core/release-passport-contract.js +2 -0
- package/packages/core/release-passport.js +60 -3
- package/scripts/auditable-demo.mjs +520 -28
- package/scripts/build-contract-core.mjs +31 -0
- package/scripts/buildchain-channel-router.mjs +8 -2
- package/scripts/check-inventory.mjs +9 -0
- package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +864 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
- package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
- package/scripts/generate-channel-promotion-workflow.mjs +6 -0
- package/scripts/generate-site-bundle.mjs +20 -0
- package/scripts/import-artifact-signing-results.mjs +76 -0
- package/scripts/inspect-artifact-signing-requests.mjs +101 -0
- package/scripts/locked-source-checkout.mjs +48 -0
- package/scripts/materialize-artifact-signing-request.mjs +66 -0
- package/scripts/merge-artifact-signing-results.mjs +76 -0
- package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
- package/scripts/reconcile-github-governance.mjs +158 -25
- package/scripts/release-candidate-resolver.mjs +12 -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/shifu-gate-profile.mjs +26 -20
- package/scripts/sign-detached-artifact-requests.mjs +237 -0
- package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
- package/scripts/verify-artifact-signing-results.mjs +99 -0
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.10",
|
|
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",
|
|
@@ -17,9 +17,15 @@
|
|
|
17
17
|
"./core": "./packages/core/index.js",
|
|
18
18
|
"./artifact-passport": "./packages/core/artifact-passport.js",
|
|
19
19
|
"./artifact-verification-envelope": "./packages/core/artifact-verification-envelope.js",
|
|
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",
|
|
20
24
|
"./anchored-version-material": "./packages/core/anchored-version-material.js",
|
|
21
25
|
"./buildchain-contract": "./packages/core/buildchain-contract.js",
|
|
22
26
|
"./candidate-timeline": "./packages/core/candidate-timeline.js",
|
|
27
|
+
"./channel-candidate": "./packages/core/channel-candidate.js",
|
|
28
|
+
"./cache-evidence": "./packages/core/cache-evidence.js",
|
|
23
29
|
"./controller-evidence": "./packages/core/controller-evidence.js",
|
|
24
30
|
"./diagnostics": "./packages/core/diagnostics.js",
|
|
25
31
|
"./homebrew": "./packages/core/homebrew.js",
|
|
@@ -79,6 +85,7 @@
|
|
|
79
85
|
"AGENTS.md",
|
|
80
86
|
"bin/",
|
|
81
87
|
"CONTRIBUTING.md",
|
|
88
|
+
"contracts/",
|
|
82
89
|
"LICENSE-POLICY.md",
|
|
83
90
|
"scripts/*.mjs",
|
|
84
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
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const ARTIFACT_SIGNING_REQUEST_CONTRACT =
|
|
4
|
+
"kungfu-buildchain-artifact-signing-request/v1";
|
|
5
|
+
export const ARTIFACT_SIGNING_RECEIPT_CONTRACT =
|
|
6
|
+
"kungfu-buildchain-artifact-signing-receipt/v1";
|
|
7
|
+
export const ARTIFACT_SIGNING_AUTHORITY_CONTRACT =
|
|
8
|
+
"kungfu-buildchain-artifact-signing-authority/v1";
|
|
9
|
+
|
|
10
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
11
|
+
const SOURCE_SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
12
|
+
const FORBIDDEN_CREDENTIAL_KEYS =
|
|
13
|
+
/(?:certificate|password|private.?key|secret|token|notary|issuer|team.?id|environment)/iu;
|
|
14
|
+
|
|
15
|
+
const PROFILE_REGISTRY = Object.freeze({
|
|
16
|
+
"apple-developer-id": Object.freeze({
|
|
17
|
+
id: "apple-developer-id",
|
|
18
|
+
provider: "apple",
|
|
19
|
+
semantics: "native-platform-signature",
|
|
20
|
+
platforms: ["macos"],
|
|
21
|
+
artifactKinds: [
|
|
22
|
+
"mach-o",
|
|
23
|
+
"app-bundle",
|
|
24
|
+
"framework-bundle",
|
|
25
|
+
"plugin-bundle",
|
|
26
|
+
"xpc-bundle",
|
|
27
|
+
"dylib",
|
|
28
|
+
"pkg",
|
|
29
|
+
"dmg",
|
|
30
|
+
],
|
|
31
|
+
}),
|
|
32
|
+
"windows-authenticode": Object.freeze({
|
|
33
|
+
id: "windows-authenticode",
|
|
34
|
+
provider: "microsoft-authenticode",
|
|
35
|
+
semantics: "native-platform-signature",
|
|
36
|
+
platforms: ["windows"],
|
|
37
|
+
artifactKinds: ["pe", "binary"],
|
|
38
|
+
}),
|
|
39
|
+
"detached-signature-v1": Object.freeze({
|
|
40
|
+
id: "detached-signature-v1",
|
|
41
|
+
provider: "buildchain-detached",
|
|
42
|
+
semantics: "detached-cryptographic-signature",
|
|
43
|
+
platforms: ["any"],
|
|
44
|
+
artifactKinds: ["binary", "archive", "blob", "directory"],
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function stableJson(value) {
|
|
49
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
50
|
+
if (value && typeof value === "object") {
|
|
51
|
+
return `{${Object.keys(value)
|
|
52
|
+
.sort()
|
|
53
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
54
|
+
.join(",")}}`;
|
|
55
|
+
}
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function artifactSigningDigest(value) {
|
|
60
|
+
return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function documentDigest(value) {
|
|
64
|
+
const { digest: _digest, ...basis } = value;
|
|
65
|
+
return artifactSigningDigest(basis);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nonEmptyString(value, label) {
|
|
69
|
+
const normalized = String(value || "").trim();
|
|
70
|
+
if (!normalized) throw new Error(`${label} must be a non-empty string`);
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function exactDigest(value, label) {
|
|
75
|
+
const normalized = nonEmptyString(value, label).toLowerCase();
|
|
76
|
+
if (!SHA256_PATTERN.test(normalized)) {
|
|
77
|
+
throw new Error(`${label} must be a sha256 digest`);
|
|
78
|
+
}
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function exactSourceSha(value, label) {
|
|
83
|
+
const normalized = nonEmptyString(value, label).toLowerCase();
|
|
84
|
+
if (!SOURCE_SHA_PATTERN.test(normalized)) {
|
|
85
|
+
throw new Error(`${label} must be a 40-character Git SHA`);
|
|
86
|
+
}
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertNoCredentialMaterial(value, path = "request") {
|
|
91
|
+
if (!value || typeof value !== "object") return;
|
|
92
|
+
for (const [key, child] of Object.entries(value)) {
|
|
93
|
+
if (FORBIDDEN_CREDENTIAL_KEYS.test(key)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${path}.${key} is credential configuration; signing requests may only declare desired signature state`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
assertNoCredentialMaterial(child, `${path}.${key}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function listArtifactSigningProfiles() {
|
|
103
|
+
return Object.values(PROFILE_REGISTRY).map((profile) => ({
|
|
104
|
+
...profile,
|
|
105
|
+
platforms: [...profile.platforms],
|
|
106
|
+
artifactKinds: [...profile.artifactKinds],
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveArtifactSigningProfile({
|
|
111
|
+
profile = "auto",
|
|
112
|
+
platform = "",
|
|
113
|
+
artifactKind = "binary",
|
|
114
|
+
} = {}) {
|
|
115
|
+
const normalizedPlatform = String(platform || "")
|
|
116
|
+
.trim()
|
|
117
|
+
.toLowerCase();
|
|
118
|
+
const normalizedKind = nonEmptyString(
|
|
119
|
+
artifactKind,
|
|
120
|
+
"artifact kind",
|
|
121
|
+
).toLowerCase();
|
|
122
|
+
let profileId = String(profile || "auto")
|
|
123
|
+
.trim()
|
|
124
|
+
.toLowerCase();
|
|
125
|
+
if (profileId === "auto") {
|
|
126
|
+
const apple = PROFILE_REGISTRY["apple-developer-id"];
|
|
127
|
+
if (
|
|
128
|
+
normalizedPlatform === "macos" &&
|
|
129
|
+
apple.artifactKinds.includes(normalizedKind)
|
|
130
|
+
) {
|
|
131
|
+
profileId = apple.id;
|
|
132
|
+
} else if (
|
|
133
|
+
normalizedPlatform === "windows" &&
|
|
134
|
+
PROFILE_REGISTRY["windows-authenticode"].artifactKinds.includes(
|
|
135
|
+
normalizedKind,
|
|
136
|
+
)
|
|
137
|
+
) {
|
|
138
|
+
profileId = "windows-authenticode";
|
|
139
|
+
} else {
|
|
140
|
+
profileId = "detached-signature-v1";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const resolved = PROFILE_REGISTRY[profileId];
|
|
144
|
+
if (!resolved)
|
|
145
|
+
throw new Error(`unsupported artifact signing profile: ${profileId}`);
|
|
146
|
+
if (
|
|
147
|
+
!resolved.platforms.includes("any") &&
|
|
148
|
+
!resolved.platforms.includes(normalizedPlatform)
|
|
149
|
+
) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`artifact signing profile ${profileId} does not support platform ${normalizedPlatform || "<empty>"}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (!resolved.artifactKinds.includes(normalizedKind)) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`artifact signing profile ${profileId} does not support artifact kind ${normalizedKind}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
...resolved,
|
|
161
|
+
platforms: [...resolved.platforms],
|
|
162
|
+
artifactKinds: [...resolved.artifactKinds],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createArtifactSigningRequest({
|
|
167
|
+
source = {},
|
|
168
|
+
runtime = {},
|
|
169
|
+
artifact = {},
|
|
170
|
+
signature = {},
|
|
171
|
+
delivery = {},
|
|
172
|
+
} = {}) {
|
|
173
|
+
assertNoCredentialMaterial({
|
|
174
|
+
source,
|
|
175
|
+
runtime,
|
|
176
|
+
artifact,
|
|
177
|
+
signature,
|
|
178
|
+
delivery,
|
|
179
|
+
});
|
|
180
|
+
const platform = nonEmptyString(
|
|
181
|
+
artifact.platform || source.platform,
|
|
182
|
+
"artifact platform",
|
|
183
|
+
).toLowerCase();
|
|
184
|
+
const kind = nonEmptyString(
|
|
185
|
+
artifact.kind || "binary",
|
|
186
|
+
"artifact kind",
|
|
187
|
+
).toLowerCase();
|
|
188
|
+
const profile = resolveArtifactSigningProfile({
|
|
189
|
+
profile: signature.profile || "auto",
|
|
190
|
+
platform,
|
|
191
|
+
artifactKind: kind,
|
|
192
|
+
});
|
|
193
|
+
const request = {
|
|
194
|
+
schemaVersion: 1,
|
|
195
|
+
contract: ARTIFACT_SIGNING_REQUEST_CONTRACT,
|
|
196
|
+
authority: {
|
|
197
|
+
contract: ARTIFACT_SIGNING_AUTHORITY_CONTRACT,
|
|
198
|
+
id: "kungfu-systems/buildchain",
|
|
199
|
+
},
|
|
200
|
+
source: {
|
|
201
|
+
repository: nonEmptyString(source.repository, "source repository"),
|
|
202
|
+
sha: exactSourceSha(source.sha, "source SHA"),
|
|
203
|
+
treeSha: exactSourceSha(source.treeSha, "source tree SHA"),
|
|
204
|
+
},
|
|
205
|
+
runtime: {
|
|
206
|
+
repository: nonEmptyString(
|
|
207
|
+
runtime.repository || "kungfu-systems/buildchain",
|
|
208
|
+
"runtime repository",
|
|
209
|
+
),
|
|
210
|
+
sha: exactSourceSha(runtime.sha, "runtime SHA"),
|
|
211
|
+
},
|
|
212
|
+
artifact: {
|
|
213
|
+
id: nonEmptyString(artifact.id || artifact.path, "artifact id"),
|
|
214
|
+
path: nonEmptyString(artifact.path, "artifact path"),
|
|
215
|
+
kind,
|
|
216
|
+
platform,
|
|
217
|
+
...(artifact.arch
|
|
218
|
+
? { arch: nonEmptyString(artifact.arch, "artifact architecture") }
|
|
219
|
+
: {}),
|
|
220
|
+
bytes: Number(artifact.bytes),
|
|
221
|
+
digest: exactDigest(artifact.digest, "artifact digest"),
|
|
222
|
+
...(artifact.mediaType
|
|
223
|
+
? {
|
|
224
|
+
mediaType: nonEmptyString(
|
|
225
|
+
artifact.mediaType,
|
|
226
|
+
"artifact media type",
|
|
227
|
+
),
|
|
228
|
+
}
|
|
229
|
+
: {}),
|
|
230
|
+
...(artifact.transport
|
|
231
|
+
? {
|
|
232
|
+
transport: {
|
|
233
|
+
file: nonEmptyString(
|
|
234
|
+
artifact.transport.file,
|
|
235
|
+
"artifact transport file",
|
|
236
|
+
),
|
|
237
|
+
format: nonEmptyString(
|
|
238
|
+
artifact.transport.format,
|
|
239
|
+
"artifact transport format",
|
|
240
|
+
),
|
|
241
|
+
bytes: Number(artifact.transport.bytes),
|
|
242
|
+
digest: exactDigest(
|
|
243
|
+
artifact.transport.digest,
|
|
244
|
+
"artifact transport digest",
|
|
245
|
+
),
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
},
|
|
250
|
+
signature: {
|
|
251
|
+
required: signature.required !== false,
|
|
252
|
+
profile: profile.id,
|
|
253
|
+
provider: profile.provider,
|
|
254
|
+
semantics: profile.semantics,
|
|
255
|
+
},
|
|
256
|
+
delivery: {
|
|
257
|
+
mode: nonEmptyString(
|
|
258
|
+
delivery.mode || "buildchain-authority",
|
|
259
|
+
"delivery mode",
|
|
260
|
+
),
|
|
261
|
+
...(delivery.recipientKey
|
|
262
|
+
? {
|
|
263
|
+
recipientKey: nonEmptyString(
|
|
264
|
+
delivery.recipientKey,
|
|
265
|
+
"delivery recipient key",
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
if (
|
|
272
|
+
!Number.isSafeInteger(request.artifact.bytes) ||
|
|
273
|
+
request.artifact.bytes < 0
|
|
274
|
+
) {
|
|
275
|
+
throw new Error("artifact bytes must be a non-negative safe integer");
|
|
276
|
+
}
|
|
277
|
+
if (
|
|
278
|
+
request.artifact.transport &&
|
|
279
|
+
(!Number.isSafeInteger(request.artifact.transport.bytes) ||
|
|
280
|
+
request.artifact.transport.bytes < 0)
|
|
281
|
+
) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
"artifact transport bytes must be a non-negative safe integer",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
request.digest = documentDigest(request);
|
|
287
|
+
return request;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function validateArtifactSigningRequest(request) {
|
|
291
|
+
const issues = [];
|
|
292
|
+
try {
|
|
293
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
294
|
+
throw new Error("artifact signing request must be an object");
|
|
295
|
+
}
|
|
296
|
+
if (request.contract !== ARTIFACT_SIGNING_REQUEST_CONTRACT) {
|
|
297
|
+
issues.push("request contract mismatch");
|
|
298
|
+
}
|
|
299
|
+
assertNoCredentialMaterial(request);
|
|
300
|
+
const rebuilt = createArtifactSigningRequest(request);
|
|
301
|
+
if (request.digest !== rebuilt.digest)
|
|
302
|
+
issues.push("request digest mismatch");
|
|
303
|
+
if (stableJson(request) !== stableJson(rebuilt)) {
|
|
304
|
+
issues.push("request contains non-canonical or unsupported fields");
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
issues.push(String(error?.message || error));
|
|
308
|
+
}
|
|
309
|
+
return { ok: issues.length === 0, issues };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createArtifactSigningReceipt({
|
|
313
|
+
request,
|
|
314
|
+
status = "passed",
|
|
315
|
+
authority = {},
|
|
316
|
+
result = {},
|
|
317
|
+
signatures = [],
|
|
318
|
+
reason = "",
|
|
319
|
+
} = {}) {
|
|
320
|
+
const requestCheck = validateArtifactSigningRequest(request);
|
|
321
|
+
if (!requestCheck.ok) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`invalid artifact signing request: ${requestCheck.issues.join(", ")}`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (!["passed", "failed", "rejected"].includes(status)) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
"artifact signing receipt status must be passed, failed, or rejected",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const receipt = {
|
|
332
|
+
schemaVersion: 1,
|
|
333
|
+
contract: ARTIFACT_SIGNING_RECEIPT_CONTRACT,
|
|
334
|
+
requestDigest: request.digest,
|
|
335
|
+
status,
|
|
336
|
+
authority: {
|
|
337
|
+
contract: ARTIFACT_SIGNING_AUTHORITY_CONTRACT,
|
|
338
|
+
id: nonEmptyString(authority.id || request.authority.id, "authority id"),
|
|
339
|
+
runtimeSha: exactSourceSha(
|
|
340
|
+
authority.runtimeSha || request.runtime.sha,
|
|
341
|
+
"authority runtime SHA",
|
|
342
|
+
),
|
|
343
|
+
},
|
|
344
|
+
signature: { ...request.signature },
|
|
345
|
+
...(status === "passed"
|
|
346
|
+
? {
|
|
347
|
+
result: {
|
|
348
|
+
artifactDigest: exactDigest(
|
|
349
|
+
result.artifactDigest,
|
|
350
|
+
"result artifact digest",
|
|
351
|
+
),
|
|
352
|
+
evidenceDigest: exactDigest(
|
|
353
|
+
result.evidenceDigest,
|
|
354
|
+
"result evidence digest",
|
|
355
|
+
),
|
|
356
|
+
},
|
|
357
|
+
signatures: signatures.map((entry, index) => ({
|
|
358
|
+
kind: nonEmptyString(entry.kind, `signatures[${index}].kind`),
|
|
359
|
+
digest: exactDigest(entry.digest, `signatures[${index}].digest`),
|
|
360
|
+
})),
|
|
361
|
+
}
|
|
362
|
+
: { reason: nonEmptyString(reason, "receipt reason") }),
|
|
363
|
+
};
|
|
364
|
+
if (status === "passed" && receipt.signatures.length === 0) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
"passed artifact signing receipt requires signature evidence",
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
receipt.digest = documentDigest(receipt);
|
|
370
|
+
return receipt;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function validateArtifactSigningReceipt(receipt, { request } = {}) {
|
|
374
|
+
const issues = [];
|
|
375
|
+
try {
|
|
376
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
377
|
+
throw new Error("artifact signing receipt must be an object");
|
|
378
|
+
}
|
|
379
|
+
if (receipt.contract !== ARTIFACT_SIGNING_RECEIPT_CONTRACT) {
|
|
380
|
+
issues.push("receipt contract mismatch");
|
|
381
|
+
}
|
|
382
|
+
if (receipt.digest !== documentDigest(receipt))
|
|
383
|
+
issues.push("receipt digest mismatch");
|
|
384
|
+
if (request) {
|
|
385
|
+
const requestCheck = validateArtifactSigningRequest(request);
|
|
386
|
+
if (!requestCheck.ok) issues.push(...requestCheck.issues);
|
|
387
|
+
if (receipt.requestDigest !== request.digest)
|
|
388
|
+
issues.push("receipt request digest mismatch");
|
|
389
|
+
if (stableJson(receipt.signature) !== stableJson(request.signature)) {
|
|
390
|
+
issues.push("receipt signature policy mismatch");
|
|
391
|
+
}
|
|
392
|
+
if (receipt.authority?.runtimeSha !== request.runtime.sha) {
|
|
393
|
+
issues.push("receipt authority runtime mismatch");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (receipt.status === "passed") {
|
|
397
|
+
exactDigest(receipt.result?.artifactDigest, "result artifact digest");
|
|
398
|
+
exactDigest(receipt.result?.evidenceDigest, "result evidence digest");
|
|
399
|
+
if (
|
|
400
|
+
!Array.isArray(receipt.signatures) ||
|
|
401
|
+
receipt.signatures.length === 0
|
|
402
|
+
) {
|
|
403
|
+
issues.push("passed receipt has no signature evidence");
|
|
404
|
+
}
|
|
405
|
+
} else if (!["failed", "rejected"].includes(receipt.status)) {
|
|
406
|
+
issues.push("receipt status is invalid");
|
|
407
|
+
}
|
|
408
|
+
} catch (error) {
|
|
409
|
+
issues.push(String(error?.message || error));
|
|
410
|
+
}
|
|
411
|
+
return { ok: issues.length === 0, issues };
|
|
412
|
+
}
|