@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.
Files changed (54) hide show
  1. package/README.md +3 -2
  2. package/contracts/auditable-demo-media-profiles-v1.json +168 -0
  3. package/contracts/evidence/auditable-demo-web-delivery-v1.json +103 -0
  4. package/contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt +2 -0
  5. package/contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json +16 -0
  6. package/contracts/fixtures/auditable-demo-web-delivery-v1/scene.json +12 -0
  7. package/dist/site/buildchain-contract.json +44 -26
  8. package/dist/site/buildchain-site.json +42 -32
  9. package/dist/site/capability-registry.json +3 -3
  10. package/dist/site/controller-registry.json +11 -3
  11. package/dist/site/kfd-claims.json +74 -8
  12. package/dist/site/kfd-upstream-aggregate.json +1 -1
  13. package/dist/site/manual-registry.json +7 -7
  14. package/dist/site/node-api-registry.json +44 -5
  15. package/dist/site/page-registry.json +30 -20
  16. package/dist/site/public-surface-audit.json +38 -10
  17. package/dist/site/publication-authority-registry.json +26 -1
  18. package/dist/site/publication-registry.json +4 -4
  19. package/dist/site/release-provenance.json +3 -0
  20. package/dist/site/site-manifest.json +11 -11
  21. package/dist/site/workflow-registry.json +41 -7
  22. package/docs/MAP.md +2 -0
  23. package/docs/auditable-demo.md +55 -3
  24. package/docs/dev-alpha-candidate-patrol.md +9 -0
  25. package/docs/github-artifact-attestation.md +1 -1
  26. package/docs/release-governance.md +32 -0
  27. package/docs/reusable-build-surface.md +73 -58
  28. package/docs/runtime-train-validation.md +21 -0
  29. package/docs/versioning.md +1 -0
  30. package/package.json +5 -1
  31. package/packages/core/artifact-signing-result.js +228 -0
  32. package/packages/core/artifact-signing.js +412 -0
  33. package/packages/core/buildchain-config.js +58 -0
  34. package/packages/core/buildchain-contract.js +8 -0
  35. package/packages/core/buildchain-publication-authority.js +1 -0
  36. package/packages/core/detached-artifact-signature.js +121 -0
  37. package/packages/core/github-governance-authority.js +6 -0
  38. package/packages/core/index.js +27 -0
  39. package/scripts/auditable-demo.mjs +491 -22
  40. package/scripts/buildchain-channel-router.mjs +8 -2
  41. package/scripts/check-inventory.mjs +5 -0
  42. package/scripts/dev-alpha-candidate-patrol.mjs +18 -0
  43. package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
  44. package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
  45. package/scripts/generate-site-bundle.mjs +3 -0
  46. package/scripts/import-artifact-signing-results.mjs +76 -0
  47. package/scripts/inspect-artifact-signing-requests.mjs +101 -0
  48. package/scripts/materialize-artifact-signing-request.mjs +66 -0
  49. package/scripts/merge-artifact-signing-results.mjs +76 -0
  50. package/scripts/release-line-policy.mjs +27 -0
  51. package/scripts/runtime-ref-core.mjs +10 -6
  52. package/scripts/seal-artifact-signing-requests.mjs +368 -0
  53. package/scripts/sign-detached-artifact-requests.mjs +237 -0
  54. 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),