@xemahq/edition-tooling 0.0.0-stage → 0.1.0

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 (219) hide show
  1. package/LICENSE +175 -0
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +47 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/edition.d.ts +4 -0
  7. package/dist/commands/edition.d.ts.map +1 -0
  8. package/dist/commands/edition.js +58 -0
  9. package/dist/commands/edition.js.map +1 -0
  10. package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
  11. package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
  12. package/dist/lib/exit-codes.d.ts +6 -0
  13. package/dist/lib/exit-codes.d.ts.map +1 -0
  14. package/dist/lib/exit-codes.js +21 -0
  15. package/dist/lib/exit-codes.js.map +1 -0
  16. package/dist/runtime/edition/run-entry.d.ts +10 -0
  17. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  18. package/dist/runtime/edition/run-entry.js +49 -0
  19. package/dist/runtime/edition/run-entry.js.map +1 -0
  20. package/dist/runtime/edition/verb-map.d.ts +70 -0
  21. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  22. package/dist/runtime/edition/verb-map.js +166 -0
  23. package/dist/runtime/edition/verb-map.js.map +1 -0
  24. package/edition/README.md +291 -0
  25. package/edition/adapter-bundled-versions.mjs +128 -0
  26. package/edition/adapter-host-versions.mjs +76 -0
  27. package/edition/assemble-producer-input.mjs +1423 -0
  28. package/edition/assemble-third-party-input.mjs +348 -0
  29. package/edition/authored-source-check.mjs +141 -0
  30. package/edition/authority-binding.mjs +23 -0
  31. package/edition/authorization-base.mjs +357 -0
  32. package/edition/biome-content.mjs +327 -0
  33. package/edition/build-biome-index.mjs +180 -0
  34. package/edition/build-ledger-completeness.mjs +67 -0
  35. package/edition/build-ledger.mjs +107 -0
  36. package/edition/build-platform-service-index.mjs +292 -0
  37. package/edition/build-runtime-image-source-index.mjs +1197 -0
  38. package/edition/built-verifier.mjs +55 -0
  39. package/edition/candidate-edition.mjs +735 -0
  40. package/edition/canonical-evidence-json.mjs +82 -0
  41. package/edition/canonical-repository-url.mjs +61 -0
  42. package/edition/catalogue-edition.mjs +49 -0
  43. package/edition/catalogue-evidence.mjs +228 -0
  44. package/edition/catalogue-fetch.mjs +124 -0
  45. package/edition/catalogue-lock-field.mjs +41 -0
  46. package/edition/catalogue-pin.mjs +71 -0
  47. package/edition/catalogue.mjs +237 -0
  48. package/edition/chart-set-source-commit.mjs +97 -0
  49. package/edition/check-committed-image-evidence.mjs +412 -0
  50. package/edition/checkout-indexed-source-repositories.mjs +283 -0
  51. package/edition/collected-refusals.mjs +59 -0
  52. package/edition/comment-blanker.mjs +75 -0
  53. package/edition/connector-source-authority.mjs +60 -0
  54. package/edition/contract-authority.mjs +122 -0
  55. package/edition/cosign-certificate-constraints.mjs +50 -0
  56. package/edition/cross-repo-build-inputs.mjs +158 -0
  57. package/edition/customer-definition.mjs +72 -0
  58. package/edition/customer-edition-verbs.mjs +610 -0
  59. package/edition/customer-flags.mjs +15 -0
  60. package/edition/definition-checkout.mjs +96 -0
  61. package/edition/develop-build-record.mjs +122 -0
  62. package/edition/develop-catalogue-free-resolve.mjs +282 -0
  63. package/edition/develop-catalogue-free-vendor.mjs +118 -0
  64. package/edition/develop-channel-evidence.mjs +23 -0
  65. package/edition/develop-channel-record.mjs +49 -0
  66. package/edition/develop-channel-satellites.mjs +65 -0
  67. package/edition/develop-chart-roster.mjs +96 -0
  68. package/edition/develop-chart-set.mjs +147 -0
  69. package/edition/develop-current-resolution.mjs +29 -0
  70. package/edition/develop-edition-invariants.mjs +142 -0
  71. package/edition/develop-edition-resolve.mjs +250 -0
  72. package/edition/develop-edition.mjs +332 -0
  73. package/edition/develop-engine-image.mjs +97 -0
  74. package/edition/develop-freeze.mjs +304 -0
  75. package/edition/develop-git-bundles.mjs +296 -0
  76. package/edition/develop-graph.mjs +34 -0
  77. package/edition/develop-image-coordinate.mjs +85 -0
  78. package/edition/develop-image-probe.mjs +122 -0
  79. package/edition/develop-image-record.mjs +260 -0
  80. package/edition/develop-resolve-report.mjs +159 -0
  81. package/edition/develop-select-records.mjs +90 -0
  82. package/edition/develop-selection-line.mjs +29 -0
  83. package/edition/develop-selection-pins.mjs +36 -0
  84. package/edition/develop-snapshot-inputs.mjs +112 -0
  85. package/edition/develop-store-root.mjs +20 -0
  86. package/edition/dispatched-runtime-components.mjs +172 -0
  87. package/edition/dist-check.mjs +1174 -0
  88. package/edition/distribution-evidence-set.mjs +1064 -0
  89. package/edition/distribution-paths.mjs +191 -0
  90. package/edition/edition-artifact.mjs +529 -0
  91. package/edition/edition-biome-manifests.mjs +85 -0
  92. package/edition/edition-ceiling.mjs +182 -0
  93. package/edition/edition-channel.mjs +27 -0
  94. package/edition/edition-chart-set.mjs +104 -0
  95. package/edition/edition-config.mjs +374 -0
  96. package/edition/edition-constants.mjs +110 -0
  97. package/edition/edition-contract-parse.mjs +179 -0
  98. package/edition/edition-contract.json +100 -0
  99. package/edition/edition-image-seal.mjs +115 -0
  100. package/edition/edition-kind.mjs +10 -0
  101. package/edition/edition-list.mjs +35 -0
  102. package/edition/edition-locks.mjs +78 -0
  103. package/edition/edition-pin-verbs.mjs +183 -0
  104. package/edition/edition-pin.mjs +17 -0
  105. package/edition/edition-pinned-inputs.mjs +116 -0
  106. package/edition/edition-protocol.mjs +285 -0
  107. package/edition/edition-publisher.mjs +21 -0
  108. package/edition/edition-seal.mjs +257 -0
  109. package/edition/edition-source-pins.mjs +211 -0
  110. package/edition/edition-unpin.mjs +16 -0
  111. package/edition/edition-workspace.mjs +205 -0
  112. package/edition/evidence-checkout.mjs +439 -0
  113. package/edition/evidence-grammars.mjs +18 -0
  114. package/edition/evidence-input-mode.mjs +136 -0
  115. package/edition/evidence-trust-pin.mjs +392 -0
  116. package/edition/evidence-trust-policy.mjs +374 -0
  117. package/edition/external-source-walk.mjs +486 -0
  118. package/edition/first-party-source-ids.mjs +62 -0
  119. package/edition/fleet-coordinate-finality.mjs +363 -0
  120. package/edition/fulcio-certificate-extensions.mjs +278 -0
  121. package/edition/git-json.mjs +54 -0
  122. package/edition/git-source-auth.mjs +131 -0
  123. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  124. package/edition/image-evidence-replay-state.mjs +242 -0
  125. package/edition/image-evidence.mjs +1328 -0
  126. package/edition/image-lock-service-rows.mjs +26 -0
  127. package/edition/image-subject-identity.mjs +19 -0
  128. package/edition/images-from-record.mjs +216 -0
  129. package/edition/indexes-at-pins.mjs +13 -0
  130. package/edition/infra-images-lock.mjs +105 -0
  131. package/edition/kernel-capability-domain.mjs +30 -0
  132. package/edition/kernel-contract-authority.mjs +526 -0
  133. package/edition/kernel-contract-source-path-set.mjs +96 -0
  134. package/edition/kernel-package.mjs +237 -0
  135. package/edition/kernel-pin-source.mjs +63 -0
  136. package/edition/kernel-protocol-choice.mjs +239 -0
  137. package/edition/kernel-signer-kind.mjs +23 -0
  138. package/edition/lib.mjs +3322 -0
  139. package/edition/locked-bundle-signer.mjs +297 -0
  140. package/edition/map-with-concurrency.mjs +19 -0
  141. package/edition/no-follow-file.mjs +94 -0
  142. package/edition/oci-evidence-verbs.mjs +421 -0
  143. package/edition/oci-image-source-evidence.mjs +377 -0
  144. package/edition/operation-grant-admission.mjs +285 -0
  145. package/edition/operation-requirements.mjs +282 -0
  146. package/edition/oras-process.mjs +35 -0
  147. package/edition/oras.mjs +664 -0
  148. package/edition/pin-source.mjs +242 -0
  149. package/edition/pinned-cosign.mjs +86 -0
  150. package/edition/plan-image-evidence-refresh.mjs +428 -0
  151. package/edition/platform-lock-readability.mjs +903 -0
  152. package/edition/platform-service-class.mjs +85 -0
  153. package/edition/platform-service-ownership.mjs +91 -0
  154. package/edition/platform-service-source-authority.mjs +105 -0
  155. package/edition/positive-closure.mjs +162 -0
  156. package/edition/previous-image-lock.mjs +25 -0
  157. package/edition/produce-distribution-completeness-evidence.mjs +317 -0
  158. package/edition/produce-edition-catalogue-evidence.mjs +252 -0
  159. package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
  160. package/edition/produce-image-source-evidence.mjs +788 -0
  161. package/edition/producer-evidence-bundle.mjs +201 -0
  162. package/edition/producer-evidence-index-file.mjs +4 -0
  163. package/edition/record-refused.mjs +11 -0
  164. package/edition/registry-client-factory.mjs +21 -0
  165. package/edition/registry-credentials.mjs +225 -0
  166. package/edition/release-catalogue.mjs +48 -0
  167. package/edition/release-channel-guard.mjs +108 -0
  168. package/edition/release-edition-artifact.mjs +152 -0
  169. package/edition/release-edition.mjs +54 -0
  170. package/edition/remedy-command.mjs +89 -0
  171. package/edition/repin-evidence-trust.mjs +109 -0
  172. package/edition/required-resource-provider.mjs +58 -0
  173. package/edition/resolve-chart-set.mjs +376 -0
  174. package/edition/resolve-customer-edition.mjs +374 -0
  175. package/edition/resolve-distribution.mjs +2642 -0
  176. package/edition/resolve-edition.mjs +192 -0
  177. package/edition/resolve-images.mjs +2344 -0
  178. package/edition/resolve-infra-images.mjs +88 -0
  179. package/edition/seal-image-digests.mjs +954 -0
  180. package/edition/sealed-distribution-lock.mjs +22 -0
  181. package/edition/sealed-evidence-accept.mjs +82 -0
  182. package/edition/shallow-checkout.mjs +18 -0
  183. package/edition/sigstore-bundle-media-type.mjs +4 -0
  184. package/edition/source-authority.mjs +713 -0
  185. package/edition/source-closure.mjs +115 -0
  186. package/edition/store-publisher-trust.mjs +40 -0
  187. package/edition/strict-json.mjs +121 -0
  188. package/edition/string-order.mjs +5 -0
  189. package/edition/third-party-admission.mjs +118 -0
  190. package/edition/third-party-bundle-registry.mjs +349 -0
  191. package/edition/third-party-certificate-extensions.mjs +58 -0
  192. package/edition/third-party-image-pins.mjs +41 -0
  193. package/edition/third-party-pins-trusted.mjs +24 -0
  194. package/edition/third-party-producer-evidence.mjs +76 -0
  195. package/edition/third-party-source-check.mjs +28 -0
  196. package/edition/third-party-source-context.mjs +41 -0
  197. package/edition/third-party-source-evidence.mjs +326 -0
  198. package/edition/third-party-source-flags.mjs +148 -0
  199. package/edition/third-party-source-pins.mjs +270 -0
  200. package/edition/third-party-source-uniqueness.mjs +54 -0
  201. package/edition/third-party-source-walk.mjs +118 -0
  202. package/edition/third-party-source-writer.mjs +228 -0
  203. package/edition/third-party-subjects.mjs +128 -0
  204. package/edition/trust-policy-arming-verb.mjs +60 -0
  205. package/edition/trust-policy-arming.mjs +144 -0
  206. package/edition/trust-policy-image-ownership.mjs +85 -0
  207. package/edition/trusted-executable.mjs +166 -0
  208. package/edition/verifier-binding.mjs +45 -0
  209. package/edition/verifier-digest-pin.mjs +17 -0
  210. package/edition/verifier-files.mjs +8 -0
  211. package/edition/verifier-kernel-schemas.mjs +22 -0
  212. package/edition/verifier-provenance.mjs +64 -0
  213. package/edition/verify-catalogue-evidence.mjs +247 -0
  214. package/edition/verify-image-evidence.mjs +493 -0
  215. package/edition/verify-image-platforms.mjs +523 -0
  216. package/edition/why.mjs +56 -0
  217. package/edition/workspace-sections.mjs +287 -0
  218. package/package.json +83 -4
  219. package/README.md +0 -3
