@xemahq/xema 0.40.3 → 0.41.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.
- 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 +281 -9
- package/dist/edition/verify-image-evidence.bundle.provenance.json +65 -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/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 +7 -0
- 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/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
|
@@ -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
|
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// verify-catalogue-evidence — the `catalogue-evidence` mode of the evidence verifier bundle.
|
|
2
|
+
//
|
|
3
|
+
// <bundle> catalogue-evidence arming --channel <release|develop> --data-root <dir>
|
|
4
|
+
// <bundle> catalogue-evidence verify --channel <c> --data-root <dir> --evidence-root <abs dir>
|
|
5
|
+
// --statement-digest <sha256:..> --bundle-digest <sha256:..> --workflow-sha <40hex> --offering-id <id>
|
|
6
|
+
// --subject-digest <sha256:..> --catalogue-layer-digest <sha256:..> --lock-layer-digest <sha256:..>
|
|
7
|
+
// --cosign <abs path>
|
|
8
|
+
//
|
|
9
|
+
// A customer verifies the E4 statement of a release catalogue BEFORE the catalogue's kernel is adopted, so there is no
|
|
10
|
+
// kernel package to load schemas from yet. The only kernel-dependent parts of the judgement are the policy schema
|
|
11
|
+
// (parseSigstoreOfflineTrustPolicy) and the predicate-type/channel constants; before adoption they come from the pinned
|
|
12
|
+
// bundle's own embedded kernel, through `loadVerifierKernelSchemas` and nowhere else. This mode never takes `--kernel`.
|
|
13
|
+
//
|
|
14
|
+
// The anchors are the customer's pinned policy and the pinned verifier itself (the bundle refuses to run unless its own
|
|
15
|
+
// bytes are the ones the data-root pin records). There is NO git grading in this mode: it reads no checkout of an editions
|
|
16
|
+
// root, only the declared policy and pin of the data root, both of which the pin binds by digest.
|
|
17
|
+
//
|
|
18
|
+
// ONE arming rule (trust-policy-arming.mjs): the policy must be ARMED on the channel with the E4 row required
|
|
19
|
+
// (`consumesCatalogue: true`); `arming` stops there. `verify` then holds the statement to that policy and to the exact
|
|
20
|
+
// manifest, catalogue layer and lock layer the caller read, with the kernel's own `editionCatalogueRefusal`.
|
|
21
|
+
|
|
22
|
+
import { editionWorkspaceConfig, resetEditionWorkspaceConfig } from './edition-config.mjs';
|
|
23
|
+
import { EDITION_CONTRACT } from './edition-constants.mjs';
|
|
24
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot } from './edition-workspace.mjs';
|
|
25
|
+
import {
|
|
26
|
+
assertPinnedPolicy,
|
|
27
|
+
assertPinnedVerifier,
|
|
28
|
+
loadDeclaredTrustPolicy,
|
|
29
|
+
readEvidenceTrustPin,
|
|
30
|
+
verifierDigests,
|
|
31
|
+
} from './evidence-trust-pin.mjs';
|
|
32
|
+
import { prepareTrustedCosignBinary, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
|
|
33
|
+
import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
|
|
34
|
+
import { TrustPolicyArmingState, assessTrustPolicyArming } from './trust-policy-arming.mjs';
|
|
35
|
+
import { assertSanitizedOperatorEnvironment, readRunningVerifier } from './verifier-binding.mjs';
|
|
36
|
+
import { loadVerifierKernelSchemas } from './verifier-kernel-schemas.mjs';
|
|
37
|
+
|
|
38
|
+
/** The first argument that selects this mode of the bundle. */
|
|
39
|
+
export const CATALOGUE_EVIDENCE_MODE = 'catalogue-evidence';
|
|
40
|
+
export const CATALOGUE_EVIDENCE_USAGE =
|
|
41
|
+
'usage: catalogue-evidence arming --channel <release|develop> --data-root <dir> | ' +
|
|
42
|
+
'catalogue-evidence verify --channel <c> --data-root <dir> --evidence-root <abs dir> --statement-digest <sha256:..> ' +
|
|
43
|
+
'--bundle-digest <sha256:..> --workflow-sha <40-hex> --offering-id <id> --subject-digest <sha256:..> ' +
|
|
44
|
+
'--catalogue-layer-digest <sha256:..> --lock-layer-digest <sha256:..> --cosign <abs path>';
|
|
45
|
+
|
|
46
|
+
export const CATALOGUE_EVIDENCE_STATEMENT_FILE = 'statement.json';
|
|
47
|
+
export const CATALOGUE_EVIDENCE_BUNDLE_FILE = 'bundle.sigstore.json';
|
|
48
|
+
/** The code of the refusal that the data root's policy is not armed (also the code of the library's error). */
|
|
49
|
+
export const CATALOGUE_EVIDENCE_UNARMED_CODE = EDITION_CONTRACT.evidenceTrust.refusal.code;
|
|
50
|
+
|
|
51
|
+
export const CatalogueEvidenceRefusalCode = Object.freeze({
|
|
52
|
+
SubjectMismatch: 'catalogue_evidence_subject_mismatch',
|
|
53
|
+
OfferingMismatch: 'catalogue_evidence_offering_mismatch',
|
|
54
|
+
LockDigestMismatch: 'catalogue_evidence_lock_digest_mismatch',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
58
|
+
const COMMIT = /^[a-f0-9]{40}$/;
|
|
59
|
+
const ARMING_FLAGS = Object.freeze(['--channel']);
|
|
60
|
+
const VERIFY_FLAGS = Object.freeze([
|
|
61
|
+
'--channel',
|
|
62
|
+
'--evidence-root',
|
|
63
|
+
'--statement-digest',
|
|
64
|
+
'--bundle-digest',
|
|
65
|
+
'--workflow-sha',
|
|
66
|
+
'--offering-id',
|
|
67
|
+
'--subject-digest',
|
|
68
|
+
'--catalogue-layer-digest',
|
|
69
|
+
'--lock-layer-digest',
|
|
70
|
+
'--cosign',
|
|
71
|
+
]);
|
|
72
|
+
const ABSOLUTE_FLAGS = Object.freeze(['--evidence-root', '--cosign']);
|
|
73
|
+
const DIGEST_FLAGS = Object.freeze(['--statement-digest', '--bundle-digest', '--subject-digest', '--catalogue-layer-digest', '--lock-layer-digest']);
|
|
74
|
+
|
|
75
|
+
/** A bad command line: exit 2. */
|
|
76
|
+
export class CatalogueEvidenceUsageError extends Error {
|
|
77
|
+
constructor(message) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = 'CatalogueEvidenceUsageError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The data root's policy is not armed (or declared) for a catalogue consumer on the channel. Carries the stable code. */
|
|
84
|
+
export class CatalogueEvidenceUnarmedError extends Error {
|
|
85
|
+
constructor(message) {
|
|
86
|
+
super(`${CATALOGUE_EVIDENCE_UNARMED_CODE}: ${message}`);
|
|
87
|
+
this.name = 'EvidenceTrustRequiredError';
|
|
88
|
+
this.code = CATALOGUE_EVIDENCE_UNARMED_CODE;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The statement does not bind what the caller read; `code` is the refusal's stable name. */
|
|
93
|
+
export class CatalogueEvidenceRefusal extends Error {
|
|
94
|
+
constructor(code, message) {
|
|
95
|
+
super(`${code}: ${message}`);
|
|
96
|
+
this.name = 'CatalogueEvidenceRefusal';
|
|
97
|
+
this.code = code;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hexOf = (digest) => digest.slice('sha256:'.length);
|
|
102
|
+
|
|
103
|
+
function parseFlags(args, allowed) {
|
|
104
|
+
const flags = new Map();
|
|
105
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
106
|
+
const flag = args[index];
|
|
107
|
+
const value = args[index + 1];
|
|
108
|
+
if (typeof flag !== 'string' || !allowed.includes(flag)) {
|
|
109
|
+
throw new CatalogueEvidenceUsageError(`unknown or unexpected argument ${JSON.stringify(flag)}`);
|
|
110
|
+
}
|
|
111
|
+
if (value === undefined || value.startsWith('--')) {throw new CatalogueEvidenceUsageError(`${flag} requires a value`);}
|
|
112
|
+
if (flags.has(flag)) {throw new CatalogueEvidenceUsageError(`${flag} was given twice`);}
|
|
113
|
+
flags.set(flag, value);
|
|
114
|
+
}
|
|
115
|
+
for (const flag of allowed) {
|
|
116
|
+
if (!flags.has(flag)) {throw new CatalogueEvidenceUsageError(`${flag} is required`);}
|
|
117
|
+
}
|
|
118
|
+
for (const flag of ABSOLUTE_FLAGS.filter((candidate) => flags.has(candidate))) {
|
|
119
|
+
if (!flags.get(flag).startsWith('/')) {throw new CatalogueEvidenceUsageError(`${flag} must be an absolute path`);}
|
|
120
|
+
}
|
|
121
|
+
for (const flag of DIGEST_FLAGS.filter((candidate) => flags.has(candidate))) {
|
|
122
|
+
if (!SHA256_DIGEST.test(flags.get(flag))) {throw new CatalogueEvidenceUsageError(`${flag} must be sha256:<64 lowercase hex>`);}
|
|
123
|
+
}
|
|
124
|
+
if (flags.has('--workflow-sha') && !COMMIT.test(flags.get('--workflow-sha'))) {
|
|
125
|
+
throw new CatalogueEvidenceUsageError('--workflow-sha must be a 40-character lowercase hexadecimal git commit SHA');
|
|
126
|
+
}
|
|
127
|
+
return flags;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The anchors, then the ONE arming rule: the sanitized launch, the running verifier, the declared policy held to the
|
|
132
|
+
* data-root pin of both the policy and THIS verifier, and the policy ARMED on `channel` with the E4 row required.
|
|
133
|
+
*/
|
|
134
|
+
async function armedAnchors({ channel, env, bundlePath }) {
|
|
135
|
+
assertSanitizedOperatorEnvironment(env);
|
|
136
|
+
const running = readRunningVerifier(bundlePath);
|
|
137
|
+
const { schemas } = await loadVerifierKernelSchemas();
|
|
138
|
+
resetEditionWorkspaceConfig();
|
|
139
|
+
const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
|
|
140
|
+
if (declaration.policy === null) {
|
|
141
|
+
throw new CatalogueEvidenceUnarmedError(`${dataRoot()} declares no evidenceTrustPolicy in edition-workspace.json`);
|
|
142
|
+
}
|
|
143
|
+
const { policy, policyDigest } = loadDeclaredTrustPolicy({ dataRoot: dataRoot(), declaration, schemas });
|
|
144
|
+
const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
|
|
145
|
+
assertPinnedPolicy({ pin, policyDigest });
|
|
146
|
+
assertPinnedVerifier({ pin, actual: verifierDigests(running) });
|
|
147
|
+
const { state, unmet } = assessTrustPolicyArming(policy, schemas, channel, { consumesCatalogue: true });
|
|
148
|
+
if (state !== TrustPolicyArmingState.ARMED) {
|
|
149
|
+
throw new CatalogueEvidenceUnarmedError(
|
|
150
|
+
`the declared policy is ${state.toUpperCase()} on the ${channel} channel for a catalogue consumer: ${unmet.join('; ')}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return { schemas, policy };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run the mode. `argv` is the bundle's arguments (`['catalogue-evidence', '<phase>', ...]`). Resolves to the JSON line to
|
|
158
|
+
* print on success; rejects with a named error otherwise.
|
|
159
|
+
*/
|
|
160
|
+
export async function runCatalogueEvidence({ argv, env, bundlePath }) {
|
|
161
|
+
const [mode, phase, ...tail] = argv;
|
|
162
|
+
if (mode !== CATALOGUE_EVIDENCE_MODE || (phase !== 'arming' && phase !== 'verify')) {
|
|
163
|
+
throw new CatalogueEvidenceUsageError(CATALOGUE_EVIDENCE_USAGE);
|
|
164
|
+
}
|
|
165
|
+
// `--kernel` is no root of this mode: the schemas are the bundle's own.
|
|
166
|
+
let rest;
|
|
167
|
+
try {
|
|
168
|
+
rest = configureEditionWorkspaceFromArgv(tail, [WorkspaceRoot.Data], { refuseOthers: true });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new CatalogueEvidenceUsageError(error.message);
|
|
171
|
+
}
|
|
172
|
+
const flags = parseFlags(rest, phase === 'arming' ? ARMING_FLAGS : VERIFY_FLAGS);
|
|
173
|
+
const channel = flags.get('--channel');
|
|
174
|
+
const { schemas, policy } = await armedAnchors({ channel, env, bundlePath });
|
|
175
|
+
if (phase === 'arming') {
|
|
176
|
+
return `${JSON.stringify({ channel, state: TrustPolicyArmingState.ARMED })}\n`;
|
|
177
|
+
}
|
|
178
|
+
const offeringId = flags.get('--offering-id');
|
|
179
|
+
const descriptor = {
|
|
180
|
+
statementPath: CATALOGUE_EVIDENCE_STATEMENT_FILE,
|
|
181
|
+
statementDigest: flags.get('--statement-digest'),
|
|
182
|
+
bundlePath: CATALOGUE_EVIDENCE_BUNDLE_FILE,
|
|
183
|
+
bundleDigest: flags.get('--bundle-digest'),
|
|
184
|
+
bundleMediaType: BUNDLE_MEDIA_TYPE,
|
|
185
|
+
};
|
|
186
|
+
const trustedCosign = prepareTrustedCosignBinary({ cosignBinary: flags.get('--cosign'), policy });
|
|
187
|
+
let statement;
|
|
188
|
+
try {
|
|
189
|
+
({ statement } = verifySigstoreEvidenceDescriptor({
|
|
190
|
+
evidenceRoot: flags.get('--evidence-root'),
|
|
191
|
+
descriptor,
|
|
192
|
+
policy,
|
|
193
|
+
predicateType: schemas.EDITION_CATALOGUE_PREDICATE_TYPE,
|
|
194
|
+
channel,
|
|
195
|
+
expectedWorkflowSha: flags.get('--workflow-sha'),
|
|
196
|
+
parseStatement: schemas.parseEditionCatalogueStatement,
|
|
197
|
+
policyRoot: dataRoot(),
|
|
198
|
+
trustedCosign,
|
|
199
|
+
}));
|
|
200
|
+
} finally {
|
|
201
|
+
trustedCosign.dispose();
|
|
202
|
+
}
|
|
203
|
+
const [subject] = statement.subject;
|
|
204
|
+
const expectedSubject = `xema-edition-${offeringId}`;
|
|
205
|
+
if (subject.digest.sha256 !== hexOf(flags.get('--subject-digest')) || subject.name !== expectedSubject) {
|
|
206
|
+
throw new CatalogueEvidenceRefusal(
|
|
207
|
+
CatalogueEvidenceRefusalCode.SubjectMismatch,
|
|
208
|
+
`the statement is over ${subject.name}@sha256:${subject.digest.sha256}, not ${expectedSubject}@${flags.get('--subject-digest')}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const refusal = schemas.editionCatalogueRefusal(statement.predicate, {
|
|
212
|
+
channel,
|
|
213
|
+
catalogueLayerDigest: flags.get('--catalogue-layer-digest'),
|
|
214
|
+
});
|
|
215
|
+
if (refusal !== undefined) {
|
|
216
|
+
throw new CatalogueEvidenceRefusal(
|
|
217
|
+
refusal,
|
|
218
|
+
`the statement states channel ${statement.predicate.channel} and catalogue ${statement.predicate.catalogueDigest}; ` +
|
|
219
|
+
`this catalogue is on ${channel} with layer ${flags.get('--catalogue-layer-digest')}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (statement.predicate.offeringId !== offeringId) {
|
|
223
|
+
throw new CatalogueEvidenceRefusal(
|
|
224
|
+
CatalogueEvidenceRefusalCode.OfferingMismatch,
|
|
225
|
+
`the statement is for offering ${statement.predicate.offeringId}, not ${offeringId}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (statement.predicate.lockDigest !== hexOf(flags.get('--lock-layer-digest'))) {
|
|
229
|
+
throw new CatalogueEvidenceRefusal(
|
|
230
|
+
CatalogueEvidenceRefusalCode.LockDigestMismatch,
|
|
231
|
+
`the statement states lock digest ${statement.predicate.lockDigest}, not that of the lock layer ${flags.get('--lock-layer-digest')}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return `${JSON.stringify({ verified: true, offeringId, channel, statementDigest: descriptor.statementDigest })}\n`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Run the mode as the bundle's entry: print its line, or the named refusal on stderr with exit 1 (usage: 2). */
|
|
238
|
+
export async function runCatalogueEvidenceEntry({ argv, env, bundlePath }) {
|
|
239
|
+
try {
|
|
240
|
+
process.stdout.write(await runCatalogueEvidence({ argv, env, bundlePath }));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
process.stderr.write(`catalogue-evidence: ${error.name}: ${error.message}\n`);
|
|
243
|
+
process.exitCode = error instanceof CatalogueEvidenceUsageError ? 2 : 1;
|
|
244
|
+
}
|
|
245
|
+
}
|