@xemahq/xema 0.25.1 → 0.27.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 (239) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/appliance-base/images.lock.json +126 -126
  3. package/dist/assets/appliance-lean/images.lock.json +123 -123
  4. package/dist/assets/cloud/images.lock.json +126 -126
  5. package/dist/assets/core/images.lock.json +120 -120
  6. package/dist/assets/distribution-assets.manifest.json +15 -16
  7. package/dist/assets/minimal/images.lock.json +108 -108
  8. package/dist/assets/oss/images.lock.json +126 -126
  9. package/dist/assets/starter/images.lock.json +123 -123
  10. package/dist/cli.js +2 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/dev.js +1 -0
  13. package/dist/commands/dev.js.map +1 -1
  14. package/dist/commands/down.d.ts.map +1 -1
  15. package/dist/commands/down.js +7 -1
  16. package/dist/commands/down.js.map +1 -1
  17. package/dist/commands/edition.d.ts +3 -0
  18. package/dist/commands/edition.d.ts.map +1 -0
  19. package/dist/commands/edition.js +46 -0
  20. package/dist/commands/edition.js.map +1 -0
  21. package/dist/commands/init.js +2 -2
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/serve.js +1 -0
  24. package/dist/commands/serve.js.map +1 -1
  25. package/dist/commands/up.js +1 -0
  26. package/dist/commands/up.js.map +1 -1
  27. package/dist/dev/env-defaults.d.ts.map +1 -1
  28. package/dist/dev/env-defaults.js +6 -1
  29. package/dist/dev/env-defaults.js.map +1 -1
  30. package/dist/dev/infra-preflight.d.ts +3 -2
  31. package/dist/dev/infra-preflight.d.ts.map +1 -1
  32. package/dist/dev/infra-preflight.js +18 -8
  33. package/dist/dev/infra-preflight.js.map +1 -1
  34. package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
  35. package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
  36. package/dist/runtime/backing-registration.d.ts.map +1 -1
  37. package/dist/runtime/backing-registration.js +7 -2
  38. package/dist/runtime/backing-registration.js.map +1 -1
  39. package/dist/runtime/config/schema.d.ts +1 -0
  40. package/dist/runtime/config/schema.d.ts.map +1 -1
  41. package/dist/runtime/custody/local-custody.d.ts +12 -0
  42. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  43. package/dist/runtime/custody/local-custody.js +22 -0
  44. package/dist/runtime/custody/local-custody.js.map +1 -0
  45. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  46. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  47. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  48. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  49. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  50. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  51. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  52. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  53. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  54. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  55. package/dist/runtime/custody/openbao-http-client.js +154 -0
  56. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  57. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  58. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  59. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  60. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  61. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  62. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  63. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  64. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  65. package/dist/runtime/edition/run-entry.d.ts +9 -0
  66. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  67. package/dist/runtime/edition/run-entry.js +36 -0
  68. package/dist/runtime/edition/run-entry.js.map +1 -0
  69. package/dist/runtime/edition/verb-map.d.ts +60 -0
  70. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  71. package/dist/runtime/edition/verb-map.js +146 -0
  72. package/dist/runtime/edition/verb-map.js.map +1 -0
  73. package/dist/runtime/env-seeder.d.ts +3 -0
  74. package/dist/runtime/env-seeder.d.ts.map +1 -1
  75. package/dist/runtime/env-seeder.js +20 -3
  76. package/dist/runtime/env-seeder.js.map +1 -1
  77. package/dist/runtime/launch.d.ts +3 -1
  78. package/dist/runtime/launch.d.ts.map +1 -1
  79. package/dist/runtime/launch.js +37 -8
  80. package/dist/runtime/launch.js.map +1 -1
  81. package/dist/runtime/paths.d.ts +1 -2
  82. package/dist/runtime/paths.d.ts.map +1 -1
  83. package/dist/runtime/paths.js +0 -10
  84. package/dist/runtime/paths.js.map +1 -1
  85. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  86. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  87. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  88. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  89. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  90. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  91. package/dist/runtime/runner/chart-preflight.js +30 -69
  92. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  93. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  94. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  95. package/dist/runtime/runner/chart-pull.js +96 -0
  96. package/dist/runtime/runner/chart-pull.js.map +1 -0
  97. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  98. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  99. package/dist/runtime/runner/chart-resolver.js +16 -62
  100. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  101. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  102. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  103. package/dist/runtime/runner/chart-set-lock.js +55 -99
  104. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  105. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  106. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  107. package/dist/runtime/runner/helm-cli.js +9 -7
  108. package/dist/runtime/runner/helm-cli.js.map +1 -1
  109. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  110. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  111. package/dist/runtime/runner/helm-runner.js +18 -16
  112. package/dist/runtime/runner/helm-runner.js.map +1 -1
  113. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  114. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  115. package/dist/runtime/sources/kinds/git.js +8 -5
  116. package/dist/runtime/sources/kinds/git.js.map +1 -1
  117. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  118. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  119. package/dist/runtime/sources/manifest-walk.js +8 -3
  120. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  121. package/dist/runtime/sources/schema.d.ts +3 -0
  122. package/dist/runtime/sources/schema.d.ts.map +1 -1
  123. package/dist/runtime/sources/schema.js +6 -1
  124. package/dist/runtime/sources/schema.js.map +1 -1
  125. package/dist/runtime/supervisor.d.ts +3 -0
  126. package/dist/runtime/supervisor.d.ts.map +1 -1
  127. package/dist/runtime/supervisor.js +2 -0
  128. package/dist/runtime/supervisor.js.map +1 -1
  129. package/edition/README.md +315 -0
  130. package/edition/assemble-producer-input.mjs +1251 -0
  131. package/edition/authorization-base.mjs +347 -0
  132. package/edition/biome-content.mjs +268 -0
  133. package/edition/build-biome-index.mjs +175 -0
  134. package/edition/build-platform-service-index.mjs +288 -0
  135. package/edition/build-runtime-image-source-index.mjs +821 -0
  136. package/edition/built-verifier.mjs +55 -0
  137. package/edition/catalogue-edition.mjs +49 -0
  138. package/edition/catalogue-fetch.mjs +64 -0
  139. package/edition/catalogue.mjs +135 -0
  140. package/edition/chart-set-source-commit.mjs +92 -0
  141. package/edition/check-committed-image-evidence.mjs +383 -0
  142. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  143. package/edition/comment-blanker.mjs +75 -0
  144. package/edition/connector-source-authority.mjs +60 -0
  145. package/edition/customer-definition.mjs +183 -0
  146. package/edition/customer-flags.mjs +7 -0
  147. package/edition/develop-customer-edition.mjs +99 -0
  148. package/edition/develop-edition-resolve.mjs +65 -0
  149. package/edition/develop-edition.mjs +274 -0
  150. package/edition/develop-freeze.mjs +173 -0
  151. package/edition/develop-image-coordinate.mjs +54 -0
  152. package/edition/develop-seal.mjs +77 -0
  153. package/edition/develop-snapshot-inputs.mjs +104 -0
  154. package/edition/dist-check.mjs +986 -0
  155. package/edition/distribution-evidence-set.mjs +987 -0
  156. package/edition/distribution-paths.mjs +191 -0
  157. package/edition/edition-artifact.mjs +270 -0
  158. package/edition/edition-biome-manifests.mjs +85 -0
  159. package/edition/edition-channel.mjs +6 -0
  160. package/edition/edition-chart-set.mjs +93 -0
  161. package/edition/edition-config.mjs +207 -0
  162. package/edition/edition-constants.mjs +57 -0
  163. package/edition/edition-contract.json +23 -0
  164. package/edition/edition-list.mjs +35 -0
  165. package/edition/edition-locks.mjs +69 -0
  166. package/edition/edition-protocol.mjs +228 -0
  167. package/edition/edition-workspace.mjs +176 -0
  168. package/edition/evidence-checkout.mjs +418 -0
  169. package/edition/evidence-grammars.mjs +16 -0
  170. package/edition/evidence-trust-pin.mjs +304 -0
  171. package/edition/evidence-trust-policy.mjs +256 -0
  172. package/edition/external-source-walk.mjs +463 -0
  173. package/edition/first-party-source-ids.mjs +59 -0
  174. package/edition/fleet-coordinate-finality.mjs +350 -0
  175. package/edition/git-source-auth.mjs +52 -0
  176. package/edition/image-evidence-replay-state.mjs +242 -0
  177. package/edition/image-evidence.mjs +1225 -0
  178. package/edition/kernel-contract-authority.mjs +443 -0
  179. package/edition/lib.mjs +3018 -0
  180. package/edition/no-follow-file.mjs +94 -0
  181. package/edition/oci-evidence-verbs.mjs +392 -0
  182. package/edition/oci-image-source-evidence.mjs +239 -0
  183. package/edition/oras.mjs +648 -0
  184. package/edition/pin-source.mjs +227 -0
  185. package/edition/plan-image-evidence-refresh.mjs +408 -0
  186. package/edition/platform-lock-readability.mjs +892 -0
  187. package/edition/platform-service-class.mjs +18 -0
  188. package/edition/platform-service-source-authority.mjs +105 -0
  189. package/edition/positive-closure.mjs +162 -0
  190. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  191. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  192. package/edition/produce-image-source-evidence.mjs +765 -0
  193. package/edition/producer-evidence-bundle.mjs +157 -0
  194. package/edition/registry-credentials.mjs +193 -0
  195. package/edition/release-catalogue.mjs +39 -0
  196. package/edition/release-channel-guard.mjs +99 -0
  197. package/edition/release-edition-artifact.mjs +158 -0
  198. package/edition/remedy-command.mjs +89 -0
  199. package/edition/repin-evidence-trust.mjs +101 -0
  200. package/edition/required-resource-provider.mjs +58 -0
  201. package/edition/resolve-chart-set.mjs +415 -0
  202. package/edition/resolve-customer-edition.mjs +288 -0
  203. package/edition/resolve-distribution.mjs +2318 -0
  204. package/edition/resolve-edition.mjs +186 -0
  205. package/edition/resolve-images.mjs +1606 -0
  206. package/edition/seal-image-digests.mjs +855 -0
  207. package/edition/sealed-evidence-accept.mjs +82 -0
  208. package/edition/source-authority.mjs +725 -0
  209. package/edition/source-closure.mjs +116 -0
  210. package/edition/strict-json.mjs +121 -0
  211. package/edition/string-order.mjs +5 -0
  212. package/edition/trust-policy-arming.mjs +120 -0
  213. package/edition/trusted-executable.mjs +166 -0
  214. package/edition/verifier-binding.mjs +45 -0
  215. package/edition/verifier-files.mjs +8 -0
  216. package/edition/verifier-kernel-schemas.mjs +22 -0
  217. package/edition/verifier-provenance.mjs +64 -0
  218. package/edition/verify-image-evidence.mjs +430 -0
  219. package/edition/verify-image-platforms.mjs +495 -0
  220. package/edition/why.mjs +56 -0
  221. package/edition/workspace-sections.mjs +195 -0
  222. package/examples/06-biome-sources.yaml +1 -0
  223. package/package.json +18 -7
  224. package/dist/commands/dist.d.ts +0 -16
  225. package/dist/commands/dist.d.ts.map +0 -1
  226. package/dist/commands/dist.js +0 -182
  227. package/dist/commands/dist.js.map +0 -1
  228. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  229. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  230. package/dist/runtime/distribution/available-biome.js +0 -154
  231. package/dist/runtime/distribution/available-biome.js.map +0 -1
  232. package/dist/runtime/distribution/compose.d.ts +0 -23
  233. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  234. package/dist/runtime/distribution/compose.js +0 -48
  235. package/dist/runtime/distribution/compose.js.map +0 -1
  236. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  237. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  238. package/dist/runtime/runner/chart-trust.js +0 -270
  239. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,304 @@
