@xemahq/xema 0.40.3 → 0.41.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.
- package/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +1 -0
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +289 -19
- package/dist/edition/verify-image-evidence.bundle.provenance.json +71 -47
- package/dist/runtime/edition/verb-map.d.ts +3 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +4 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/edition/README.md +5 -0
- package/edition/catalogue-evidence.mjs +225 -0
- package/edition/catalogue-fetch.mjs +40 -7
- package/edition/customer-edition-verbs.mjs +477 -0
- package/edition/customer-flags.mjs +2 -0
- package/edition/develop-coverage-readers.mjs +212 -0
- package/edition/develop-coverage-run.mjs +48 -0
- package/edition/develop-coverage-verdict.mjs +153 -0
- package/edition/develop-coverage.mjs +120 -0
- package/edition/develop-edition-resolve.mjs +18 -7
- package/edition/develop-edition.mjs +47 -220
- package/edition/develop-freeze.mjs +50 -13
- package/edition/develop-image-probe.mjs +7 -7
- package/edition/dispatched-runtime-components.mjs +90 -1
- package/edition/edition-artifact.mjs +31 -14
- package/edition/edition-constants.mjs +9 -0
- package/edition/edition-contract.json +2 -1
- package/edition/{develop-seal.mjs → edition-image-seal.mjs} +36 -17
- package/edition/edition-workspace.mjs +4 -3
- package/edition/evidence-trust-pin.mjs +28 -8
- package/edition/evidence-trust-policy.mjs +51 -7
- package/edition/image-evidence.mjs +2 -2
- package/edition/lib.mjs +20 -11
- package/edition/oci-image-source-evidence.mjs +3 -0
- package/edition/oras-process.mjs +3 -3
- package/edition/oras.mjs +47 -5
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-source-evidence.mjs +11 -2
- package/edition/release-edition.mjs +54 -0
- package/edition/repin-evidence-trust.mjs +5 -3
- package/edition/resolve-images.mjs +40 -5
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/trust-policy-arming.mjs +14 -1
- package/edition/verify-catalogue-evidence.mjs +245 -0
- package/edition/verify-image-evidence.mjs +15 -1
- package/package.json +2 -2
- package/edition/develop-customer-edition.mjs +0 -149
package/edition/oras.mjs
CHANGED
|
@@ -255,7 +255,7 @@ export function prepareTrustedOras({ env = process.env, trustedDigests, notice =
|
|
|
255
255
|
/** The default runner: the trusted oras of the configured data root, under the scrubbed environment. */
|
|
256
256
|
function defaultRunner(trust = configuredOrasTrust) {
|
|
257
257
|
let prepared;
|
|
258
|
-
return async (args, { stdin } = {}) => {
|
|
258
|
+
return async (args, { stdin, cwd } = {}) => {
|
|
259
259
|
if (prepared === undefined) {
|
|
260
260
|
prepared = prepareTrustedOras({ trustedDigests: trust() });
|
|
261
261
|
process.once('exit', prepared.dispose);
|
|
@@ -263,6 +263,7 @@ function defaultRunner(trust = configuredOrasTrust) {
|
|
|
263
263
|
try {
|
|
264
264
|
return await runExecutable(prepared.path(), args, {
|
|
265
265
|
input: stdin,
|
|
266
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
266
267
|
env: orasEnvironment(prepared.home),
|
|
267
268
|
timeoutMs: ORAS_RUN_TIMEOUT_MS,
|
|
268
269
|
maxBufferBytes: ORAS_MAX_BUFFER_BYTES,
|
|
@@ -364,7 +365,7 @@ export function disposeRegistrySessions() {
|
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
/**
|
|
367
|
-
*
|
|
368
|
+
* An OCI registry client (reads, and `attach`) bound to ONE registry authority and ONE
|
|
368
369
|
* resolved credential, doing every operation with the pinned oras.
|
|
369
370
|
*/
|
|
370
371
|
export class OrasRegistry {
|
|
@@ -456,7 +457,7 @@ export class OrasRegistry {
|
|
|
456
457
|
}
|
|
457
458
|
|
|
458
459
|
/** One oras invocation under this registry's shared empty config; returns stdout bytes. */
|
|
459
|
-
async #oras(subcommand, flags, ref) {
|
|
460
|
+
async #oras(subcommand, flags, ref, { cwd, operands = [] } = {}) {
|
|
460
461
|
await this.#ensureRealmIsSameOrigin();
|
|
461
462
|
const { configPath } = registrySession(this.authority);
|
|
462
463
|
const args = [...subcommand, '--registry-config', configPath];
|
|
@@ -465,9 +466,9 @@ export class OrasRegistry {
|
|
|
465
466
|
args.push('--username', this.#username, '--password-stdin');
|
|
466
467
|
stdin = this.#token;
|
|
467
468
|
}
|
|
468
|
-
args.push(...flags, ref);
|
|
469
|
+
args.push(...flags, ref, ...operands);
|
|
469
470
|
try {
|
|
470
|
-
const out = await this.#run(args, { stdin });
|
|
471
|
+
const out = await this.#run(args, { stdin, ...(cwd === undefined ? {} : { cwd }) });
|
|
471
472
|
return Buffer.isBuffer(out) ? out : Buffer.from(out ?? '');
|
|
472
473
|
} catch (error) {
|
|
473
474
|
throw this.#failure(ref, error);
|
|
@@ -559,6 +560,47 @@ export class OrasRegistry {
|
|
|
559
560
|
return descriptor;
|
|
560
561
|
}
|
|
561
562
|
|
|
563
|
+
/**
|
|
564
|
+
* ATTACH `files` (paths relative to `cwd`, each with its media type) as an artifact of `artifactType` that is a referrer
|
|
565
|
+
* of `subjectDigest`, under the same referrers form discover reads ({@link REFERRERS_DISTRIBUTION_SPEC}). Returns the
|
|
566
|
+
* digest of the referrer manifest oras reports. The only write this client does; the caller states every byte.
|
|
567
|
+
*
|
|
568
|
+
* @param {string} repository
|
|
569
|
+
* @param {string} subjectDigest
|
|
570
|
+
* @param {{ artifactType: string, files: {path: string, mediaType: string}[], annotations?: Record<string,string>, cwd: string }} attachment
|
|
571
|
+
*/
|
|
572
|
+
async attach(repository, subjectDigest, { artifactType, files, annotations = {}, cwd }) {
|
|
573
|
+
if (!SHA256_DIGEST_PATTERN.test(subjectDigest)) {
|
|
574
|
+
throw new Error(`"${subjectDigest}" is not a canonical lowercase sha256 digest`);
|
|
575
|
+
}
|
|
576
|
+
if (typeof artifactType !== 'string' || artifactType.length === 0) {
|
|
577
|
+
throw new Error('an attach requires a non-empty artifact type');
|
|
578
|
+
}
|
|
579
|
+
if (!Array.isArray(files) || files.length === 0 || typeof cwd !== 'string' || !isAbsolute(cwd)) {
|
|
580
|
+
throw new Error('an attach requires at least one file and an absolute working directory');
|
|
581
|
+
}
|
|
582
|
+
const ref = `${this.#ref(repository)}@${subjectDigest}`;
|
|
583
|
+
const flags = ['--distribution-spec', REFERRERS_DISTRIBUTION_SPEC, '--artifact-type', artifactType];
|
|
584
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
585
|
+
flags.push('--annotation', `${key}=${value}`);
|
|
586
|
+
}
|
|
587
|
+
flags.push('--format', 'json');
|
|
588
|
+
const out = await this.#oras(['attach'], flags, ref, {
|
|
589
|
+
cwd,
|
|
590
|
+
operands: files.map((file) => `${file.path}:${file.mediaType}`),
|
|
591
|
+
});
|
|
592
|
+
let document;
|
|
593
|
+
try {
|
|
594
|
+
document = JSON.parse(out.toString('utf8'));
|
|
595
|
+
} catch (error) {
|
|
596
|
+
throw new Error(`${ref} attach did not report JSON: ${error.message}`);
|
|
597
|
+
}
|
|
598
|
+
if (!SHA256_DIGEST_PATTERN.test(document?.digest)) {
|
|
599
|
+
throw new Error(`${ref} attach reported no canonical referrer digest`);
|
|
600
|
+
}
|
|
601
|
+
return document.digest;
|
|
602
|
+
}
|
|
603
|
+
|
|
562
604
|
/** Every tag of a repository; oras follows the registry's pagination. A failure throws, never an empty list. */
|
|
563
605
|
async listTags(repository) {
|
|
564
606
|
const ref = this.#ref(repository);
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The edition catalogue statement (E4): "this offering artifact's catalogue layer and lock layer are these digests, on this
|
|
3
|
+
// channel", signed by the vendor's release workflow and attached to the artifact as an `oras attach` referrer.
|
|
4
|
+
//
|
|
5
|
+
// xema edition evidence statement catalogue stage|accept …
|
|
6
|
+
//
|
|
7
|
+
// `stage` reads the artifact manifest by digest, derives the statement from its catalogue layer and lock layer and writes
|
|
8
|
+
// the canonical statement into a NEW staging directory, printing the signing plan; the caller signs it (`cosign
|
|
9
|
+
// attest-blob`); `accept` re-derives the statement from the same flags, refuses staged bytes that differ, verifies the
|
|
10
|
+
// signed bundle with cosign against the declared signer, seals the output (built from the re-derived files, never renamed
|
|
11
|
+
// from the staging) and ATTACHES statement.json and bundle.sigstore.json to the artifact digest as a referrer carrying the
|
|
12
|
+
// signing workflow's commit. A customer's pinned verifier reads exactly that referrer (catalogue-evidence.mjs).
|
|
13
|
+
// This module never signs and never requests an OIDC token.
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
16
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
CATALOGUE_EVIDENCE_BUNDLE_TITLE,
|
|
20
|
+
CATALOGUE_EVIDENCE_STATEMENT_MEDIA_TYPE,
|
|
21
|
+
CATALOGUE_EVIDENCE_STATEMENT_TITLE,
|
|
22
|
+
CATALOGUE_EVIDENCE_WORKFLOW_SHA_ANNOTATION,
|
|
23
|
+
} from './catalogue-evidence.mjs';
|
|
24
|
+
import { layerTitled, parseCatalogueRef } from './catalogue-fetch.mjs';
|
|
25
|
+
import { CATALOGUE_FILE } from './catalogue.mjs';
|
|
26
|
+
import {
|
|
27
|
+
acceptSignedEvidenceStatement,
|
|
28
|
+
assertPinnedCosignVersion,
|
|
29
|
+
evidenceChannelOfSigner,
|
|
30
|
+
loadPinnedTrustPolicy,
|
|
31
|
+
parseEvidenceCliFlags,
|
|
32
|
+
stageEvidenceStatement,
|
|
33
|
+
} from './distribution-evidence-set.mjs';
|
|
34
|
+
import { EDITION_ARTIFACT_TYPE, EditionLockFile } from './edition-constants.mjs';
|
|
35
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot, isEntryModule } from './edition-workspace.mjs';
|
|
36
|
+
import { serializeJson } from './lib.mjs';
|
|
37
|
+
import { EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE, createRegistryClientResolver } from './oci-image-source-evidence.mjs';
|
|
38
|
+
import { githubWorkflowSigner } from './produce-distribution-completeness-evidence.mjs';
|
|
39
|
+
import {
|
|
40
|
+
BUNDLE_MEDIA_TYPE,
|
|
41
|
+
assertGitHubProducerContext,
|
|
42
|
+
createCosignVerifier,
|
|
43
|
+
loadProducerKernelSchemas,
|
|
44
|
+
} from './produce-image-source-evidence.mjs';
|
|
45
|
+
import { assertStagedExactly, discardStaging, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
|
|
46
|
+
|
|
47
|
+
const STATEMENT_TYPE = 'https://in-toto.io/Statement/v1';
|
|
48
|
+
const hexOf = (digest) => digest.slice('sha256:'.length);
|
|
49
|
+
|
|
50
|
+
const SHARED_FLAGS = Object.freeze([
|
|
51
|
+
'artifact',
|
|
52
|
+
'offering',
|
|
53
|
+
'staging',
|
|
54
|
+
'signer-repository',
|
|
55
|
+
'signer-ref',
|
|
56
|
+
'signer-workflow-path',
|
|
57
|
+
'github-repository',
|
|
58
|
+
'github-ref',
|
|
59
|
+
'github-sha',
|
|
60
|
+
'github-workflow-ref',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The E4 statement of the offering artifact `artifact` (`oci://<repository>@sha256:<d>`), derived from the manifest the
|
|
65
|
+
* registry serves for that digest: its catalogue layer and lock layer, exactly as a customer's `fetchCatalogue` reads them.
|
|
66
|
+
* The channel is the signer's, and must be the repository's.
|
|
67
|
+
*/
|
|
68
|
+
export async function deriveEditionCatalogueStatement({ artifact, offeringId, signer, schemas, client }) {
|
|
69
|
+
const { repository, digest, edition } = parseCatalogueRef(artifact);
|
|
70
|
+
const channel = evidenceChannelOfSigner(signer, schemas);
|
|
71
|
+
if (edition.distributionId !== offeringId) {
|
|
72
|
+
throw new Error(`--offering ${offeringId} is not the offering of ${artifact} (its repository is the offering ${edition.distributionId})`);
|
|
73
|
+
}
|
|
74
|
+
if (edition.channel !== channel) {
|
|
75
|
+
throw new Error(`${artifact} is in a ${edition.channel} repository, but the signer signs for the ${channel} channel`);
|
|
76
|
+
}
|
|
77
|
+
const manifest = await client.getManifestByDigest(repository, digest);
|
|
78
|
+
if (manifest.isIndex || manifest.document.artifactType !== EDITION_ARTIFACT_TYPE) {
|
|
79
|
+
throw new Error(`${artifact} is not an edition artifact (artifactType ${JSON.stringify(manifest.document.artifactType)}, expected ${EDITION_ARTIFACT_TYPE})`);
|
|
80
|
+
}
|
|
81
|
+
const catalogueLayer = layerTitled(manifest, CATALOGUE_FILE, artifact);
|
|
82
|
+
const lockLayer = layerTitled(manifest, EditionLockFile.Distribution, artifact);
|
|
83
|
+
const statement = {
|
|
84
|
+
_type: STATEMENT_TYPE,
|
|
85
|
+
subject: [{ name: `xema-edition-${offeringId}`, digest: { sha256: hexOf(digest) } }],
|
|
86
|
+
predicateType: schemas.EDITION_CATALOGUE_PREDICATE_TYPE,
|
|
87
|
+
predicate: {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
offeringId,
|
|
90
|
+
channel,
|
|
91
|
+
catalogueDigest: catalogueLayer.digest,
|
|
92
|
+
lockDigest: hexOf(lockLayer.digest),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
schemas.parseEditionCatalogueStatement(statement);
|
|
96
|
+
return { statement, channel, repository, subjectDigest: digest };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function signingOf({ derived, options }) {
|
|
100
|
+
return {
|
|
101
|
+
statement: derived.statement,
|
|
102
|
+
root: options.stagingDirectory,
|
|
103
|
+
statementPath: CATALOGUE_EVIDENCE_STATEMENT_TITLE,
|
|
104
|
+
bundlePath: CATALOGUE_EVIDENCE_BUNDLE_TITLE,
|
|
105
|
+
signer: options.signer,
|
|
106
|
+
certificateWorkflowSha: options.signingWorkflowSha,
|
|
107
|
+
predicateType: options.schemas.EDITION_CATALOGUE_PREDICATE_TYPE,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Stage the catalogue statement of an artifact for signing. Returns the signing plan and the statement. */
|
|
112
|
+
export async function stageEditionCatalogueEvidence(options) {
|
|
113
|
+
const { stagingDirectory } = options;
|
|
114
|
+
if (typeof stagingDirectory !== 'string' || !isAbsolute(stagingDirectory)) {
|
|
115
|
+
throw new Error('the catalogue staging directory must be absolute');
|
|
116
|
+
}
|
|
117
|
+
if (existsSync(stagingDirectory)) {
|
|
118
|
+
throw new Error(`catalogue staging directory already exists: ${stagingDirectory}`);
|
|
119
|
+
}
|
|
120
|
+
assertGitHubProducerContext({ signer: options.signer, producerWorkflowSha: options.signingWorkflowSha }, options.githubContext);
|
|
121
|
+
const derived = await deriveEditionCatalogueStatement(options);
|
|
122
|
+
mkdirSync(stagingDirectory, { recursive: true, mode: 0o700 });
|
|
123
|
+
try {
|
|
124
|
+
const plan = stageEvidenceStatement(signingOf({ derived, options }));
|
|
125
|
+
return {
|
|
126
|
+
plan: { ...plan, kind: 'edition-catalogue', offeringId: options.offeringId, channel: derived.channel, subjectDigest: derived.subjectDigest },
|
|
127
|
+
statement: derived.statement,
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
rmSync(stagingDirectory, { recursive: true, force: true });
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Accept the signed catalogue bundle, seal `outputDirectory` and attach its two files to the artifact digest as an
|
|
137
|
+
* `EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE` referrer. The output is BUILT from the re-derived statement and the
|
|
138
|
+
* canonicalized bundle, never renamed from the staging, which must hold exactly what stage recorded and does not outlive
|
|
139
|
+
* the accept.
|
|
140
|
+
*/
|
|
141
|
+
export async function acceptEditionCatalogueEvidence(options) {
|
|
142
|
+
const { outputDirectory, stagingDirectory, readCosignVersion, runCosign, client } = options;
|
|
143
|
+
if (
|
|
144
|
+
typeof outputDirectory !== 'string' ||
|
|
145
|
+
!isAbsolute(outputDirectory) ||
|
|
146
|
+
typeof stagingDirectory !== 'string' ||
|
|
147
|
+
!isAbsolute(stagingDirectory)
|
|
148
|
+
) {
|
|
149
|
+
throw new Error('the catalogue output and staging directories must be absolute');
|
|
150
|
+
}
|
|
151
|
+
if (typeof readCosignVersion !== 'function' || typeof runCosign !== 'function') {
|
|
152
|
+
throw new Error('cosign seams are required');
|
|
153
|
+
}
|
|
154
|
+
const sealing = reserveSealing(outputDirectory, 'catalogue');
|
|
155
|
+
let derived;
|
|
156
|
+
try {
|
|
157
|
+
assertGitHubProducerContext({ signer: options.signer, producerWorkflowSha: options.signingWorkflowSha }, options.githubContext);
|
|
158
|
+
assertPinnedCosignVersion(readCosignVersion(), options.policy?.cosignVersion);
|
|
159
|
+
derived = await deriveEditionCatalogueStatement(options);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
discardStaging(stagingDirectory);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
const descriptor = sealAccepted({
|
|
165
|
+
stagingDirectory,
|
|
166
|
+
sealing,
|
|
167
|
+
outputDirectory,
|
|
168
|
+
build: () => {
|
|
169
|
+
assertStagedExactly(stagingDirectory, [CATALOGUE_EVIDENCE_STATEMENT_TITLE, CATALOGUE_EVIDENCE_BUNDLE_TITLE]);
|
|
170
|
+
return acceptSignedEvidenceStatement({ ...signingOf({ derived, options }), sealingRoot: sealing, runCosign });
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const referrerDigest = await client.attach(derived.repository, derived.subjectDigest, {
|
|
174
|
+
artifactType: EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE,
|
|
175
|
+
files: [
|
|
176
|
+
{ path: descriptor.statementPath, mediaType: CATALOGUE_EVIDENCE_STATEMENT_MEDIA_TYPE },
|
|
177
|
+
{ path: descriptor.bundlePath, mediaType: BUNDLE_MEDIA_TYPE },
|
|
178
|
+
],
|
|
179
|
+
annotations: { [CATALOGUE_EVIDENCE_WORKFLOW_SHA_ANNOTATION]: options.signingWorkflowSha },
|
|
180
|
+
cwd: outputDirectory,
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
outputDirectory,
|
|
184
|
+
statement: derived.statement,
|
|
185
|
+
channel: derived.channel,
|
|
186
|
+
subjectDigest: derived.subjectDigest,
|
|
187
|
+
referrerDigest,
|
|
188
|
+
descriptor,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function main() {
|
|
193
|
+
const [phase, ...args] = configureEditionWorkspaceFromArgv(process.argv.slice(2), [WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
|
|
194
|
+
if (phase !== 'stage' && phase !== 'accept') {
|
|
195
|
+
throw new Error('usage: xema edition evidence statement catalogue <stage|accept> …');
|
|
196
|
+
}
|
|
197
|
+
const flags = parseEvidenceCliFlags(args, {
|
|
198
|
+
single: phase === 'stage' ? SHARED_FLAGS : [...SHARED_FLAGS, 'output', 'cosign'],
|
|
199
|
+
repeated: [],
|
|
200
|
+
});
|
|
201
|
+
const { schemas } = await loadProducerKernelSchemas({ dataRoot: dataRoot() });
|
|
202
|
+
const artifact = flags.require('artifact');
|
|
203
|
+
const githubSha = flags.require('github-sha');
|
|
204
|
+
const { authority } = parseCatalogueRef(artifact);
|
|
205
|
+
const options = {
|
|
206
|
+
artifact,
|
|
207
|
+
offeringId: flags.require('offering'),
|
|
208
|
+
stagingDirectory: resolve(flags.require('staging')),
|
|
209
|
+
schemas,
|
|
210
|
+
policy: loadPinnedTrustPolicy({ dataRoot: dataRoot(), schemas }),
|
|
211
|
+
client: createRegistryClientResolver({ dataRoot: dataRoot() })(authority),
|
|
212
|
+
signer: githubWorkflowSigner({
|
|
213
|
+
repository: flags.require('signer-repository'),
|
|
214
|
+
ref: flags.require('signer-ref'),
|
|
215
|
+
path: flags.require('signer-workflow-path'),
|
|
216
|
+
}),
|
|
217
|
+
githubContext: {
|
|
218
|
+
repository: flags.require('github-repository'),
|
|
219
|
+
ref: flags.require('github-ref'),
|
|
220
|
+
sha: githubSha,
|
|
221
|
+
workflowRef: flags.require('github-workflow-ref'),
|
|
222
|
+
},
|
|
223
|
+
signingWorkflowSha: githubSha,
|
|
224
|
+
};
|
|
225
|
+
if (phase === 'stage') {
|
|
226
|
+
process.stdout.write(serializeJson((await stageEditionCatalogueEvidence(options)).plan));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const result = await acceptEditionCatalogueEvidence({
|
|
230
|
+
...options,
|
|
231
|
+
outputDirectory: resolve(flags.require('output')),
|
|
232
|
+
...createCosignVerifier(resolve(flags.require('cosign'))),
|
|
233
|
+
});
|
|
234
|
+
process.stdout.write(
|
|
235
|
+
serializeJson({
|
|
236
|
+
schemaVersion: 1,
|
|
237
|
+
kind: 'edition-catalogue',
|
|
238
|
+
offeringId: options.offeringId,
|
|
239
|
+
channel: result.channel,
|
|
240
|
+
subjectDigest: result.subjectDigest,
|
|
241
|
+
referrerDigest: result.referrerDigest,
|
|
242
|
+
evidence: result.descriptor,
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (isEntryModule(import.meta.url)) {
|
|
248
|
+
main().catch((error) => {
|
|
249
|
+
console.error(`[edition-catalogue] ${error.message}`);
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
} from './edition-workspace.mjs';
|
|
37
37
|
import { COMMIT_PATTERN, GITHUB_REF_PATTERN, GITHUB_REPOSITORY_PATTERN, SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
38
38
|
import {
|
|
39
|
+
KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE,
|
|
40
|
+
KernelContractAuthorityPinError,
|
|
39
41
|
RUNTIME_IMAGE_SOURCE_INDEX_FILE,
|
|
40
42
|
loadPinnedDeclaredTrustPolicy,
|
|
41
43
|
readEvidenceTrustPin,
|
|
@@ -50,9 +52,9 @@ import {
|
|
|
50
52
|
import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
|
|
51
53
|
import { serializeJson } from './lib.mjs';
|
|
52
54
|
import { assertStagedExactly, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
|
|
55
|
+
import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
|
|
53
56
|
|
|
54
|
-
export
|
|
55
|
-
'application/vnd.dev.sigstore.bundle.v0.3+json';
|
|
57
|
+
export { BUNDLE_MEDIA_TYPE };
|
|
56
58
|
const STAGED_STATEMENTS_DIRECTORY = 'statements';
|
|
57
59
|
const STAGED_BUNDLES_DIRECTORY = 'bundles';
|
|
58
60
|
|
|
@@ -135,6 +137,13 @@ export async function loadProducerKernelSchemas({
|
|
|
135
137
|
}
|
|
136
138
|
const expected = readRawContractAuthority(root);
|
|
137
139
|
const pinned = readEvidenceTrustPin({ dataRoot: root, declaration }).kernelContractAuthority;
|
|
140
|
+
if (pinned === null) {
|
|
141
|
+
throw new KernelContractAuthorityPinError(
|
|
142
|
+
KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE,
|
|
143
|
+
`${RUNTIME_IMAGE_SOURCE_INDEX_FILE} at ${root} states kernel contract authority commit ${expected.sourceCommit} ` +
|
|
144
|
+
'but the evidence-trust pin records none; re-pin with `xema edition repin-evidence-trust`',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
138
147
|
if (expected.sourceCommit !== pinned.sourceCommit) {
|
|
139
148
|
throw new Error(
|
|
140
149
|
`${RUNTIME_IMAGE_SOURCE_INDEX_FILE} at ${root} states kernel contract authority commit ` +
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// release-edition — the customer RELEASE edition's command line: `xema edition release <verb> --definition <file>`.
|
|
3
|
+
//
|
|
4
|
+
// It is `xema edition develop <verb> --definition <file>` on the release channel: the same parser, the same frame
|
|
5
|
+
// and the same verbs (customer-edition-verbs.mjs); only the inputs differ. The id is the definition's, and the
|
|
6
|
+
// catalogue is the one pinned for `release` in catalogue.pin.json beside the definition.
|
|
7
|
+
//
|
|
8
|
+
// freeze-sources --definition <file> --out <file>
|
|
9
|
+
// one `sources/<name>` pin per source the definition declares, each at the source's DECLARED ref (a branch,
|
|
10
|
+
// or a tag taken at its peeled commit); a ref that is both a branch and a tag, or missing, is refused.
|
|
11
|
+
// Written exclusively (wx).
|
|
12
|
+
// target --definition <file> --pins <file> --registry <root> --engine-image <repository>@sha256:<digest>
|
|
13
|
+
// [--source-image <source>=<registry>/<prefix>]...
|
|
14
|
+
// print <root>/<release segment>/xema-edition-<id>:<tag>, the tag being snapshotEditionTag of the CLI
|
|
15
|
+
// version, the engine image, the pins and the source-image map (as on develop).
|
|
16
|
+
// check-existing --definition <file> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
17
|
+
// [--catalogue-username <name> --catalogue-token-env <VAR>] [--source-image <source>=<registry>/<prefix>]...
|
|
18
|
+
// refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
|
|
19
|
+
// resolve --definition <file> --pins <file> --out <dir> --registry <root> --engine-image <repository>@sha256:<digest>
|
|
20
|
+
// [--source-image <source>=<registry>/<prefix>]... [--catalogue-username <name> --catalogue-token-env <VAR>] [--local]
|
|
21
|
+
// resolve the edition against the pinned release catalogue, seal the customer's own image rows at the digests
|
|
22
|
+
// the registry serves for `<repository>:<contentHash>` (it never builds an image), and write the descriptor and
|
|
23
|
+
// the four locks to <dir>.
|
|
24
|
+
//
|
|
25
|
+
// `resolve` and `check-existing` read the release catalogue only after its signed E4 evidence verified against the
|
|
26
|
+
// CUSTOMER's own evidence trust policy (catalogue-evidence.mjs), and require that policy armed on the release channel.
|
|
27
|
+
// Until the E4 referrer layout lands, no catalogue verifies and both refuse. Takes the roots of the
|
|
28
|
+
// develop customer form (--data-root, --editions-root, --sources-root, --cache-dir per verb), never the kernel.
|
|
29
|
+
|
|
30
|
+
import { customerRootsOf, parseEditionVerbArgs, runCustomerVerb } from './customer-edition-verbs.mjs';
|
|
31
|
+
import { EditionChannel } from './edition-constants.mjs';
|
|
32
|
+
import { configureEditionWorkspaceFromProcess, isEntryModule } from './edition-workspace.mjs';
|
|
33
|
+
|
|
34
|
+
if (isEntryModule(import.meta.url)) {
|
|
35
|
+
const [command] = process.argv.slice(2);
|
|
36
|
+
try {
|
|
37
|
+
configureEditionWorkspaceFromProcess(customerRootsOf(command), { refuseOthers: true });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`[release-edition] ${error.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function run(argv, env) {
|
|
45
|
+
const { command, options } = parseEditionVerbArgs(EditionChannel.Release, argv);
|
|
46
|
+
await runCustomerVerb({ channel: EditionChannel.Release, command, options, env });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isEntryModule(import.meta.url)) {
|
|
50
|
+
run(process.argv.slice(2), process.env).catch((error) => {
|
|
51
|
+
console.error(`[release-edition] ${error.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// `xema edition repin-evidence-trust` - write the data root's evidence trust
|
|
3
3
|
// pin: the digest of the declared policy, the digests of THIS CLI's built
|
|
4
4
|
// verifier bundle and provenance, the kernel contract authority the data
|
|
5
|
-
// root's runtime image source index states
|
|
6
|
-
//
|
|
5
|
+
// root's runtime image source index states (a customer data root has no such
|
|
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
8
|
// generated into the library.
|
|
8
9
|
//
|
|
9
10
|
// [--trusted-oras-digest sha256:<64 hex>]… the oras binaries to trust. Without
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from './edition-workspace.mjs';
|
|
25
26
|
import {
|
|
26
27
|
loadDeclaredTrustPolicy,
|
|
28
|
+
hasRuntimeImageSourceIndex,
|
|
27
29
|
readEvidenceTrustPin,
|
|
28
30
|
readStatedKernelContractAuthority,
|
|
29
31
|
requireOrasDigests,
|
|
@@ -62,7 +64,7 @@ export async function repinEvidenceTrust({
|
|
|
62
64
|
declaration,
|
|
63
65
|
policyDigest,
|
|
64
66
|
verifier,
|
|
65
|
-
kernelContractAuthority: readStatedKernelContractAuthority({ dataRoot: root, schemas }),
|
|
67
|
+
kernelContractAuthority: hasRuntimeImageSourceIndex(root) ? readStatedKernelContractAuthority({ dataRoot: root, schemas }) : null,
|
|
66
68
|
trustedOrasBinaryDigests: orasDigests,
|
|
67
69
|
});
|
|
68
70
|
}
|
|
@@ -40,7 +40,10 @@ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
|
40
40
|
import { assertBuildLedgerComplete } from './build-ledger-completeness.mjs';
|
|
41
41
|
import { BUILD_LEDGER_FILE, readBuildLedger } from './build-ledger.mjs';
|
|
42
42
|
import { CROSS_REPO_BUILD_INPUTS_PATH, crossRepoInputKey, resolveCrossRepoVendoredInputs } from './cross-repo-build-inputs.mjs';
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
resolveDispatchedRuntimeComponents,
|
|
45
|
+
UndeclaredSupplementalRuntimeComponentError,
|
|
46
|
+
} from './dispatched-runtime-components.mjs';
|
|
44
47
|
import {
|
|
45
48
|
assertCanonicalDistributionId,
|
|
46
49
|
distributionOutputPath,
|
|
@@ -681,6 +684,28 @@ export function resolveImageStructure(
|
|
|
681
684
|
dispatchers,
|
|
682
685
|
biomeIndex,
|
|
683
686
|
)) {
|
|
687
|
+
if (dispatched.supplementalRuntimeComponentId !== undefined) {
|
|
688
|
+
// THE REFUSAL: a dispatched supplemental runtime component must be one the
|
|
689
|
+
// runtime image source index declares as a runtime-component subject.
|
|
690
|
+
// Every image lock is produced here, so this is the one place it is checked.
|
|
691
|
+
const declared = runtimeSourceIndex.supplementalSubjects.find(
|
|
692
|
+
(subject) =>
|
|
693
|
+
subject.kind === enums.RuntimeImageSubjectKind.RuntimeComponent &&
|
|
694
|
+
subject.id === dispatched.supplementalRuntimeComponentId,
|
|
695
|
+
);
|
|
696
|
+
if (declared === undefined) {
|
|
697
|
+
throw new UndeclaredSupplementalRuntimeComponentError(
|
|
698
|
+
dispatched.supplementalRuntimeComponentId,
|
|
699
|
+
dispatched.consumers,
|
|
700
|
+
`distribution "${lock.distributionId}"`,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
addSupplementalSubject(
|
|
704
|
+
declared,
|
|
705
|
+
dispatched.consumers.map((biomeId) => ({ kind: enums.RuntimeImageOwnerKind.Biome, biomeId })),
|
|
706
|
+
);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
684
709
|
const { target } = dispatched;
|
|
685
710
|
addSubject(subjects, {
|
|
686
711
|
kind: enums.RuntimeImageSubjectKind.RuntimeComponent,
|
|
@@ -717,6 +742,16 @@ export function resolveImageStructure(
|
|
|
717
742
|
const boundSubjects = new Set(
|
|
718
743
|
runtimeSourceIndex.platformServiceBindings.map((binding) => bindingKey(binding)),
|
|
719
744
|
);
|
|
745
|
+
// A biome's `supplemental:<id>` dispatch edge also carries the subject: the
|
|
746
|
+
// catalogue check reads the whole first-party index, not this roster, so a
|
|
747
|
+
// subject reached only by a dispatch is not "unreachable" in any edition.
|
|
748
|
+
const dispatchedSubjects = new Set(
|
|
749
|
+
biomeIndex
|
|
750
|
+
.filter((entry) => entry.source === undefined)
|
|
751
|
+
.flatMap((entry) => entry.dispatchesRuntimeComponents ?? [])
|
|
752
|
+
.filter((edge) => edge.supplementalRuntimeComponentId !== undefined)
|
|
753
|
+
.map((edge) => `${enums.RuntimeImageSubjectKind.RuntimeComponent}\0${edge.supplementalRuntimeComponentId}`),
|
|
754
|
+
);
|
|
720
755
|
const launchers = new Map(
|
|
721
756
|
supplementalSources
|
|
722
757
|
.filter((source) => source.launchedBy !== undefined)
|
|
@@ -733,11 +768,11 @@ export function resolveImageStructure(
|
|
|
733
768
|
for (const [key, supplemental] of supplementalSubjects) {
|
|
734
769
|
const launcher = launchers.get(key);
|
|
735
770
|
if (launcher === undefined) {
|
|
736
|
-
if (!boundSubjects.has(key)) {
|
|
771
|
+
if (!boundSubjects.has(key) && !dispatchedSubjects.has(key)) {
|
|
737
772
|
throw new Error(
|
|
738
|
-
`supplemental subject "${supplemental.kind}:${supplemental.id}" has neither a launchedBy
|
|
739
|
-
'platform-service binding, so no edition could ever carry it — declare what launches it ' +
|
|
740
|
-
'(runtimeImageSources.supplementalSources[].launchedBy)
|
|
773
|
+
`supplemental subject "${supplemental.kind}:${supplemental.id}" has neither a launchedBy, a ` +
|
|
774
|
+
'platform-service binding nor a biome dispatching it, so no edition could ever carry it — declare what launches it ' +
|
|
775
|
+
'(runtimeImageSources.supplementalSources[].launchedBy), bind it to a platform service or dispatch it from a biome',
|
|
741
776
|
);
|
|
742
777
|
}
|
|
743
778
|
continue;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The one Sigstore bundle media type this plane stores. Its own module because the evidence verifier bundle needs it and
|
|
2
|
+
// must not import a producer entry (every module of a bundle shares one `import.meta.url`, so a producer's entry guard
|
|
3
|
+
// would run inside the verifier).
|
|
4
|
+
export const BUNDLE_MEDIA_TYPE = 'application/vnd.dev.sigstore.bundle.v0.3+json';
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
// trustedGitBinaryDigests -> same, for the git binary that pins the
|
|
22
22
|
// graded checkouts.
|
|
23
23
|
//
|
|
24
|
+
// THE REQUIRED RELEASE ROWS ARE ONE RULE: the edition-catalogue (E4) predicate iff the edition consumes a catalogue
|
|
25
|
+
// (`consumesCatalogue`: a customer edition does, a vendor edition does not), plus the image producer, distribution
|
|
26
|
+
// completeness and delivery entitlement predicates. The same function judges the data root before the catalogue's
|
|
27
|
+
// kernel is adopted (inside the pinned verifier bundle, with its own embedded kernel) and after it (descriptorEvidenceTrust).
|
|
28
|
+
//
|
|
24
29
|
// The three predicate types are checked separately because a policy can be
|
|
25
30
|
// non-empty and still be unarmed for a stage: the verifier resolves candidates
|
|
26
31
|
// per predicate type, so authorizing only the producer identity leaves the
|
|
@@ -60,7 +65,7 @@ export function assertArmingChannel(schemas, channel) {
|
|
|
60
65
|
* field and what makes verification impossible, so the failure is actionable
|
|
61
66
|
* without reading the verifier.
|
|
62
67
|
*/
|
|
63
|
-
export function assessTrustPolicyArming(policy, schemas, channel) {
|
|
68
|
+
export function assessTrustPolicyArming(policy, schemas, channel, { consumesCatalogue = false } = {}) {
|
|
64
69
|
assertArmingChannel(schemas, channel);
|
|
65
70
|
const unmet = [];
|
|
66
71
|
const stages = [
|
|
@@ -68,6 +73,14 @@ export function assessTrustPolicyArming(policy, schemas, channel) {
|
|
|
68
73
|
['distribution-completeness', schemas.DISTRIBUTION_IMAGES_PREDICATE_TYPE],
|
|
69
74
|
['delivery-entitlement', schemas.IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE],
|
|
70
75
|
];
|
|
76
|
+
if (consumesCatalogue) {
|
|
77
|
+
// A kernel that states no E4 predicate cannot arm a catalogue consumer: an unmet entry, never a skip.
|
|
78
|
+
if (typeof schemas.EDITION_CATALOGUE_PREDICATE_TYPE !== 'string') {
|
|
79
|
+
unmet.push('this kernel states no edition-catalogue predicate (EDITION_CATALOGUE_PREDICATE_TYPE), so a catalogue consumer cannot be armed against it.');
|
|
80
|
+
} else {
|
|
81
|
+
stages.push(['edition-catalogue', schemas.EDITION_CATALOGUE_PREDICATE_TYPE]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
71
84
|
const onChannel = policy.policies.filter((candidate) => candidate.channel === channel);
|
|
72
85
|
const authorized = stages.filter(([, predicateType]) => onChannel.some((candidate) => candidate.predicateTypes.includes(predicateType)));
|
|
73
86
|
|