@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,225 @@
|
|
|
1
|
+
// catalogue-evidence — the catalogue's signed E4 evidence, verified BEFORE the catalogue is read.
|
|
2
|
+
//
|
|
3
|
+
// A release catalogue is the ceiling of everything resolved against it, so its bytes are not parsed, and not even
|
|
4
|
+
// fetched, until the E4 statement over its digest, catalogue layer and lock layer has been verified against the evidence
|
|
5
|
+
// trust policy of the CALLER'S OWN data root (never the vendor's). The statement is an `oras attach` referrer of the
|
|
6
|
+
// edition artifact digest (`EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE`): two layers, `statement.json` (in-toto) and
|
|
7
|
+
// `bundle.sigstore.json` (the Sigstore bundle), and the exact commit of the signing workflow as an annotation.
|
|
8
|
+
//
|
|
9
|
+
// The verification is ONE seam, `verifyCatalogueEvidenceReferrer`, and it is done by the data root's PINNED built
|
|
10
|
+
// verifier (`catalogue-evidence verify`, verify-catalogue-evidence.mjs): there is no second verifier. The only
|
|
11
|
+
// kernel-dependent parts of the judgement are the policy schema (parseSigstoreOfflineTrustPolicy) and the
|
|
12
|
+
// predicate-type/channel constants; before the catalogue's kernel is adopted (kernel-package.mjs) there is no kernel
|
|
13
|
+
// package to load them from, so they come from the pinned bundle's own embedded kernel. The library never judges the
|
|
14
|
+
// statement itself: it checks the referrer's layout, hands the two blobs to the bundle (only after the bundle's digests
|
|
15
|
+
// equal the data-root pin; otherwise CatalogueVerifierNotPinnedError, before anything is spawned) and reads its verdict.
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { locateBuiltVerifier, readBuiltVerifierDigests } from './built-verifier.mjs';
|
|
24
|
+
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
25
|
+
import { dataRoot as configuredDataRoot } from './edition-workspace.mjs';
|
|
26
|
+
import { assertPinnedVerifier, readEvidenceTrustPin } from './evidence-trust-pin.mjs';
|
|
27
|
+
import { EvidenceTrustRequiredError } from './evidence-trust-policy.mjs';
|
|
28
|
+
import { EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE } from './oci-image-source-evidence.mjs';
|
|
29
|
+
import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
|
|
30
|
+
|
|
31
|
+
const TITLE = 'org.opencontainers.image.title';
|
|
32
|
+
/** The titles of the two layers of an E4 referrer, the in-toto statement's media type and the annotation naming the signing workflow commit. */
|
|
33
|
+
export const CATALOGUE_EVIDENCE_STATEMENT_TITLE = 'statement.json';
|
|
34
|
+
export const CATALOGUE_EVIDENCE_BUNDLE_TITLE = 'bundle.sigstore.json';
|
|
35
|
+
export const CATALOGUE_EVIDENCE_STATEMENT_MEDIA_TYPE = 'application/vnd.in-toto+json';
|
|
36
|
+
export const CATALOGUE_EVIDENCE_WORKFLOW_SHA_ANNOTATION = 'dev.xema.evidence.signer-workflow-sha';
|
|
37
|
+
/** The variables the pinned verifier bundle is launched with (it refuses NODE_OPTIONS, NODE_PATH, DYLD_*, LD_*, GIT_*). */
|
|
38
|
+
export const VERIFIER_LAUNCH_ENV_KEYS = Object.freeze(['PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL', 'TZ']);
|
|
39
|
+
const STATEMENT_MAX_BYTES = 2 * 1024 * 1024;
|
|
40
|
+
const BUNDLE_MAX_BYTES = 16 * 1024 * 1024;
|
|
41
|
+
const SPAWN_TIMEOUT_MS = 120_000;
|
|
42
|
+
const COMMIT = /^[a-f0-9]{40}$/;
|
|
43
|
+
const sha256Of = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
44
|
+
|
|
45
|
+
/** No E4 referrer is attached to the artifact: the catalogue has no signed evidence at all. */
|
|
46
|
+
export class CatalogueEvidenceMissingError extends Error {
|
|
47
|
+
constructor(ref) {
|
|
48
|
+
super(
|
|
49
|
+
`${ref} carries no ${EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE} referrer: its catalogue has no signed edition evidence (E4), ` +
|
|
50
|
+
'so it is refused before it is read',
|
|
51
|
+
);
|
|
52
|
+
this.name = 'CatalogueEvidenceMissingError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** An E4 referrer exists but does not verify against the caller's evidence trust policy. */
|
|
57
|
+
export class CatalogueEvidenceNotVerifiedError extends Error {
|
|
58
|
+
constructor(ref, reason) {
|
|
59
|
+
super(`${ref}: the catalogue's edition evidence (E4) is not verified: ${reason}; the catalogue is refused before it is read`);
|
|
60
|
+
this.name = 'CatalogueEvidenceNotVerifiedError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The built verifier is not the one the data root pins (or is missing): nothing is spawned. */
|
|
65
|
+
export class CatalogueVerifierNotPinnedError extends Error {
|
|
66
|
+
constructor(reason) {
|
|
67
|
+
super(`the catalogue's edition evidence (E4) cannot be verified: this CLI's built evidence verifier is not the one the data root pins: ${reason}`);
|
|
68
|
+
this.name = 'CatalogueVerifierNotPinnedError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The pinned built verifier's bundle path, after the data root's pin of it held; refuses before anything is spawned. */
|
|
73
|
+
function pinnedVerifierBundle(locateVerifier) {
|
|
74
|
+
const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
|
|
75
|
+
const root = configuredDataRoot();
|
|
76
|
+
if (declaration.policy === null) {throw new EvidenceTrustRequiredError(root, 'declares no policy');}
|
|
77
|
+
try {
|
|
78
|
+
const pin = readEvidenceTrustPin({ dataRoot: root, declaration });
|
|
79
|
+
assertPinnedVerifier({ pin, actual: readBuiltVerifierDigests(locateVerifier) });
|
|
80
|
+
return { bundlePath: locateVerifier().bundlePath, root };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new CatalogueVerifierNotPinnedError(error.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function launchEnvironment(source = process.env) {
|
|
87
|
+
const reduced = {};
|
|
88
|
+
for (const key of VERIFIER_LAUNCH_ENV_KEYS) {
|
|
89
|
+
if (source[key] !== undefined) {reduced[key] = source[key];}
|
|
90
|
+
}
|
|
91
|
+
return reduced;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runVerifier({ spawn, bundlePath, args }) {
|
|
95
|
+
return spawn(process.execPath, [bundlePath, 'catalogue-evidence', ...args], {
|
|
96
|
+
encoding: 'utf8',
|
|
97
|
+
env: launchEnvironment(),
|
|
98
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
99
|
+
maxBuffer: 1024 * 1024,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stderrOf = (result) => String(result.stderr ?? '').trim() || (result.error ? result.error.message : `exit ${String(result.status)}`);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Before any registry call: the caller's data root declares a policy, the built verifier is the one the pin records, and
|
|
107
|
+
* the policy is ARMED on `channel` with the E4 row required (the one arming rule, judged by the pinned bundle with its own
|
|
108
|
+
* kernel). Throws EvidenceTrustRequiredError for no policy / not armed, CatalogueVerifierNotPinnedError for a verifier the
|
|
109
|
+
* pin does not record.
|
|
110
|
+
*
|
|
111
|
+
* @param {{ channel: string, locateVerifier?: Function, spawn?: Function }} options
|
|
112
|
+
*/
|
|
113
|
+
export function assertCatalogueConsumerArmed({ channel, locateVerifier = locateBuiltVerifier, spawn = spawnSync }) {
|
|
114
|
+
const { bundlePath, root } = pinnedVerifierBundle(locateVerifier);
|
|
115
|
+
const result = runVerifier({ spawn, bundlePath, args: ['arming', '--channel', channel, '--data-root', root] });
|
|
116
|
+
if (result.status === 0) {return;}
|
|
117
|
+
const stderr = stderrOf(result);
|
|
118
|
+
if (stderr.includes('EVIDENCE_TRUST_REQUIRED_UNARMED')) {
|
|
119
|
+
throw new EvidenceTrustRequiredError(root, `declares a policy that is not armed on the ${channel} channel for a catalogue consumer (${stderr})`);
|
|
120
|
+
}
|
|
121
|
+
throw new CatalogueEvidenceNotVerifiedError(`the ${channel} catalogue`, `the arming check failed: ${stderr}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function layerOf(layers, title, mediaType, ref) {
|
|
125
|
+
const found = layers.filter((layer) => layer?.annotations?.[TITLE] === title);
|
|
126
|
+
if (found.length !== 1 || found[0].mediaType !== mediaType || typeof found[0].digest !== 'string') {
|
|
127
|
+
throw new CatalogueEvidenceNotVerifiedError(ref, `the referrer must carry exactly one ${title} layer of media type ${mediaType}`);
|
|
128
|
+
}
|
|
129
|
+
return found[0];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readReferrerLayers({ client, repository, referrer, manifestDigest, ref }) {
|
|
133
|
+
const manifest = await client.getManifestByDigest(repository, referrer.digest);
|
|
134
|
+
const document = manifest.document ?? {};
|
|
135
|
+
if (document.artifactType !== EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE) {
|
|
136
|
+
throw new CatalogueEvidenceNotVerifiedError(ref, `referrer ${referrer.digest} is not a ${EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE} artifact`);
|
|
137
|
+
}
|
|
138
|
+
if (document.subject?.digest !== manifestDigest) {
|
|
139
|
+
throw new CatalogueEvidenceNotVerifiedError(ref, `referrer ${referrer.digest} is a referrer of ${String(document.subject?.digest)}, not of ${manifestDigest}`);
|
|
140
|
+
}
|
|
141
|
+
const layers = Array.isArray(document.layers) ? document.layers : [];
|
|
142
|
+
if (layers.length !== 2) {
|
|
143
|
+
throw new CatalogueEvidenceNotVerifiedError(ref, `referrer ${referrer.digest} must carry exactly two layers (${CATALOGUE_EVIDENCE_STATEMENT_TITLE}, ${CATALOGUE_EVIDENCE_BUNDLE_TITLE}), it carries ${layers.length}`);
|
|
144
|
+
}
|
|
145
|
+
const statement = layerOf(layers, CATALOGUE_EVIDENCE_STATEMENT_TITLE, CATALOGUE_EVIDENCE_STATEMENT_MEDIA_TYPE, ref);
|
|
146
|
+
const bundle = layerOf(layers, CATALOGUE_EVIDENCE_BUNDLE_TITLE, BUNDLE_MEDIA_TYPE, ref);
|
|
147
|
+
const workflowSha = document.annotations?.[CATALOGUE_EVIDENCE_WORKFLOW_SHA_ANNOTATION];
|
|
148
|
+
if (typeof workflowSha !== 'string' || !COMMIT.test(workflowSha)) {
|
|
149
|
+
throw new CatalogueEvidenceNotVerifiedError(ref, `referrer ${referrer.digest} states no 40-hex ${CATALOGUE_EVIDENCE_WORKFLOW_SHA_ANNOTATION} annotation`);
|
|
150
|
+
}
|
|
151
|
+
return { statement, bundle, workflowSha };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The seam. Hold one E4 referrer to the caller's policy and to the exact catalogue and lock layers: read its two layers
|
|
156
|
+
* (bounded) into a private temporary directory and have the PINNED built verifier judge them. Throws when it does not
|
|
157
|
+
* verify, returns when it does. The verifier's pin is re-checked here, before anything is spawned.
|
|
158
|
+
*
|
|
159
|
+
* @param {{ ref: string, client: object, repository: string, referrer: object, channel: string, offeringId: string, manifestDigest: string, catalogueLayerDigest: string, lockLayerDigest: string, cosign: string, locateVerifier?: Function, spawn?: Function }} subject
|
|
160
|
+
*/
|
|
161
|
+
export async function verifyCatalogueEvidenceReferrer({
|
|
162
|
+
ref,
|
|
163
|
+
client,
|
|
164
|
+
repository,
|
|
165
|
+
referrer,
|
|
166
|
+
channel,
|
|
167
|
+
offeringId,
|
|
168
|
+
manifestDigest,
|
|
169
|
+
catalogueLayerDigest,
|
|
170
|
+
lockLayerDigest,
|
|
171
|
+
cosign,
|
|
172
|
+
locateVerifier = locateBuiltVerifier,
|
|
173
|
+
spawn = spawnSync,
|
|
174
|
+
}) {
|
|
175
|
+
if (typeof cosign !== 'string' || !cosign.startsWith('/')) {
|
|
176
|
+
throw new CatalogueEvidenceNotVerifiedError(ref, 'a release catalogue is verified with a cosign binary: pass --cosign <absolute path>');
|
|
177
|
+
}
|
|
178
|
+
const layers = await readReferrerLayers({ client, repository, referrer, manifestDigest, ref });
|
|
179
|
+
const statementBytes = await client.getBlob(repository, layers.statement.digest, STATEMENT_MAX_BYTES);
|
|
180
|
+
const bundleBytes = await client.getBlob(repository, layers.bundle.digest, BUNDLE_MAX_BYTES);
|
|
181
|
+
if (sha256Of(statementBytes) !== layers.statement.digest || sha256Of(bundleBytes) !== layers.bundle.digest) {
|
|
182
|
+
throw new CatalogueEvidenceNotVerifiedError(ref, 'a referrer layer does not hash to its digest');
|
|
183
|
+
}
|
|
184
|
+
const { bundlePath, root } = pinnedVerifierBundle(locateVerifier);
|
|
185
|
+
const evidenceRoot = mkdtempSync(join(tmpdir(), 'xema-catalogue-evidence-'));
|
|
186
|
+
try {
|
|
187
|
+
writeFileSync(join(evidenceRoot, CATALOGUE_EVIDENCE_STATEMENT_TITLE), statementBytes, { flag: 'wx', mode: 0o600 });
|
|
188
|
+
writeFileSync(join(evidenceRoot, CATALOGUE_EVIDENCE_BUNDLE_TITLE), bundleBytes, { flag: 'wx', mode: 0o600 });
|
|
189
|
+
const result = runVerifier({
|
|
190
|
+
spawn,
|
|
191
|
+
bundlePath,
|
|
192
|
+
args: [
|
|
193
|
+
'verify',
|
|
194
|
+
'--channel', channel,
|
|
195
|
+
'--data-root', root,
|
|
196
|
+
'--evidence-root', evidenceRoot,
|
|
197
|
+
'--statement-digest', layers.statement.digest,
|
|
198
|
+
'--bundle-digest', layers.bundle.digest,
|
|
199
|
+
'--workflow-sha', layers.workflowSha,
|
|
200
|
+
'--offering-id', offeringId,
|
|
201
|
+
'--subject-digest', manifestDigest,
|
|
202
|
+
'--catalogue-layer-digest', catalogueLayerDigest,
|
|
203
|
+
'--lock-layer-digest', lockLayerDigest,
|
|
204
|
+
'--cosign', cosign,
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
if (result.status !== 0) {throw new CatalogueEvidenceNotVerifiedError(ref, stderrOf(result));}
|
|
208
|
+
} finally {
|
|
209
|
+
rmSync(evidenceRoot, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Verify the catalogue's E4 evidence from the registry, before any catalogue byte is fetched or parsed.
|
|
215
|
+
*
|
|
216
|
+
* @param {{ client: object, ref: string, repository: string, manifestDigest: string, channel: string, offeringId: string, catalogueLayerDigest: string, lockLayerDigest: string, cosign?: string, locateVerifier?: Function, verifyReferrer?: Function }} options
|
|
217
|
+
*/
|
|
218
|
+
export async function verifyCatalogueEvidence({ client, ref, repository, manifestDigest, channel, offeringId, catalogueLayerDigest, lockLayerDigest, cosign, locateVerifier, verifyReferrer = verifyCatalogueEvidenceReferrer }) {
|
|
219
|
+
const listed = await client.listReferrers(repository, manifestDigest, EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE);
|
|
220
|
+
const referrers = listed.filter((descriptor) => descriptor?.artifactType === EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE);
|
|
221
|
+
if (referrers.length === 0) {throw new CatalogueEvidenceMissingError(ref);}
|
|
222
|
+
for (const referrer of referrers) {
|
|
223
|
+
await verifyReferrer({ ref, client, repository, referrer, channel, offeringId, manifestDigest, catalogueLayerDigest, lockLayerDigest, cosign, ...(locateVerifier === undefined ? {} : { locateVerifier }) });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
import { Buffer } from 'node:buffer';
|
|
15
15
|
|
|
16
|
+
import { verifyCatalogueEvidence } from './catalogue-evidence.mjs';
|
|
16
17
|
import { CATALOGUE_FILE, CatalogueError, parseCatalogue } from './catalogue.mjs';
|
|
17
18
|
import { parseEditionRepository } from './edition-artifact.mjs';
|
|
18
|
-
import { EDITION_ARTIFACT_TYPE } from './edition-constants.mjs';
|
|
19
|
+
import { EDITION_ARTIFACT_TYPE, EditionChannel, EditionLockFile } from './edition-constants.mjs';
|
|
19
20
|
import { OrasRegistry } from './oras.mjs';
|
|
20
21
|
import { splitImageRef } from './registry-credentials.mjs';
|
|
21
22
|
|
|
@@ -55,8 +56,23 @@ export function catalogueCredential({ authority, username, tokenEnv, env = proce
|
|
|
55
56
|
return { authority, access: 'authenticated', basicAuth: Buffer.from(`${username}:${token}`, 'utf8').toString('base64') };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
-
export
|
|
59
|
+
/** The one layer of `manifest` titled `title`; an offering artifact carries exactly one of each. */
|
|
60
|
+
export function layerTitled(manifest, title, ref) {
|
|
61
|
+
const layers = (manifest.document.layers ?? []).filter((layer) => layer.annotations?.[TITLE] === title);
|
|
62
|
+
if (layers.length !== 1) {
|
|
63
|
+
throw new CatalogueError(`${ref} carries ${layers.length} layers titled ${title}; an offering artifact carries exactly one`);
|
|
64
|
+
}
|
|
65
|
+
return layers[0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fetch and strictly parse the catalogue of the artifact at `ref`, resolving for `channel`. A RELEASE catalogue is read
|
|
70
|
+
* only after its E4 evidence verified against the caller's own evidence trust policy (catalogue-evidence.mjs): the
|
|
71
|
+
* verification precedes the blob fetch and the parse, so an unverified catalogue is never read. It is done by the data
|
|
72
|
+
* root's pinned built verifier, with the absolute `cosign` the caller names. `client` and `verifyEvidence` are injectable
|
|
73
|
+
* for tests.
|
|
74
|
+
*/
|
|
75
|
+
export async function fetchCatalogue({ ref, channel, username, tokenEnv, env = process.env, client, cosign, locateVerifier, verifyEvidence = verifyCatalogueEvidence }) {
|
|
60
76
|
const { authority, repository, digest, edition } = parseCatalogueRef(ref);
|
|
61
77
|
if (edition.channel !== channel) {
|
|
62
78
|
throw new CatalogueError(
|
|
@@ -69,11 +85,22 @@ export async function fetchCatalogue({ ref, channel, username, tokenEnv, env = p
|
|
|
69
85
|
if (manifest.isIndex || manifest.document.artifactType !== EDITION_ARTIFACT_TYPE) {
|
|
70
86
|
throw new CatalogueError(`${ref} is not an edition artifact (artifactType ${JSON.stringify(manifest.document.artifactType)}, expected ${EDITION_ARTIFACT_TYPE})`);
|
|
71
87
|
}
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
88
|
+
const catalogueLayer = layerTitled(manifest, CATALOGUE_FILE, ref);
|
|
89
|
+
if (channel === EditionChannel.Release) {
|
|
90
|
+
await verifyEvidence({
|
|
91
|
+
client: registry,
|
|
92
|
+
ref,
|
|
93
|
+
repository,
|
|
94
|
+
manifestDigest: digest,
|
|
95
|
+
channel,
|
|
96
|
+
offeringId: edition.distributionId,
|
|
97
|
+
cosign,
|
|
98
|
+
locateVerifier,
|
|
99
|
+
catalogueLayerDigest: catalogueLayer.digest,
|
|
100
|
+
lockLayerDigest: layerTitled(manifest, EditionLockFile.Distribution, ref).digest,
|
|
101
|
+
});
|
|
75
102
|
}
|
|
76
|
-
const bytes = await registry.getBlob(repository,
|
|
103
|
+
const bytes = await registry.getBlob(repository, catalogueLayer.digest, CATALOGUE_MAX_BYTES);
|
|
77
104
|
let document;
|
|
78
105
|
try {
|
|
79
106
|
document = JSON.parse(bytes.toString('utf8'));
|
|
@@ -81,6 +108,12 @@ export async function fetchCatalogue({ ref, channel, username, tokenEnv, env = p
|
|
|
81
108
|
throw new CatalogueError(`${ref}: ${CATALOGUE_FILE} is not JSON: ${error.message}`);
|
|
82
109
|
}
|
|
83
110
|
const catalogue = parseCatalogue(document);
|
|
111
|
+
if (catalogue.offeringId !== edition.distributionId) {
|
|
112
|
+
throw new CatalogueError(
|
|
113
|
+
`${ref}: ${CATALOGUE_FILE} is the catalogue of offering ${catalogue.offeringId}, but it was fetched from the repository of offering ${edition.distributionId}; ` +
|
|
114
|
+
'an artifact whose stated offering differs from its repository is refused',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
84
117
|
if (catalogue.channel !== edition.channel) {
|
|
85
118
|
throw new CatalogueError(
|
|
86
119
|
`${ref}: ${CATALOGUE_FILE} states channel ${catalogue.channel}, but it was fetched from the ${edition.channel} repository ${authority}/${repository}; ` +
|