@kungfu-tech/buildchain 3.0.2-alpha.7 → 3.0.2-alpha.9
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 +44 -26
- 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
|
@@ -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
|
+
}
|
|
@@ -55,6 +55,17 @@ const SUPPORTED_INFRA_APPLY_MODES = new Set(["disabled", "manual-approval", "env
|
|
|
55
55
|
const SUPPORTED_FACT_VERSION_SOURCE_TYPES = new Set(["static", "json", "toml", "regex", "command"]);
|
|
56
56
|
const SUPPORTED_FACT_LEGACY_PROJECTIONS = new Set(["kungfu-buildinfo"]);
|
|
57
57
|
const SUPPORTED_PUBLICATION_TOOLCHAINS = new Set(["custom-command", "latex-docker"]);
|
|
58
|
+
const SUPPORTED_SIGNING_PROFILES = new Set([
|
|
59
|
+
"auto",
|
|
60
|
+
"apple-developer-id",
|
|
61
|
+
"windows-authenticode",
|
|
62
|
+
"detached-signature-v1",
|
|
63
|
+
]);
|
|
64
|
+
const SUPPORTED_SIGNING_ARTIFACT_KINDS = new Set([
|
|
65
|
+
"auto", "binary", "archive", "blob", "directory", "mach-o", "app-bundle",
|
|
66
|
+
"framework-bundle", "plugin-bundle", "xpc-bundle", "dylib", "pkg", "dmg",
|
|
67
|
+
"pe",
|
|
68
|
+
]);
|
|
58
69
|
|
|
59
70
|
function posixPath(value) {
|
|
60
71
|
return String(value || "").split(path.sep).join("/");
|
|
@@ -218,6 +229,9 @@ export function normalizeBuildchainConfig(config) {
|
|
|
218
229
|
if (normalized.facts !== undefined) {
|
|
219
230
|
normalized.facts = normalizeFactsSection(normalized.facts);
|
|
220
231
|
}
|
|
232
|
+
if (normalized.signing !== undefined) {
|
|
233
|
+
normalized.signing = normalizeSigningSection(normalized.signing);
|
|
234
|
+
}
|
|
221
235
|
validateWebSurfaceConfig(normalized);
|
|
222
236
|
validateInfraContractConfig(normalized);
|
|
223
237
|
validateAnchoredDerivedVersionMaterialConfig(normalized);
|
|
@@ -335,6 +349,49 @@ function normalizeFactsSection(facts) {
|
|
|
335
349
|
};
|
|
336
350
|
}
|
|
337
351
|
|
|
352
|
+
function normalizeSigningSection(signing) {
|
|
353
|
+
assertPlainObject(signing, "signing");
|
|
354
|
+
for (const key of Object.keys(signing)) {
|
|
355
|
+
if (key !== "artifacts") {
|
|
356
|
+
throw new Error(`signing.${key} is not supported; consumer signing declarations cannot configure credentials or authority infrastructure`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!Array.isArray(signing.artifacts) || signing.artifacts.length === 0) {
|
|
360
|
+
throw new Error("signing.artifacts must declare at least one artifact");
|
|
361
|
+
}
|
|
362
|
+
const artifacts = signing.artifacts.map((artifact, index) => {
|
|
363
|
+
const label = `signing.artifacts[${index}]`;
|
|
364
|
+
assertPlainObject(artifact, label);
|
|
365
|
+
const allowedKeys = new Set(["id", "path", "profile", "kind", "platforms", "required"]);
|
|
366
|
+
for (const key of Object.keys(artifact)) {
|
|
367
|
+
if (!allowedKeys.has(key)) {
|
|
368
|
+
throw new Error(`${label}.${key} is not supported; declare desired signature state only`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const artifactPath = posixPath(assertString(artifact.path, `${label}.path`));
|
|
372
|
+
const profile = artifact.profile === undefined ? "auto" : assertString(artifact.profile, `${label}.profile`);
|
|
373
|
+
if (!SUPPORTED_SIGNING_PROFILES.has(profile)) {
|
|
374
|
+
throw new Error(`${label}.profile must be one of auto, apple-developer-id, windows-authenticode, or detached-signature-v1`);
|
|
375
|
+
}
|
|
376
|
+
const kind = artifact.kind === undefined ? "auto" : assertString(artifact.kind, `${label}.kind`);
|
|
377
|
+
if (!SUPPORTED_SIGNING_ARTIFACT_KINDS.has(kind)) {
|
|
378
|
+
throw new Error(`${label}.kind is not a supported signing artifact kind`);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
id: artifact.id === undefined ? artifactPath : assertString(artifact.id, `${label}.id`),
|
|
382
|
+
path: artifactPath,
|
|
383
|
+
profile,
|
|
384
|
+
kind,
|
|
385
|
+
platforms: normalizeStringArray(artifact.platforms, `${label}.platforms`),
|
|
386
|
+
required: optionalBoolean(artifact.required, true),
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
if (new Set(artifacts.map((artifact) => artifact.id)).size !== artifacts.length) {
|
|
390
|
+
throw new Error("signing.artifacts must use unique ids");
|
|
391
|
+
}
|
|
392
|
+
return { artifacts };
|
|
393
|
+
}
|
|
394
|
+
|
|
338
395
|
function normalizeFactVersionSources(sources = []) {
|
|
339
396
|
if (sources === undefined) {
|
|
340
397
|
return [];
|
|
@@ -1615,6 +1672,7 @@ export function validateBuildchainConfig(
|
|
|
1615
1672
|
release: loadedConfig.config.release,
|
|
1616
1673
|
governance: loadedConfig.config.governance,
|
|
1617
1674
|
facts: loadedConfig.config.facts,
|
|
1675
|
+
signing: loadedConfig.config.signing,
|
|
1618
1676
|
publication: loadedConfig.config.publication,
|
|
1619
1677
|
};
|
|
1620
1678
|
}
|
|
@@ -329,10 +329,13 @@ export function createBuildchainContractWorld({
|
|
|
329
329
|
"media-artifact-digest",
|
|
330
330
|
"media-artifact-url",
|
|
331
331
|
"media-root",
|
|
332
|
+
"media-profile",
|
|
333
|
+
"media-qualification-root",
|
|
332
334
|
],
|
|
333
335
|
breakingDefaults: {
|
|
334
336
|
trustedEventRequired: true,
|
|
335
337
|
renderMediaDefault: false,
|
|
338
|
+
mediaProfileDefault: "archive-v1",
|
|
336
339
|
artifactRetentionDaysDefault: 14,
|
|
337
340
|
gatePolicy: "required-before-selective-render",
|
|
338
341
|
},
|
|
@@ -340,6 +343,7 @@ export function createBuildchainContractWorld({
|
|
|
340
343
|
"buildchain-repository",
|
|
341
344
|
"source-ref",
|
|
342
345
|
"render-media",
|
|
346
|
+
"media-profile",
|
|
343
347
|
"artifact-retention-days",
|
|
344
348
|
"require-trusted-event",
|
|
345
349
|
],
|
|
@@ -349,6 +353,10 @@ export function createBuildchainContractWorld({
|
|
|
349
353
|
"every invocation runs an immutable network-disabled renderer smoke before emitting a qualified Gate bundle",
|
|
350
354
|
"complete media rendering is optional and consumes only an exact passing Gate bundle",
|
|
351
355
|
"Gate and media bundles expose both GitHub Artifact archive coordinates and deterministic member roots",
|
|
356
|
+
"explicit web-delivery profiles independently verify codec, container, audio, dimensions, duration, byte budgets, rendition roles, and MP4 fast-start evidence",
|
|
357
|
+
"media receipts expose content-addressed rendition facts without requiring consumers to infer roles from filenames",
|
|
358
|
+
"Build Images owns encoding, Buildchain owns qualification and receipts, and site repositories own browser loading and accessibility behavior",
|
|
359
|
+
"media qualification does not claim browser playback, responsive layout, reduced-motion behavior, accessibility, or production deployment",
|
|
352
360
|
],
|
|
353
361
|
}),
|
|
354
362
|
surface(root, {
|
|
@@ -18,6 +18,7 @@ const DESCRIPTORS = Object.freeze([
|
|
|
18
18
|
[".github/workflows/.binary-release-assets.yml", "product-publication", true, ["github-release"], "github-token", "buildchain-release-assets"],
|
|
19
19
|
[".github/workflows/binary-distribution.yml", "evidence-publication"],
|
|
20
20
|
[".github/workflows/binary-release-assets.yml", "governance-write"],
|
|
21
|
+
[".github/workflows/artifact-signing-authority.yml", "evidence-publication"],
|
|
21
22
|
[".github/workflows/build-surface-fixture.yml", "non-publication-oidc"],
|
|
22
23
|
[".github/workflows/build.yml", "non-publication-oidc"],
|
|
23
24
|
[".github/workflows/buildchain-alpha-self-dogfood.yml", "non-publication-oidc"],
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
artifactSigningDigest,
|
|
5
|
+
createArtifactSigningReceipt,
|
|
6
|
+
validateArtifactSigningRequest,
|
|
7
|
+
} from "./artifact-signing.js";
|
|
8
|
+
import { artifactSigningEvidenceDigest } from "./artifact-signing-result.js";
|
|
9
|
+
|
|
10
|
+
export const DETACHED_ARTIFACT_SIGNATURE_CONTRACT =
|
|
11
|
+
"kungfu-buildchain-detached-artifact-signature/v1";
|
|
12
|
+
|
|
13
|
+
function required(value, label) {
|
|
14
|
+
const normalized = String(value || "").trim();
|
|
15
|
+
if (!normalized) throw new Error(`${label} must be a non-empty string`);
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function signatureBasis(request, keyId) {
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
contract: DETACHED_ARTIFACT_SIGNATURE_CONTRACT,
|
|
23
|
+
algorithm: "ed25519",
|
|
24
|
+
keyId: required(keyId, "key id"),
|
|
25
|
+
requestDigest: request.digest,
|
|
26
|
+
artifactDigest: request.artifact.digest,
|
|
27
|
+
runtimeSha: request.runtime.sha,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function envelopeDigest(envelope) {
|
|
32
|
+
const { digest: _digest, ...basis } = envelope;
|
|
33
|
+
return artifactSigningDigest(basis);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function signDetachedArtifactRequest({
|
|
37
|
+
request,
|
|
38
|
+
privateKey,
|
|
39
|
+
keyId,
|
|
40
|
+
authority = {},
|
|
41
|
+
} = {}) {
|
|
42
|
+
const check = validateArtifactSigningRequest(request);
|
|
43
|
+
if (!check.ok) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`invalid artifact signing request: ${check.issues.join(", ")}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (request.signature.profile !== "detached-signature-v1") {
|
|
49
|
+
throw new Error("detached signer requires detached-signature-v1 profile");
|
|
50
|
+
}
|
|
51
|
+
const basis = signatureBasis(request, keyId);
|
|
52
|
+
const payload = Buffer.from(JSON.stringify(basis), "utf8");
|
|
53
|
+
const signature = crypto.sign(null, payload, privateKey);
|
|
54
|
+
const envelope = {
|
|
55
|
+
...basis,
|
|
56
|
+
signature: signature.toString("base64"),
|
|
57
|
+
};
|
|
58
|
+
envelope.digest = envelopeDigest(envelope);
|
|
59
|
+
const receipt = createArtifactSigningReceipt({
|
|
60
|
+
request,
|
|
61
|
+
authority,
|
|
62
|
+
result: {
|
|
63
|
+
artifactDigest: request.artifact.digest,
|
|
64
|
+
evidenceDigest: artifactSigningEvidenceDigest([
|
|
65
|
+
{
|
|
66
|
+
kind: "ed25519-detached",
|
|
67
|
+
path: "signature.json",
|
|
68
|
+
digest: envelope.digest,
|
|
69
|
+
},
|
|
70
|
+
]),
|
|
71
|
+
},
|
|
72
|
+
signatures: [
|
|
73
|
+
{
|
|
74
|
+
kind: "ed25519-detached",
|
|
75
|
+
digest: `sha256:${crypto.createHash("sha256").update(signature).digest("hex")}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
return { envelope, receipt };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function verifyDetachedArtifactSignature({
|
|
83
|
+
request,
|
|
84
|
+
envelope,
|
|
85
|
+
publicKey,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const issues = [];
|
|
88
|
+
try {
|
|
89
|
+
const check = validateArtifactSigningRequest(request);
|
|
90
|
+
if (!check.ok) issues.push(...check.issues);
|
|
91
|
+
if (envelope?.contract !== DETACHED_ARTIFACT_SIGNATURE_CONTRACT) {
|
|
92
|
+
issues.push("detached signature contract mismatch");
|
|
93
|
+
}
|
|
94
|
+
const basis = signatureBasis(request, envelope?.keyId);
|
|
95
|
+
for (const [key, expected] of Object.entries(basis)) {
|
|
96
|
+
if (envelope?.[key] !== expected) {
|
|
97
|
+
issues.push(`detached signature ${key} mismatch`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const signature = Buffer.from(
|
|
101
|
+
required(envelope?.signature, "signature"),
|
|
102
|
+
"base64",
|
|
103
|
+
);
|
|
104
|
+
if (
|
|
105
|
+
!crypto.verify(
|
|
106
|
+
null,
|
|
107
|
+
Buffer.from(JSON.stringify(basis), "utf8"),
|
|
108
|
+
publicKey,
|
|
109
|
+
signature,
|
|
110
|
+
)
|
|
111
|
+
) {
|
|
112
|
+
issues.push("detached signature verification failed");
|
|
113
|
+
}
|
|
114
|
+
if (envelope?.digest !== envelopeDigest(envelope)) {
|
|
115
|
+
issues.push("detached signature envelope digest mismatch");
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
issues.push(String(error?.message || error));
|
|
119
|
+
}
|
|
120
|
+
return { ok: issues.length === 0, issues };
|
|
121
|
+
}
|
|
@@ -49,6 +49,7 @@ const PUBLIC_REPOSITORY_TARGETS = Object.freeze({
|
|
|
49
49
|
target("dev/v3/v3.0", [check("check")], false),
|
|
50
50
|
target("alpha/v3/v3.0", [check("check"), check("verify")], false),
|
|
51
51
|
target("release/v3/v3.0", [check("check")], true),
|
|
52
|
+
target("authority/v3/v3.0/artifact-signing", [check("check"), check("verify")], true),
|
|
52
53
|
target("publish-gate/major", [check("check")], true),
|
|
53
54
|
],
|
|
54
55
|
"homebrew-tap": [
|
|
@@ -65,6 +66,11 @@ const PUBLIC_REPOSITORY_TARGETS = Object.freeze({
|
|
|
65
66
|
check("signoff"),
|
|
66
67
|
check("validate"),
|
|
67
68
|
], true),
|
|
69
|
+
target("release/v4/v4.0", [
|
|
70
|
+
check("build", null),
|
|
71
|
+
check("signoff"),
|
|
72
|
+
check("validate"),
|
|
73
|
+
], true),
|
|
68
74
|
],
|
|
69
75
|
libnode: [
|
|
70
76
|
target("dev/v22/v22.22", [check("build")], true),
|