@xemahq/xema 0.48.0 → 0.49.1

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 (98) hide show
  1. package/dist/assets/appliance-base/distribution.lock.json +3936 -1515
  2. package/dist/assets/appliance-base/images.lock.json +878 -1161
  3. package/dist/assets/appliance-lean/distribution.lock.json +3663 -916
  4. package/dist/assets/appliance-lean/images.lock.json +825 -924
  5. package/dist/assets/boot-graph.json +398 -93
  6. package/dist/assets/distribution-assets.manifest.json +10 -10
  7. package/dist/commands/edition.d.ts.map +1 -1
  8. package/dist/commands/edition.js +5 -3
  9. package/dist/commands/edition.js.map +1 -1
  10. package/dist/dev/env-defaults.d.ts.map +1 -1
  11. package/dist/dev/env-defaults.js +0 -1
  12. package/dist/dev/env-defaults.js.map +1 -1
  13. package/dist/edition/verify-image-evidence.bundle.mjs +4386 -2503
  14. package/dist/edition/verify-image-evidence.bundle.provenance.json +590 -488
  15. package/dist/runtime/auth/cli-application.d.ts +18 -0
  16. package/dist/runtime/auth/cli-application.d.ts.map +1 -0
  17. package/dist/runtime/auth/cli-application.js +39 -0
  18. package/dist/runtime/auth/cli-application.js.map +1 -0
  19. package/dist/runtime/edition/verb-map.d.ts +3 -1
  20. package/dist/runtime/edition/verb-map.d.ts.map +1 -1
  21. package/dist/runtime/edition/verb-map.js +5 -1
  22. package/dist/runtime/edition/verb-map.js.map +1 -1
  23. package/dist/runtime/env-seeder.d.ts.map +1 -1
  24. package/dist/runtime/env-seeder.js +21 -0
  25. package/dist/runtime/env-seeder.js.map +1 -1
  26. package/dist/runtime/launch.d.ts.map +1 -1
  27. package/dist/runtime/launch.js +0 -18
  28. package/dist/runtime/launch.js.map +1 -1
  29. package/edition/README.md +20 -3
  30. package/edition/biome-content.mjs +5 -2
  31. package/edition/build-runtime-image-source-index.mjs +79 -5
  32. package/edition/canonical-evidence-json.mjs +82 -0
  33. package/edition/catalogue-evidence.mjs +8 -5
  34. package/edition/customer-edition-verbs.mjs +9 -3
  35. package/edition/develop-coverage-run.mjs +2 -1
  36. package/edition/develop-coverage.mjs +12 -1
  37. package/edition/develop-edition-resolve.mjs +3 -2
  38. package/edition/develop-edition.mjs +15 -3
  39. package/edition/develop-record-verify.mjs +216 -0
  40. package/edition/develop-select-records.mjs +24 -225
  41. package/edition/develop-verify-image-record.mjs +90 -0
  42. package/edition/dist-check.mjs +24 -3
  43. package/edition/distribution-evidence-set.mjs +30 -2
  44. package/edition/edition-artifact.mjs +110 -22
  45. package/edition/edition-ceiling.mjs +180 -0
  46. package/edition/edition-config.mjs +91 -0
  47. package/edition/edition-constants.mjs +21 -3
  48. package/edition/edition-contract-parse.mjs +88 -3
  49. package/edition/edition-contract.json +47 -4
  50. package/edition/edition-image-seal.mjs +9 -2
  51. package/edition/edition-kind.mjs +10 -0
  52. package/edition/edition-protocol.mjs +7 -1
  53. package/edition/edition-publisher.mjs +21 -0
  54. package/edition/edition-seal.mjs +257 -0
  55. package/edition/evidence-grammars.mjs +2 -0
  56. package/edition/evidence-trust-pin.mjs +66 -10
  57. package/edition/evidence-trust-policy.mjs +39 -29
  58. package/edition/external-source-walk.mjs +16 -3
  59. package/edition/fleet-coordinate-finality.mjs +18 -5
  60. package/edition/fulcio-certificate-extensions.mjs +231 -0
  61. package/edition/image-evidence.mjs +34 -79
  62. package/edition/locked-bundle-signer.mjs +241 -0
  63. package/edition/oci-evidence-verbs.mjs +7 -1
  64. package/edition/pinned-cosign.mjs +70 -0
  65. package/edition/plan-image-evidence-refresh.mjs +3 -1
  66. package/edition/producer-evidence-bundle.mjs +3 -1
  67. package/edition/producer-evidence-index-file.mjs +4 -0
  68. package/edition/release-edition-artifact.mjs +3 -3
  69. package/edition/repin-evidence-trust.mjs +6 -1
  70. package/edition/resolve-distribution.mjs +90 -7
  71. package/edition/resolve-edition.mjs +1 -1
  72. package/edition/resolve-images.mjs +53 -7
  73. package/edition/seal-image-digests.mjs +19 -8
  74. package/edition/sealed-distribution-lock.mjs +2 -2
  75. package/edition/third-party-admission.mjs +91 -0
  76. package/edition/third-party-bundle-registry.mjs +147 -0
  77. package/edition/third-party-certificate-extensions.mjs +21 -0
  78. package/edition/third-party-image-pins.mjs +41 -0
  79. package/edition/third-party-pins-trusted.mjs +24 -0
  80. package/edition/third-party-producer-evidence.mjs +76 -0
  81. package/edition/third-party-source-check.mjs +28 -0
  82. package/edition/third-party-source-context.mjs +41 -0
  83. package/edition/third-party-source-evidence.mjs +255 -0
  84. package/edition/third-party-source-flags.mjs +134 -0
  85. package/edition/third-party-source-pins.mjs +259 -0
  86. package/edition/third-party-source-uniqueness.mjs +54 -0
  87. package/edition/third-party-source-walk.mjs +115 -0
  88. package/edition/third-party-source-writer.mjs +229 -0
  89. package/edition/third-party-subjects.mjs +128 -0
  90. package/edition/trust-policy-image-ownership.mjs +85 -0
  91. package/edition/verify-catalogue-evidence.mjs +2 -0
  92. package/edition/verify-image-evidence.mjs +2 -0
  93. package/edition/verify-image-platforms.mjs +33 -9
  94. package/package.json +5 -5
  95. package/dist/runtime/auth/client-bootstrap.d.ts +0 -11
  96. package/dist/runtime/auth/client-bootstrap.d.ts.map +0 -1
  97. package/dist/runtime/auth/client-bootstrap.js +0 -60
  98. package/dist/runtime/auth/client-bootstrap.js.map +0 -1