1
+ // The data root's declared evidence trust policy and the PIN that binds it.
2
+ //
3
+ // `edition-workspace.json` may declare `evidenceTrustPolicy: {policy, pin}`. The
4
+ // policy is the offline Sigstore trust policy (who may sign, which binaries may
5
+ // verify); the pin is a reviewed data-root file that fixes the digest of that
6
+ // policy, the digests of the one verifier bundle and its provenance, the
7
+ // kernel contract authority every evidence statement is held to, and the oras
8
+ // binaries that may read a registry. So the policy cannot change, the verifier
9
+ // that enforces it cannot change, the kernel contract the evidence binds cannot
10
+ // change and the registry reader cannot change without a reviewed data-root
11
+ // commit.
12
+ //
13
+ // The kernel contract authority is pinned HERE because nothing else can hold
14
+ // it: `runtime-image-source-index.json` states the authority, and the image
15
+ // locks and statements are compared with what that file states. Comparing the
16
+ // file with itself decides nothing, so its authority is first held to this
17
+ // pin, and only the pinned authority is what evidence is compared with.
18
+
19
+ import { closeSync, constants as fsConstants, openSync, writeFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+
22
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
23
+ import {
24
+ canonicalJsonBytes,
25
+ readCanonicalJsonFromRoot,
26
+ sha256Digest,
27
+ validatePinnedTrustedRoot,
28
+ } from './image-evidence.mjs';
29
+ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
30
+ import { parseStrictJson } from './strict-json.mjs';
31
+
32
+ export const EVIDENCE_TRUST_PIN_SCHEMA_VERSION = 2;
33
+ export const RUNTIME_IMAGE_SOURCE_INDEX_FILE = 'runtime-image-source-index.json';
34
+
35
+ /** A data root that declares no evidence trust has no pin to hold the kernel contract authority to. */
36
+ export const KERNEL_CONTRACT_AUTHORITY_UNPINNED = 'KERNEL_CONTRACT_AUTHORITY_UNPINNED';
37
+ /** The data root's runtime image source index states another authority than the pin records. */
38
+ export const KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE = 'KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE';
39
+
40
+ export class KernelContractAuthorityPinError extends Error {
41
+ /** @param {string} code @param {string} message */
42
+ constructor(code, message) {
43
+ super(message);
44
+ this.name = 'KernelContractAuthorityPinError';
45
+ this.code = code;
46
+ }
47
+ }
48
+ export const REPIN_COMMAND = 'xema edition repin-evidence-trust';
49
+
50
+ const MAX_PIN_BYTES = 64 * 1024;
51
+ const MAX_INDEX_BYTES = 16 * 1024 * 1024;
52
+ const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
53
+ const PIN_KEYS = Object.freeze([
54
+ 'kernelContractAuthority',
55
+ 'policyDigest',
56
+ 'schemaVersion',
57
+ 'trustedOrasBinaryDigests',
58
+ 'verifier',
59
+ ]);
60
+ const VERIFIER_KEYS = Object.freeze(['bundleDigest', 'provenanceDigest']);
61
+ const AUTHORITY_PIN_KEYS = Object.freeze(['digest', 'sourceCommit']);
62
+ const PIN_SHAPE =
63
+ '{schemaVersion, policyDigest, verifier: {bundleDigest, provenanceDigest}, ' +
64
+ 'kernelContractAuthority: {sourceCommit, digest}, trustedOrasBinaryDigests: [sha256:…, …]}';
65
+
66
+ const exactKeys = (value, keys) =>
67
+ value !== null &&
68
+ typeof value === 'object' &&
69
+ !Array.isArray(value) &&
70
+ Object.keys(value).sort().join() === keys.join();
71
+
72
+ function requireDigest(value, what) {
73
+ if (typeof value !== 'string' || !SHA256_DIGEST.test(value)) {
74
+ throw new Error(`evidence trust pin: ${what} must be sha256:<64 hex>`);
75
+ }
76
+ return value;
77
+ }
78
+
79
+ /** Digest of the parsed policy in its canonical JSON form. */
80
+ export const trustPolicyDigest = (policy) =>
81
+ sha256Digest(canonicalJsonBytes(policy));
82
+
83
+ /** The verifier digests a pin records: the bundle's bytes and its provenance's bytes. */
84
+ export function verifierDigests({ bundleBytes, provenanceBytes }) {
85
+ return Object.freeze({
86
+ bundleDigest: sha256Digest(bundleBytes),
87
+ provenanceDigest: sha256Digest(provenanceBytes),
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Read, parse and validate the declared policy: kernel schema, then the pinned
93
+ * Sigstore trusted root it names (resolved against the data root).
94
+ */
95
+ export function loadDeclaredTrustPolicy({ dataRoot, declaration, schemas }) {
96
+ const policy = schemas.parseSigstoreOfflineTrustPolicy(
97
+ readCanonicalJsonFromRoot(
98
+ dataRoot,
99
+ declaration.policy,
100
+ 'declared image evidence trust policy',
101
+ ),
102
+ );
103
+ validatePinnedTrustedRoot(dataRoot, policy);
104
+ return Object.freeze({ policy, policyDigest: trustPolicyDigest(policy) });
105
+ }
106
+
107
+ /** Read the declared pin file: strict JSON, exact keys. */
108
+ export function readEvidenceTrustPin({ dataRoot, declaration }) {
109
+ const label = 'declared image evidence trust pin';
110
+ const { bytes } = readBoundedNoFollowFile(
111
+ dataRoot,
112
+ declaration.pin,
113
+ label,
114
+ MAX_PIN_BYTES,
115
+ );
116
+ const pin = parseStrictJson(bytes.toString('utf8'), declaration.pin);
117
+ if (
118
+ !exactKeys(pin, PIN_KEYS) ||
119
+ !exactKeys(pin.verifier, VERIFIER_KEYS) ||
120
+ !exactKeys(pin.kernelContractAuthority, AUTHORITY_PIN_KEYS) ||
121
+ !Array.isArray(pin.trustedOrasBinaryDigests)
122
+ ) {
123
+ throw new Error(`${declaration.pin}: the pin is exactly ${PIN_SHAPE}`);
124
+ }
125
+ if (pin.schemaVersion !== EVIDENCE_TRUST_PIN_SCHEMA_VERSION) {
126
+ throw new Error(
127
+ `${declaration.pin}: schemaVersion must be ${EVIDENCE_TRUST_PIN_SCHEMA_VERSION}`,
128
+ );
129
+ }
130
+ return Object.freeze({
131
+ schemaVersion: pin.schemaVersion,
132
+ policyDigest: requireDigest(pin.policyDigest, 'policyDigest'),
133
+ verifier: Object.freeze({
134
+ bundleDigest: requireDigest(pin.verifier.bundleDigest, 'verifier.bundleDigest'),
135
+ provenanceDigest: requireDigest(
136
+ pin.verifier.provenanceDigest,
137
+ 'verifier.provenanceDigest',
138
+ ),
139
+ }),
140
+ kernelContractAuthority: Object.freeze({
141
+ sourceCommit: requireCommit(pin.kernelContractAuthority.sourceCommit),
142
+ digest: requireDigest(pin.kernelContractAuthority.digest, 'kernelContractAuthority.digest'),
143
+ }),
144
+ trustedOrasBinaryDigests: requireOrasDigests(pin.trustedOrasBinaryDigests),
145
+ });
146
+ }
147
+
148
+ function requireCommit(value) {
149
+ if (typeof value !== 'string' || !COMMIT_PATTERN.test(value)) {
150
+ throw new Error('evidence trust pin: kernelContractAuthority.sourceCommit must be an exact 40-hex commit');
151
+ }
152
+ return value;
153
+ }
154
+
155
+ /** At least one digest, each canonical, none repeated: a pin that trusts no oras cannot read a registry. */
156
+ export function requireOrasDigests(value) {
157
+ if (!Array.isArray(value) || value.length === 0) {
158
+ throw new Error(
159
+ 'evidence trust pin: trustedOrasBinaryDigests must list at least one sha256 digest of an oras binary ' +
160
+ '(`shasum -a 256` of the real file); state it with --trusted-oras-digest when re-pinning',
161
+ );
162
+ }
163
+ const digests = value.map((digest, index) => requireDigest(digest, `trustedOrasBinaryDigests[${index}]`));
164
+ if (new Set(digests).size !== digests.length) {
165
+ throw new Error('evidence trust pin: trustedOrasBinaryDigests repeats a digest');
166
+ }
167
+ return Object.freeze([...digests]);
168
+ }
169
+
170
+ /** What a pin records of a kernel contract authority: its commit, and the digest of the whole authority. */
171
+ export function kernelContractAuthorityPin(authority) {
172
+ return Object.freeze({
173
+ sourceCommit: requireCommit(authority?.sourceCommit),
174
+ digest: sha256Digest(canonicalJsonBytes(authority)),
175
+ });
176
+ }
177
+
178
+ /** The kernel contract authority the data root's runtime image source index STATES; nothing is trusted yet. */
179
+ export function readStatedKernelContractAuthority({ dataRoot, schemas }) {
180
+ if (!schemas?.parseRuntimeImageSourceIndex) {
181
+ throw new Error('trusted kernel distribution schemas are required');
182
+ }
183
+ const { bytes } = readBoundedNoFollowFile(
184
+ dataRoot,
185
+ RUNTIME_IMAGE_SOURCE_INDEX_FILE,
186
+ 'runtime image source index',
187
+ MAX_INDEX_BYTES,
188
+ );
189
+ return schemas.parseRuntimeImageSourceIndex(JSON.parse(bytes.toString('utf8'))).contractAuthority;
190
+ }
191
+
192
+ /** Refuse an authority that is not the one the pin records. */
193
+ export function assertPinnedKernelContractAuthority({ pin, authority, label }) {
194
+ const actual = kernelContractAuthorityPin(authority);
195
+ if (
196
+ actual.sourceCommit !== pin.kernelContractAuthority.sourceCommit ||
197
+ actual.digest !== pin.kernelContractAuthority.digest
198
+ ) {
199
+ throw new KernelContractAuthorityPinError(
200
+ KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE,
201
+ `${label} states kernel contract authority ${actual.sourceCommit} (${actual.digest}) but the pin records ` +
202
+ `${pin.kernelContractAuthority.sourceCommit} (${pin.kernelContractAuthority.digest}); ` +
203
+ `review the kernel contract change, then re-pin with \`${REPIN_COMMAND}\``,
204
+ );
205
+ }
206
+ return authority;
207
+ }
208
+
209
+ /**
210
+ * The kernel contract authority evidence is held to: the one the data root's
211
+ * runtime image source index states, ONLY once it is the one the data root's
212
+ * evidence-trust pin records. A data root that declares no evidence trust has
213
+ * no pin, so it has no authority to hold evidence to and this refuses.
214
+ *
215
+ * @param {{dataRoot: string, declaration: {policy: string|null, pin: string|null}, schemas: object}} options
216
+ */
217
+ export function readPinnedKernelContractAuthority({ dataRoot, declaration, schemas }) {
218
+ if (declaration.policy === null) {
219
+ throw new KernelContractAuthorityPinError(
220
+ KERNEL_CONTRACT_AUTHORITY_UNPINNED,
221
+ `${dataRoot} declares no evidenceTrustPolicy in edition-workspace.json, so no pin records the kernel contract ` +
222
+ 'authority and no image evidence can be produced, planned or checked against it',
223
+ );
224
+ }
225
+ return assertPinnedKernelContractAuthority({
226
+ pin: readEvidenceTrustPin({ dataRoot, declaration }),
227
+ authority: readStatedKernelContractAuthority({ dataRoot, schemas }),
228
+ label: `${dataRoot}/${RUNTIME_IMAGE_SOURCE_INDEX_FILE}`,
229
+ });
230
+ }
231
+
232
+ /** Refuse a pin whose policy digest is not the declared policy's. */
233
+ export function assertPinnedPolicy({ pin, policyDigest }) {
234
+ if (pin.policyDigest !== policyDigest) {
235
+ throw new Error(
236
+ `the trust policy hashes to ${policyDigest} but the pin records ${pin.policyDigest}; ` +
237
+ `review the policy change, then re-pin it with \`${REPIN_COMMAND}\``,
238
+ );
239
+ }
240
+ }
241
+
242
+ /**
243
+ * The declared policy, held to the pin's record of it. Refuses a data root that
244
+ * declares none: there is no policy to produce or accept evidence under.
245
+ */
246
+ export function loadPinnedDeclaredTrustPolicy({ dataRoot, declaration, schemas }) {
247
+ if (declaration.policy === null) {
248
+ throw new Error(
249
+ `${dataRoot} declares no evidenceTrustPolicy in edition-workspace.json; there is no policy to produce evidence under`,
250
+ );
251
+ }
252
+ const { policy, policyDigest } = loadDeclaredTrustPolicy({ dataRoot, declaration, schemas });
253
+ assertPinnedPolicy({ pin: readEvidenceTrustPin({ dataRoot, declaration }), policyDigest });
254
+ return policy;
255
+ }
256
+
257
+ /** Refuse a pin whose verifier digests are not the verifier's actual ones. */
258
+ export function assertPinnedVerifier({ pin, actual }) {
259
+ for (const key of VERIFIER_KEYS) {
260
+ if (pin.verifier[key] !== actual[key]) {
261
+ throw new Error(
262
+ `the verifier's ${key} is ${actual[key]} but the pin records ${pin.verifier[key]}; ` +
263
+ `this CLI's verifier is not the one the data root pinned. Install the pinned ` +
264
+ `CLI version, or review the verifier change and re-pin with \`${REPIN_COMMAND}\``,
265
+ );
266
+ }
267
+ }
268
+ }
269
+
270
+ /** Write the pin for the declared policy, the verifier digests, the kernel contract authority and the oras digests. */
271
+ export function writeEvidenceTrustPin({
272
+ dataRoot,
273
+ declaration,
274
+ policyDigest,
275
+ verifier,
276
+ kernelContractAuthority,
277
+ trustedOrasBinaryDigests,
278
+ }) {
279
+ const pin = {
280
+ schemaVersion: EVIDENCE_TRUST_PIN_SCHEMA_VERSION,
281
+ policyDigest,
282
+ verifier: {
283
+ bundleDigest: verifier.bundleDigest,
284
+ provenanceDigest: verifier.provenanceDigest,
285
+ },
286
+ kernelContractAuthority: { ...kernelContractAuthorityPin(kernelContractAuthority) },
287
+ trustedOrasBinaryDigests: [...requireOrasDigests(trustedOrasBinaryDigests)].sort(),
288
+ };
289
+ const text = `${JSON.stringify(pin, null, 2)}\n`;
290
+ const descriptor = openSync(
291
+ join(dataRoot, declaration.pin),
292
+ fsConstants.O_WRONLY |
293
+ fsConstants.O_CREAT |
294
+ fsConstants.O_TRUNC |
295
+ fsConstants.O_NOFOLLOW,
296
+ 0o644,
297
+ );
298
+ try {
299
+ writeFileSync(descriptor, text);
300
+ } finally {
301
+ closeSync(descriptor);
302
+ }
303
+ return pin;
304
+ }
@@ -0,0 +1,256 @@
1
+ // The evidence-trust part of `xema edition check`.
2
+ //
3
+ // A data root states whether its editions MUST be evidence-gated
4
+ // (`evidenceTrustPolicy.required` in `edition-workspace.json`, no default) and
5
+ // may DECLARE a policy (`policy` + `pin`). This gate says which, in words, and
6
+ // refuses every state in which the answer would be a silent one:
7
+ //
8
+ // * required but no policy -> refused (EvidenceTrustRequiredError, code
9
+ // EVIDENCE_TRUST_REQUIRED_UNARMED): a data root that
10
+ // insists on gating cannot run ungated. Sealing an
11
+ // edition (descriptorEvidenceTrust) refuses the same
12
+ // way for a RELEASE edition of a required data
13
+ // root whose declared policy is UNARMED or PARTIAL
14
+ // on the release channel;
15
+ // * not required, no policy and clean -> passes, printing that the editions are NOT
16
+ // evidence-gated (an explicit absence, not a skip);
17
+ // * no policy declared but a policy file is present at the top level or under
18
+ // `trust/` -> refused: a policy nothing reads is a policy
19
+ // someone believes is enforced;
20
+ // * declared -> the policy parses with the kernel schema, its
21
+ // pinned Sigstore root matches, the data-root pin
22
+ // records the policy's digest AND this CLI's built
23
+ // verifier (a missing bundle is refused, never
24
+ // skipped), and the authority configuration is
25
+ // judged PER CHANNEL (trust-policy-arming.mjs): on
26
+ // the release channel it is unarmed (passes,
27
+ // delivery fail-closed) or armed; the develop
28
+ // channel admits not-armed and says so in one
29
+ // line; a channel that is partially armed is refused.
30
+
31
+ import { readFileSync, readdirSync } from 'node:fs';
32
+ import { join } from 'node:path';
33
+
34
+ import { locateBuiltVerifier, readBuiltVerifierDigests } from './built-verifier.mjs';
35
+ import { EDITION_WORKSPACE_CONFIG_FILE, editionWorkspaceConfig } from './edition-config.mjs';
36
+ import { EDITION_CONTRACT, EditionChannel, EvidenceTrustState } from './edition-constants.mjs';
37
+ import { dataRoot as configuredDataRoot } from './edition-workspace.mjs';
38
+ import {
39
+ assertPinnedPolicy,
40
+ assertPinnedVerifier,
41
+ loadDeclaredTrustPolicy,
42
+ readEvidenceTrustPin,
43
+ } from './evidence-trust-pin.mjs';
44
+ import { loadKernelDistributionSchemas } from './lib.mjs';
45
+ import {
46
+ TrustPolicyArmingState,
47
+ assertArmingChannel,
48
+ assessTrustPolicyArming,
49
+ } from './trust-policy-arming.mjs';
50
+
51
+ const TAG = '[dist:check]';
52
+
53
+ /**
54
+ * The stable code of the refusal to write a `not-armed` edition for a data root
55
+ * that requires evidence gating. Consumers (xema-deploy, workflows) match on the
56
+ * code, never on the message; it is also listed in edition-contract.json.
57
+ */
58
+ export const EVIDENCE_TRUST_REQUIRED_UNARMED = EDITION_CONTRACT.evidenceTrust.refusal.code;
59
+
60
+ /**
61
+ * The data root requires evidence gating (`required: true`) but its evidence
62
+ * trust is not armed: it declares no policy, or declares one that is UNARMED or
63
+ * PARTIALLY ARMED. Sealing an edition or writing an artifact is refused with this
64
+ * error; `not-armed` is never recorded for `required: true`.
65
+ */
66
+ export class EvidenceTrustRequiredError extends Error {
67
+ /** @param {string} dataRoot @param {string} reason what is not armed, in words */
68
+ constructor(dataRoot, reason) {
69
+ super(
70
+ `${join(dataRoot, EDITION_WORKSPACE_CONFIG_FILE)} sets evidenceTrustPolicy.required to true but ${reason}: ` +
71
+ 'arm the declared policy (`xema edition check` says what is unmet), declare {"required": true, "policy": ..., "pin": ...}, ' +
72
+ 'or set required to false to run this data root NOT ARMED.',
73
+ );
74
+ this.name = 'EvidenceTrustRequiredError';
75
+ this.code = EVIDENCE_TRUST_REQUIRED_UNARMED;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Load the declared policy and hold it to its data-root pin: the policy must
81
+ * hash to what the pin records, and this CLI's built verifier must be the one
82
+ * the pin records (a missing bundle is refused, never skipped).
83
+ */
84
+ function loadPinnedPolicy({ dataRoot, declaration, schemas, locateVerifier }) {
85
+ const { policy, policyDigest } = loadDeclaredTrustPolicy({ dataRoot, declaration, schemas });
86
+ const pin = readEvidenceTrustPin({ dataRoot, declaration });
87
+ assertPinnedPolicy({ pin, policyDigest });
88
+ assertPinnedVerifier({ pin, actual: readBuiltVerifierDigests(locateVerifier) });
89
+ return { policy, policyDigest };
90
+ }
91
+
92
+ /** What a develop edition says, once, when it is written or checked without an armed develop channel. */
93
+ export const DEVELOP_NOT_ARMED_NOTICE = 'develop channel: trust policy not armed, evidence is not signature-verified';
94
+
95
+ /**
96
+ * The evidence state an edition of `channel` records in its descriptor, two
97
+ * states and fail closed. Arming is judged for that ONE channel
98
+ * (`assessTrustPolicyArming`): the signers of the other channel count for nothing.
99
+ *
100
+ * * `armed` - the data root declares a policy, the policy is pinned (the
101
+ * policy and this CLI's verifier match the data-root pin) and
102
+ * ARMED on `channel`;
103
+ * * `not-armed` - a release edition: exactly `required: false` and no armed
104
+ * policy. A develop edition: its channel is not armed, whatever
105
+ * `required` says, and it says so in one stderr line.
106
+ *
107
+ * Every other state is refused: a release edition of a `required: true` data
108
+ * root that is not ARMED on the release channel (no policy, or UNARMED or
109
+ * PARTIAL there) throws EvidenceTrustRequiredError; a data root that requires
110
+ * gating and declares no policy at all, a policy edited after pinning, a
111
+ * drifted verifier, and a policy PARTIALLY ARMED on `channel` are refused on
112
+ * either channel.
113
+ *
114
+ * @param {{ channel: string, locateVerifier?: () => {bundlePath: string, provenancePath: string} }} options
115
+ */
116
+ export async function descriptorEvidenceTrust({ channel, locateVerifier = locateBuiltVerifier }) {
117
+ const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
118
+ const root = configuredDataRoot();
119
+ const schemas = await loadKernelDistributionSchemas();
120
+ assertArmingChannel(schemas, channel);
121
+ const notArmed = () => {
122
+ if (channel === EditionChannel.Develop) {console.error(DEVELOP_NOT_ARMED_NOTICE);}
123
+ return EvidenceTrustState.NotArmed;
124
+ };
125
+ if (declaration.policy === null) {
126
+ if (declaration.required) {throw new EvidenceTrustRequiredError(root, 'declares no policy');}
127
+ return notArmed();
128
+ }
129
+ const { policy } = loadPinnedPolicy({ dataRoot: root, declaration, schemas, locateVerifier });
130
+ const { state, unmet } = assessTrustPolicyArming(policy, schemas, channel);
131
+ if (state === TrustPolicyArmingState.PARTIAL) {
132
+ if (declaration.required) {throw new EvidenceTrustRequiredError(root, `its declared policy is PARTIALLY ARMED on the ${channel} channel (${unmet.join('; ')})`);}
133
+ throw new Error(`the declared evidence trust policy is PARTIALLY ARMED on the ${channel} channel (${unmet.join('; ')}); an edition cannot be written against it`);
134
+ }
135
+ if (state === TrustPolicyArmingState.ARMED) {return EvidenceTrustState.Armed;}
136
+ if (declaration.required && channel !== EditionChannel.Develop) {
137
+ throw new EvidenceTrustRequiredError(root, `its declared policy is UNARMED on the ${channel} channel`);
138
+ }
139
+ return notArmed();
140
+ }
141
+
142
+ export const UNDECLARED_TRUST_DIRECTORY = 'trust';
143
+
144
+ /**
145
+ * Policy files the data root holds without declaring them: a top-level `*.json`
146
+ * the kernel parses as an offline trust policy, or a `trust/` directory. A file
147
+ * the kernel schema does not accept is not a policy, which is the whole test.
148
+ */
149
+ function undeclaredPolicyArtifacts(dataRoot, schemas) {
150
+ const found = [];
151
+ for (const entry of readdirSync(dataRoot, { withFileTypes: true })) {
152
+ if (entry.isDirectory() && entry.name === UNDECLARED_TRUST_DIRECTORY) {
153
+ found.push(`${UNDECLARED_TRUST_DIRECTORY}/`);
154
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
155
+ if (isTrustPolicy(join(dataRoot, entry.name), schemas)) {
156
+ found.push(entry.name);
157
+ }
158
+ }
159
+ }
160
+ return found.sort();
161
+ }
162
+
163
+ function isTrustPolicy(path, schemas) {
164
+ try {
165
+ schemas.parseSigstoreOfflineTrustPolicy(JSON.parse(readFileSync(path, 'utf8')));
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Run the gate against the configured data root. Returns true when green; every
174
+ * refusal is printed, naming the repair.
175
+ *
176
+ * @param {{ locateVerifier?: () => {bundlePath: string, provenancePath: string} }} [options]
177
+ * `locateVerifier` is a parameter so a test can supply a fixture bundle.
178
+ */
179
+ export async function checkEvidenceTrustPolicy({
180
+ locateVerifier = locateBuiltVerifier,
181
+ } = {}) {
182
+ try {
183
+ const dataRoot = configuredDataRoot();
184
+ const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
185
+ const schemas = await loadKernelDistributionSchemas();
186
+ if (declaration.policy === null) {
187
+ if (declaration.required) {throw new EvidenceTrustRequiredError(dataRoot, 'declares no policy');}
188
+ const undeclared = undeclaredPolicyArtifacts(dataRoot, schemas);
189
+ if (undeclared.length > 0) {
190
+ console.error(
191
+ `${TAG} image-evidence trust policy FAILED: ${dataRoot} holds ${undeclared.join(', ')} ` +
192
+ `but ${EDITION_WORKSPACE_CONFIG_FILE} declares evidenceTrustPolicy {"required": false} with no policy, so nothing reads it. ` +
193
+ 'Declare it ({"required": ..., "policy": ..., "pin": ...}) or remove it.',
194
+ );
195
+ return false;
196
+ }
197
+ console.log(
198
+ `${TAG} image-evidence trust policy: NOT ARMED — ${join(dataRoot, EDITION_WORKSPACE_CONFIG_FILE)} ` +
199
+ 'declares evidenceTrustPolicy {"required": false} with no policy, so this data root\'s editions are not evidence-gated.',
200
+ );
201
+ return true;
202
+ }
203
+
204
+ const { policy, policyDigest } = loadPinnedPolicy({ dataRoot, declaration, schemas, locateVerifier });
205
+ console.log(
206
+ `${TAG} image-evidence trust policy pin: green — ${declaration.pin} pins ${declaration.policy} ` +
207
+ `at ${policyDigest} and this CLI's verifier.`,
208
+ );
209
+
210
+ console.log(
211
+ `${TAG} image-evidence trust policy: schema green — pinned ${policy.cosignVersion}, ` +
212
+ `${policy.policies.length} authorized signer identities, ` +
213
+ `${policy.trustedCosignBinaryDigests.length} cosign / ` +
214
+ `${policy.trustedGitBinaryDigests.length} git binary pins.`,
215
+ );
216
+ // Each channel is judged on its own signers. The release channel is the one
217
+ // `required` speaks for; the develop channel admits not-armed and says so.
218
+ const partial = (channel, unmet) => {
219
+ console.error(
220
+ `${TAG} image-evidence trust policy is PARTIALLY ARMED on the ${channel} channel — this mixed state is invalid ` +
221
+ `and no ${channel} image evidence can verify against it:`,
222
+ );
223
+ for (const reason of unmet) {
224
+ console.error(`${TAG} - ${reason}`);
225
+ }
226
+ };
227
+ const develop = assessTrustPolicyArming(policy, schemas, EditionChannel.Develop);
228
+ if (develop.state === TrustPolicyArmingState.PARTIAL) {
229
+ partial(EditionChannel.Develop, develop.unmet);
230
+ return false;
231
+ }
232
+ const release = assessTrustPolicyArming(policy, schemas, EditionChannel.Release);
233
+ if (release.state === TrustPolicyArmingState.PARTIAL) {
234
+ partial(EditionChannel.Release, release.unmet);
235
+ return false;
236
+ }
237
+ if (release.state === TrustPolicyArmingState.UNARMED && declaration.required) {
238
+ const refusal = new EvidenceTrustRequiredError(dataRoot, 'its declared policy is UNARMED on the release channel');
239
+ console.error(`${TAG} ${refusal.code}: ${refusal.message}`);
240
+ return false;
241
+ }
242
+ if (develop.state === TrustPolicyArmingState.UNARMED) {console.error(`${TAG} ${DEVELOP_NOT_ARMED_NOTICE}`);}
243
+ if (release.state === TrustPolicyArmingState.UNARMED) {
244
+ console.warn(
245
+ `${TAG} image-evidence trust policy: declared but UNARMED on the release channel — delivery stays fail-closed ` +
246
+ 'until `xema edition check` reports it armed.',
247
+ );
248
+ return true;
249
+ }
250
+ console.log(`${TAG} image-evidence trust policy: ARMED on the release channel${develop.armed ? ' and on the develop channel' : ''}.`);
251
+ return true;
252
+ } catch (error) {
253
+ console.error(`${TAG} image-evidence trust policy FAILED: ${error.message}`);
254
+ return false;
255
+ }
256
+ }