@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,493 @@
1
+ // The evidence verifier. This module is the ENTRY of the verifier bundle
2
+ // (`scripts/build-evidence-verifier.mjs` -> dist/edition/verify-image-evidence
3
+ // .bundle.mjs); `xema-edition verify-evidence` runs that built file, never this
4
+ // source. Run as the bundle it refuses, before it trusts anything, unless:
5
+ //
6
+ // * its launch environment carries no NODE_OPTIONS/NODE_PATH/DYLD_*/LD_*/GIT_*;
7
+ // * its own file and provenance hash to the digests the data root PINS, and
8
+ // its install path and every ancestor are not group/world writable or
9
+ // foreign-owned;
10
+ // * the kernel `--kernel` states declares, as data, exactly the edition
11
+ // protocol compiled into the bundle, and this CLI supports it;
12
+ // * the data root and the editions root are clean git checkouts at an exact
13
+ // HEAD with every file read from them bound to its HEAD blob.
14
+ //
15
+ // A second mode, `catalogue-evidence arming|verify` (verify-catalogue-evidence.mjs), judges a release catalogue's E4
16
+ // statement for a customer before the catalogue's kernel is adopted; it is selected by the first argument.
17
+ //
18
+ // usage: verify-image-evidence <distributionId> --evidence-root <dir>
19
+ // --descriptor <absolute-path> --state <absolute-path> --cosign <absolute-path> --git <absolute-path>
20
+ // [--evidence-set <relative-path>] --data-root <dir> --editions-root <dir>
21
+ // --kernel <dir>
22
+
23
+ import { readFileSync, realpathSync } from 'node:fs';
24
+ import { isAbsolute } from 'node:path';
25
+
26
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
27
+ import { parseEditionDescriptor } from './edition-artifact.mjs';
28
+ import { EditionChannel, assertEditionChannel } from './edition-channel.mjs';
29
+ import { editionWorkspaceConfig, resetEditionWorkspaceConfig } from './edition-config.mjs';
30
+ import { assertKernelSpeaksBundledProtocol } from './edition-protocol.mjs';
31
+ import {
32
+ WorkspaceRoot,
33
+ configureEditionWorkspaceFromArgv,
34
+ dataRoot,
35
+ editionsRoot,
36
+ isEntryModule,
37
+ kernelPackageDir,
38
+ } from './edition-workspace.mjs';
39
+ import {
40
+ assertExternalInput,
41
+ gradeTrustedCheckouts,
42
+ prepareTrustedGitBinary,
43
+ } from './evidence-checkout.mjs';
44
+ import {
45
+ RUNTIME_IMAGE_SOURCE_INDEX_FILE,
46
+ assertPinnedKernelContractAuthority,
47
+ assertPinnedPolicy,
48
+ assertPinnedThirdPartySourcePins,
49
+ assertPinnedVerifier,
50
+ readStatedKernelContractAuthority,
51
+ loadDeclaredTrustPolicy,
52
+ readEvidenceTrustPin,
53
+ trustPolicyDigest,
54
+ verifierDigests,
55
+ } from './evidence-trust-pin.mjs';
56
+ import {
57
+ assertEntitlementDescriptorStates,
58
+ entitlementEvidenceRoot,
59
+ parseEntitlementDescriptorDocument,
60
+ } from './image-delivery-entitlement-descriptor.mjs';
61
+ import { verifyAndAdvanceReplayState } from './image-evidence-replay-state.mjs';
62
+ import {
63
+ readCanonicalJsonFromRoot,
64
+ verifyDistributionImageEvidenceSet,
65
+ verifyImageDeliveryEntitlement,
66
+ } from './image-evidence.mjs';
67
+ import {
68
+ assertSanitizedOperatorEnvironment,
69
+ readRunningVerifier,
70
+ } from './verifier-binding.mjs';
71
+ import { isVerifierDigestPinned } from './verifier-digest-pin.mjs';
72
+ import { loadVerifierKernelSchemas } from './verifier-kernel-schemas.mjs';
73
+ import {
74
+ CATALOGUE_EVIDENCE_MODE,
75
+ runCatalogueEvidenceEntry,
76
+ } from './verify-catalogue-evidence.mjs';
77
+
78
+ const USAGE =
79
+ 'usage: verify-image-evidence <distributionId> --evidence-root <dir> --descriptor <absolute-path> --state <absolute-path> ' +
80
+ '--cosign <absolute-path> --git <absolute-path> [--channel <develop|release>] [--evidence-set <relative-path>] [--json] ' +
81
+ '[--entitlement <relative-path> --entitlement-workflow-sha <40-hex git sha> --customer <id> --mode <mode> [--target-registry <registry>]] ' +
82
+ '--data-root <dir> --editions-root <dir> --kernel <dir>';
83
+ // The edition channel -> the kernel's evidence channel, stated once and explicitly.
84
+ const EVIDENCE_CHANNEL_OF_EDITION_CHANNEL = Object.freeze({
85
+ [EditionChannel.Release]: 'release',
86
+ [EditionChannel.Develop]: 'develop',
87
+ });
88
+ const GIT_COMMIT_SHA = /^[a-f0-9]{40}$/;
89
+ const DATA_ROOT_FILES = Object.freeze(['edition-workspace.json', 'runtime-image-source-index.json']);
90
+
91
+ function requireOptionValue(args, index, flag) {
92
+ const value = args[index + 1];
93
+ if (value === undefined || value.startsWith('--')) {
94
+ throw new Error(`${flag} requires a value`);
95
+ }
96
+ return value;
97
+ }
98
+
99
+ function parseArgs(args) {
100
+ const [distributionId, ...withJson] = args;
101
+ const jsonFlags = withJson.filter((arg) => arg === '--json').length;
102
+ if (jsonFlags > 1) {throw new Error('--json was given twice');}
103
+ const rest = withJson.filter((arg) => arg !== '--json');
104
+ try {
105
+ assertCanonicalDistributionId(distributionId);
106
+ } catch {
107
+ throw new Error(USAGE);
108
+ }
109
+ const options = {
110
+ distributionId,
111
+ evidenceRoot: null,
112
+ evidenceSetPath: 'evidence-set.json',
113
+ json: jsonFlags === 1,
114
+ descriptorPath: null,
115
+ channel: EditionChannel.Release,
116
+ statePath: null,
117
+ cosignBinary: null,
118
+ gitBinary: null,
119
+ entitlementDescriptorPath: null,
120
+ entitlementWorkflowSha: null,
121
+ customerId: null,
122
+ mode: null,
123
+ targetRegistry: null,
124
+ };
125
+ const seen = new Set();
126
+ for (let index = 0; index < rest.length; index += 2) {
127
+ const flag = rest[index];
128
+ const value = requireOptionValue(rest, index, flag);
129
+ if (seen.has(flag)) {throw new Error(`${flag} was given twice`);}
130
+ seen.add(flag);
131
+ if (flag === '--evidence-root') {options.evidenceRoot = value;}
132
+ else if (flag === '--evidence-set') {options.evidenceSetPath = value;}
133
+ else if (flag === '--descriptor') {options.descriptorPath = value;}
134
+ else if (flag === '--channel') {assertEditionChannel(value); options.channel = value;}
135
+ else if (flag === '--state') {options.statePath = value;}
136
+ else if (flag === '--cosign') {options.cosignBinary = value;}
137
+ else if (flag === '--git') {options.gitBinary = value;}
138
+ else if (flag === '--entitlement') {options.entitlementDescriptorPath = value;}
139
+ else if (flag === '--entitlement-workflow-sha') {options.entitlementWorkflowSha = value;}
140
+ else if (flag === '--customer') {options.customerId = value;}
141
+ else if (flag === '--mode') {options.mode = value;}
142
+ else if (flag === '--target-registry') {options.targetRegistry = value;}
143
+ else {throw new Error(`unknown option "${flag}"`);}
144
+ }
145
+ if (!options.evidenceRoot) {
146
+ throw new Error('--evidence-root is required');
147
+ }
148
+ for (const [flag, value] of [
149
+ ['--descriptor', options.descriptorPath],
150
+ ['--state', options.statePath],
151
+ ['--cosign', options.cosignBinary],
152
+ ['--git', options.gitBinary],
153
+ ]) {
154
+ if (!value || !isAbsolute(value)) {
155
+ throw new Error(`${flag} is required and must be an absolute path`);
156
+ }
157
+ }
158
+ // An entitlement is verified whole or not at all: the descriptor names the
159
+ // signed statement, the workflow SHA is the exact commit its certificate is
160
+ // held to (stated by the caller, never read from the evidence), and the
161
+ // customer and mode are what it must be bound to.
162
+ const entitlementFlags = [
163
+ ['--entitlement', options.entitlementDescriptorPath],
164
+ ['--entitlement-workflow-sha', options.entitlementWorkflowSha],
165
+ ['--customer', options.customerId],
166
+ ['--mode', options.mode],
167
+ ];
168
+ const stated = entitlementFlags.filter(([, value]) => value !== null);
169
+ if (stated.length !== 0 && stated.length !== entitlementFlags.length) {
170
+ throw new Error(
171
+ `${entitlementFlags.map(([flag]) => flag).join(', ')} are given together or not at all`,
172
+ );
173
+ }
174
+ if (stated.length === 0 && options.targetRegistry !== null) {
175
+ throw new Error('--target-registry needs --entitlement, --entitlement-workflow-sha, --customer and --mode');
176
+ }
177
+ if (options.entitlementWorkflowSha !== null && !GIT_COMMIT_SHA.test(options.entitlementWorkflowSha)) {
178
+ throw new Error('--entitlement-workflow-sha must be a 40-character lowercase hexadecimal git commit SHA');
179
+ }
180
+ if (options.entitlementDescriptorPath !== null && isAbsolute(options.entitlementDescriptorPath)) {
181
+ throw new Error('--entitlement must be an evidence-root-relative path');
182
+ }
183
+ return options;
184
+ }
185
+
186
+ /**
187
+ * The channel the descriptor states (compared with the invoked channel, never trusted as it): the descriptor's
188
+ * `provenance.channel`, mapped to the kernel's evidence channel. The descriptor
189
+ * must be the one of the requested distribution; an unmapped channel is refused.
190
+ */
191
+ /** The descriptor states a channel other than the one verification was invoked on: nothing is verified. */
192
+ export class EvidenceChannelMismatchError extends Error {
193
+ constructor({ invoked, descriptor }) {
194
+ super(`verification was invoked on the ${invoked} channel but --descriptor states the ${descriptor} channel; pass --channel ${descriptor} to verify a ${descriptor} edition`);
195
+ this.name = 'EvidenceChannelMismatchError';
196
+ }
197
+ }
198
+
199
+ function readEvidenceChannel({ descriptorPath, distributionId, schemas }) {
200
+ const descriptor = parseEditionDescriptor(JSON.parse(readFileSync(descriptorPath, 'utf8')));
201
+ if (descriptor.distributionId !== distributionId) {
202
+ throw new Error(
203
+ `--descriptor is the descriptor of "${descriptor.distributionId}", not of the requested distribution "${distributionId}"`,
204
+ );
205
+ }
206
+ const channel = EVIDENCE_CHANNEL_OF_EDITION_CHANNEL[descriptor.provenance.channel];
207
+ if (channel === undefined || !schemas.EVIDENCE_CHANNELS.includes(channel)) {
208
+ throw new Error(
209
+ `the descriptor channel "${descriptor.provenance.channel}" maps to no kernel evidence channel`,
210
+ );
211
+ }
212
+ return channel;
213
+ }
214
+
215
+ export const EDITION_EVIDENCE_VERIFICATION_SCHEMA = 'xema.edition-evidence-verification/v1';
216
+
217
+ /**
218
+ * What `--json` prints once the evidence is verified: the channel it was verified
219
+ * on, the release sequence it proved, and the statementDigest (the digest of the verified in-toto statement, not of the image) of every statement verified
220
+ * (producers by subject, then completeness, then the entitlement if one was asked for).
221
+ * `entitlementDescriptor` is the Sigstore evidence descriptor `entitlement.json` holds.
222
+ */
223
+ export function evidenceVerificationDocument({ distributionId, channel, evidenceSet, verified, entitlementDescriptor }) {
224
+ const producers = evidenceSet.producerEvidence
225
+ .map((producer) => {
226
+ const statement = verified.producerStatements.get(`${producer.subjectKind}\0${producer.subjectId}`);
227
+ return {
228
+ predicateType: statement.predicateType,
229
+ subject: statement.subject[0].name,
230
+ statementDigest: producer.evidence.statementDigest,
231
+ };
232
+ })
233
+ .sort((left, right) => (left.subject < right.subject ? -1 : left.subject > right.subject ? 1 : 0));
234
+ const statements = [
235
+ ...producers,
236
+ {
237
+ predicateType: verified.completenessStatement.predicateType,
238
+ subject: verified.completenessStatement.subject[0].name,
239
+ statementDigest: evidenceSet.completeness.statementDigest,
240
+ },
241
+ ];
242
+ if (verified.entitlementStatement !== null) {
243
+ statements.push({
244
+ predicateType: verified.entitlementStatement.predicateType,
245
+ subject: verified.entitlementStatement.subject[0].name,
246
+ statementDigest: entitlementDescriptor.statementDigest,
247
+ });
248
+ }
249
+ return {
250
+ schema: EDITION_EVIDENCE_VERIFICATION_SCHEMA,
251
+ distributionId,
252
+ channel,
253
+ verifierPinned: isVerifierDigestPinned(channel),
254
+ releaseSequence: verified.completenessStatement.predicate.releaseSequence,
255
+ statements,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * The declared policy, held to the data-root pin of both the policy and THIS
261
+ * running verifier. Read from the working tree: it is read twice, before the
262
+ * checkouts are graded (the policy names the git binary that grades them) and
263
+ * after, and the two readings must be the same policy.
264
+ */
265
+ function readTrustAnchors({ schemas, running, channel }) {
266
+ resetEditionWorkspaceConfig();
267
+ const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
268
+ if (declaration.policy === null) {
269
+ throw new Error(
270
+ `${dataRoot()} declares no evidenceTrustPolicy in edition-workspace.json; there is no policy to verify against`,
271
+ );
272
+ }
273
+ const { policy, policyDigest } = loadDeclaredTrustPolicy({
274
+ dataRoot: dataRoot(),
275
+ declaration,
276
+ schemas,
277
+ });
278
+ const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
279
+ assertPinnedPolicy({ pin, policyDigest });
280
+ assertPinnedThirdPartySourcePins({ pin, dataRoot: dataRoot() });
281
+ assertPinnedVerifier({ pin, actual: verifierDigests(running), channel });
282
+ // The authority the producer statements are held to is read from the data
283
+ // root's index downstream; it is the pinned one, or nothing is verified.
284
+ assertPinnedKernelContractAuthority({
285
+ pin,
286
+ authority: readStatedKernelContractAuthority({ dataRoot: dataRoot(), schemas }),
287
+ label: RUNTIME_IMAGE_SOURCE_INDEX_FILE,
288
+ });
289
+ return { declaration, policy, policyDigest };
290
+ }
291
+
292
+ /**
293
+ * Verify the evidence for one edition. `bundlePath` is the running bundle
294
+ * (`process.argv[1]` when run as one).
295
+ */
296
+ export async function verifyEvidence({ argv, env, bundlePath }) {
297
+ assertSanitizedOperatorEnvironment(env);
298
+ const options = parseArgs(
299
+ configureEditionWorkspaceFromArgv(argv, [
300
+ WorkspaceRoot.Data,
301
+ WorkspaceRoot.Editions,
302
+ WorkspaceRoot.Kernel,
303
+ ]),
304
+ );
305
+ const evidenceRoot = realpathSync(options.evidenceRoot);
306
+ const running = readRunningVerifier(bundlePath);
307
+ const { schemas, editionProtocol } = await loadVerifierKernelSchemas();
308
+ assertKernelSpeaksBundledProtocol({
309
+ kernelPackageDir: kernelPackageDir(),
310
+ bundled: editionProtocol,
311
+ });
312
+ // The channel is the invoker's (`--channel`, release when absent), never the descriptor's: the descriptor is the data
313
+ // under verification and must not choose its own trust level (whether the verifier's digests are pinned).
314
+ const channel = options.channel;
315
+ const descriptorChannel = readEvidenceChannel({
316
+ descriptorPath: options.descriptorPath,
317
+ distributionId: options.distributionId,
318
+ schemas,
319
+ });
320
+ if (descriptorChannel !== channel) {
321
+ throw new EvidenceChannelMismatchError({ invoked: channel, descriptor: descriptorChannel });
322
+ }
323
+ const first = readTrustAnchors({ schemas, running, channel });
324
+ const trustedGit = prepareTrustedGitBinary({
325
+ gitBinary: options.gitBinary,
326
+ policy: first.policy,
327
+ });
328
+ try {
329
+ const grades = [
330
+ {
331
+ root: dataRoot(),
332
+ label: 'data root',
333
+ requiredPaths: [
334
+ ...DATA_ROOT_FILES,
335
+ first.declaration.policy,
336
+ first.declaration.pin,
337
+ first.policy.trustedRootPath,
338
+ ],
339
+ },
340
+ {
341
+ root: editionsRoot(),
342
+ label: 'editions root',
343
+ requiredPaths: [
344
+ 'distribution.lock.json',
345
+ 'images.lock.json',
346
+ 'xema-distribution.json',
347
+ ].map((file) => `${options.distributionId}/${file}`),
348
+ },
349
+ ];
350
+ const graded = gradeTrustedCheckouts({ grades, trustedGit });
351
+ const repositoryRoots = new Set(
352
+ [...graded.values()].map((checkout) => checkout.repositoryRoot),
353
+ );
354
+ // Now that the files are bound to HEAD, the policy and pin read above must
355
+ // be the ones that were graded.
356
+ const anchors = readTrustAnchors({ schemas, running, channel });
357
+ if (anchors.policyDigest !== first.policyDigest) {
358
+ throw new Error('the trust policy changed while the checkouts were graded');
359
+ }
360
+ assertExternalInput(repositoryRoots, evidenceRoot, 'evidence root');
361
+ assertExternalInput(repositoryRoots, options.statePath, 'replay state');
362
+ assertExternalInput(repositoryRoots, options.descriptorPath, 'edition descriptor');
363
+ const releaseSourceCommit = graded.get(realpathSync(editionsRoot())).commit;
364
+ const assertCheckoutsUnchanged = () =>
365
+ gradeTrustedCheckouts({ grades, trustedGit, expected: graded });
366
+ const evidenceSet = schemas.parseDistributionImageEvidenceSet(
367
+ readCanonicalJsonFromRoot(
368
+ evidenceRoot,
369
+ options.evidenceSetPath,
370
+ 'distribution image evidence set',
371
+ ),
372
+ );
373
+ const policyDigest = trustPolicyDigest(anchors.policy);
374
+ const { verified } = await verifyAndAdvanceReplayState({
375
+ statePath: options.statePath,
376
+ distributionId: options.distributionId,
377
+ trustPolicyDigest: policyDigest,
378
+ parseReplayState: schemas.parseDistributionImageReplayState,
379
+ verify: async () => {
380
+ assertCheckoutsUnchanged();
381
+ const result = await verifyDistributionImageEvidenceSet({
382
+ distributionId: options.distributionId,
383
+ channel,
384
+ dataRoot: dataRoot(),
385
+ editionsRoot: editionsRoot(),
386
+ evidenceRoot,
387
+ evidenceSet,
388
+ policy: anchors.policy,
389
+ policyRoot: dataRoot(),
390
+ now: new Date(),
391
+ expectedReleaseSourceCommit: releaseSourceCommit,
392
+ cosignBinary: options.cosignBinary,
393
+ schemas,
394
+ });
395
+ const entitlement =
396
+ options.entitlementDescriptorPath === null
397
+ ? null
398
+ : parseEntitlementDescriptorDocument(
399
+ readCanonicalJsonFromRoot(
400
+ evidenceRoot,
401
+ options.entitlementDescriptorPath,
402
+ 'delivery entitlement descriptor',
403
+ ),
404
+ schemas,
405
+ );
406
+ if (entitlement !== null) {
407
+ assertEntitlementDescriptorStates(entitlement, {
408
+ distributionId: options.distributionId,
409
+ customerId: options.customerId,
410
+ mode: options.mode,
411
+ targetRegistry: options.targetRegistry,
412
+ distributionStatementDigest: result.completenessStatementDigest,
413
+ });
414
+ }
415
+ const entitlementDescriptor = entitlement === null ? null : entitlement.evidence;
416
+ const entitlementStatement =
417
+ entitlementDescriptor === null
418
+ ? null
419
+ : await verifyImageDeliveryEntitlement({
420
+ evidenceRoot: entitlementEvidenceRoot(evidenceRoot, options.entitlementDescriptorPath),
421
+ descriptor: entitlementDescriptor,
422
+ policy: anchors.policy,
423
+ policyRoot: dataRoot(),
424
+ completenessStatementDigest: result.completenessStatementDigest,
425
+ customerId: options.customerId,
426
+ distributionId: options.distributionId,
427
+ distributionLock: result.distributionLock,
428
+ channel,
429
+ mode: options.mode,
430
+ targetRegistry: options.targetRegistry,
431
+ now: new Date(),
432
+ // The entitlement predicate carries no producer run: the commit
433
+ // its certificate is held to is the caller's statement.
434
+ expectedWorkflowSha: options.entitlementWorkflowSha,
435
+ // A nonce spent by a producer or completeness statement of
436
+ // this release cannot be reused by its entitlement.
437
+ consumedNonces: result.verifiedNonces,
438
+ cosignBinary: options.cosignBinary,
439
+ schemas,
440
+ });
441
+ assertCheckoutsUnchanged();
442
+ return { ...result, entitlementStatement, entitlementDescriptor };
443
+ },
444
+ });
445
+ // With --json the human text goes to stderr and stdout carries the one document.
446
+ (options.json ? process.stderr : process.stdout).write(
447
+ `verified ${verified.producerStatements.size} image producers and distribution completeness for ` +
448
+ `"${options.distributionId}" at release sequence ` +
449
+ `${verified.completenessStatement.predicate.releaseSequence}\n` +
450
+ `edition read from ${editionsRoot()} at editions checkout commit ${releaseSourceCommit}\n` +
451
+ (verified.entitlementStatement === null
452
+ ? ''
453
+ : `verified the delivery entitlement of "${options.customerId}" (${options.mode})\n`),
454
+ );
455
+ if (options.json) {
456
+ process.stdout.write(
457
+ `${JSON.stringify(
458
+ evidenceVerificationDocument({
459
+ distributionId: options.distributionId,
460
+ channel,
461
+ evidenceSet,
462
+ verified,
463
+ entitlementDescriptor: verified.entitlementDescriptor,
464
+ }),
465
+ null,
466
+ 2,
467
+ )}\n`,
468
+ );
469
+ }
470
+ } finally {
471
+ trustedGit.dispose();
472
+ }
473
+ }
474
+
475
+ if (isEntryModule(import.meta.url) && process.argv[2] === CATALOGUE_EVIDENCE_MODE) {
476
+ // The customer's pre-adoption mode: the E4 statement of a catalogue, judged with this bundle's own embedded kernel.
477
+ await runCatalogueEvidenceEntry({
478
+ argv: process.argv.slice(2),
479
+ env: process.env,
480
+ bundlePath: process.argv[1],
481
+ });
482
+ } else if (isEntryModule(import.meta.url)) {
483
+ await verifyEvidence({
484
+ argv: process.argv.slice(2),
485
+ env: process.env,
486
+ bundlePath: process.argv[1],
487
+ }).catch((error) => {
488
+ // A named refusal (e.g. EditionProtocolMismatchError) is printed with its name.
489
+ const named = error.name && error.name !== 'Error' ? `${error.name}: ` : '';
490
+ console.error(`verify-image-evidence: ${named}${error.message}`);
491
+ process.exitCode = 1;
492
+ });
493
+ }