@@ -1,5 +1,4 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
2
  import {
4
3
  mkdtempSync,
5
4
  readFileSync,
@@ -15,8 +14,8 @@ import {
15
14
  resolve,
16
15
  sep,
17
16
  } from 'node:path';
18
- import { TextDecoder } from 'node:util';
19
17
 
18
+ import { canonicalJson, canonicalJsonBytes, decodeExactUtf8, parseCanonicalJsonBytes, sha256Digest } from './canonical-evidence-json.mjs';
20
19
  import { certificateConstraintArgs } from './cosign-certificate-constraints.mjs';
21
20
  import { assertCanonicalDistributionId } from './distribution-paths.mjs';
22
21
  import { effectiveRepositoryPins, manifestSourcePins } from './edition-source-pins.mjs';
@@ -27,22 +26,25 @@ import {
27
26
  validateImmutableImageLock,
28
27
  } from './lib.mjs';
29
28
  import { readBoundedNoFollowFile } from './no-follow-file.mjs';
29
+ import {
30
+ assertThirdPartySubjectAdmission,
31
+ thirdPartySourceRepositoryPins,
32
+ thirdPartySourcesByRepository,
33
+ } from './third-party-admission.mjs';
34
+ import { loadThirdPartySourcePins } from './third-party-source-pins.mjs';
30
35
  import {
31
36
  assertPreparedExecutable,
32
37
  exactExecutableOutput,
33
38
  prepareDigestPinnedExecutable,
34
39
  } from './trusted-executable.mjs';
35
40
 
36
- const STRICT_UTF8 = new TextDecoder('utf-8', {
37
- fatal: true,
38
- ignoreBOM: true,
39
- });
40
41
  const MAX_CANONICAL_JSON_BYTES = 2 * 1024 * 1024;
41
42
  const MAX_STATEMENT_BYTES = 2 * 1024 * 1024;
42
43
  const MAX_SIGSTORE_BUNDLE_BYTES = 16 * 1024 * 1024;
43
44
  const MAX_TRUSTED_ROOT_BYTES = 8 * 1024 * 1024;
44
45
  /** The kernel's `SignerKind.OidcCertificate`: the one signer kind this verifier runs (cosign keyless). */
45
46
  const OIDC_CERTIFICATE_SIGNER = 'oidc-certificate';
47
+ export { canonicalJson, canonicalJsonBytes, decodeExactUtf8, parseCanonicalJsonBytes, sha256Digest };
46
48
  export const COSIGN_ENV = Object.freeze({
47
49
  LANG: 'C',
48
50
  LC_ALL: 'C',
@@ -50,76 +52,6 @@ export const COSIGN_ENV = Object.freeze({
50
52
  TZ: 'UTC',
51
53
  });
52
54
 
53
- export function canonicalJson(value) {
54
- if (
55
- value === null ||
56
- typeof value === 'boolean' ||
57
- typeof value === 'string'
58
- ) {
59
- return JSON.stringify(value);
60
- }
61
- if (typeof value === 'number') {
62
- if (!Number.isSafeInteger(value)) {
63
- throw new Error('canonical evidence JSON permits only safe integers');
64
- }
65
- return JSON.stringify(value);
66
- }
67
- if (Array.isArray(value)) {
68
- return `[${value.map(canonicalJson).join(',')}]`;
69
- }
70
- if (typeof value === 'object') {
71
- const entries = Object.entries(value).sort(([left], [right]) =>
72
- compareStrings(left, right),
73
- );
74
- return `{${entries
75
- .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
76
- .join(',')}}`;
77
- }
78
- throw new Error(`unsupported canonical evidence JSON value ${typeof value}`);
79
- }
80
-
81
- export function canonicalJsonBytes(value) {
82
- return Buffer.from(`${canonicalJson(value)}\n`, 'utf8');
83
- }
84
-
85
- export function sha256Digest(bytes) {
86
- return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
87
- }
88
-
89
- /**
90
- * Exact UTF-8 decode. Exported because the producers need byte-for-byte the
91
- * same strictness the verifier applies — a producer that accepts bytes the
92
- * verifier will reject signs evidence that can never verify.
93
- */
94
- export function decodeExactUtf8(bytes, label) {
95
- let text;
96
- try {
97
- text = STRICT_UTF8.decode(bytes);
98
- } catch {
99
- throw new Error(`${label} contains invalid UTF-8`);
100
- }
101
- if (!Buffer.from(text, 'utf8').equals(bytes)) {
102
- throw new Error(`${label} is not exact UTF-8`);
103
- }
104
- return text;
105
- }
106
-
107
- export function parseCanonicalJsonBytes(bytes, label) {
108
- const text = decodeExactUtf8(bytes, label);
109
- let value;
110
- try {
111
- value = JSON.parse(text);
112
- } catch (error) {
113
- throw new Error(`${label} is not valid JSON: ${error.message}`);
114
- }
115
- const canonical = canonicalJsonBytes(value);
116
- if (!bytes.equals(canonical)) {
117
- throw new Error(
118
- `${label} is not canonical JSON (duplicate keys, non-canonical ordering, or whitespace are refused)`,
119
- );
120
- }
121
- return value;
122
- }
123
55
 
124
56
  export function resolveContainedFile(root, candidate, label) {
125
57
  if (
@@ -415,7 +347,7 @@ export function validateProducerStatementBinding({
415
347
  if (typeof subject.imageDigest !== 'string') {
416
348
  throw new Error(
417
349
  `${label}: images.lock.json seals no imageDigest for ${subject.kind}:${subject.id}, so the ` +
418
- 'attested manifest is bound to nothing. Run `xema edition seal` first.',
350
+ 'attested manifest is bound to nothing. Run `xema edition resolve-digests` first.',
419
351
  );
420
352
  }
421
353
  if (subject.imageDigest !== predicate.artifact.manifestDigest) {
@@ -690,6 +622,9 @@ export function verifySigstoreEvidenceDescriptor({
690
622
  policyRoot = evidenceRoot,
691
623
  trustedCosign,
692
624
  holdProducerWorkflow,
625
+ // The statement bytes are the DSSE payload a third party's tool signed (cosign's provenance), which is
626
+ // exact JSON but not this CLI's canonical form; only that caller replaces the canonical decoder.
627
+ decodeStatement = parseCanonicalJsonBytes,
693
628
  }) {
694
629
  assertEvidenceChannel(channel);
695
630
  const statementFile = readExactDescriptorFile(
@@ -738,7 +673,7 @@ export function verifySigstoreEvidenceDescriptor({
738
673
  );
739
674
  }
740
675
  const statement = parseStatement(
741
- parseCanonicalJsonBytes(statementFile.bytes, 'evidence statement'),
676
+ decodeStatement(statementFile.bytes, 'evidence statement'),
742
677
  );
743
678
  if (statement.predicateType !== predicateType) {
744
679
  throw new Error('evidence statement predicateType is not authorized');
@@ -850,6 +785,7 @@ export function verifySigstoreEvidenceDescriptor({
850
785
  return {
851
786
  statement,
852
787
  statementPath: statementFile.path,
788
+ bundle,
853
789
  identityPolicy: verifiedIdentityPolicy,
854
790
  };
855
791
  }
@@ -896,6 +832,7 @@ async function verifyDistributionImageEvidenceSetWithCosign({
896
832
  imageLock,
897
833
  repositoryPins,
898
834
  runtimeSourceIndex,
835
+ thirdPartySources,
899
836
  } = await loadValidatedLocks(distributionId, dataRoot, schemas, {
900
837
  editionsRoot,
901
838
  channel,
@@ -935,7 +872,7 @@ async function verifyDistributionImageEvidenceSetWithCosign({
935
872
  `runtime subject "${subject.kind}:${subject.id}" has no committed repository pin`,
936
873
  );
937
874
  }
938
- const { statement, identityPolicy } = verifySigstoreEvidenceDescriptor({
875
+ const { statement, identityPolicy, bundle } = verifySigstoreEvidenceDescriptor({
939
876
  evidenceRoot,
940
877
  descriptor,
941
878
  policy,
@@ -947,6 +884,12 @@ async function verifyDistributionImageEvidenceSetWithCosign({
947
884
  trustedCosign,
948
885
  holdProducerWorkflow: (producer) => schemas.producerWorkflowPinRefusal(runtimeSourceIndex, producer),
949
886
  });
887
+ const thirdPartySource = thirdPartySources.get(subject.repo);
888
+ if (thirdPartySource !== undefined) {
889
+ assertThirdPartySubjectAdmission({ subject, source: thirdPartySource, statement, identityPolicy, bundle });
890
+ }
891
+ // XEMA-1611 d9-1 first-party form pending d9: no repository-agreement assertion is made for a first-party subject
892
+ // (a monorepo path is not the signing row's repository, and the xema-deploy rows sign other repositories' images).
950
893
  validateProducerStatementBinding({
951
894
  statement,
952
895
  subject,
@@ -1347,10 +1290,22 @@ export async function loadValidatedImageLocks(
1347
1290
  })) {
1348
1291
  repositoryPins.set(repo, pin);
1349
1292
  }
1293
+ // Third-party OCI sources: pinned by the data root's third-party source pins (held to the evidence trust pin by the entry).
1294
+ const thirdPartyPins = loadThirdPartySourcePins(dataRoot);
1295
+ const thirdPartyRepositoryPins = thirdPartySourceRepositoryPins({
1296
+ distribution,
1297
+ imageLock,
1298
+ repositoryPins,
1299
+ thirdPartyPins,
1300
+ });
1301
+ for (const [repo, pin] of thirdPartyRepositoryPins) {
1302
+ repositoryPins.set(repo, pin);
1303
+ }
1350
1304
  return {
1351
1305
  schemas,
1352
1306
  imageLock,
1353
1307
  runtimeSourceIndex,
1354
1308
  repositoryPins,
1309
+ thirdPartySources: thirdPartySourcesByRepository(thirdPartyRepositoryPins, thirdPartyPins),
1355
1310
  };
1356
1311
  }
@@ -0,0 +1,241 @@
1
+ // The ONE place the edition CLI selects and checks the signer of an OCI bundle row (XEMA-1631).
2
+ //
3
+ // Every `bundleSource: {kind: 'oci'}` row of a lock (a biome's, or one of its host modules') states WHO signs its bundle:
4
+ // `bundleSigner: {policyId, signer}`, the kernel's `lockedBundleSignerOf` projection of ONE row of the data root's evidence
5
+ // trust policy. The row is selected, never defaulted:
6
+ // - a git-sourced row (one with a lock `source`) carries no OCI bundle at all, and a lock stating one is refused;
7
+ // - a third-party row (no `source`, `isThirdPartyOciRow`) by its pin (`third-party-source-pins.json`), whose `bundle.signerPolicyId` names an
8
+ // `oidc-certificate` row held to the checks `thirdPartySignerRow` makes;
9
+ // - a first-party row by the data root's `firstPartyBundleSigner.policyId`, a `public-key` row whose key is bound to
10
+ // no other row.
11
+ // A resolve fills the signer; a seal and a verify re-derive the selection and refuse a lock that states anything else.
12
+ //
13
+ // The kernel exports are loaded lazily, from the built kernel's `distribution` module as `kernel-signer-kind.mjs` does,
14
+ // never through lib.mjs (whose imports must stay loadable by v28-v31 kernels): a lock with no OCI row never touches them.
15
+
16
+ import { createRequire } from 'node:module';
17
+
18
+ import { editionWorkspaceConfig } from './edition-config.mjs';
19
+ import { assertKernelEditionProtocol } from './edition-protocol.mjs';
20
+ import { dataRoot, kernelDistPath, kernelPackageDir } from './edition-workspace.mjs';
21
+ import { loadPinnedDeclaredTrustPolicy } from './evidence-trust-pin.mjs';
22
+ import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs';
23
+ import { thirdPartySignerRow } from './third-party-source-evidence.mjs';
24
+ import { OCI_BUNDLE_SOURCE_KIND, isThirdPartyOciRow } from './third-party-subjects.mjs';
25
+
26
+ const requireFromHere = createRequire(import.meta.url);
27
+
28
+ const OIDC_CERTIFICATE_KIND = 'oidc-certificate';
29
+ const PUBLIC_KEY_KIND = 'public-key';
30
+
31
+ const KERNEL_EXPORTS = Object.freeze(['lockedBundleSignerOf', 'assertLockedBundleSignerMatches', 'LockedBundleSignerUnknownPolicyError', 'LockedBundleSignerAmbiguousPolicyError']);
32
+
33
+ /** A named refusal of the bundle signer plane. */
34
+ class BundleSignerError extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = new.target.name;
38
+ }
39
+ }
40
+
41
+ /** No trust policy row can be selected for the subject: no pin, no `firstPartyBundleSigner`, or no policy at all. */
42
+ export class BundleSignerPolicyAbsentError extends BundleSignerError {}
43
+ /** The selected row is not of the kind its origin requires. */
44
+ export class BundleSignerKindMismatchError extends BundleSignerError {}
45
+ /** A first-party bundle key is also carried by another policy row. */
46
+ export class BundleSignerKeyBoundElsewhereError extends BundleSignerError {}
47
+ /** The lock names a policy that exists but is not the one selection picks for the subject. */
48
+ export class BundleSignerPolicyNotSelectedError extends BundleSignerError {}
49
+ /** An OCI row of a lock states no `bundleSigner`. */
50
+ export class BundleSignerMissingError extends BundleSignerError {}
51
+ /** A lock row drawn from a git `source` states an OCI `bundleSource`: a git-sourced biome is built from that source, never pulled as a bundle. */
52
+ export class BundleSourceOnGitRowError extends BundleSignerError {}
53
+ /** A resolve found an OCI host-module bundle on a git-sourced biome: such a bundle reaches an edition only through an `oci` source. */
54
+ export class GitSourcedHostModuleBundleError extends BundleSignerError {}
55
+ /** The loaded kernel does not export what the bundle signer plane needs. */
56
+ export class BundleSignerKernelUnsupportedError extends BundleSignerError {}
57
+
58
+ /** What the kernel must export, validated once: `module` is the loaded kernel `distribution` module. */
59
+ export function bundleSignerKernelFrom(module, location) {
60
+ const missing = KERNEL_EXPORTS.filter((name) => module?.[name] === undefined);
61
+ if (missing.length > 0) {
62
+ throw new BundleSignerKernelUnsupportedError(
63
+ `an OCI bundle row needs a kernel speaking edition protocol v33 or later; the kernel distribution module at ${location} lacks ${missing.join(', ')}`,
64
+ );
65
+ }
66
+ return Object.freeze(Object.fromEntries(KERNEL_EXPORTS.map((name) => [name, module[name]])));
67
+ }
68
+
69
+ /** The kernel's bundle signer exports, from the built kernel; refuses a kernel that speaks an older protocol. */
70
+ export function loadBundleSignerKernel() {
71
+ assertKernelEditionProtocol(kernelPackageDir());
72
+ const location = kernelDistPath('distribution', 'index.js');
73
+ return bundleSignerKernelFrom(requireFromHere(location), location);
74
+ }
75
+
76
+ const describeSubject = ({ biomeId, componentKey }) =>
77
+ componentKey === undefined ? `biome "${biomeId}"` : `host module "${componentKey}" of biome "${biomeId}"`;
78
+
79
+ /** A git-sourced row carries no OCI bundle (resolve omits it), so no signer is selected for one; a lock stating one is refused. */
80
+ function assertNotGitSourced(subject) {
81
+ if (subject.source !== undefined) {
82
+ throw new BundleSourceOnGitRowError(
83
+ `${describeSubject(subject)} is drawn from git source "${subject.source?.name ?? subject.source}" and states an OCI bundleSource; a git-sourced biome carries none ` +
84
+ '(a third-party biome is an OCI bundle only as an `oci` source with a third-party pin)',
85
+ );
86
+ }
87
+ }
88
+
89
+ const rowsOf = (policy, policyId) => policy.policies.filter((row) => row.policyId === policyId);
90
+
91
+ /**
92
+ * @param {{policy: {policies: readonly object[]}|null, thirdPartyPins: {byRepository: (repository: string) => object|undefined}|undefined,
93
+ * firstPartyPolicyId: string|undefined, schemas: object, kernel?: object}} options
94
+ * @returns {{policyIdFor: (subject: object) => string, signerFor: (subject: object) => object, assertSealed: (subject: object, stated: object|undefined) => void}}
95
+ */
96
+ export function createBundleSignerSelection({ policy, thirdPartyPins, firstPartyPolicyId, schemas, kernel }) {
97
+ let loaded = kernel;
98
+ const kernelExports = () => (loaded ??= loadBundleSignerKernel());
99
+
100
+ function onlyRow(policyId) {
101
+ const { LockedBundleSignerUnknownPolicyError, LockedBundleSignerAmbiguousPolicyError } = kernelExports();
102
+ const rows = rowsOf(policy, policyId);
103
+ if (rows.length === 0) {throw new LockedBundleSignerUnknownPolicyError(policyId);}
104
+ if (rows.length > 1) {throw new LockedBundleSignerAmbiguousPolicyError(policyId, rows.length);}
105
+ return rows[0];
106
+ }
107
+
108
+ function requireKind(subject, row, kind) {
109
+ if (row.kind !== kind) {
110
+ throw new BundleSignerKindMismatchError(
111
+ `${describeSubject(subject)}: trust policy row "${row.policyId}" is of kind ${JSON.stringify(row.kind)}, but a ${isThirdPartyOciRow(subject) ? 'third-party' : 'first-party'} bundle is signed by a "${kind}" row`,
112
+ );
113
+ }
114
+ }
115
+
116
+ function thirdPartyRow(subject) {
117
+ const pin = thirdPartyPins?.byRepository(subject.repo);
118
+ if (pin === undefined) {
119
+ throw new BundleSignerPolicyAbsentError(
120
+ `${describeSubject(subject)} is a third-party OCI bundle, but no third-party source pin names repository "${subject.repo}" to select its bundle signer`,
121
+ );
122
+ }
123
+ const policyId = pin.bundle.signerPolicyId;
124
+ const row = onlyRow(policyId);
125
+ requireKind(subject, row, OIDC_CERTIFICATE_KIND);
126
+ return thirdPartySignerRow({ policy, policyId, source: pin, schemas, label: `${describeSubject(subject)} bundle signer` });
127
+ }
128
+
129
+ function firstPartyRow(subject) {
130
+ if (firstPartyPolicyId === undefined) {
131
+ throw new BundleSignerPolicyAbsentError(
132
+ `${describeSubject(subject)} is a first-party OCI bundle, but the data root declares no "firstPartyBundleSigner" to select its bundle signer`,
133
+ );
134
+ }
135
+ const row = onlyRow(firstPartyPolicyId);
136
+ requireKind(subject, row, PUBLIC_KEY_KIND);
137
+ const bound = policy.policies.find((other) => other !== row && other.publicKey?.sha256 === row.publicKey.sha256);
138
+ if (bound !== undefined) {
139
+ throw new BundleSignerKeyBoundElsewhereError(
140
+ `${describeSubject(subject)}: the bundle key of row "${row.policyId}" is also carried by row "${bound.policyId}"; the bundle key must be bound to exactly one row, the bundle-signing one`,
141
+ );
142
+ }
143
+ return row;
144
+ }
145
+
146
+ function requirePolicy(subject) {
147
+ if (policy === null) {
148
+ throw new BundleSignerPolicyAbsentError(
149
+ `${describeSubject(subject)} is an OCI bundle, but the data root declares no evidenceTrustPolicy, so there is no row to select its bundle signer from`,
150
+ );
151
+ }
152
+ }
153
+
154
+ function selectedRow(subject) {
155
+ assertNotGitSourced(subject);
156
+ requirePolicy(subject);
157
+ return isThirdPartyOciRow(subject) ? thirdPartyRow(subject) : firstPartyRow(subject);
158
+ }
159
+
160
+ const signerFor = (subject) => {
161
+ const row = selectedRow(subject);
162
+ const { lockedBundleSignerOf, assertLockedBundleSignerMatches } = kernelExports();
163
+ const bundleSigner = lockedBundleSignerOf(row);
164
+ assertLockedBundleSignerMatches(bundleSigner, policy);
165
+ return bundleSigner;
166
+ };
167
+
168
+ return {
169
+ policyIdFor: (subject) => selectedRow(subject).policyId,
170
+ signerFor,
171
+ assertSealed(subject, stated) {
172
+ if (stated === undefined) {
173
+ throw new BundleSignerMissingError(`${describeSubject(subject)} is an OCI bundle row and states no bundleSigner`);
174
+ }
175
+ requirePolicy(subject);
176
+ const row = kernelExports().assertLockedBundleSignerMatches(stated, policy);
177
+ const expected = selectedRow(subject).policyId;
178
+ if (row.policyId !== expected) {
179
+ throw new BundleSignerPolicyNotSelectedError(
180
+ `${describeSubject(subject)} states bundle signer policy "${row.policyId}", but its bundle signer is selected from policy "${expected}"`,
181
+ );
182
+ }
183
+ },
184
+ };
185
+ }
186
+
187
+ /**
188
+ * The data root's selection, LAZY: nothing is read until the first OCI subject asks, so a lock with no OCI row never
189
+ * needs a policy, a pin or a kernel that speaks v33.
190
+ */
191
+ export function loadBundleSignerSelection({ dataRoot, schemas }) {
192
+ let selection;
193
+ const resolved = () => {
194
+ if (selection === undefined) {
195
+ const config = editionWorkspaceConfig();
196
+ const declaration = config.evidenceTrustPolicy;
197
+ selection = createBundleSignerSelection({
198
+ policy: declaration.policy === null ? null : loadPinnedDeclaredTrustPolicy({ dataRoot, declaration, schemas }),
199
+ thirdPartyPins: loadTrustedThirdPartySourcePins(dataRoot),
200
+ firstPartyPolicyId: config.firstPartyBundleSigner?.policyId,
201
+ schemas,
202
+ });
203
+ }
204
+ return selection;
205
+ };
206
+ return Object.freeze({
207
+ policyIdFor: (subject) => resolved().policyIdFor(subject),
208
+ signerFor: (subject) => resolved().signerFor(subject),
209
+ assertSealed: (subject, stated) => resolved().assertSealed(subject, stated),
210
+ });
211
+ }
212
+
213
+ const isOci = (row) => row.bundleSource?.kind === OCI_BUNDLE_SOURCE_KIND;
214
+
215
+ /** Every OCI subject of a lock: each biome and each host module whose `bundleSource` is OCI, with the biome's origin and repo. */
216
+ export function lockBundleSubjects(lock) {
217
+ return lock.biomes.flatMap((biome) => [
218
+ ...(isOci(biome) ? [{ row: biome, subject: { biomeId: biome.id, origin: biome.origin, repo: biome.repo, source: biome.source, bundleSource: biome.bundleSource } }] : []),
219
+ ...(biome.hostModules ?? [])
220
+ .filter(isOci)
221
+ .map((hostModule) => ({
222
+ row: hostModule,
223
+ subject: { biomeId: biome.id, componentKey: hostModule.componentKey, origin: biome.origin, repo: biome.repo, source: biome.source, bundleSource: hostModule.bundleSource },
224
+ })),
225
+ ]);
226
+ }
227
+
228
+ /**
229
+ * Refuse a lock whose OCI rows do not state exactly the bundle signer the selection picks for them. A lock with no OCI
230
+ * row is never checked and never resolves a data root: `selection` is used when given, else the data root's is loaded
231
+ * on the first OCI subject.
232
+ */
233
+ export function assertLockBundleSigners({ lock, selection, schemas }) {
234
+ const subjects = lockBundleSubjects(lock);
235
+ if (subjects.length === 0) {return;}
236
+ const chosen = selection ?? loadBundleSignerSelection({ dataRoot: dataRoot(), schemas });
237
+ for (const { row, subject } of subjects) {
238
+ assertNotGitSourced(subject);
239
+ chosen.assertSealed(subject, row.bundleSigner);
240
+ }
241
+ }
@@ -232,10 +232,16 @@ export async function planOciEvidenceBackfill({
232
232
  const missingGit = new Map();
233
233
  for (const distributionId of distributionIds) {
234
234
  assertCanonicalDistributionId(distributionId);
235
- const { imageLock, repositoryPins } = await loadValidatedImageLocks(distributionId, data, schemas, {
235
+ const { imageLock, repositoryPins, thirdPartySources } = await loadValidatedImageLocks(distributionId, data, schemas, {
236
236
  editionsRoot: editions,
237
237
  });
238
238
  for (const subject of imageLock.subjects) {
239
+ if (thirdPartySources.has(subject.repo)) {
240
+ // Evidence is attached to the registry image of a producer this fleet owns. A third-party image belongs to its
241
+ // publisher (its contentHash is not a tag here): its evidence is the data root's committed evidence, bound by its pin.
242
+ process.stderr.write(`[oci-evidence] ${distributionId} ${subject.kind}:${subject.id} is a third-party subject (${subject.repo}): no OCI evidence is attached to a publisher's image\n`);
243
+ continue;
244
+ }
239
245
  const source = expectedProducerSource({
240
246
  subject,
241
247
  repositoryPin: repositoryPins.get(subject.repo),
@@ -0,0 +1,70 @@
1
+ // pinned-cosign — the one way this CLI runs `cosign verify`: the caller's absolute cosign, held to the
2
+ // cosign digest the data root's pinned evidence trust policy names; the policy's pinned Sigstore trusted
3
+ // root written to a private scratch file for `--offline --trusted-root`; and cosign's registry credential
4
+ // taken from the SAME declaration oras uses, in its environment, never argv. Every injection exists for
5
+ // tests; the defaults are the real readers. Shared by the build-record verifier (develop-record-verify.mjs)
6
+ // and the edition seal (edition-seal.mjs), so there is one copy of this trust path.
7
+
8
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ import { basicAuthParts, defaultRunCosign } from './develop-coverage-readers.mjs';
13
+ import { dataRoot } from './edition-workspace.mjs';
14
+ import { loadDeclaredPinnedPolicy } from './evidence-trust-policy.mjs';
15
+ import { COSIGN_ENV, prepareTrustedCosignBinary, validatePinnedTrustedRoot } from './image-evidence.mjs';
16
+ import { loadRegistryCredentialDeclarations, resolveRegistryCredential } from './registry-credentials.mjs';
17
+ import { assertPreparedExecutable } from './trusted-executable.mjs';
18
+ import { registryClientsFor } from './verify-image-platforms.mjs';
19
+
20
+ /**
21
+ * Run `use({ policy, runCosign, cosignEnvFor, clientFor, trustedRootPath, ...prepared })` with a pinned cosign.
22
+ * `afterPolicy()` runs after the policy is loaded and before cosign is prepared; what it returns is spread into
23
+ * the argument of `use`. The scratch trusted root and the prepared cosign are disposed whatever `use` does.
24
+ */
25
+ export async function withPinnedCosign({
26
+ cosignBinary,
27
+ env,
28
+ loadPolicy = loadDeclaredPinnedPolicy,
29
+ prepareCosign = prepareTrustedCosignBinary,
30
+ trustedRootBytes = (root, pinnedPolicy) => validatePinnedTrustedRoot(root, pinnedPolicy).bytes,
31
+ cosignPathOf = (handle) => assertPreparedExecutable(handle, 'cosign'),
32
+ runCosign: injectedRunCosign,
33
+ clientsOverride,
34
+ afterPolicy = async () => ({}),
35
+ }, use) {
36
+ const { policy, dataRoot: policyRoot } = await loadPolicy();
37
+ const prepared = await afterPolicy();
38
+ const trustedCosign = prepareCosign({ cosignBinary, policy });
39
+ const cosignPath = cosignPathOf(trustedCosign);
40
+ const runCosign = injectedRunCosign ?? ((args, cosignEnv) => defaultRunCosign(cosignPath, args, cosignEnv));
41
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-pinned-cosign-'));
42
+ try {
43
+ const trustedRootPath = join(scratch, 'trusted-root.json');
44
+ writeFileSync(trustedRootPath, trustedRootBytes(policyRoot, policy), { flag: 'wx', mode: 0o600 });
45
+ const declarations = clientsOverride === undefined ? loadRegistryCredentialDeclarations(dataRoot()) : null;
46
+ const clients = new Map();
47
+ const clientFor = (authority) => {
48
+ if (clientsOverride !== undefined) {return clientsOverride(authority);}
49
+ if (!clients.has(authority)) {
50
+ clients.set(authority, registryClientsFor([{ imageRef: `${authority}/x` }], { declarations, env }).get(authority));
51
+ }
52
+ return clients.get(authority);
53
+ };
54
+ // cosign gets the SAME declared credential oras uses, in its environment, never argv.
55
+ const cosignEnvFor = (authority) => {
56
+ if (clientsOverride !== undefined) {return {};}
57
+ const credential = resolveRegistryCredential({ authority, declarations, env });
58
+ if (credential.access === 'anonymous') {return {};}
59
+ const { username, password } = basicAuthParts(credential);
60
+ return { COSIGN_REGISTRY_USERNAME: username, COSIGN_REGISTRY_PASSWORD: password };
61
+ };
62
+ return await use({ ...prepared, policy, runCosign, cosignEnvFor, clientFor, trustedRootPath });
63
+ } finally {
64
+ rmSync(scratch, { recursive: true, force: true });
65
+ trustedCosign.dispose();
66
+ }
67
+ }
68
+
69
+ /** The environment cosign runs with for one authority: the pinned base plus its declared credential. */
70
+ export const cosignEnvironment = (cosignEnvFor, authority) => ({ ...COSIGN_ENV, ...cosignEnvFor(authority) });
@@ -144,7 +144,7 @@ export async function planImageEvidenceRefresh({
144
144
 
145
145
  const subjects = [];
146
146
  for (const distributionId of [...distributionIds].sort(compareStrings)) {
147
- const { imageLock, repositoryPins, runtimeSourceIndex } = await loadValidatedImageLocks(
147
+ const { imageLock, repositoryPins, runtimeSourceIndex, thirdPartySources } = await loadValidatedImageLocks(
148
148
  distributionId,
149
149
  dataRootDirectory,
150
150
  schemas,
@@ -156,6 +156,8 @@ export async function planImageEvidenceRefresh({
156
156
  'runtime-image-source-index.json',
157
157
  );
158
158
  for (const subject of imageLock.subjects) {
159
+ // A third-party subject's contentHash is publisher-attested (no tag, no registry referrer here); its evidence is the committed bundle.
160
+ if (thirdPartySources.has(subject.repo)) {continue;}
159
161
  const repositoryPin = repositoryPins.get(subject.repo);
160
162
  const expectedSource = expectedProducerSource({
161
163
  subject,
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
2
  import { isAbsolute, join } from 'node:path';
3
3
 
4
+
4
5
  import {
5
6
  canonicalJson,
6
7
  decodeExactUtf8,
@@ -10,8 +11,9 @@ import {
10
11
  BUNDLE_MEDIA_TYPE,
11
12
  validateProducedBundle,
12
13
  } from './produce-image-source-evidence.mjs';
14
+ import { PRODUCER_EVIDENCE_INDEX_FILE } from './producer-evidence-index-file.mjs';
13
15
 
14
- export const PRODUCER_EVIDENCE_INDEX_FILE = 'producer-evidence.json';
16
+ export { PRODUCER_EVIDENCE_INDEX_FILE };
15
17
  export const PRODUCER_EVIDENCE_INDEX_MEDIA_TYPE =
16
18
  'application/vnd.xema.image-source-evidence.index.v1+json';
17
19
  export const PRODUCER_STATEMENT_MEDIA_TYPE = 'application/vnd.in-toto+json';
@@ -0,0 +1,4 @@
1
+ // The file name of a producer evidence directory's index. Its own module because the evidence verifier bundle needs it and must
2
+ // not import a producer entry (every module of a bundle shares one `import.meta.url`, so a producer's entry guard would run
3
+ // inside the verifier).
4
+ export const PRODUCER_EVIDENCE_INDEX_FILE = 'producer-evidence.json';
@@ -20,7 +20,7 @@ import { writeCatalogue } from './catalogue.mjs';
20
20
  import { assertCanonicalDistributionId } from './distribution-paths.mjs';
21
21
  import { LOCAL_WORKFLOW, assertEngineImage, editionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
22
22
  import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
23
- import { EditionChannel, EditionLockFile } from './edition-constants.mjs';
23
+ import { EditionChannel, EditionKind, EditionLockFile } from './edition-constants.mjs';
24
24
  import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
25
25
  import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule } from './edition-workspace.mjs';
26
26
  import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
@@ -82,7 +82,7 @@ export function assertSealedImageLock(lock, label) {
82
82
  if (unsealed.length > 0) {
83
83
  throw new Error(
84
84
  `${label} is not sealed: ${unsealed.length} of ${subjects.length} subjects carry no imageDigest ` +
85
- `(first: ${unsealed[0].kind}:${unsealed[0].id}). Run \`xema edition seal\` first.`,
85
+ `(first: ${unsealed[0].kind}:${unsealed[0].id}). Run \`xema edition resolve-digests\` first.`,
86
86
  );
87
87
  }
88
88
  }
@@ -128,7 +128,7 @@ async function run(argv, env) {
128
128
  const locks = await readCommittedEdition(id);
129
129
  // Refuses (EvidenceTrustRequiredError) unless the policy is ARMED on the
130
130
  // release channel: a release artifact is never written not-armed.
131
- const evidenceTrustPolicy = await descriptorEvidenceTrust({ channel: EditionChannel.Release });
131
+ const evidenceTrustPolicy = await descriptorEvidenceTrust({ kind: EditionKind.Resolved, channel: EditionChannel.Release });
132
132
  const outDir = resolve(options['--out']);
133
133
  writeEditionArtifact({
134
134
  outDir,
@@ -4,7 +4,8 @@
4
4
  // verifier bundle and provenance, the kernel contract authority the data
5
5
  // root's runtime image source index states (a customer data root has no such
6
6
  // index: the pin then records none and pins only what exists), and the oras
7
- // binaries that may read a registry. The result is a data-root change to review; nothing here is
7
+ // binaries that may read a registry, and (only when the data root holds `third-party-source-pins.json`) that
8
+ // file's sha256. The result is a data-root change to review; nothing here is
8
9
  // generated into the library.
9
10
  //
10
11
  // [--trusted-oras-digest sha256:<64 hex>]… the oras binaries to trust. Without
@@ -32,6 +33,7 @@ import {
32
33
  writeEvidenceTrustPin,
33
34
  } from './evidence-trust-pin.mjs';
34
35
  import { loadKernelDistributionSchemas } from './lib.mjs';
36
+ import { loadThirdPartySourcePins, thirdPartySourcePinsDigest } from './third-party-source-pins.mjs';
35
37
 
36
38
  /**
37
39
  * Write the pin for the configured data root and return it.
@@ -59,6 +61,8 @@ export async function repinEvidenceTrust({
59
61
  }
60
62
  orasDigests = readEvidenceTrustPin({ dataRoot: root, declaration }).trustedOrasBinaryDigests;
61
63
  }
64
+ // A present file is a reviewed record: a schema-invalid one is refused here, never pinned.
65
+ loadThirdPartySourcePins(root);
62
66
  return writeEvidenceTrustPin({
63
67
  dataRoot: root,
64
68
  declaration,
@@ -66,6 +70,7 @@ export async function repinEvidenceTrust({
66
70
  verifier,
67
71
  kernelContractAuthority: hasRuntimeImageSourceIndex(root) ? readStatedKernelContractAuthority({ dataRoot: root, schemas }) : null,
68
72
  trustedOrasBinaryDigests: orasDigests,
73
+ thirdPartySourcePinsDigest: thirdPartySourcePinsDigest(root),
69
74
  });
70
75
  }
71
76