@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,326 @@
1
+ // Re-verification of ONE third-party source pin from its COMMITTED evidence (XEMA-1611 design section 3, checks
2
+ // (a) to (d)). The trusted writer runs it on the evidence it is about to commit, and `--check-third-party` runs it
3
+ // on the committed tree, so the two cannot disagree about what a pin proves. Nothing here reads a network: the
4
+ // signatures are verified by `verifySigstoreEvidenceDescriptor` (cosign `verify-blob-attestation --offline` against the
5
+ // pinned trusted root, the tlog inclusion proof taken from the committed bundle, never `--insecure-ignore-tlog`).
6
+ //
7
+ // Where the evidence lives, relative to the data root:
8
+ // third-party-provenance/sha256-<bundle manifest hex>/{statement.json,bundle.sigstore.json}
9
+ // the SLSA provenance attestation of the biome bundle (the bytes cosign signed, which are not canonical JSON),
10
+ // and its signature as a Sigstore bundle v0.1 (`PROVENANCE_BUNDLE_MEDIA_TYPE`): publishers attest in the classic
11
+ // cosign format, whose Rekor entry carries a signed entry timestamp (an inclusion promise), not an inclusion proof;
12
+ // evidence/sha256-<image manifest hex>/producer-evidence.json (+ statements/, bundles/)
13
+ // the image-source/v2 evidence, stored the way a first-party producer commits it.
14
+
15
+ import { EvidenceSubjectKind, isAuthorityBound } from './authority-binding.mjs';
16
+ import { assertProducerTtlCeiling, decodeExactUtf8, evidenceDirectoryForDigest, policyCandidates, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
17
+ import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
18
+ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
19
+ import { PRODUCER_EVIDENCE_INDEX_FILE } from './producer-evidence-index-file.mjs';
20
+ import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
21
+ import { soleImageSignerRow } from './third-party-admission.mjs';
22
+ import { assertCertificateExtensions } from './third-party-certificate-extensions.mjs';
23
+
24
+ export const THIRD_PARTY_PROVENANCE_ROOT = 'third-party-provenance';
25
+ export const PROVENANCE_STATEMENT_FILE = 'statement.json';
26
+ export const PROVENANCE_BUNDLE_FILE = 'bundle.sigstore.json';
27
+ /**
28
+ * The committed provenance signature is a Sigstore bundle v0.1, the form a classic `cosign attest` entry (certificate,
29
+ * Rekor v1 entry with its signed entry timestamp) takes; v0.2+ requires an inclusion proof a classic entry does not carry.
30
+ */
31
+ export const PROVENANCE_BUNDLE_MEDIA_TYPE = 'application/vnd.dev.sigstore.bundle+json;version=0.1';
32
+ /** The SLSA v1 provenance predicate a biome bundle is attested with (what the fetcher's `verify-attestation --type` matches). */
33
+ export const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1';
34
+ export const GITHUB_ACTIONS_OIDC_ISSUER = 'https://token.actions.githubusercontent.com';
35
+ /** The only channel a third-party source is admitted on: signed from refs/heads/main. */
36
+ export const THIRD_PARTY_EVIDENCE_CHANNEL = 'release';
37
+
38
+ const MAX_EVIDENCE_INDEX_BYTES = 4 * 1024 * 1024;
39
+ /** `cosign attest --type slsaprovenance1` wraps the predicate in an in-toto v0.1 statement; v1 is the same subject shape. */
40
+ const IN_TOTO_STATEMENT_TYPES = new Set(['https://in-toto.io/Statement/v0.1', 'https://in-toto.io/Statement/v1']);
41
+
42
+ /** `third-party-provenance/sha256-<hex>` of an OCI manifest digest. */
43
+ export function provenanceDirectoryForDigest(manifestDigest) {
44
+ return `${THIRD_PARTY_PROVENANCE_ROOT}/${evidenceDirectoryForDigest(manifestDigest).split('/')[1]}`;
45
+ }
46
+
47
+ /** The digest of an `<oci repository>@sha256:<hex>` reference. */
48
+ export const digestOfOciRef = (ociRef) => ociRef.slice(ociRef.indexOf('@') + 1);
49
+
50
+ /** The statement bytes are the DSSE payload cosign signed: exact UTF-8 JSON, not this CLI's canonical form. */
51
+ export function decodeJsonStatement(bytes, label) {
52
+ try {
53
+ return JSON.parse(decodeExactUtf8(bytes, label));
54
+ } catch (error) {
55
+ throw new Error(`${label} is not exact UTF-8 JSON: ${error.message}`);
56
+ }
57
+ }
58
+
59
+ /** The in-toto statement of a provenance attestation, held to the shape this plane reads (subject digest, predicate type). */
60
+ export function parseProvenanceStatement(value) {
61
+ const digest = value?.subject?.[0]?.digest?.sha256;
62
+ if (
63
+ !IN_TOTO_STATEMENT_TYPES.has(value?._type) ||
64
+ value.predicateType !== SLSA_PROVENANCE_V1 ||
65
+ !Array.isArray(value.subject) ||
66
+ value.subject.length !== 1 ||
67
+ typeof digest !== 'string'
68
+ ) {
69
+ throw new Error(
70
+ `the bundle provenance is not an in-toto statement of ${SLSA_PROVENANCE_V1} over exactly one subject`,
71
+ );
72
+ }
73
+ return value;
74
+ }
75
+
76
+ const readEvidenceFile = (root, path, label, maxBytes) => readBoundedNoFollowFile(root, path, label, maxBytes);
77
+
78
+ /**
79
+ * A keyless signer row of `channel`, held to everything such a row must be: the channel signed from that channel's ref, the
80
+ * GitHub Actions issuer and the exact identity of its own workflow. Throws naming the row and the field.
81
+ */
82
+ export function assertChannelSignerRow({ row, channel, schemas, label }) {
83
+ const ref = schemas.EVIDENCE_CHANNEL_REFS[channel];
84
+ if (row.channel !== channel || row.githubWorkflow?.ref !== ref) {
85
+ throw new Error(
86
+ `${label}: row "${row.policyId}" is not a ${channel} row signed from ${ref} ` +
87
+ `(channel ${JSON.stringify(row.channel)}, ref ${JSON.stringify(row.githubWorkflow?.ref)})`,
88
+ );
89
+ }
90
+ if (row.oidcIssuer !== GITHUB_ACTIONS_OIDC_ISSUER) {
91
+ throw new Error(`${label}: row "${row.policyId}" oidcIssuer ${JSON.stringify(row.oidcIssuer)} is not ${GITHUB_ACTIONS_OIDC_ISSUER}`);
92
+ }
93
+ const { repository, path } = row.githubWorkflow;
94
+ const identity = `https://github.com/${repository}/${path}@${ref}`;
95
+ if (row.certificateIdentity !== identity) {
96
+ throw new Error(`${label}: row "${row.policyId}" certificateIdentity ${JSON.stringify(row.certificateIdentity)} is not the exact workflow identity ${identity}`);
97
+ }
98
+ return row;
99
+ }
100
+
101
+ /**
102
+ * The ONE `channel` row that signs the SLSA provenance of `repository`'s biome bundles, held to `assertChannelSignerRow`.
103
+ * None, or several, is refused naming the repository and the channel.
104
+ */
105
+ export function bundleSignerRowFor({ policy, repository, channel, schemas, label }) {
106
+ const rows = policy.policies.filter(
107
+ (row) => row.channel === channel && row.predicateTypes?.includes(SLSA_PROVENANCE_V1) && row.githubWorkflow?.repository === repository,
108
+ );
109
+ if (rows.length !== 1) {
110
+ throw new Error(
111
+ `${label}: the trust policy has ${rows.length} ${channel} rows authorizing ${SLSA_PROVENANCE_V1} for ${repository}; exactly one bundle-signer row is required`,
112
+ );
113
+ }
114
+ return assertChannelSignerRow({ row: rows[0], channel, schemas, label });
115
+ }
116
+
117
+ /**
118
+ * The trust-policy row `policyId` names, held to everything a third-party signer row must be: one row, the release
119
+ * channel signed from refs/heads/main, the GitHub Actions issuer, the exact identity of its own workflow, the source's
120
+ * repository, and `workflowPath`, the workflow the pin records for the ONE artifact this row signs (the bundle's
121
+ * `signerWorkflow`, an image's `producerWorkflow`). Throws naming the row and the field.
122
+ */
123
+ export function thirdPartySignerRow({ policy, policyId, source, workflowPath, schemas, label }) {
124
+ const rows = policy.policies.filter((row) => row.policyId === policyId);
125
+ if (rows.length !== 1) {
126
+ throw new Error(`${label}: the trust policy has ${rows.length} rows with policyId "${policyId}"; exactly one is required`);
127
+ }
128
+ const row = thirdPartySourceRow({ row: rows[0], source, schemas, label });
129
+ if (row.githubWorkflow.path !== workflowPath) {
130
+ throw new Error(`${label}: row "${policyId}" signs workflow ${row.githubWorkflow.path}, but the pin records workflow ${workflowPath}`);
131
+ }
132
+ return row;
133
+ }
134
+
135
+ /** `row` held to `assertChannelSignerRow` on the release channel and to the source's repository. */
136
+ function thirdPartySourceRow({ row, source, schemas, label }) {
137
+ assertChannelSignerRow({ row, channel: THIRD_PARTY_EVIDENCE_CHANNEL, schemas, label });
138
+ if (row.githubWorkflow.repository !== source.repository) {
139
+ throw new Error(`${label}: row "${row.policyId}" signs for ${row.githubWorkflow.repository}, but the source is pinned to ${source.repository}`);
140
+ }
141
+ return row;
142
+ }
143
+
144
+ /**
145
+ * The ONE release image-source row listing `imageRef` (`soleImageSignerRow`), held to `thirdPartySourceRow`: none,
146
+ * several, or one of another repository is refused. This is the row the writer pins for the image.
147
+ */
148
+ export function soleThirdPartyImageRow({ policy, source, imageRef, schemas, label }) {
149
+ const rows = policyCandidates(policy, schemas.IMAGE_PRODUCER_PREDICATE_TYPE, THIRD_PARTY_EVIDENCE_CHANNEL, imageRef);
150
+ return thirdPartySourceRow({ row: soleImageSignerRow({ rows, imageRef, channel: THIRD_PARTY_EVIDENCE_CHANNEL, label }), source, schemas, label });
151
+ }
152
+
153
+ /** The policy narrowed to ONE row, so cosign is run with exactly that row's identity and issuer. */
154
+ const exactRowPolicy = (policy, row) => ({ ...policy, policies: [row] });
155
+
156
+ function verifiedBundleProvenance({ source, evidenceRoot, context, hold }) {
157
+ const label = `third-party source "${source.name}" bundle ${source.bundle.ociRef}`;
158
+ const row = thirdPartySignerRow({
159
+ policy: context.policy,
160
+ policyId: source.bundle.signerPolicyId,
161
+ source,
162
+ workflowPath: source.bundle.signerWorkflow,
163
+ schemas: context.schemas,
164
+ label,
165
+ });
166
+ if (!row.predicateTypes.includes(SLSA_PROVENANCE_V1)) {
167
+ throw new Error(`${label}: row "${row.policyId}" does not authorize ${SLSA_PROVENANCE_V1}, so it is not a bundle signer`);
168
+ }
169
+ const directory = provenanceDirectoryForDigest(digestOfOciRef(source.bundle.ociRef));
170
+ let verified;
171
+ try {
172
+ verified = verifySigstoreEvidenceDescriptor({
173
+ evidenceRoot,
174
+ descriptor: {
175
+ statementPath: `${directory}/${PROVENANCE_STATEMENT_FILE}`,
176
+ statementDigest: source.bundle.statementDigest,
177
+ bundlePath: `${directory}/${PROVENANCE_BUNDLE_FILE}`,
178
+ bundleDigest: source.bundle.bundleDigest,
179
+ bundleMediaType: PROVENANCE_BUNDLE_MEDIA_TYPE,
180
+ },
181
+ policy: exactRowPolicy(context.policy, row),
182
+ policyRoot: context.policyRoot,
183
+ predicateType: SLSA_PROVENANCE_V1,
184
+ channel: THIRD_PARTY_EVIDENCE_CHANNEL,
185
+ expectedWorkflowSha: source.sourceCommit,
186
+ parseStatement: parseProvenanceStatement,
187
+ decodeStatement: decodeJsonStatement,
188
+ trustedCosign: context.trustedCosign,
189
+ holdProducerWorkflow: hold,
190
+ });
191
+ } catch (error) {
192
+ throw new Error(`${label}: ${error.message}`);
193
+ }
194
+ const attested = `sha256:${verified.statement.subject[0].digest.sha256}`;
195
+ if (attested !== digestOfOciRef(source.bundle.ociRef)) {
196
+ throw new Error(`${label}: the provenance attests ${attested}, not the pinned bundle digest ${digestOfOciRef(source.bundle.ociRef)}`);
197
+ }
198
+ const bundleBytes = readEvidenceFile(evidenceRoot, `${directory}/${PROVENANCE_BUNDLE_FILE}`, `${label} bundle`, 16 * 1024 * 1024).bytes;
199
+ assertCertificateExtensions({ bundle: JSON.parse(decodeExactUtf8(bundleBytes, `${label} bundle`)), source, signerIdentity: row.certificateIdentity, label });
200
+ return { row, statement: verified.statement };
201
+ }
202
+
203
+ /** The `statementPath`/`bundlePath` the committed `producer-evidence.json` of an image's evidence directory gives its subject. */
204
+ export function committedImageDescriptor({ image, evidenceRoot, label }) {
205
+ const directory = evidenceDirectoryForDigest(image.manifestDigest);
206
+ const index = JSON.parse(
207
+ decodeExactUtf8(readEvidenceFile(evidenceRoot, `${directory}/${PRODUCER_EVIDENCE_INDEX_FILE}`, `${label} producer evidence index`, MAX_EVIDENCE_INDEX_BYTES).bytes, `${label} producer evidence index`),
208
+ );
209
+ const entries = Array.isArray(index?.producerEvidence) ? index.producerEvidence : [];
210
+ const entry = entries.find((candidate) => candidate?.subjectKind === image.subjectKind && candidate?.subjectId === image.subjectId);
211
+ if (entry === undefined) {
212
+ throw new Error(`${label}: ${directory}/${PRODUCER_EVIDENCE_INDEX_FILE} carries no evidence for ${image.subjectKind}:${image.subjectId}`);
213
+ }
214
+ const { statementPath, bundlePath } = entry.evidence;
215
+ return {
216
+ statementPath: `${directory}/${statementPath}`,
217
+ statementDigest: image.statementDigest,
218
+ bundlePath: `${directory}/${bundlePath}`,
219
+ bundleDigest: image.bundleDigest,
220
+ bundleMediaType: BUNDLE_MEDIA_TYPE,
221
+ };
222
+ }
223
+
224
+ const repositoryPathOf = (repositoryUrl) => new URL(repositoryUrl).pathname.replace(/^\//u, '').replace(/\.git$/u, '');
225
+
226
+ /** (c) for one image statement: one repository, one commit, the pinned subject and authority. */
227
+ export function assertImageStatementBindsPin({ statement, source, image, expectedContractAuthority, channel, label }) {
228
+ const { predicate } = statement;
229
+ const claimed = predicate.source;
230
+ const mismatch = (what, actual, expected) => {
231
+ throw new Error(`${label}: ${what} is ${JSON.stringify(actual)}, but the pin requires ${JSON.stringify(expected)}`);
232
+ };
233
+ if (isAuthorityBound(channel, EvidenceSubjectKind.ThirdParty)) {
234
+ assertKernelContractAuthorityEqual(predicate.contractAuthority, expectedContractAuthority, label);
235
+ }
236
+ if (repositoryPathOf(claimed.repositoryUrl) !== source.repository) {
237
+ mismatch('predicate source.repositoryUrl repository', repositoryPathOf(claimed.repositoryUrl), source.repository);
238
+ }
239
+ if (claimed.repo !== source.repository) {mismatch('predicate source.repo', claimed.repo, source.repository);}
240
+ if (claimed.path !== claimed.repo && !claimed.path.startsWith(`${claimed.repo}/`)) {
241
+ mismatch('predicate source.path (it must be the repository or a directory of it)', claimed.path, `${source.repository}[/<directory>]`);
242
+ }
243
+ if (predicate.producerRun.producerWorkflowSha !== source.sourceCommit) {
244
+ mismatch('predicate producerRun.producerWorkflowSha (the certificate workflow commit)', predicate.producerRun.producerWorkflowSha, source.sourceCommit);
245
+ }
246
+ if (claimed.sourceCommit !== predicate.producerRun.producerWorkflowSha) {
247
+ mismatch('predicate source.sourceCommit (it must be the commit the certificate was issued for)', claimed.sourceCommit, predicate.producerRun.producerWorkflowSha);
248
+ }
249
+ if (claimed.sourceManifestHash !== source.bundle.sourceManifestHash) {
250
+ mismatch('predicate source.sourceManifestHash', claimed.sourceManifestHash, source.bundle.sourceManifestHash);
251
+ }
252
+ if (claimed.kind !== image.subjectKind || claimed.id !== image.subjectId) {
253
+ mismatch('predicate source kind/id', `${claimed.kind}:${claimed.id}`, `${image.subjectKind}:${image.subjectId}`);
254
+ }
255
+ if (claimed.sourceTreeHash !== image.sourceTreeHash) {mismatch('predicate source.sourceTreeHash', claimed.sourceTreeHash, image.sourceTreeHash);}
256
+ if (claimed.sourceHashAlgorithm !== image.sourceHashAlgorithm) {
257
+ mismatch('predicate source.sourceHashAlgorithm', claimed.sourceHashAlgorithm, image.sourceHashAlgorithm);
258
+ }
259
+ if (predicate.artifact.imageRepository !== image.imageRef || statement.subject[0].name !== image.imageRef) {
260
+ mismatch('predicate artifact.imageRepository', predicate.artifact.imageRepository, image.imageRef);
261
+ }
262
+ if (predicate.artifact.manifestDigest !== image.manifestDigest) {
263
+ mismatch('predicate artifact.manifestDigest', predicate.artifact.manifestDigest, image.manifestDigest);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * The row an image's evidence is held to: the ONE release row listing the image repository (`soleThirdPartyImageRow`),
269
+ * which must be the row its pin names, signed from refs/heads/main by the workflow the pin records for this image (not
270
+ * necessarily the bundle signer's). The commit is the source commit, held by cosign and by `assertImageStatementBindsPin`.
271
+ */
272
+ export function thirdPartyImageSignerRow({ policy, source, image, schemas, label }) {
273
+ const sole = soleThirdPartyImageRow({ policy, source, imageRef: image.imageRef, schemas, label });
274
+ if (sole.policyId !== image.policyId) {
275
+ throw new Error(`${label}: the one ${THIRD_PARTY_EVIDENCE_CHANNEL} row listing ${image.imageRef} is "${sole.policyId}", but the pin names row "${image.policyId}"`);
276
+ }
277
+ return thirdPartySignerRow({ policy, policyId: image.policyId, source, workflowPath: image.producerWorkflow, schemas, label });
278
+ }
279
+
280
+ function verifiedImageEvidence({ source, image, evidenceRoot, context, hold, channel }) {
281
+ const label = `third-party source "${source.name}" image ${image.imageRef}@${image.manifestDigest}`;
282
+ const row = thirdPartyImageSignerRow({ policy: context.policy, source, image, schemas: context.schemas, label });
283
+ const descriptor = committedImageDescriptor({ image, evidenceRoot, label });
284
+ let verified;
285
+ try {
286
+ verified = verifySigstoreEvidenceDescriptor({
287
+ evidenceRoot,
288
+ descriptor,
289
+ policy: exactRowPolicy(context.policy, row),
290
+ policyRoot: context.policyRoot,
291
+ predicateType: context.schemas.IMAGE_PRODUCER_PREDICATE_TYPE,
292
+ channel: THIRD_PARTY_EVIDENCE_CHANNEL,
293
+ imageRepository: image.imageRef,
294
+ expectedWorkflowSha: source.sourceCommit,
295
+ parseStatement: context.schemas.parseImageProducerStatement,
296
+ trustedCosign: context.trustedCosign,
297
+ holdProducerWorkflow: hold,
298
+ });
299
+ } catch (error) {
300
+ throw new Error(`${label}: ${error.message}`);
301
+ }
302
+ assertImageStatementBindsPin({ statement: verified.statement, source, image, expectedContractAuthority: context.expectedContractAuthority, channel, label });
303
+ // The ceiling is held here, at the publisher's door, as well as by verify-evidence: a too-long lifetime is never pinned.
304
+ assertProducerTtlCeiling({
305
+ issuedAt: verified.statement.predicate.producerRun.issuedAt,
306
+ expiresAt: verified.statement.predicate.producerRun.expiresAt,
307
+ maxTtlSeconds: context.policy.maxProducerTtlSeconds,
308
+ label,
309
+ });
310
+ const bundleBytes = readEvidenceFile(evidenceRoot, descriptor.bundlePath, `${label} bundle`, 16 * 1024 * 1024).bytes;
311
+ assertCertificateExtensions({ bundle: JSON.parse(decodeExactUtf8(bundleBytes, `${label} bundle`)), source, signerIdentity: row.certificateIdentity, label });
312
+ return verified.statement;
313
+ }
314
+
315
+ /**
316
+ * Checks (a) to (d) for one pin, from the evidence under `evidenceRoot`. `context` is
317
+ * `{policy, policyRoot, schemas, trustedCosign, expectedContractAuthority, runtimeSourceIndex}`; the runtime
318
+ * image source index must already carry `{repository, workflowCommit: sourceCommit}` in its `producerWorkflowPins`
319
+ * (the writer passes the index it is about to write), which `holdProducerWorkflow` then enforces unchanged.
320
+ */
321
+ export function verifyThirdPartySourceEvidence({ source, evidenceRoot, context, channel }) {
322
+ const hold = (producer) => context.schemas.producerWorkflowPinRefusal(context.runtimeSourceIndex, producer);
323
+ const bundle = verifiedBundleProvenance({ source, evidenceRoot, context, hold });
324
+ const images = source.images.map((image) => verifiedImageEvidence({ source, image, evidenceRoot, context, hold, channel }));
325
+ return { bundle, images };
326
+ }
@@ -0,0 +1,148 @@
1
+ // The flags of `xema-edition index runtime-image-source --trusted-write --third-party <name>` and of
2
+ // `--check-third-party` (XEMA-1611 design section 3). The writer takes DECLARED facts and proves them; nothing here
3
+ // is derived from a clone of the third-party repository.
4
+
5
+ import { isAbsolute } from 'node:path';
6
+
7
+ import { EditionChannel, parseChannelArgs } from './edition-channel.mjs';
8
+ import { COMMIT_PATTERN, GITHUB_REPOSITORY_PATTERN, SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
9
+ import { RunnerEnvironment, parseDeclaredRunnerEnvironment } from './fulcio-certificate-extensions.mjs';
10
+
11
+ export const THIRD_PARTY_FLAG = '--third-party';
12
+ export const CHECK_THIRD_PARTY_FLAG = '--check-third-party';
13
+ export const COSIGN_FLAG = '--cosign';
14
+ export const CHANNEL_FLAG = '--channel';
15
+ /** The environment variable `dist-check` reads the cosign binary of the third-party check from (an absolute path). */
16
+ export const COSIGN_BIN_ENV = 'XEMA_COSIGN_BIN';
17
+
18
+ const WRITER_FLAGS = Object.freeze({
19
+ repository: '--repository',
20
+ repositoryId: '--repository-id',
21
+ repositoryOwnerId: '--repository-owner-id',
22
+ sourceCommit: '--source-commit',
23
+ bundle: '--bundle',
24
+ biomeId: '--biome-id',
25
+ runnerEnvironment: '--runner-environment',
26
+ });
27
+ const IMAGE_FLAG = '--image';
28
+
29
+ export const THIRD_PARTY_WRITER_USAGE =
30
+ `${THIRD_PARTY_FLAG} <name> --repository <owner/name> --repository-id <github id> --repository-owner-id <github id> ` +
31
+ '--source-commit <40-hex> --bundle <oci repository>@sha256:<64-hex> --biome-id <id> ' +
32
+ `${IMAGE_FLAG} <image repository>@sha256:<64-hex> [${IMAGE_FLAG} ...] ${COSIGN_FLAG} <absolute cosign path> ` +
33
+ `[${WRITER_FLAGS.runnerEnvironment} ${RunnerEnvironment.SelfHosted} (verify the signer workflow's runs-on by hand first), default ${RunnerEnvironment.GithubHosted}] ` +
34
+ `[${CHANNEL_FLAG} <${Object.values(EditionChannel).join('|')}>, default ${EditionChannel.Release}]`;
35
+
36
+ /** Remove every `flag value` pair named `flag` from argv; the values in order, and the argv without them. */
37
+ function take(argv, flag) {
38
+ const values = [];
39
+ const remaining = [];
40
+ for (let index = 0; index < argv.length; index += 1) {
41
+ if (argv[index] !== flag) {
42
+ remaining.push(argv[index]);
43
+ continue;
44
+ }
45
+ const value = argv[index + 1];
46
+ if (value === undefined || value.startsWith('--')) {
47
+ throw new Error(`${flag} requires a value`);
48
+ }
49
+ values.push(value);
50
+ index += 1;
51
+ }
52
+ return { values, remaining };
53
+ }
54
+
55
+ function single(argv, flag) {
56
+ const { values, remaining } = take(argv, flag);
57
+ if (values.length > 1) {
58
+ throw new Error(`${flag} was given more than once`);
59
+ }
60
+ return { value: values[0], remaining };
61
+ }
62
+
63
+ function matching(flag, value, pattern, expectation) {
64
+ if (!pattern.test(value)) {
65
+ throw new Error(`${flag} must be ${expectation}; got ${JSON.stringify(value)}`);
66
+ }
67
+ return value;
68
+ }
69
+
70
+ export const OCI_REPOSITORY_AT_DIGEST = /^[^@\s]+@sha256:[a-f0-9]{64}$/u;
71
+ const NUMERIC_ID = /^[1-9][0-9]*$/u;
72
+
73
+ function parseImage(value) {
74
+ matching(IMAGE_FLAG, value, OCI_REPOSITORY_AT_DIGEST, '<image repository>@sha256:<64-hex>');
75
+ const at = value.indexOf('@');
76
+ const manifestDigest = value.slice(at + 1);
77
+ matching(IMAGE_FLAG, manifestDigest, SHA256_DIGEST_PATTERN, 'a sha256 digest');
78
+ return { imageRef: value.slice(0, at), manifestDigest };
79
+ }
80
+
81
+ /**
82
+ * Take the third-party flags out of `argv`. `writer` is the parsed write request (or `undefined` when `--third-party`
83
+ * is absent, in which case any writer-only flag is refused); `cosignBinary` is `--cosign` (absolute) or `undefined`;
84
+ * `channel` is `--channel` parsed by `parseChannelArgs`, or `undefined` when absent (the caller applies the release default).
85
+ */
86
+ export function takeThirdPartyFlags(argv) {
87
+ let rest = argv;
88
+ const name = single(rest, THIRD_PARTY_FLAG);
89
+ rest = name.remaining;
90
+ const cosign = single(rest, COSIGN_FLAG);
91
+ rest = cosign.remaining;
92
+ const channelFlag = single(rest, CHANNEL_FLAG);
93
+ rest = channelFlag.remaining;
94
+ const channel = channelFlag.value === undefined ? undefined : parseChannelArgs([CHANNEL_FLAG, channelFlag.value]).channel;
95
+ const taken = {};
96
+ for (const [key, flag] of Object.entries(WRITER_FLAGS)) {
97
+ const result = single(rest, flag);
98
+ rest = result.remaining;
99
+ taken[key] = result.value;
100
+ }
101
+ const images = take(rest, IMAGE_FLAG);
102
+ rest = images.remaining;
103
+ if (cosign.value !== undefined && !isAbsolute(cosign.value)) {
104
+ throw new Error(`${COSIGN_FLAG} must be an absolute path to the cosign binary`);
105
+ }
106
+ const writerFlagsGiven = Object.values(taken).some((value) => value !== undefined) || images.values.length > 0;
107
+ if (name.value === undefined) {
108
+ if (writerFlagsGiven) {
109
+ throw new Error(`the writer flags (${[...Object.values(WRITER_FLAGS), IMAGE_FLAG].join(', ')}) belong to ${THIRD_PARTY_FLAG} <name>`);
110
+ }
111
+ return { writer: undefined, cosignBinary: cosign.value, channel, remaining: rest };
112
+ }
113
+ const require = (key) => {
114
+ if (taken[key] === undefined) {
115
+ throw new Error(`${THIRD_PARTY_FLAG} requires ${WRITER_FLAGS[key]}\nusage: ${THIRD_PARTY_WRITER_USAGE}`);
116
+ }
117
+ return taken[key];
118
+ };
119
+ if (images.values.length === 0) {
120
+ throw new Error(`${THIRD_PARTY_FLAG} requires ${IMAGE_FLAG} at least once\nusage: ${THIRD_PARTY_WRITER_USAGE}`);
121
+ }
122
+ if (cosign.value === undefined) {
123
+ throw new Error(`${THIRD_PARTY_FLAG} requires ${COSIGN_FLAG}\nusage: ${THIRD_PARTY_WRITER_USAGE}`);
124
+ }
125
+ const parsedImages = images.values.map(parseImage);
126
+ if (new Set(parsedImages.map((image) => image.imageRef)).size !== parsedImages.length) {
127
+ throw new Error(`${IMAGE_FLAG} names an image repository more than once`);
128
+ }
129
+ const bundle = matching(WRITER_FLAGS.bundle, require('bundle'), OCI_REPOSITORY_AT_DIGEST, '<oci repository>@sha256:<64-hex>');
130
+ return {
131
+ writer: {
132
+ name: name.value,
133
+ repository: matching(WRITER_FLAGS.repository, require('repository'), GITHUB_REPOSITORY_PATTERN, 'owner/name'),
134
+ repositoryId: matching(WRITER_FLAGS.repositoryId, require('repositoryId'), NUMERIC_ID, 'a decimal GitHub repository id'),
135
+ repositoryOwnerId: matching(WRITER_FLAGS.repositoryOwnerId, require('repositoryOwnerId'), NUMERIC_ID, 'a decimal GitHub owner id'),
136
+ sourceCommit: matching(WRITER_FLAGS.sourceCommit, require('sourceCommit'), COMMIT_PATTERN, 'an exact 40-hex commit'),
137
+ bundleOciRef: bundle,
138
+ biomeId: require('biomeId'),
139
+ ...(taken.runnerEnvironment === undefined
140
+ ? {}
141
+ : { runnerEnvironment: parseDeclaredRunnerEnvironment(taken.runnerEnvironment, WRITER_FLAGS.runnerEnvironment) }),
142
+ images: parsedImages,
143
+ },
144
+ cosignBinary: cosign.value,
145
+ channel,
146
+ remaining: rest,
147
+ };
148
+ }