@xemahq/xema 0.48.0 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/appliance-base/distribution.lock.json +3936 -1515
- package/dist/assets/appliance-base/images.lock.json +878 -1161
- package/dist/assets/appliance-lean/distribution.lock.json +3663 -916
- package/dist/assets/appliance-lean/images.lock.json +825 -924
- package/dist/assets/boot-graph.json +398 -93
- package/dist/assets/distribution-assets.manifest.json +10 -10
- package/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +5 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +0 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +4386 -2503
- package/dist/edition/verify-image-evidence.bundle.provenance.json +590 -488
- package/dist/runtime/auth/cli-application.d.ts +18 -0
- package/dist/runtime/auth/cli-application.d.ts.map +1 -0
- package/dist/runtime/auth/cli-application.js +39 -0
- package/dist/runtime/auth/cli-application.js.map +1 -0
- 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 +5 -1
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +21 -0
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +0 -18
- package/dist/runtime/launch.js.map +1 -1
- package/edition/README.md +20 -3
- package/edition/biome-content.mjs +5 -2
- package/edition/build-runtime-image-source-index.mjs +79 -5
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/catalogue-evidence.mjs +8 -5
- package/edition/customer-edition-verbs.mjs +9 -3
- package/edition/develop-coverage-run.mjs +2 -1
- package/edition/develop-coverage.mjs +12 -1
- package/edition/develop-edition-resolve.mjs +3 -2
- package/edition/develop-edition.mjs +15 -3
- package/edition/develop-record-verify.mjs +216 -0
- package/edition/develop-select-records.mjs +24 -225
- package/edition/develop-verify-image-record.mjs +90 -0
- package/edition/dist-check.mjs +24 -3
- package/edition/distribution-evidence-set.mjs +30 -2
- package/edition/edition-artifact.mjs +110 -22
- package/edition/edition-ceiling.mjs +180 -0
- package/edition/edition-config.mjs +91 -0
- package/edition/edition-constants.mjs +21 -3
- package/edition/edition-contract-parse.mjs +88 -3
- package/edition/edition-contract.json +47 -4
- package/edition/edition-image-seal.mjs +9 -2
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-protocol.mjs +7 -1
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/evidence-grammars.mjs +2 -0
- package/edition/evidence-trust-pin.mjs +66 -10
- package/edition/evidence-trust-policy.mjs +39 -29
- package/edition/external-source-walk.mjs +16 -3
- package/edition/fleet-coordinate-finality.mjs +18 -5
- package/edition/fulcio-certificate-extensions.mjs +231 -0
- package/edition/image-evidence.mjs +34 -79
- package/edition/locked-bundle-signer.mjs +241 -0
- package/edition/oci-evidence-verbs.mjs +7 -1
- package/edition/pinned-cosign.mjs +70 -0
- package/edition/plan-image-evidence-refresh.mjs +3 -1
- package/edition/producer-evidence-bundle.mjs +3 -1
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/release-edition-artifact.mjs +3 -3
- package/edition/repin-evidence-trust.mjs +6 -1
- package/edition/resolve-distribution.mjs +90 -7
- package/edition/resolve-edition.mjs +1 -1
- package/edition/resolve-images.mjs +53 -7
- package/edition/seal-image-digests.mjs +19 -8
- package/edition/sealed-distribution-lock.mjs +2 -2
- package/edition/third-party-admission.mjs +91 -0
- package/edition/third-party-bundle-registry.mjs +147 -0
- package/edition/third-party-certificate-extensions.mjs +21 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +255 -0
- package/edition/third-party-source-flags.mjs +134 -0
- package/edition/third-party-source-pins.mjs +259 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +115 -0
- package/edition/third-party-source-writer.mjs +229 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/verify-catalogue-evidence.mjs +2 -0
- package/edition/verify-image-evidence.mjs +2 -0
- package/edition/verify-image-platforms.mjs +33 -9
- package/package.json +5 -5
- package/dist/runtime/auth/client-bootstrap.d.ts +0 -11
- package/dist/runtime/auth/client-bootstrap.d.ts.map +0 -1
- package/dist/runtime/auth/client-bootstrap.js +0 -60
- package/dist/runtime/auth/client-bootstrap.js.map +0 -1
|
@@ -21,179 +21,32 @@
|
|
|
21
21
|
// the newest remaining valid one. The guard is deploy-latest's (part 07): its owner refuses a snapshot that goes
|
|
22
22
|
// backwards. This verb checks nothing against current pins.
|
|
23
23
|
|
|
24
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
|
-
import { tmpdir } from 'node:os';
|
|
26
|
-
import { join } from 'node:path';
|
|
27
|
-
|
|
28
|
-
import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
|
|
29
|
-
import { certificateConstraintArgs } from './cosign-certificate-constraints.mjs';
|
|
30
24
|
import { writeOnce } from './customer-edition-verbs.mjs';
|
|
31
|
-
import { basicAuthParts, defaultRunCosign } from './develop-coverage-readers.mjs';
|
|
32
25
|
import { developGraphMembers } from './develop-graph.mjs';
|
|
33
26
|
import { PublishedRecordMissingError, newestCommitOnDevelop } from './develop-record-ancestry.mjs';
|
|
27
|
+
import {
|
|
28
|
+
cosignEnvironment,
|
|
29
|
+
listBuildRecordRuns,
|
|
30
|
+
loadVerifiedRecord,
|
|
31
|
+
recordSignerOrigin,
|
|
32
|
+
requireBuildRecordKernel,
|
|
33
|
+
withRecordVerifier,
|
|
34
|
+
} from './develop-record-verify.mjs';
|
|
34
35
|
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
35
|
-
import {
|
|
36
|
-
import { loadDeclaredPinnedPolicy } from './evidence-trust-policy.mjs';
|
|
37
|
-
import { COSIGN_ENV, prepareTrustedCosignBinary, validatePinnedTrustedRoot } from './image-evidence.mjs';
|
|
38
|
-
import { compareStrings, loadKernelDistributionSchemas } from './lib.mjs';
|
|
36
|
+
import { compareStrings } from './lib.mjs';
|
|
39
37
|
import { RecordRefused } from './record-refused.mjs';
|
|
40
|
-
import { loadRegistryCredentialDeclarations, resolveRegistryCredential, splitImageRef } from './registry-credentials.mjs';
|
|
41
|
-
import { isNotPublishedYet } from './seal-image-digests.mjs';
|
|
42
38
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
43
|
-
import { assertPreparedExecutable } from './trusted-executable.mjs';
|
|
44
|
-
import { registryClientsFor } from './verify-image-platforms.mjs';
|
|
45
|
-
|
|
46
|
-
/** The repository segment, under the registry root, that holds a member's build records. */
|
|
47
|
-
export const BUILD_RECORDS_SEGMENT = 'build-records';
|
|
48
|
-
export const RECORD_SIGNER_WORKFLOW = '.github/workflows/record-build.yaml';
|
|
49
|
-
export const RECORD_SIGNER_OIDC_ISSUER = 'https://token.actions.githubusercontent.com';
|
|
50
|
-
/** The branches a record signer is declared for, each with the ref the record workflow runs at. */
|
|
51
|
-
const RECORD_SIGNER_REFS = Object.freeze({ develop: 'refs/heads/develop' });
|
|
52
|
-
/**
|
|
53
|
-
* A cosign exit whose stderr shows the registry was not reached before any verdict. Only this, a cosign that
|
|
54
|
-
* could not run, and a timeout are operational; every other non-zero exit of `cosign verify` is a verdict.
|
|
55
|
-
*/
|
|
56
|
-
const COSIGN_TRANSPORT_FAILURE = /dial tcp|i\/o timeout|connection (refused|reset)|no such host|TLS handshake timeout|context deadline exceeded/i;
|
|
57
|
-
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
58
|
-
|
|
59
|
-
/** The record's producer is a kind this CLI has no claims for: a closed set, never guessed at. */
|
|
60
|
-
export class UnknownBuildRecordProducerError extends RecordRefused {
|
|
61
|
-
constructor(repo, kind) {
|
|
62
|
-
super(repo, `the record's producer kind ${JSON.stringify(kind)} is not one select-records can verify`);
|
|
63
|
-
this.name = 'UnknownBuildRecordProducerError';
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export class DataRootRepositoryUndeclaredError extends Error {
|
|
68
|
-
constructor() {
|
|
69
|
-
super('edition-workspace.json declares no dataRootRepositoryUrl; select-records cannot name the record signer');
|
|
70
|
-
this.name = 'DataRootRepositoryUndeclaredError';
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export class KernelLacksBuildRecordSchemaError extends Error {
|
|
75
|
-
constructor(missing) {
|
|
76
|
-
super(`the kernel's distribution module does not export ${missing.join(', ')}; select-records needs a kernel with the build-record contract`);
|
|
77
|
-
this.name = 'KernelLacksBuildRecordSchemaError';
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** The data root's `owner/name`, from the workspace declaration; refused by name when it declares none. */
|
|
82
|
-
export function declaredDataRootRepositoryUrl() {
|
|
83
|
-
const url = editionWorkspaceConfig().dataRootRepositoryUrl;
|
|
84
|
-
if (url === undefined) {throw new DataRootRepositoryUndeclaredError();}
|
|
85
|
-
return url;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The kernel's build-record exports, refused as a set when any is absent. */
|
|
89
|
-
export function requireBuildRecordKernel(schemas) {
|
|
90
|
-
const names = ['BUILD_RECORD_MEDIA_TYPE', 'BuildRecordSchema', 'buildRecordTag', 'parseBuildRecordTag', 'compareBuildRecordRuns'];
|
|
91
|
-
const missing = names.filter((name) => schemas?.[name] === undefined);
|
|
92
|
-
if (missing.length > 0 || typeof schemas.BuildRecordSchema?.safeParse !== 'function') {
|
|
93
|
-
throw new KernelLacksBuildRecordSchemaError(missing.length > 0 ? missing : ['a BuildRecordSchema with safeParse']);
|
|
94
|
-
}
|
|
95
|
-
return schemas;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** The ref and data-root repository a branch's records are signed by; refused before any network call. */
|
|
99
|
-
export function recordSignerOrigin({ branch, dataRootRepositoryUrl }) {
|
|
100
|
-
const ref = Object.hasOwn(RECORD_SIGNER_REFS, branch) ? RECORD_SIGNER_REFS[branch] : undefined;
|
|
101
|
-
if (ref === undefined) {throw new Error(`no record signer declared for branch ${branch}`);}
|
|
102
|
-
if (dataRootRepositoryUrl === undefined) {throw new DataRootRepositoryUndeclaredError();}
|
|
103
|
-
const dataRepository = repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(dataRootRepositoryUrl, 'dataRootRepositoryUrl'), 'dataRootRepositoryUrl');
|
|
104
|
-
return { ref, dataRepository };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The signer of the branch's records: the data root's record workflow at the branch, as the certificate states
|
|
109
|
-
* it, with the repository that CALLED the workflow (the member) as the workflow-repository claim.
|
|
110
|
-
*/
|
|
111
|
-
export function recordSigner({ branch, dataRootRepositoryUrl, memberRepositoryUrl }) {
|
|
112
|
-
const { ref, dataRepository } = recordSignerOrigin({ branch, dataRootRepositoryUrl });
|
|
113
|
-
const member = repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(memberRepositoryUrl, 'repositoryUrl'), 'repositoryUrl');
|
|
114
|
-
return {
|
|
115
|
-
certificateIdentity: `https://github.com/${dataRepository}/${RECORD_SIGNER_WORKFLOW}@${ref}`,
|
|
116
|
-
oidcIssuer: RECORD_SIGNER_OIDC_ISSUER,
|
|
117
|
-
githubWorkflow: { repository: member, ref },
|
|
118
|
-
memberRepository: member,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* What a record's producer says, read through the ONE switch over its kind: the repository that called the
|
|
124
|
-
* record workflow (the cosign caller claim, held equal to the member's) and the run the snapshot names.
|
|
125
|
-
*/
|
|
126
|
-
export function producerClaims({ record, repo, memberRepository }) {
|
|
127
|
-
switch (record.producer?.kind) {
|
|
128
|
-
case 'github-actions': {
|
|
129
|
-
const { repository, runNumber, runAttempt } = record.producer;
|
|
130
|
-
if (repository !== memberRepository) {throw new RecordRefused(repo, `record is produced by repository ${repository}, not ${memberRepository}`);}
|
|
131
|
-
return { repository, snapshot: `${runNumber}.${runAttempt}` };
|
|
132
|
-
}
|
|
133
|
-
default:
|
|
134
|
-
throw new UnknownBuildRecordProducerError(repo, record.producer?.kind);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
39
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
40
|
+
// The record-verification names this verb's tests and callers read from here, owned by the shared core.
|
|
41
|
+
export {
|
|
42
|
+
DataRootRepositoryUndeclaredError,
|
|
43
|
+
KernelLacksBuildRecordSchemaError,
|
|
44
|
+
UnknownBuildRecordProducerError,
|
|
45
|
+
declaredDataRootRepositoryUrl,
|
|
46
|
+
recordSigner,
|
|
47
|
+
} from './develop-record-verify.mjs';
|
|
146
48
|
|
|
147
|
-
|
|
148
|
-
async function readRecord({ kernel, client, repository, repo, tag }) {
|
|
149
|
-
const served = await fromRegistry(repo, `${repository}:${tag}`, () => client.getManifestByTag(repository, tag));
|
|
150
|
-
const manifest = await fromRegistry(repo, `${repository}@${served.digest}`, () => client.getManifestByDigest(repository, served.digest));
|
|
151
|
-
const layers = manifest.document.layers;
|
|
152
|
-
if (!Array.isArray(layers) || layers.length !== 1) {
|
|
153
|
-
throw new RecordRefused(repo, `record ${tag} has ${Array.isArray(layers) ? layers.length : 'no'} layers, not exactly one`);
|
|
154
|
-
}
|
|
155
|
-
const [layer] = layers;
|
|
156
|
-
if (layer.mediaType !== kernel.BUILD_RECORD_MEDIA_TYPE) {
|
|
157
|
-
throw new RecordRefused(repo, `record ${tag} layer media type is ${JSON.stringify(layer.mediaType)}, not ${kernel.BUILD_RECORD_MEDIA_TYPE}`);
|
|
158
|
-
}
|
|
159
|
-
const bytes = await fromRegistry(repo, `${repository}@${layer.digest}`, () => client.getBlob(repository, layer.digest));
|
|
160
|
-
let document;
|
|
161
|
-
try {
|
|
162
|
-
document = JSON.parse(bytes.toString('utf8'));
|
|
163
|
-
} catch (error) {
|
|
164
|
-
throw new RecordRefused(repo, `record ${tag} is not JSON: ${error.message}`);
|
|
165
|
-
}
|
|
166
|
-
const parsed = kernel.BuildRecordSchema.safeParse(document);
|
|
167
|
-
if (!parsed.success) {throw new RecordRefused(repo, `record ${tag} does not parse as a build record: ${parsed.error.message}`);}
|
|
168
|
-
return { record: parsed.data, digest: served.digest };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** The tag's record, verified by digest against the constrained signer, then held to its own repo, branch and tag. */
|
|
172
|
-
async function loadVerifiedRecord({ kernel, client, runCosign, cosignEnv, trustedRootPath, repository, member, branch, dataRootRepositoryUrl, tag }) {
|
|
173
|
-
const repo = member.repo;
|
|
174
|
-
const { record, digest } = await readRecord({ kernel, client, repository, repo, tag });
|
|
175
|
-
const signer = recordSigner({ branch, dataRootRepositoryUrl, memberRepositoryUrl: member.repositoryUrl });
|
|
176
|
-
const { repository: callerRepository, snapshot } = producerClaims({ record, repo, memberRepository: signer.memberRepository });
|
|
177
|
-
signer.githubWorkflow.repository = callerRepository;
|
|
178
|
-
const ref = `${repository}@${digest}`;
|
|
179
|
-
const args = ['verify', '--offline', '--trusted-root', trustedRootPath, ...certificateConstraintArgs(signer, record.commit), ref];
|
|
180
|
-
let result;
|
|
181
|
-
try {
|
|
182
|
-
result = await runCosign(args, cosignEnv);
|
|
183
|
-
} catch (error) {
|
|
184
|
-
// no numeric exit code: cosign did not run (spawn failure) or did not answer (timeout, kill signal)
|
|
185
|
-
throw new Error(`select-records: ${repo}: cosign could not run for ${ref}: ${error.message}`);
|
|
186
|
-
}
|
|
187
|
-
if (result.code !== 0) {
|
|
188
|
-
if (COSIGN_TRANSPORT_FAILURE.test(result.stderr)) {
|
|
189
|
-
throw new Error(`select-records: ${repo}: cosign could not reach the registry for ${ref} (exit ${result.code}): ${result.stderr.trim()}`);
|
|
190
|
-
}
|
|
191
|
-
throw new RecordRefused(repo, `cosign refused record ${tag} (${digest}) as signed by ${signer.certificateIdentity} (exit ${result.code}): ${result.stderr.trim()}`);
|
|
192
|
-
}
|
|
193
|
-
if (record.branch !== branch) {throw new RecordRefused(repo, `record ${tag} is for branch ${record.branch}, not ${branch}`);}
|
|
194
|
-
if (kernel.buildRecordTag(record) !== tag) {throw new RecordRefused(repo, `record tagged ${tag} says it is ${kernel.buildRecordTag(record)}`);}
|
|
195
|
-
return { record, snapshot };
|
|
196
|
-
}
|
|
49
|
+
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
197
50
|
|
|
198
51
|
/**
|
|
199
52
|
* @param {object} request
|
|
@@ -221,26 +74,12 @@ export async function selectRecords({ branch, registry, at, members, dataRootRep
|
|
|
221
74
|
}
|
|
222
75
|
const selected = [];
|
|
223
76
|
for (const member of members) {
|
|
224
|
-
const repository =
|
|
225
|
-
const { authority } = splitImageRef(repository);
|
|
226
|
-
const client = clientFor(authority);
|
|
227
|
-
let tags;
|
|
228
|
-
try {
|
|
229
|
-
tags = await client.listTags(repository);
|
|
230
|
-
} catch (error) {
|
|
231
|
-
if (isNotPublishedYet(error)) {throw new RecordRefused(member.repo, `${repository} does not exist: no build record was ever published`);}
|
|
232
|
-
throw new Error(`select-records: ${member.repo}: the registry did not answer for ${repository} tags: ${error.message}`);
|
|
233
|
-
}
|
|
234
|
-
const runs = tags
|
|
235
|
-
.map((tag) => ({ tag, run: kernel.parseBuildRecordTag(tag, branch) }))
|
|
236
|
-
.filter(({ run }) => run !== undefined)
|
|
237
|
-
.sort((a, b) => kernel.compareBuildRecordRuns(b.run, a.run));
|
|
238
|
-
if (runs.length === 0) {throw new RecordRefused(member.repo, `${repository} has no build record for branch ${branch}`);}
|
|
77
|
+
const { repository, authority, client, runs } = await listBuildRecordRuns({ kernel, clientFor, registry, member, branch });
|
|
239
78
|
// EVERY candidate is read and verified; any that fails refuses the run. None is skipped.
|
|
240
79
|
const verified = [];
|
|
241
80
|
for (const { tag, run } of runs) {
|
|
242
81
|
const loaded = await loadVerifiedRecord({
|
|
243
|
-
kernel, client, runCosign, cosignEnv:
|
|
82
|
+
kernel, client, runCosign, cosignEnv: cosignEnvironment(cosignEnvFor, authority), trustedRootPath,
|
|
244
83
|
repository, member, branch, dataRootRepositoryUrl, tag,
|
|
245
84
|
});
|
|
246
85
|
verified.push({ tag, run, ...loaded });
|
|
@@ -302,55 +141,18 @@ export function readPublishedPins(path) {
|
|
|
302
141
|
export async function runSelectRecords({
|
|
303
142
|
options,
|
|
304
143
|
env = process.env,
|
|
305
|
-
loadPolicy = loadDeclaredPinnedPolicy,
|
|
306
|
-
prepareCosign = prepareTrustedCosignBinary,
|
|
307
|
-
loadKernel = loadKernelDistributionSchemas,
|
|
308
|
-
trustedRootBytes = (root, pinnedPolicy) => validatePinnedTrustedRoot(root, pinnedPolicy).bytes,
|
|
309
|
-
cosignPathOf = (handle) => assertPreparedExecutable(handle, 'cosign'),
|
|
310
144
|
members = developGraphMembers(),
|
|
311
|
-
runCosign: injectedRunCosign,
|
|
312
|
-
clientsOverride,
|
|
313
145
|
runGit,
|
|
314
146
|
write = writeOnce,
|
|
147
|
+
...verifierInjections
|
|
315
148
|
}) {
|
|
316
|
-
|
|
317
|
-
const { policy, dataRoot: policyRoot } = await loadPolicy();
|
|
318
|
-
const kernel = requireBuildRecordKernel(await loadKernel());
|
|
319
|
-
const trustedCosign = prepareCosign({ cosignBinary: options['--cosign'], policy });
|
|
320
|
-
const cosignPath = cosignPathOf(trustedCosign);
|
|
321
|
-
const runCosign = injectedRunCosign ?? ((args, cosignEnv) => defaultRunCosign(cosignPath, args, cosignEnv));
|
|
322
|
-
const scratch = mkdtempSync(join(tmpdir(), 'xema-select-records-'));
|
|
323
|
-
try {
|
|
324
|
-
const trustedRootPath = join(scratch, 'trusted-root.json');
|
|
325
|
-
writeFileSync(trustedRootPath, trustedRootBytes(policyRoot, policy), { flag: 'wx', mode: 0o600 });
|
|
326
|
-
const declarations = clientsOverride === undefined ? loadRegistryCredentialDeclarations(dataRoot()) : null;
|
|
327
|
-
const clients = new Map();
|
|
328
|
-
const clientFor = (authority) => {
|
|
329
|
-
if (clientsOverride !== undefined) {return clientsOverride(authority);}
|
|
330
|
-
if (!clients.has(authority)) {
|
|
331
|
-
clients.set(authority, registryClientsFor([{ imageRef: `${authority}/x` }], { declarations, env }).get(authority));
|
|
332
|
-
}
|
|
333
|
-
return clients.get(authority);
|
|
334
|
-
};
|
|
335
|
-
// cosign gets the SAME declared credential oras uses, in its environment, never argv.
|
|
336
|
-
const cosignEnvFor = (authority) => {
|
|
337
|
-
if (clientsOverride !== undefined) {return {};}
|
|
338
|
-
const credential = resolveRegistryCredential({ authority, declarations, env });
|
|
339
|
-
if (credential.access === 'anonymous') {return {};}
|
|
340
|
-
const { username, password } = basicAuthParts(credential);
|
|
341
|
-
return { COSIGN_REGISTRY_USERNAME: username, COSIGN_REGISTRY_PASSWORD: password };
|
|
342
|
-
};
|
|
149
|
+
return withRecordVerifier({ cosignBinary: options['--cosign'], env, ...verifierInjections }, async (verifier) => {
|
|
343
150
|
const selection = await selectRecords({
|
|
344
151
|
branch: options['--branch'],
|
|
345
152
|
registry: options['--registry'],
|
|
346
153
|
at: options['--at'],
|
|
347
154
|
members,
|
|
348
|
-
|
|
349
|
-
kernel,
|
|
350
|
-
clientFor,
|
|
351
|
-
runCosign,
|
|
352
|
-
cosignEnvFor,
|
|
353
|
-
trustedRootPath,
|
|
155
|
+
...verifier,
|
|
354
156
|
publishedPins: options['--published-pins'] === undefined ? undefined : readPublishedPins(options['--published-pins']),
|
|
355
157
|
workspace: editionWorkspaceConfig(),
|
|
356
158
|
env,
|
|
@@ -358,8 +160,5 @@ export async function runSelectRecords({
|
|
|
358
160
|
});
|
|
359
161
|
write(options['--out'], selection);
|
|
360
162
|
return selection;
|
|
361
|
-
}
|
|
362
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
363
|
-
trustedCosign.dispose();
|
|
364
|
-
}
|
|
163
|
+
});
|
|
365
164
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// develop-verify-image-record — the body of `xema edition develop verify-image-record`.
|
|
2
|
+
//
|
|
3
|
+
// Does an authenticated build record bind this image digest to this producer? The producer's records of the
|
|
4
|
+
// develop branch are read newest run first and each is verified by the shared core (`develop-record-verify.mjs`:
|
|
5
|
+
// cosign keyless identity of the data root's record workflow, caller repository = the producer, ref, workflow
|
|
6
|
+
// sha = the record's commit, and the record's producer repository = the producer). Only a record produced by the
|
|
7
|
+
// producer's own CI workflow (`.github/workflows/ci.yaml`) can name the images it serves: a verified record of
|
|
8
|
+
// another workflow is skipped, and the answer is the newest ci.yaml record that lists the exact `name@digest`.
|
|
9
|
+
//
|
|
10
|
+
// Deterministic: `listBuildRecordRuns` (develop-record-verify.mjs:205) sorts the tags by the kernel's
|
|
11
|
+
// `compareBuildRecordRuns` (build-record.ts:218-228: run number, then attempt), highest first, and the first
|
|
12
|
+
// match wins; a record that fails verification refuses the call before any older one is read.
|
|
13
|
+
//
|
|
14
|
+
// The returned `commit` is the commit of the newest verified ci.yaml record (by run number, then attempt) that lists
|
|
15
|
+
// the digest. That can be an older commit when a later run targeted it (a manual ci.yaml run on an older commit gets a
|
|
16
|
+
// higher run number), and for an image the producer REUSED by content hash it is NOT the commit that built the image.
|
|
17
|
+
// This is safe because the digest is what binds the bytes: every ci.yaml record lists every image served at its commit,
|
|
18
|
+
// built or reused, so the record names the running digest without a rebuild.
|
|
19
|
+
//
|
|
20
|
+
// Every refusal is a `RecordRefused` naming the producer: no ci.yaml record lists the image, the record names
|
|
21
|
+
// another producer, or cosign refuses the identity. A registry or cosign that could not answer is a plain Error.
|
|
22
|
+
// On success it prints `{image, digest, producer, commit}`.
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
cosignEnvironment,
|
|
26
|
+
listBuildRecordRuns,
|
|
27
|
+
loadVerifiedRecord,
|
|
28
|
+
withRecordVerifier,
|
|
29
|
+
} from './develop-record-verify.mjs';
|
|
30
|
+
import { serializeJson } from './lib.mjs';
|
|
31
|
+
import { RecordRefused } from './record-refused.mjs';
|
|
32
|
+
|
|
33
|
+
/** The only branch with a declared record signer. */
|
|
34
|
+
export const VERIFY_IMAGE_RECORD_BRANCH = 'develop';
|
|
35
|
+
/** The only workflow whose records name a producer's served images. */
|
|
36
|
+
export const PRODUCER_IMAGE_WORKFLOW = '.github/workflows/ci.yaml';
|
|
37
|
+
|
|
38
|
+
const IMAGE_REFERENCE = /^([^@\s]+)@(sha256:[0-9a-f]{64})$/;
|
|
39
|
+
const PRODUCER_REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
40
|
+
|
|
41
|
+
/** `name@sha256:<64 hex>` split into its name and digest; anything else is refused. */
|
|
42
|
+
export function parseImageReference(image) {
|
|
43
|
+
const match = IMAGE_REFERENCE.exec(image);
|
|
44
|
+
if (match === null) {throw new Error(`--image must be <name>@sha256:<64 hex digest>; got ${JSON.stringify(image)}`);}
|
|
45
|
+
return { name: match[1], digest: match[2] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The producer member the records are read for: its repository `owner/name` and the URL the signer constrains. */
|
|
49
|
+
function producerMember(producer) {
|
|
50
|
+
if (!PRODUCER_REPOSITORY.test(producer)) {throw new Error(`--producer must be <owner>/<repo>; got ${JSON.stringify(producer)}`);}
|
|
51
|
+
return { repo: producer, repositoryUrl: `https://github.com/${producer}.git` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {object} request
|
|
56
|
+
* @param {string} request.image `<name>@sha256:<digest>`
|
|
57
|
+
* @param {string} request.producer `<owner>/<repo>`
|
|
58
|
+
* @param {string} request.registry the registry root that holds `build-records/<repo>`
|
|
59
|
+
* @returns {Promise<{image: string, digest: string, producer: string, commit: string}>}
|
|
60
|
+
*/
|
|
61
|
+
export async function verifyImageRecord({ image, producer, registry, dataRootRepositoryUrl, kernel, clientFor, runCosign, cosignEnvFor, trustedRootPath }) {
|
|
62
|
+
const { name, digest } = parseImageReference(image);
|
|
63
|
+
const member = producerMember(producer);
|
|
64
|
+
const branch = VERIFY_IMAGE_RECORD_BRANCH;
|
|
65
|
+
const { repository, authority, client, runs } = await listBuildRecordRuns({ kernel, clientFor, registry, member, branch });
|
|
66
|
+
for (const { tag } of runs) {
|
|
67
|
+
const { record } = await loadVerifiedRecord({
|
|
68
|
+
kernel, client, runCosign, cosignEnv: cosignEnvironment(cosignEnvFor, authority), trustedRootPath,
|
|
69
|
+
repository, member, branch, dataRootRepositoryUrl, tag,
|
|
70
|
+
});
|
|
71
|
+
if (record.producer.workflow !== PRODUCER_IMAGE_WORKFLOW) {continue;}
|
|
72
|
+
if (!record.images.some((listed) => listed.name === name && listed.digest === digest)) {continue;}
|
|
73
|
+
return { image: name, digest, producer, commit: record.commit };
|
|
74
|
+
}
|
|
75
|
+
throw new RecordRefused(producer, `no verified ${PRODUCER_IMAGE_WORKFLOW} build record of ${repository} for branch ${branch} lists ${name}@${digest}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The `verify-image-record` verb. */
|
|
79
|
+
export async function runVerifyImageRecord({ options, env = process.env, write = (text) => process.stdout.write(text), ...verifierInjections }) {
|
|
80
|
+
return withRecordVerifier({ cosignBinary: options['--cosign'], env, ...verifierInjections }, async (verifier) => {
|
|
81
|
+
const verified = await verifyImageRecord({
|
|
82
|
+
image: options['--image'],
|
|
83
|
+
producer: options['--producer'],
|
|
84
|
+
registry: options['--registry'],
|
|
85
|
+
...verifier,
|
|
86
|
+
});
|
|
87
|
+
write(serializeJson(verified));
|
|
88
|
+
return verified;
|
|
89
|
+
});
|
|
90
|
+
}
|
package/edition/dist-check.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
|
48
48
|
import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
|
|
49
49
|
import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
50
50
|
import { describeEditionHolds } from './edition-source-pins.mjs';
|
|
51
|
-
import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, kernelPackageDir, platformServiceIndexPath, runtimeImageSourceIndexPath, workspaceArgv } from './edition-workspace.mjs';
|
|
51
|
+
import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, dataRoot, editionsRoot, isEntryModule, kernelPackageDir, platformServiceIndexPath, runtimeImageSourceIndexPath, workspaceArgv } from './edition-workspace.mjs';
|
|
52
52
|
import { checkEvidenceTrustPolicy } from './evidence-trust-policy.mjs';
|
|
53
53
|
import {
|
|
54
54
|
compareIndexEntries,
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
validateServiceDependencyOwnerParity,
|
|
65
65
|
} from './lib.mjs';
|
|
66
66
|
import { commandFor } from './remedy-command.mjs';
|
|
67
|
-
import { resolveDistributionWithIndex } from './resolve-distribution.mjs';
|
|
67
|
+
import { assertReleaseGitSourcesDeclareNoBundle, resolveDistributionWithIndex } from './resolve-distribution.mjs';
|
|
68
68
|
import {
|
|
69
69
|
resolveDistributionDeclaredSources,
|
|
70
70
|
resolveDistributionRequiredPlatforms,
|
|
@@ -72,12 +72,15 @@ import {
|
|
|
72
72
|
resolveImages,
|
|
73
73
|
resolveComposedProcessStructure,
|
|
74
74
|
resolveImageStructure,
|
|
75
|
+
structureSubjects,
|
|
75
76
|
sourceTreeLocator,
|
|
76
77
|
supplementalSourcesFor,
|
|
77
78
|
} from './resolve-images.mjs';
|
|
78
79
|
import { withoutSealedDigests } from './seal-image-digests.mjs';
|
|
79
80
|
import { createPinnedSourceAuthority } from './source-authority.mjs';
|
|
80
81
|
import { readStorePublisherTrust } from './store-publisher-trust.mjs';
|
|
82
|
+
import { CHECK_THIRD_PARTY_FLAG, COSIGN_BIN_ENV, COSIGN_FLAG } from './third-party-source-flags.mjs';
|
|
83
|
+
import { loadThirdPartySourcePins } from './third-party-source-pins.mjs';
|
|
81
84
|
|
|
82
85
|
if (isEntryModule(import.meta.url)) {
|
|
83
86
|
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache]);
|
|
@@ -157,6 +160,7 @@ export async function checkDistributionLock(id) {
|
|
|
157
160
|
const lockPath = join(editionsRoot(), id, 'distribution.lock.json');
|
|
158
161
|
try {
|
|
159
162
|
const { lock: fresh, index } = await resolveDistributionWithIndex(id);
|
|
163
|
+
assertReleaseGitSourcesDeclareNoBundle(fresh, index);
|
|
160
164
|
const verdict = await compareCommittedLock(lockPath, fresh);
|
|
161
165
|
if (verdict === LockCheck.MISSING) {
|
|
162
166
|
console.error(
|
|
@@ -302,7 +306,11 @@ export async function checkImagesLock(id, checkSourceEvidence) {
|
|
|
302
306
|
schemas,
|
|
303
307
|
requiredPlatforms,
|
|
304
308
|
resolveDistributionDeclaredSources(schemas, id),
|
|
305
|
-
{
|
|
309
|
+
{
|
|
310
|
+
biomeIndex: pinned.biomeIndex,
|
|
311
|
+
supplementalSources: supplementalSourcesFor(runtimeSourceIndex),
|
|
312
|
+
thirdPartyPins: loadThirdPartySourcePins(dataRoot()),
|
|
313
|
+
},
|
|
306
314
|
),
|
|
307
315
|
schemas,
|
|
308
316
|
requiredPlatforms,
|
|
@@ -311,6 +319,7 @@ export async function checkImagesLock(id, checkSourceEvidence) {
|
|
|
311
319
|
distributionId: id,
|
|
312
320
|
contractAuthority: runtimeSourceIndex.contractAuthority,
|
|
313
321
|
...structure,
|
|
322
|
+
subjects: structureSubjects(structure),
|
|
314
323
|
};
|
|
315
324
|
if (!existsSync(imageLockPath)) {
|
|
316
325
|
console.error(
|
|
@@ -385,6 +394,18 @@ export async function checkCommittedRuntimeImageSourceIndex(checkSources) {
|
|
|
385
394
|
],
|
|
386
395
|
{ stdio: 'inherit' },
|
|
387
396
|
);
|
|
397
|
+
// Every pinned third-party source, from its committed evidence (a data root with none passes trivially).
|
|
398
|
+
const cosign = process.env[COSIGN_BIN_ENV];
|
|
399
|
+
execFileSync(
|
|
400
|
+
process.execPath,
|
|
401
|
+
[
|
|
402
|
+
join(HERE, 'build-runtime-image-source-index.mjs'),
|
|
403
|
+
CHECK_THIRD_PARTY_FLAG,
|
|
404
|
+
...(cosign === undefined || cosign === '' ? [] : [COSIGN_FLAG, cosign]),
|
|
405
|
+
...workspaceArgv(),
|
|
406
|
+
],
|
|
407
|
+
{ stdio: 'inherit' },
|
|
408
|
+
);
|
|
388
409
|
return true;
|
|
389
410
|
} catch (error) {
|
|
390
411
|
console.error(
|
|
@@ -67,6 +67,8 @@ import {
|
|
|
67
67
|
} from './oci-image-source-evidence.mjs';
|
|
68
68
|
import { BUNDLE_MEDIA_TYPE, canonicalSignedBundleBytes, cosignVerifyAttestationArgs, validateProducedBundle } from './produce-image-source-evidence.mjs';
|
|
69
69
|
import { splitImageRef } from './registry-credentials.mjs';
|
|
70
|
+
import { imageSubjectKey, thirdPartyImagePins } from './third-party-image-pins.mjs';
|
|
71
|
+
import { thirdPartyProducerRecord } from './third-party-producer-evidence.mjs';
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
74
|
* The ONE Sigstore bundle media type this plane stores — re-exported, never
|
|
@@ -467,6 +469,8 @@ export async function resolveProducerEvidence({
|
|
|
467
469
|
expectedContractAuthority,
|
|
468
470
|
registryClientFactory,
|
|
469
471
|
channel,
|
|
472
|
+
// The reader of the edition's validated locks; a seam so the assembly is testable without a whole edition tree.
|
|
473
|
+
loadLocks = loadValidatedLocks,
|
|
470
474
|
}) {
|
|
471
475
|
assertCanonicalDistributionId(distributionId);
|
|
472
476
|
assertAbsolutePath(dataRoot, 'data root');
|
|
@@ -474,8 +478,8 @@ export async function resolveProducerEvidence({
|
|
|
474
478
|
if (!expectedContractAuthority) {
|
|
475
479
|
throw new Error('the expected kernel contract authority is required');
|
|
476
480
|
}
|
|
477
|
-
const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex } =
|
|
478
|
-
await
|
|
481
|
+
const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex, thirdPartySources } =
|
|
482
|
+
await loadLocks(distributionId, dataRoot, schemas, { editionsRoot, channel });
|
|
479
483
|
assertKernelContractAuthorityEqual(
|
|
480
484
|
runtimeSourceIndex.contractAuthority,
|
|
481
485
|
expectedContractAuthority,
|
|
@@ -486,8 +490,32 @@ export async function resolveProducerEvidence({
|
|
|
486
490
|
factory: registryClientFactory,
|
|
487
491
|
});
|
|
488
492
|
|
|
493
|
+
const thirdPartyImages = thirdPartyImagePins({
|
|
494
|
+
pins: { byRepository: (repository) => thirdPartySources.get(repository) },
|
|
495
|
+
subjects: imageLock.subjects,
|
|
496
|
+
});
|
|
489
497
|
const producers = [];
|
|
490
498
|
for (const subject of imageLock.subjects) {
|
|
499
|
+
const pinImage = thirdPartyImages.get(imageSubjectKey(subject));
|
|
500
|
+
if (pinImage !== undefined) {
|
|
501
|
+
// A third-party subject's contentHash is publisher-attested, not a tag: its evidence is the committed evidence its pin
|
|
502
|
+
// binds, read from the data root with no registry call.
|
|
503
|
+
producers.push(
|
|
504
|
+
thirdPartyProducerRecord({
|
|
505
|
+
subject,
|
|
506
|
+
pinImage,
|
|
507
|
+
dataRoot,
|
|
508
|
+
schemas,
|
|
509
|
+
expectedSource: expectedProducerSource({
|
|
510
|
+
subject,
|
|
511
|
+
repositoryPin: repositoryPins.get(subject.repo),
|
|
512
|
+
sourceHashAlgorithm: imageLock.sourceHashAlgorithm,
|
|
513
|
+
}),
|
|
514
|
+
stem: subjectStem(subject),
|
|
515
|
+
}),
|
|
516
|
+
);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
491
519
|
const expectedSource = expectedProducerSource({
|
|
492
520
|
subject,
|
|
493
521
|
repositoryPin: repositoryPins.get(subject.repo),
|