@@ -0,0 +1,349 @@
1
+ // The registry side of the third-party writer (XEMA-1611 design section 3 (a)): the biome bundle, by digest, and its
2
+ // SLSA provenance in the CLASSIC cosign format (`sha256-<hex>.sig` / `.att` tags). The provenance statement is the
3
+ // payload `cosign verify-attestation` verified; the certificate and tlog entry beside it are read from the `.att` tag
4
+ // and verified offline by cosign (`third-party-source-evidence.mjs`) before anything is committed.
5
+
6
+ import { execFileSync } from 'node:child_process';
7
+ import { X509Certificate } from 'node:crypto';
8
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ import { certificateConstraintArgs, certificateIdentityArgs } from './cosign-certificate-constraints.mjs';
13
+ import { COSIGN_ENV, classicCosignTrustMaterial, decodeExactUtf8, sha256Digest, validatePinnedTrustedRoot } from './image-evidence.mjs';
14
+ import { splitImageRef } from './registry-credentials.mjs';
15
+ import {
16
+ PROVENANCE_BUNDLE_MEDIA_TYPE,
17
+ SLSA_PROVENANCE_V1,
18
+ decodeJsonStatement,
19
+ digestOfOciRef,
20
+ parseProvenanceStatement,
21
+ } from './third-party-source-evidence.mjs';
22
+ import { assertPreparedExecutable } from './trusted-executable.mjs';
23
+
24
+ const BIOME_MANIFEST_FILE = 'xema-biome.json';
25
+ const BIOME_GENERATED_INDEX_FILE = 'xema-biome.index.json';
26
+ const OCI_TITLE_ANNOTATION = 'org.opencontainers.image.title';
27
+ const MAX_BIOME_MANIFEST_BYTES = 4 * 1024 * 1024;
28
+ const MAX_BUNDLE_BYTES = 16 * 1024 * 1024;
29
+ const REGISTRY_COSIGN_TIMEOUT_MS = 120_000;
30
+ /** What `cosign verify-attestation --type` calls the SLSA v1 provenance predicate. */
31
+ const COSIGN_SLSA_PROVENANCE_ALIAS = 'slsaprovenance1';
32
+ /** The classic cosign attestation tag layout (`cosign attest`): one DSSE envelope per layer, its material in annotations. */
33
+ const CLASSIC_DSSE_LAYER_MEDIA_TYPE = 'application/vnd.dsse.envelope.v1+json';
34
+ const CLASSIC_CERTIFICATE_ANNOTATION = 'dev.sigstore.cosign/certificate';
35
+ const CLASSIC_REKOR_BUNDLE_ANNOTATION = 'dev.sigstore.cosign/bundle';
36
+
37
+ const repositoryOf = (ociRef) => splitImageRef(ociRef.slice(0, ociRef.indexOf('@')));
38
+
39
+ /**
40
+ * The biome bundle manifest JSON (`xema-biome.json`) of an OCI artifact, fetched by digest.
41
+ *
42
+ * @returns {Promise<{manifest: object, bytes: Buffer}>}
43
+ */
44
+ export async function fetchBundleManifest({ clientFor, ociRef }) {
45
+ const bytes = await fetchTitledLayer({ clientFor, ociRef, title: BIOME_MANIFEST_FILE });
46
+ return { manifest: decodeJsonStatement(bytes, `${ociRef} ${BIOME_MANIFEST_FILE}`), bytes };
47
+ }
48
+
49
+ /**
50
+ * The committed generated index (`xema-biome.index.json`) of an OCI biome bundle, fetched by digest: the authority for
51
+ * the biomes the bundle requires, read from the same bundle bytes as its manifest, so it cannot describe another revision.
52
+ */
53
+ export async function fetchBundleGeneratedIndex({ clientFor, ociRef }) {
54
+ const bytes = await fetchTitledLayer({ clientFor, ociRef, title: BIOME_GENERATED_INDEX_FILE });
55
+ return decodeJsonStatement(bytes, `${ociRef} ${BIOME_GENERATED_INDEX_FILE}`);
56
+ }
57
+
58
+ /** The one layer of the bundle manifest at `ociRef` (by digest) annotated with `title`. */
59
+ async function fetchTitledLayer({ clientFor, ociRef, title }) {
60
+ const { authority, repository } = repositoryOf(ociRef);
61
+ const client = clientFor(authority);
62
+ const { document } = await client.getManifestByDigest(repository, digestOfOciRef(ociRef));
63
+ const layers = (document.layers ?? []).filter((layer) => layer.annotations?.[OCI_TITLE_ANNOTATION] === title);
64
+ if (layers.length !== 1) {
65
+ throw new Error(`${ociRef} has ${layers.length} layers titled ${title}; a biome bundle has exactly one`);
66
+ }
67
+ const bytes = await client.getBlob(repository, layers[0].digest, MAX_BIOME_MANIFEST_BYTES);
68
+ if (sha256Digest(bytes) !== layers[0].digest) {
69
+ throw new Error(`${ociRef} layer titled ${title} does not match its descriptor`);
70
+ }
71
+ return bytes;
72
+ }
73
+
74
+ /**
75
+ * The image names a biome manifest ships: its service and worker components with an `oci-image` artifact, named the way
76
+ * the index builder names them (a service by `protocol.serviceName`, a worker by its component `key`).
77
+ */
78
+ export function bundleImageNames(manifest) {
79
+ const components = Array.isArray(manifest?.xema?.components) ? manifest.xema.components : [];
80
+ return new Set(
81
+ components
82
+ .filter((component) => (component?.kind === 'service' || component?.kind === 'worker') && component.artifact?.kind === 'oci-image')
83
+ .map((component) => (component.kind === 'service' ? component.protocol?.serviceName : component.key)),
84
+ );
85
+ }
86
+
87
+ const IN_TOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json';
88
+ const STRICT_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
89
+
90
+ /**
91
+ * The SLSA provenance of the bundle, taken from what `cosign verify-attestation` VERIFIED: cosign prints each verified
92
+ * DSSE envelope on stdout, one JSON object per line. Exactly one envelope is required; it must be an in-toto DSSE
93
+ * envelope with one signature, its payload an in-toto statement of `https://slsa.dev/provenance/v1` over exactly one
94
+ * subject, the bundle digest. Nothing is read from the registry here: the statement is the payload cosign verified.
95
+ *
96
+ * @returns {{statementBytes: Buffer, envelope: {payloadType: string, payload: string, signatures: object[]}}}
97
+ */
98
+ export function verifiedProvenanceOf(output, ociRef) {
99
+ const lines = String(output).split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
100
+ if (lines.length !== 1) {
101
+ throw new Error(
102
+ `cosign verify-attestation of ${ociRef} printed ${lines.length} verified attestation envelopes; exactly one SLSA provenance (${SLSA_PROVENANCE_V1}) attestation is required, so the evidence is unambiguous`,
103
+ );
104
+ }
105
+ let envelope;
106
+ try {
107
+ envelope = JSON.parse(lines[0]);
108
+ } catch {
109
+ throw new Error(`cosign verify-attestation of ${ociRef} printed a verified attestation that is not a JSON DSSE envelope`);
110
+ }
111
+ if (
112
+ envelope === null ||
113
+ typeof envelope !== 'object' ||
114
+ envelope.payloadType !== IN_TOTO_PAYLOAD_TYPE ||
115
+ typeof envelope.payload !== 'string' ||
116
+ !STRICT_BASE64.test(envelope.payload) ||
117
+ !Array.isArray(envelope.signatures) ||
118
+ envelope.signatures.length !== 1 ||
119
+ typeof envelope.signatures[0]?.sig !== 'string'
120
+ ) {
121
+ throw new Error(`cosign verify-attestation of ${ociRef} printed a verified attestation that is not an in-toto DSSE envelope with exactly one signature`);
122
+ }
123
+ const statementBytes = Buffer.from(envelope.payload, 'base64');
124
+ let statement;
125
+ try {
126
+ statement = parseProvenanceStatement(decodeJsonStatement(statementBytes, `${ociRef} verified attestation payload`));
127
+ } catch (error) {
128
+ throw new Error(`cosign verify-attestation of ${ociRef}: ${error.message}`);
129
+ }
130
+ const attested = `sha256:${statement.subject[0].digest.sha256}`;
131
+ if (attested !== digestOfOciRef(ociRef)) {
132
+ throw new Error(`the verified SLSA provenance of ${ociRef} attests ${attested}, not the bundle digest ${digestOfOciRef(ociRef)}`);
133
+ }
134
+ return { statementBytes, envelope: { payloadType: envelope.payloadType, payload: envelope.payload, signatures: envelope.signatures } };
135
+ }
136
+
137
+ const sameSignatures = (left, right) =>
138
+ Array.isArray(left) && left.length === right.length && left.every((signature, index) => signature?.sig === right[index].sig);
139
+
140
+ /**
141
+ * The committed, offline-verifiable form of the VERIFIED provenance envelope: a Sigstore bundle v0.1
142
+ * (`PROVENANCE_BUNDLE_MEDIA_TYPE`) built from the classic `sha256-<hex>.att` layer that carries exactly that envelope,
143
+ * its Fulcio certificate (`dev.sigstore.cosign/certificate`) and its Rekor v1 entry with the signed entry timestamp
144
+ * (`dev.sigstore.cosign/bundle`). v0.1 because a classic entry carries an inclusion promise, not an inclusion proof.
145
+ * The envelope is cosign's verified one, never the registry's; the certificate and the tlog entry are unauthenticated
146
+ * here and are proven by the offline `verify-blob-attestation` the writer runs on the staged evidence before commit.
147
+ */
148
+ export async function fetchClassicProvenanceBundle({ clientFor, ociRef, envelope }) {
149
+ const { authority, repository } = repositoryOf(ociRef);
150
+ const client = clientFor(authority);
151
+ const tag = `${digestOfOciRef(ociRef).replace(':', '-')}.att`;
152
+ const { document } = await client.getManifestByTag(repository, tag);
153
+ const matching = [];
154
+ for (const layer of document.layers ?? []) {
155
+ if (layer.mediaType !== CLASSIC_DSSE_LAYER_MEDIA_TYPE) {continue;}
156
+ const bytes = await client.getBlob(repository, layer.digest, MAX_BUNDLE_BYTES);
157
+ if (sha256Digest(bytes) !== layer.digest) {
158
+ throw new Error(`${ociRef} attestation layer ${layer.digest} does not match its descriptor`);
159
+ }
160
+ let candidate;
161
+ try {
162
+ candidate = JSON.parse(decodeExactUtf8(bytes, 'classic attestation layer'));
163
+ } catch {
164
+ continue; // not a DSSE envelope: never the verified one
165
+ }
166
+ if (candidate?.payload === envelope.payload && sameSignatures(candidate.signatures, envelope.signatures)) {
167
+ matching.push(layer);
168
+ }
169
+ }
170
+ if (matching.length !== 1) {
171
+ throw new Error(`${ociRef} tag ${tag} carries ${matching.length} attestation layers holding the verified provenance envelope; exactly one is required`);
172
+ }
173
+ const annotations = matching[0].annotations ?? {};
174
+ const certificatePem = annotations[CLASSIC_CERTIFICATE_ANNOTATION];
175
+ const rekorBundleText = annotations[CLASSIC_REKOR_BUNDLE_ANNOTATION];
176
+ if (typeof certificatePem !== 'string') {
177
+ throw new Error(`${ociRef} attestation layer ${matching[0].digest} carries no Fulcio certificate (${CLASSIC_CERTIFICATE_ANNOTATION})`);
178
+ }
179
+ if (typeof rekorBundleText !== 'string') {
180
+ throw new Error(`${ociRef} attestation layer ${matching[0].digest} carries no transparency log entry (${CLASSIC_REKOR_BUNDLE_ANNOTATION})`);
181
+ }
182
+ let certificateDer;
183
+ try {
184
+ certificateDer = new X509Certificate(certificatePem).raw;
185
+ } catch (error) {
186
+ throw new Error(`${ociRef} attestation layer ${matching[0].digest} certificate is not a PEM X.509 certificate: ${error.message}`);
187
+ }
188
+ let rekor;
189
+ let body;
190
+ try {
191
+ rekor = JSON.parse(rekorBundleText);
192
+ body = JSON.parse(Buffer.from(rekor.Payload.body, 'base64').toString('utf8'));
193
+ } catch {
194
+ throw new Error(`${ociRef} attestation layer ${matching[0].digest} transparency log entry is not a cosign Rekor bundle`);
195
+ }
196
+ const { Payload: payload, SignedEntryTimestamp: signedEntryTimestamp } = rekor;
197
+ if (
198
+ typeof signedEntryTimestamp !== 'string' ||
199
+ !Number.isSafeInteger(payload.logIndex) ||
200
+ !Number.isSafeInteger(payload.integratedTime) ||
201
+ typeof payload.logID !== 'string' ||
202
+ !/^[0-9a-f]{64}$/u.test(payload.logID) ||
203
+ typeof body?.kind !== 'string' ||
204
+ typeof body?.apiVersion !== 'string'
205
+ ) {
206
+ throw new Error(`${ociRef} attestation layer ${matching[0].digest} transparency log entry is not a cosign Rekor bundle`);
207
+ }
208
+ const bundle = {
209
+ mediaType: PROVENANCE_BUNDLE_MEDIA_TYPE,
210
+ verificationMaterial: {
211
+ x509CertificateChain: { certificates: [{ rawBytes: certificateDer.toString('base64') }] },
212
+ tlogEntries: [
213
+ {
214
+ logIndex: String(payload.logIndex),
215
+ logId: { keyId: Buffer.from(payload.logID, 'hex').toString('base64') },
216
+ kindVersion: { kind: body.kind, version: body.apiVersion },
217
+ integratedTime: String(payload.integratedTime),
218
+ inclusionPromise: { signedEntryTimestamp },
219
+ canonicalizedBody: payload.body,
220
+ },
221
+ ],
222
+ },
223
+ dsseEnvelope: envelope,
224
+ };
225
+ return Buffer.from(JSON.stringify(bundle), 'utf8');
226
+ }
227
+
228
+ /**
229
+ * The `<repository>@sha256:<hex>` a bundle reference names: a digest reference as it is, a tag reference resolved at
230
+ * the registry (the manifest the tag serves, its bytes verified against the digest the registry described).
231
+ */
232
+ export async function bundleDigestRef({ clientFor, ociRef }) {
233
+ if (ociRef.includes('@')) {return ociRef;}
234
+ const colon = ociRef.lastIndexOf(':');
235
+ if (colon <= ociRef.lastIndexOf('/')) {
236
+ throw new Error(`bundle reference ${JSON.stringify(ociRef)} names neither a tag nor a digest`);
237
+ }
238
+ const name = ociRef.slice(0, colon);
239
+ const { authority, repository } = splitImageRef(name);
240
+ const { digest } = await clientFor(authority).getManifestByTag(repository, ociRef.slice(colon + 1));
241
+ return `${name}@${digest}`;
242
+ }
243
+
244
+ const tail = (error) => String(error?.stderr ?? error?.message ?? error).trim().split('\n').slice(-4).join(' | ').slice(0, 1024);
245
+
246
+ /**
247
+ * The cosign run of a registry verification: the trusted binary under a CLEAN environment (`COSIGN_ENV` and the
248
+ * operator's registry credential, HOME / DOCKER_CONFIG; no `SIGSTORE_*`, `COSIGN_*` or `TUF_*` of the operator is ever
249
+ * inherited). `run` takes `--offline --trusted-root <pinned>`; `runClassic` instead pins cosign's classic trust inputs
250
+ * to the keys derived from the same pinned root (`classicCosignTrustMaterial`), in a private directory removed after the
251
+ * run, with an empty private TUF_ROOT so no live TUF material is ever read.
252
+ */
253
+ function registryCosign({ trustedCosign, policy, policyRoot, row, ociRef, env }) {
254
+ const root = validatePinnedTrustedRoot(policyRoot, policy);
255
+ const environment = {
256
+ ...COSIGN_ENV,
257
+ ...(env.HOME === undefined ? {} : { HOME: env.HOME }),
258
+ ...(env.DOCKER_CONFIG === undefined ? {} : { DOCKER_CONFIG: env.DOCKER_CONFIG }),
259
+ };
260
+ const exec = (command, args, runEnv) => {
261
+ try {
262
+ return execFileSync(assertPreparedExecutable(trustedCosign, 'cosign'), [command, ...args], {
263
+ env: runEnv,
264
+ encoding: 'utf8',
265
+ stdio: ['ignore', 'pipe', 'pipe'],
266
+ timeout: REGISTRY_COSIGN_TIMEOUT_MS,
267
+ });
268
+ } catch (error) {
269
+ throw new Error(`cosign ${command} refused ${ociRef} under row "${row.policyId}": ${tail(error)}`);
270
+ }
271
+ };
272
+ return {
273
+ trustArgs: ['--offline', '--trusted-root', root.path],
274
+ run: (command, args) => exec(command, args, environment),
275
+ runClassic(command, args) {
276
+ const material = classicCosignTrustMaterial(root.bytes);
277
+ const dir = mkdtempSync(join(tmpdir(), 'xema-cosign-trust-'));
278
+ try {
279
+ const file = (name, content) => {
280
+ const path = join(dir, name);
281
+ writeFileSync(path, content, { mode: 0o600, flag: 'wx' });
282
+ return path;
283
+ };
284
+ const tufRoot = join(dir, 'tuf');
285
+ mkdirSync(tufRoot, { mode: 0o700 });
286
+ return exec(command, ['--offline', ...args], {
287
+ ...environment,
288
+ SIGSTORE_ROOT_FILE: file('fulcio.pem', material.fulcioPem),
289
+ SIGSTORE_REKOR_PUBLIC_KEY: file('rekor.pub', material.rekorPem),
290
+ SIGSTORE_CT_LOG_PUBLIC_KEY_FILE: file('ctlog.pub', material.ctLogPem),
291
+ TUF_ROOT: tufRoot,
292
+ });
293
+ } finally {
294
+ rmSync(dir, { recursive: true, force: true });
295
+ }
296
+ },
297
+ };
298
+ }
299
+
300
+ /**
301
+ * (a) against the registry: keyless `cosign verify` and `cosign verify-attestation --type slsaprovenance1` of the bundle
302
+ * artifact under EXACTLY the signer row's identity and issuer, the row's workflow repository, ref and the stated
303
+ * commit, and the trusted cosign binary, both `--offline`: the tlog entry carried with the signature is verified, the
304
+ * tlog is never ignored. The registry credential is the operator's (HOME / DOCKER_CONFIG).
305
+ *
306
+ * Publishers write the CLASSIC cosign format (`sha256-<d>.sig` / `.att` tags; `cosign sign` / `cosign attest`), not
307
+ * referrer Sigstore bundles. `verify` takes the pinned trusted root as is (`--trusted-root`, classic or bundle form).
308
+ * cosign 2.6 `verify-attestation` takes `--trusted-root` only with `--new-bundle-format`, which finds no classic
309
+ * attestation, so it runs without either, against the Fulcio, Rekor v1 and CT keys derived from the SAME pinned root
310
+ * and passed in cosign's classic trust variables (`registryCosign().runClassic`). A publisher switching to referrer
311
+ * bundles is refused loudly (cosign then requires a trusted root), never verified against anything unpinned.
312
+ *
313
+ * @returns {{provenance: {statementBytes: Buffer, envelope: object}}} the provenance cosign verified
314
+ * (`verifiedProvenanceOf`): signature first, then the attestation, then the statement from cosign's own output.
315
+ */
316
+ export function verifyBundleOnRegistry({ trustedCosign, policy, policyRoot, row, workflowSha, ociRef, env = process.env }) {
317
+ const cosign = registryCosign({ trustedCosign, policy, policyRoot, row, ociRef, env });
318
+ const claims = certificateConstraintArgs(row, workflowSha);
319
+ cosign.run('verify', [...cosign.trustArgs, ...claims, ociRef]);
320
+ const output = cosign.runClassic('verify-attestation', ['--type', COSIGN_SLSA_PROVENANCE_ALIAS, ...claims, ociRef]);
321
+ return { provenance: verifiedProvenanceOf(output, ociRef) };
322
+ }
323
+
324
+ const WORKFLOW_SHA = /^[0-9a-f]{40}$/u;
325
+
326
+ /**
327
+ * The commit the row's workflow signed the bundle artifact at, as cosign states it: `cosign verify` under the row's
328
+ * identity, issuer, workflow repository and ref (no commit: it is what is asked), and the `githubWorkflowSha` claim of
329
+ * every signature cosign verified. Exactly one commit is required; none, or two, is refused. The caller then holds the
330
+ * signature AND the provenance to that commit with `verifyBundleOnRegistry`.
331
+ */
332
+ export function verifiedBundleSignerCommit({ trustedCosign, policy, policyRoot, row, ociRef, env = process.env }) {
333
+ const cosign = registryCosign({ trustedCosign, policy, policyRoot, row, ociRef, env });
334
+ const output = cosign.run('verify', [...cosign.trustArgs, ...certificateIdentityArgs(row), '--output', 'json', ociRef]);
335
+ let verified;
336
+ try {
337
+ verified = JSON.parse(output);
338
+ } catch {
339
+ throw new Error(`cosign verify of ${ociRef} under row "${row.policyId}" printed no JSON list of verified signatures`);
340
+ }
341
+ const commits = [...new Set((Array.isArray(verified) ? verified : []).map((signature) => signature?.optional?.githubWorkflowSha))];
342
+ if (commits.length !== 1 || !WORKFLOW_SHA.test(String(commits[0]))) {
343
+ throw new Error(
344
+ `${ociRef} carries signatures by row "${row.policyId}" (${row.certificateIdentity}) stating ${commits.length} workflow commits ` +
345
+ `(${commits.map(String).join(', ')}); exactly one 40-hex commit is required`,
346
+ );
347
+ }
348
+ return commits[0];
349
+ }
@@ -0,0 +1,58 @@
1
+ // (d) of the third-party checks: the Fulcio extensions of a bundle's leaf certificate, held to a source pin. Its own module
2
+ // (it imports no evidence module) because admission in `image-evidence.mjs` and the pin verifier both use it.
3
+ //
4
+ // The runner rule (XEMA-2661) is ONE rule: the certificate's Runner Environment must equal the runner environment the pin
5
+ // declares, and a pin that declares none declares `github-hosted`. So zero configuration refuses every self-hosted
6
+ // signature, and a declaration only ever names the one runner kind its signer uses.
7
+ //
8
+ // Fulcio has no runner-label extension: a certificate cannot say WHICH self-hosted runner signed, and no code here reads
9
+ // the workflow's `runs-on`. What this module proves for a self-hosted pin is only that the declared workflow (the trust
10
+ // row's exact `<repository>/<workflow>@refs/heads/main` identity, at the pinned commit) ran AS the top-level workflow:
11
+ // Build Signer URI (.9) and Build Config URI (.18) both equal that identity, so a reusable signing workflow called from
12
+ // another workflow, which could hand it its `runs-on`, is refused. That the workflow runs on a dedicated signing runner
13
+ // (`runs-on: signing`) is a HUMAN check: whoever writes the pin with `--runner-environment self-hosted` must read that
14
+ // workflow at the pinned commit and verify it before running `repin-evidence-trust`.
15
+
16
+ import {
17
+ FulcioBuildOid,
18
+ FulcioExtensionOid,
19
+ RunnerEnvironment,
20
+ readFulcioBuildIdentity,
21
+ readFulcioExtensionsFromBundle,
22
+ } from './fulcio-certificate-extensions.mjs';
23
+
24
+ /** The runner environment a pin holds its certificates to: its declared `runnerEnvironment`, else `github-hosted`. */
25
+ export const declaredRunnerEnvironment = (source) => source.runnerEnvironment ?? RunnerEnvironment.GithubHosted;
26
+
27
+ /**
28
+ * The leaf certificate of a parsed Sigstore `bundle` states the pinned repository id, owner id and the runner environment
29
+ * the pin declares (`declaredRunnerEnvironment`). When that is not `github-hosted`, its Build Signer URI and Build Config
30
+ * URI must both be `signerIdentity`, the exact certificate identity of the trust row the evidence verified under.
31
+ */
32
+ export function assertCertificateExtensions({ bundle, source, signerIdentity, label }) {
33
+ if (typeof signerIdentity !== 'string' || signerIdentity.length === 0) {
34
+ throw new Error(`${label}: no signer identity to hold the certificate to`);
35
+ }
36
+ const read = readFulcioExtensionsFromBundle(bundle);
37
+ const runnerEnvironment = declaredRunnerEnvironment(source);
38
+ const runnerPinned = source.runnerEnvironment === undefined ? `runnerEnvironment undeclared, so ${RunnerEnvironment.GithubHosted}` : 'runnerEnvironment';
39
+ const expectations = [
40
+ [FulcioExtensionOid.SourceRepositoryIdentifier, 'Source Repository Identifier', read.sourceRepositoryIdentifier, source.repositoryId, 'repositoryId'],
41
+ [FulcioExtensionOid.SourceRepositoryOwnerIdentifier, 'Source Repository Owner Identifier', read.sourceRepositoryOwnerIdentifier, source.repositoryOwnerId, 'repositoryOwnerId'],
42
+ [FulcioExtensionOid.RunnerEnvironment, 'Runner Environment', read.runnerEnvironment, runnerEnvironment, runnerPinned],
43
+ ];
44
+ if (runnerEnvironment !== RunnerEnvironment.GithubHosted) {
45
+ const build = readFulcioBuildIdentity(bundle);
46
+ expectations.push(
47
+ [FulcioBuildOid.BuildSignerUri, 'Build Signer URI', build.buildSignerUri, signerIdentity, 'the declared signer workflow'],
48
+ [FulcioBuildOid.BuildConfigUri, 'Build Config URI', build.buildConfigUri, signerIdentity, 'the declared signer workflow, as the top-level workflow'],
49
+ );
50
+ }
51
+ for (const [oid, name, actual, expected, pinned] of expectations) {
52
+ if (actual !== expected) {
53
+ throw new Error(
54
+ `${label} certificate ${name} (${oid}) is ${JSON.stringify(actual)}, but the pin requires ${JSON.stringify(expected)} (${pinned})`,
55
+ );
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,41 @@
1
+ // The pinned images of the third-party subjects of an images lock (XEMA-1611 design sections 3 and 4).
2
+ //
3
+ // A third-party subject's `contentHash` is the PUBLISHER'S `sourceTreeHash` (publisher-attested): it is never a registry
4
+ // tag. Every seal, coverage and evidence-set path that serves such a subject goes to the pin instead: the image is read
5
+ // BY the pin's `manifestDigest` and its evidence is the committed evidence the pin's `statementDigest` and `bundleDigest`
6
+ // bind. This module is the one place a subject is joined to its pinned image.
7
+
8
+ /** The `kind:id` key of an image subject, as the seal reports and the digest plans spell it. */
9
+ export const imageSubjectKey = (subject) => `${subject.kind}:${subject.id}`;
10
+
11
+ /**
12
+ * The pinned image of every third-party subject of `subjects`, by `kind:id`. A subject is third-party when its `repo` is a
13
+ * pinned third-party repository. Refuses a third-party subject the pin pins no image for, and one whose `imageRef` or
14
+ * `contentHash` is not the pin image's `imageRef` and `sourceTreeHash`.
15
+ *
16
+ * @param {{pins: {byRepository: (repository: string) => object|undefined}, subjects: readonly object[]}} request
17
+ * @returns {Map<string, object>}
18
+ */
19
+ export function thirdPartyImagePins({ pins, subjects }) {
20
+ const pinned = new Map();
21
+ for (const subject of subjects) {
22
+ const pin = pins.byRepository(subject.repo);
23
+ if (pin === undefined) {continue;}
24
+ const key = imageSubjectKey(subject);
25
+ const image = pin.images.find((candidate) => candidate.subjectKind === subject.kind && candidate.subjectId === subject.id);
26
+ if (image === undefined) {
27
+ throw new Error(`${key} is a subject of the third-party repository "${subject.repo}" (source "${pin.name}"), but the pin pins no image for ${key}`);
28
+ }
29
+ if (subject.imageRef !== image.imageRef) {
30
+ throw new Error(`${key} (third-party source "${pin.name}") has imageRef ${subject.imageRef}, but the pin pins ${image.imageRef}`);
31
+ }
32
+ if (subject.contentHash !== image.sourceTreeHash) {
33
+ throw new Error(`${key} (third-party source "${pin.name}") has contentHash ${subject.contentHash}, but the pin's publisher-attested sourceTreeHash is ${image.sourceTreeHash}`);
34
+ }
35
+ pinned.set(key, image);
36
+ }
37
+ return pinned;
38
+ }
39
+
40
+ /** The `kind:id` -> manifestDigest map the platform verifier reads pinned subjects by. */
41
+ export const pinnedManifestDigests = (pinnedImages) => new Map([...pinnedImages].map(([key, image]) => [key, image.manifestDigest]));
@@ -0,0 +1,24 @@
1
+ // The data root's third-party source pins, read by a consumer that does not run the full evidence verification (the
2
+ // develop and release seals): held to the evidence trust pin's record of the pin file's digest exactly as the verifiers
3
+ // hold it (`assertPinnedThirdPartySourcePins`), so a pin file changed after the trust pin was last taken is refused.
4
+
5
+ import { editionWorkspaceConfig } from './edition-config.mjs';
6
+ import { assertPinnedThirdPartySourcePins, readEvidenceTrustPin } from './evidence-trust-pin.mjs';
7
+ import { THIRD_PARTY_SOURCE_PINS_FILE, loadThirdPartySourcePins, thirdPartySourcePinsDigest } from './third-party-source-pins.mjs';
8
+
9
+ /**
10
+ * @param {string} dataRoot
11
+ * @returns the loaded third-party source pins, or an empty set when the data root has none and declares no trust pin
12
+ */
13
+ export function loadTrustedThirdPartySourcePins(dataRoot) {
14
+ const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
15
+ if (declaration.policy === null) {
16
+ const digest = thirdPartySourcePinsDigest(dataRoot);
17
+ if (digest !== null) {
18
+ throw new Error(`the data root holds ${THIRD_PARTY_SOURCE_PINS_FILE} (${digest}) but declares no evidenceTrustPolicy, so there is no trust pin to hold it to`);
19
+ }
20
+ return loadThirdPartySourcePins(dataRoot);
21
+ }
22
+ assertPinnedThirdPartySourcePins({ pin: readEvidenceTrustPin({ dataRoot, declaration }), dataRoot });
23
+ return loadThirdPartySourcePins(dataRoot);
24
+ }
@@ -0,0 +1,76 @@
1
+ // The producer record of one third-party image subject in an evidence set (XEMA-1611 design section 4).
2
+ //
3
+ // A first-party subject's evidence is read from the registry (the OCI referrer of `<imageRef>:<contentHash>`). A
4
+ // third-party subject has no such tag: its `contentHash` is the publisher's attested `sourceTreeHash`. Its evidence is
5
+ // the COMMITTED evidence of the data root (`evidence/sha256-<manifest hex>/producer-evidence.json` + statements/ +
6
+ // bundles/) that its pin binds by `statementDigest` and `bundleDigest`. This reads that evidence, holds it to the pin
7
+ // and to the subject, and returns the SAME record shape the first-party branch of `resolveProducerEvidence` builds.
8
+ // It makes no registry call.
9
+
10
+ import { canonicalJson, decodeExactUtf8, evidenceDirectoryForDigest, sha256Digest } from './image-evidence.mjs';
11
+ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
12
+ import { validateProducedBundle } from './produce-image-source-evidence.mjs';
13
+ import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
14
+ import { committedImageDescriptor } from './third-party-source-evidence.mjs';
15
+
16
+ /** The bound committed evidence files are read at (the same as the first-party committed reads). */
17
+ const MAX_EVIDENCE_FILE_BYTES = 4 * 1024 * 1024;
18
+
19
+ /**
20
+ * @param {object} request
21
+ * @param {object} request.subject the third-party image subject (sealed or not)
22
+ * @param {object} request.pinImage its image in the third-party source pin
23
+ * @param {string} request.dataRoot the data root holding the committed evidence
24
+ * @param {object} request.schemas trusted kernel distribution schemas
25
+ * @param {object} request.expectedSource the canonical `predicate.source` the statement must attest
26
+ * @param {string} request.stem the producer's staging stem
27
+ */
28
+ export function thirdPartyProducerRecord({ subject, pinImage, dataRoot, schemas, expectedSource, stem }) {
29
+ const key = `${subject.kind}:${subject.id}`;
30
+ const label = `third-party ${key} committed evidence`;
31
+ const descriptor = committedImageDescriptor({ image: pinImage, evidenceRoot: dataRoot, label });
32
+ const statementBytes = readBoundedNoFollowFile(dataRoot, descriptor.statementPath, `${label} statement`, MAX_EVIDENCE_FILE_BYTES).bytes;
33
+ const bundleBytes = readBoundedNoFollowFile(dataRoot, descriptor.bundlePath, `${label} bundle`, MAX_EVIDENCE_FILE_BYTES).bytes;
34
+ if (sha256Digest(statementBytes) !== pinImage.statementDigest) {
35
+ throw new Error(`${label}: the committed statement ${descriptor.statementPath} is ${sha256Digest(statementBytes)}, not the pin's statementDigest ${pinImage.statementDigest}`);
36
+ }
37
+ if (sha256Digest(bundleBytes) !== pinImage.bundleDigest) {
38
+ throw new Error(`${label}: the committed bundle ${descriptor.bundlePath} is ${sha256Digest(bundleBytes)}, not the pin's bundleDigest ${pinImage.bundleDigest}`);
39
+ }
40
+ validateProducedBundle(statementBytes, bundleBytes);
41
+ const statement = schemas.parseImageProducerStatement(JSON.parse(decodeExactUtf8(statementBytes, 'committed statement')));
42
+ if (canonicalJson(statement.predicate.source) !== canonicalJson(expectedSource)) {
43
+ throw new Error(`${label}: the committed statement attests another source than the one ${key} is locked at (${subject.repo}@${subject.sourceCommit}, publisher-attested source tree ${subject.contentHash})`);
44
+ }
45
+ const { artifact } = statement.predicate;
46
+ if (artifact.imageRepository !== subject.imageRef || statement.subject[0].name !== subject.imageRef) {
47
+ throw new Error(`${label}: it attests image "${artifact.imageRepository}" (statement subject "${statement.subject[0].name}"), not the locked "${subject.imageRef}"`);
48
+ }
49
+ if (`sha256:${statement.subject[0].digest.sha256}` !== artifact.manifestDigest) {
50
+ throw new Error(`${label}: its statement subject digest sha256:${statement.subject[0].digest.sha256} is not its artifact.manifestDigest ${artifact.manifestDigest}`);
51
+ }
52
+ if (artifact.manifestDigest !== pinImage.manifestDigest) {
53
+ throw new Error(`${label}: the committed statement attests manifest ${artifact.manifestDigest}, not the pin's manifestDigest ${pinImage.manifestDigest}`);
54
+ }
55
+ if (subject.imageDigest !== undefined && subject.imageDigest !== artifact.manifestDigest) {
56
+ throw new Error(`${label}: ${key} is sealed at ${subject.imageDigest}, but the committed statement attests manifest ${artifact.manifestDigest}`);
57
+ }
58
+ return {
59
+ stem,
60
+ subjectKind: subject.kind,
61
+ subjectId: subject.id,
62
+ imageRepository: subject.imageRef,
63
+ manifestDigest: artifact.manifestDigest,
64
+ statement,
65
+ sourceDirectory: evidenceDirectoryForDigest(pinImage.manifestDigest),
66
+ statementBytes,
67
+ bundleBytes,
68
+ evidence: {
69
+ statementPath: `producers/${stem}/statement.json`,
70
+ statementDigest: sha256Digest(statementBytes),
71
+ bundlePath: `producers/${stem}/bundle.sigstore.json`,
72
+ bundleDigest: sha256Digest(bundleBytes),
73
+ bundleMediaType: BUNDLE_MEDIA_TYPE,
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,28 @@
1
+ // `xema-edition index runtime-image-source --check-third-party` (XEMA-1611 design section 3, `--check`): re-run checks
2
+ // (a) to (e) for every pin of `third-party-source-pins.json` from the COMMITTED evidence, offline. A data root with no
3
+ // pin file has no third-party source and passes trivially (zero-config); a pin file that is present but wrong throws.
4
+
5
+ import { loadThirdPartySourceContext } from './third-party-source-context.mjs';
6
+ import { verifyThirdPartySourceEvidence } from './third-party-source-evidence.mjs';
7
+ import { loadThirdPartySourcePins } from './third-party-source-pins.mjs';
8
+ import { assertThirdPartyUniqueness } from './third-party-source-uniqueness.mjs';
9
+
10
+ /**
11
+ * @returns {Promise<{checked: number, absence: string|null}>}
12
+ */
13
+ export async function checkThirdPartySourcePins({ dataRoot, schemas, cosignBinary, channel }) {
14
+ const pins = loadThirdPartySourcePins(dataRoot);
15
+ if (pins.sources.length === 0) {
16
+ return { checked: 0, absence: pins.absence };
17
+ }
18
+ const context = loadThirdPartySourceContext({ dataRoot, schemas, cosignBinary });
19
+ try {
20
+ assertThirdPartyUniqueness({ sources: pins.sources, ...context });
21
+ for (const source of pins.sources) {
22
+ verifyThirdPartySourceEvidence({ source, evidenceRoot: dataRoot, context, channel });
23
+ }
24
+ } finally {
25
+ context.dispose();
26
+ }
27
+ return { checked: pins.sources.length, absence: null };
28
+ }
@@ -0,0 +1,41 @@
1
+ // What the third-party writer and `--check-third-party` verify a pin AGAINST: the data root's pinned trust policy, its
2
+ // pinned kernel contract authority, its committed indexes and a cosign binary held to the policy's digest pins.
3
+ // One loader, so the two cannot be given different trust.
4
+
5
+ import { join } from 'node:path';
6
+
7
+ import { editionWorkspaceConfig } from './edition-config.mjs';
8
+ import { RUNTIME_IMAGE_SOURCE_INDEX_FILE, loadPinnedDeclaredTrustPolicy, readPinnedKernelContractAuthority } from './evidence-trust-pin.mjs';
9
+ import { prepareTrustedCosignBinary } from './image-evidence.mjs';
10
+ import { readStrictJsonFile } from './strict-json.mjs';
11
+
12
+ /**
13
+ * Load the verification context of `dataRoot`. Throws when the data root declares no evidence trust policy, when the
14
+ * policy or the third-party pin file is not the one the evidence trust pin records (review, then
15
+ * `xema-edition repin-evidence-trust`), or when the cosign binary is not one the policy pins.
16
+ *
17
+ * @returns {{policy, policyRoot, schemas, expectedContractAuthority, runtimeSourceIndex, biomeIndex, platformServiceIndex, trustedCosign, dispose: () => void}}
18
+ */
19
+ export function loadThirdPartySourceContext({ dataRoot, schemas, cosignBinary }) {
20
+ if (cosignBinary === undefined) {
21
+ throw new Error('verifying a third-party source needs a cosign binary: pass --cosign <absolute path> (`dist-check` reads XEMA_COSIGN_BIN); it must be one the trust policy pins by digest');
22
+ }
23
+ const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
24
+ const policy = loadPinnedDeclaredTrustPolicy({ dataRoot, declaration, schemas });
25
+ const expectedContractAuthority = readPinnedKernelContractAuthority({ dataRoot, declaration, schemas });
26
+ const runtimeSourceIndex = schemas.parseRuntimeImageSourceIndex(readStrictJsonFile(join(dataRoot, RUNTIME_IMAGE_SOURCE_INDEX_FILE)));
27
+ const biomeIndex = readStrictJsonFile(join(dataRoot, 'biome-index.json'));
28
+ const platformServiceIndex = readStrictJsonFile(join(dataRoot, 'platform-service-index.json'));
29
+ const trustedCosign = prepareTrustedCosignBinary({ cosignBinary, policy });
30
+ return {
31
+ policy,
32
+ policyRoot: dataRoot,
33
+ schemas,
34
+ expectedContractAuthority,
35
+ runtimeSourceIndex,
36
+ biomeIndex,
37
+ platformServiceIndex,
38
+ trustedCosign,
39
+ dispose: () => trustedCosign.dispose(),
40
+ };
41
+ }