@xemahq/xema 0.53.0 → 0.55.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/README.md +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +66 -0
- package/dist/commands/auth.js.map +1 -0
- 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 +2062 -1025
- package/dist/edition/verify-image-evidence.bundle.provenance.json +595 -505
- package/dist/runtime/edition/verb-map.d.ts +2 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +2 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/dist/runtime/lock-loader.d.ts +2 -1
- package/dist/runtime/lock-loader.d.ts.map +1 -1
- package/dist/runtime/lock-loader.js +15 -5
- package/dist/runtime/lock-loader.js.map +1 -1
- package/edition/README.md +61 -4
- package/edition/adapter-bundled-versions.mjs +2 -1
- package/edition/authorization-base.mjs +9 -8
- package/edition/biome-content.mjs +23 -29
- package/edition/candidate-edition.mjs +692 -0
- package/edition/check-committed-image-evidence.mjs +44 -15
- package/edition/checkout-indexed-source-repositories.mjs +6 -8
- package/edition/collected-refusals.mjs +59 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +18 -8
- package/edition/customer-edition-verbs.mjs +43 -10
- package/edition/develop-chart-set.mjs +7 -6
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +2 -1
- package/edition/develop-edition-resolve.mjs +110 -31
- package/edition/develop-edition.mjs +73 -11
- package/edition/develop-engine-image.mjs +95 -0
- package/edition/develop-freeze.mjs +24 -1
- package/edition/develop-graph.mjs +5 -3
- package/edition/develop-image-probe.mjs +8 -5
- package/edition/develop-image-record.mjs +194 -0
- package/edition/develop-record-ancestry.mjs +23 -5
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +29 -21
- package/edition/develop-selection-line.mjs +30 -0
- package/edition/develop-selection-pins.mjs +30 -0
- package/edition/develop-snapshot-inputs.mjs +4 -3
- package/edition/develop-verify-image-record.mjs +23 -4
- package/edition/dist-check.mjs +45 -9
- package/edition/distribution-evidence-set.mjs +14 -9
- package/edition/edition-artifact.mjs +17 -7
- package/edition/edition-image-seal.mjs +2 -1
- package/edition/edition-protocol.mjs +8 -19
- package/edition/image-evidence.mjs +6 -2
- package/edition/image-lock-service-rows.mjs +4 -4
- package/edition/image-subject-identity.mjs +24 -0
- package/edition/images-from-record.mjs +298 -0
- package/edition/lib.mjs +25 -45
- package/edition/locked-bundle-signer.mjs +15 -3
- package/edition/operation-grant-admission.mjs +51 -9
- package/edition/resolve-customer-edition.mjs +1 -1
- package/edition/resolve-distribution.mjs +95 -82
- package/edition/resolve-edition.mjs +2 -1
- package/edition/resolve-images.mjs +233 -14
- package/edition/seal-image-digests.mjs +24 -13
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/verify-image-platforms.mjs +2 -0
- package/edition/workspace-sections.mjs +33 -3
- package/package.json +7 -3
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { execFileSync } from 'node:child_process';
|
|
19
19
|
|
|
20
|
+
import { RefusalError } from './collected-refusals.mjs';
|
|
20
21
|
import { dataRoot, deployRoot, editionsRoot, kernelRepoDir, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
|
|
21
22
|
import { buildIndexesAtPins } from './indexes-at-pins.mjs';
|
|
22
23
|
import {
|
|
@@ -45,14 +46,14 @@ function git(dir, args) {
|
|
|
45
46
|
*/
|
|
46
47
|
export function assertFrozenCheckout({ dir, pins, key }) {
|
|
47
48
|
const pinned = pins[key];
|
|
48
|
-
if (pinned === undefined) {throw new
|
|
49
|
+
if (pinned === undefined) {throw new RefusalError(`the snapshot has no pin "${key}" for ${dir}`);}
|
|
49
50
|
const head = git(dir, ['rev-parse', 'HEAD']);
|
|
50
51
|
if (head !== pinned) {
|
|
51
|
-
throw new
|
|
52
|
+
throw new RefusalError(`${dir} is at ${head}, but the snapshot pins "${key}" at ${pinned}`);
|
|
52
53
|
}
|
|
53
54
|
const dirty = git(dir, ['status', '--porcelain']);
|
|
54
55
|
if (dirty.length > 0) {
|
|
55
|
-
throw new
|
|
56
|
+
throw new RefusalError(`${dir} is not clean, so it is not the frozen commit ${pinned} ("${key}"):\n${dirty}`);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -58,8 +58,28 @@ function producerMember(producer) {
|
|
|
58
58
|
* @param {string} request.registry the registry root that holds `build-records/<repo>`
|
|
59
59
|
* @returns {Promise<{image: string, digest: string, producer: string, commit: string}>}
|
|
60
60
|
*/
|
|
61
|
-
export async function verifyImageRecord({ image, producer,
|
|
61
|
+
export async function verifyImageRecord({ image, producer, ...verifier }) {
|
|
62
62
|
const { name, digest } = parseImageReference(image);
|
|
63
|
+
const record = await findVerifiedProducerImageRecord({
|
|
64
|
+
producer,
|
|
65
|
+
...verifier,
|
|
66
|
+
wanted: `lists ${name}@${digest}`,
|
|
67
|
+
matches: (candidate) => candidate.images.some((listed) => listed.name === name && listed.digest === digest),
|
|
68
|
+
});
|
|
69
|
+
return { image: name, digest, producer, commit: record.commit };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The newest verified develop ci.yaml record of `producer` (newest run first) for which `matches(record)` holds.
|
|
74
|
+
* Every record read on the way is cosign-verified, so a newer bad record refuses before an older one is read.
|
|
75
|
+
* Refused, naming `wanted`, when no verified ci.yaml record matches.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} request
|
|
78
|
+
* @param {string} request.producer `<owner>/<repo>`
|
|
79
|
+
* @param {(record: object) => boolean} request.matches
|
|
80
|
+
* @param {string} request.wanted what a match is, for the refusal ("lists <name>@<digest>")
|
|
81
|
+
*/
|
|
82
|
+
export async function findVerifiedProducerImageRecord({ producer, matches, wanted, registry, dataRootRepositoryUrl, kernel, clientFor, runCosign, cosignEnvFor, trustedRootPath }) {
|
|
63
83
|
const member = producerMember(producer);
|
|
64
84
|
const branch = VERIFY_IMAGE_RECORD_BRANCH;
|
|
65
85
|
const { repository, authority, client, runs } = await listBuildRecordRuns({ kernel, clientFor, registry, member, branch });
|
|
@@ -69,10 +89,9 @@ export async function verifyImageRecord({ image, producer, registry, dataRootRep
|
|
|
69
89
|
repository, member, branch, dataRootRepositoryUrl, tag,
|
|
70
90
|
});
|
|
71
91
|
if (record.producer.workflow !== PRODUCER_IMAGE_WORKFLOW) {continue;}
|
|
72
|
-
if (
|
|
73
|
-
return { image: name, digest, producer, commit: record.commit };
|
|
92
|
+
if (matches(record)) {return record;}
|
|
74
93
|
}
|
|
75
|
-
throw new RecordRefused(producer, `no verified ${PRODUCER_IMAGE_WORKFLOW} build record of ${repository} for branch ${branch}
|
|
94
|
+
throw new RecordRefused(producer, `no verified ${PRODUCER_IMAGE_WORKFLOW} build record of ${repository} for branch ${branch} ${wanted}`);
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
/** The `verify-image-record` verb. */
|
package/edition/dist-check.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
|
50
50
|
import { describeEditionHolds } from './edition-source-pins.mjs';
|
|
51
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
|
+
import { imageSubjectIdentity } from './image-subject-identity.mjs';
|
|
53
54
|
import {
|
|
54
55
|
compareIndexEntries,
|
|
55
56
|
comparableDistributionLock,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
loadKernelDistributionSchemas,
|
|
59
60
|
parseResolvedDistributionLock,
|
|
60
61
|
serializeJson,
|
|
62
|
+
sha256Hex,
|
|
61
63
|
validateImmutableBiomeIndex,
|
|
62
64
|
validateImmutableDistributionLock,
|
|
63
65
|
validateImmutableImageLock,
|
|
@@ -69,6 +71,8 @@ import {
|
|
|
69
71
|
resolveDistributionDeclaredSources,
|
|
70
72
|
resolveDistributionRequiredPlatforms,
|
|
71
73
|
externalSourceTrees,
|
|
74
|
+
carryComposedForwardStructurally,
|
|
75
|
+
readCommittedPreviousLock,
|
|
72
76
|
resolveImages,
|
|
73
77
|
resolveComposedProcessStructure,
|
|
74
78
|
resolveImageStructure,
|
|
@@ -237,6 +241,24 @@ async function checkAuthorizationLock(id, kernel, index) {
|
|
|
237
241
|
* with the same structure ship the same images and bundle the same web biomes,
|
|
238
242
|
* regardless of current source content (which the build re-hashes anyway).
|
|
239
243
|
*/
|
|
244
|
+
/**
|
|
245
|
+
* The committed lock with a stand-in `imageDigest` on every UNSEALED composed artifact, so the kernel can carry it forward.
|
|
246
|
+
* The kernel's v5 parse refuses two composed artifacts sharing one imageDigest, so each stand-in is unique to its subject:
|
|
247
|
+
* the sha256 of its identity (`imageSubjectIdentity`: `composed-process:<imageRef>:<contentHash>`, unique per lock).
|
|
248
|
+
* It equals a real manifest digest only by a sha256 preimage, and it never reaches a verdict: both sides of every
|
|
249
|
+
* comparison drop `imageDigest` (`withoutSealedDigests`).
|
|
250
|
+
*/
|
|
251
|
+
export function withStandInDigests(lock, schemas) {
|
|
252
|
+
return {
|
|
253
|
+
...lock,
|
|
254
|
+
subjects: lock.subjects.map((subject) =>
|
|
255
|
+
subject.kind === schemas.RuntimeImageSubjectKind.ComposedProcess && subject.imageDigest === undefined
|
|
256
|
+
? { ...subject, imageDigest: `sha256:${sha256Hex(imageSubjectIdentity(subject))}` }
|
|
257
|
+
: subject,
|
|
258
|
+
),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
240
262
|
function imageStructure(lock) {
|
|
241
263
|
return serializeJson({
|
|
242
264
|
distributionId: lock.distributionId,
|
|
@@ -315,12 +337,6 @@ export async function checkImagesLock(id, checkSourceEvidence) {
|
|
|
315
337
|
schemas,
|
|
316
338
|
requiredPlatforms,
|
|
317
339
|
);
|
|
318
|
-
const fresh = {
|
|
319
|
-
distributionId: id,
|
|
320
|
-
contractAuthority: runtimeSourceIndex.contractAuthority,
|
|
321
|
-
...structure,
|
|
322
|
-
subjects: structureSubjects(structure),
|
|
323
|
-
};
|
|
324
340
|
if (!existsSync(imageLockPath)) {
|
|
325
341
|
console.error(
|
|
326
342
|
`[dist:check] ${id}: images.lock.json is missing. Run \`${commandFor('resolve-images.mjs', id)}\`.`,
|
|
@@ -328,9 +344,29 @@ export async function checkImagesLock(id, checkSourceEvidence) {
|
|
|
328
344
|
return false;
|
|
329
345
|
}
|
|
330
346
|
const committed = validateImmutableImageLock(
|
|
331
|
-
|
|
347
|
+
(await readCommittedPreviousLock(id, imageLockPath)),
|
|
332
348
|
schemas,
|
|
333
349
|
);
|
|
350
|
+
// The committed lock is the previous lock of the fresh resolve, so the fresh view carries the composed artifacts forward
|
|
351
|
+
// exactly as a reseal does (generation, member keys, one artifact per live generation). Its digests are compared away
|
|
352
|
+
// below, so a committed lock that predates sealing is given one stand-in digest per subject to be carried.
|
|
353
|
+
const previousLock = withStandInDigests(committed, schemas);
|
|
354
|
+
let freshSubjects;
|
|
355
|
+
try {
|
|
356
|
+
freshSubjects = carryComposedForwardStructurally({ subjects: structureSubjects(structure), previousLock, schemas });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error(
|
|
359
|
+
`[dist:check] ${id}: images.lock.json is structurally out of date (its composed artifacts do not cover the declared composed processes: ${error.message}). ` +
|
|
360
|
+
`Run \`${commandFor('resolve-images.mjs', id)}\`.`,
|
|
361
|
+
);
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
const fresh = {
|
|
365
|
+
distributionId: id,
|
|
366
|
+
contractAuthority: runtimeSourceIndex.contractAuthority,
|
|
367
|
+
...structure,
|
|
368
|
+
subjects: freshSubjects,
|
|
369
|
+
};
|
|
334
370
|
if (
|
|
335
371
|
serializeJson(committed.contractAuthority) !==
|
|
336
372
|
serializeJson(runtimeSourceIndex.contractAuthority)
|
|
@@ -350,12 +386,12 @@ export async function checkImagesLock(id, checkSourceEvidence) {
|
|
|
350
386
|
return false;
|
|
351
387
|
}
|
|
352
388
|
if (checkSourceEvidence) {
|
|
353
|
-
const { imageLock: sourceResolved } = await resolveImages(id);
|
|
389
|
+
const { imageLock: sourceResolved } = await resolveImages(id, { previousLock });
|
|
354
390
|
// The same subtraction as the structural view above: the source resolve
|
|
355
391
|
// carries no digest, and the committed lock may.
|
|
356
392
|
if (
|
|
357
393
|
normalize(withoutSealedDigests(committed)) !==
|
|
358
|
-
normalize(sourceResolved)
|
|
394
|
+
normalize(withoutSealedDigests(sourceResolved))
|
|
359
395
|
) {
|
|
360
396
|
console.error(
|
|
361
397
|
`[dist:check] ${id}: images.lock.json source content hashes are out of date. ` +
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
sha256Digest,
|
|
57
57
|
sourceIdentity,
|
|
58
58
|
} from './image-evidence.mjs';
|
|
59
|
+
import { completenessImageKey } from './image-subject-identity.mjs';
|
|
59
60
|
import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
|
|
60
61
|
import { compareStrings } from './lib.mjs';
|
|
61
62
|
import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
@@ -133,7 +134,7 @@ export { compareCompletenessImages as compareEvidenceSubjects };
|
|
|
133
134
|
* Keyed by the same `kind\0id` identity `image-evidence.mjs` uses everywhere
|
|
134
135
|
* else, so two subjects can never collide and one subject can never move.
|
|
135
136
|
*/
|
|
136
|
-
function subjectStem(subject) {
|
|
137
|
+
export function subjectStem(subject) {
|
|
137
138
|
return createHash('sha256')
|
|
138
139
|
.update(sourceIdentity(subject))
|
|
139
140
|
.digest('hex');
|
|
@@ -315,10 +316,9 @@ function indexOneEvidenceDirectory({
|
|
|
315
316
|
});
|
|
316
317
|
continue;
|
|
317
318
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
});
|
|
319
|
+
// Only named in a defect: the record is indexed by the statement's own source coordinate, which already
|
|
320
|
+
// tells the generations of a composed artifact apart (its sourceTreeHash).
|
|
321
|
+
const subjectKey = `${entry.subjectKind}:${entry.subjectId}`;
|
|
322
322
|
const descriptor = entry.evidence;
|
|
323
323
|
let statementBytes;
|
|
324
324
|
let bundleBytes;
|
|
@@ -691,12 +691,17 @@ export function buildDistributionImagesStatement({
|
|
|
691
691
|
// Refusing here as well means an incomplete set can never be SIGNED, rather
|
|
692
692
|
// than being signed and rejected days later at a customer install.
|
|
693
693
|
const expectedIdentities = imageLock.subjects
|
|
694
|
-
.map((subject) =>
|
|
694
|
+
.map((subject) =>
|
|
695
|
+
completenessImageKey({
|
|
696
|
+
subjectKind: subject.kind,
|
|
697
|
+
subjectId: subject.id,
|
|
698
|
+
imageRepository: subject.imageRef,
|
|
699
|
+
manifestDigest: subject.imageDigest,
|
|
700
|
+
}),
|
|
701
|
+
)
|
|
695
702
|
.sort(compareStrings);
|
|
696
703
|
const actualIdentities = images
|
|
697
|
-
.map((image) =>
|
|
698
|
-
sourceIdentity({ kind: image.subjectKind, id: image.subjectId }),
|
|
699
|
-
)
|
|
704
|
+
.map((image) => completenessImageKey(image))
|
|
700
705
|
.sort(compareStrings);
|
|
701
706
|
if (
|
|
702
707
|
new Set(actualIdentities).size !== actualIdentities.length ||
|
|
@@ -290,7 +290,7 @@ export function canonicalSourceImages(sourceImages = new Map()) {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
/**
|
|
293
|
-
* The tag of an edition resolved from a snapshot (a develop edition, a customer release edition): sha256 of the canonical `{ cli, engine, pins, sourceImages }
|
|
293
|
+
* The tag of an edition resolved from a snapshot (a develop edition, a customer release edition): sha256 of the canonical `{ cli, engine, pins, sourceImages }`, plus `imageBuildRecord` (the record's digest) for a develop resolve --images-from. Every input of
|
|
294
294
|
* the bytes the artifact records is inside it. The pins fix the locks and the descriptor's pins: a vendor edition's
|
|
295
295
|
* commits fix its definitions; a customer edition's pins carry its sources' commits, the catalogue digest and the
|
|
296
296
|
* digest of its canonical definition (`definition`: vendor selection, image registry mirror, Store publisher anchors); the
|
|
@@ -300,25 +300,35 @@ export function canonicalSourceImages(sourceImages = new Map()) {
|
|
|
300
300
|
* only refuse the existing one (`check-existing`) or reuse a stale one. `provenance.workflow` (the run that
|
|
301
301
|
* wrote the bytes) is the one recorded field outside it: a fact about the artifact, not an input to it.
|
|
302
302
|
*/
|
|
303
|
-
export function snapshotEditionTag({ pins, engineImage, sourceImages = new Map(), version = cliVersion() }) {
|
|
303
|
+
export function snapshotEditionTag({ pins, engineImage, sourceImages = new Map(), imageBuildRecordDigest, version = cliVersion() }) {
|
|
304
304
|
return sha256Hex(
|
|
305
305
|
serializeJson({
|
|
306
306
|
cli: version,
|
|
307
307
|
engine: assertEngineImage(engineImage),
|
|
308
308
|
pins: sortedSnapshotPins(pins),
|
|
309
309
|
sourceImages: canonicalSourceImages(sourceImages),
|
|
310
|
+
// A develop resolve from an image build record (--images-from): the record fixes the image rows, and a satellite
|
|
311
|
+
// digest changes independently of the pins. Absent on the probe path, whose tag inputs are unchanged.
|
|
312
|
+
...(imageBuildRecordDigest === undefined ? {} : { imageBuildRecord: assertImageBuildRecordDigest(imageBuildRecordDigest) }),
|
|
310
313
|
}),
|
|
311
314
|
);
|
|
312
315
|
}
|
|
313
316
|
|
|
317
|
+
function assertImageBuildRecordDigest(digest) {
|
|
318
|
+
if (typeof digest !== 'string' || !/^[0-9a-f]{64}$/.test(digest)) {
|
|
319
|
+
throw new Error(`an image build record digest must be 64 lowercase hex; got ${JSON.stringify(digest)}`);
|
|
320
|
+
}
|
|
321
|
+
return digest;
|
|
322
|
+
}
|
|
323
|
+
|
|
314
324
|
/** The target of an edition resolved from a snapshot of pins, on either channel: the tag is `snapshotEditionTag`. */
|
|
315
|
-
export function snapshotEditionTarget(channel, distributionId, pins, { registry, engineImage, sourceImages }) {
|
|
316
|
-
return editionTarget(distributionId, registry, channel, snapshotEditionTag({ pins, engineImage, sourceImages }));
|
|
325
|
+
export function snapshotEditionTarget(channel, distributionId, pins, { registry, engineImage, sourceImages, imageBuildRecordDigest }) {
|
|
326
|
+
return editionTarget(distributionId, registry, channel, snapshotEditionTag({ pins, engineImage, sourceImages, imageBuildRecordDigest }));
|
|
317
327
|
}
|
|
318
328
|
|
|
319
|
-
/** A develop edition's target: the tag is `snapshotEditionTag` of the pins, the engine
|
|
320
|
-
export function developEditionTarget(distributionId, pins, { registry, engineImage, sourceImages }) {
|
|
321
|
-
return snapshotEditionTarget(EditionChannel.Develop, distributionId, pins, { registry, engineImage, sourceImages });
|
|
329
|
+
/** A develop edition's target: the tag is `snapshotEditionTag` of the pins, the engine, the source images and any image build record. */
|
|
330
|
+
export function developEditionTarget(distributionId, pins, { registry, engineImage, sourceImages, imageBuildRecordDigest }) {
|
|
331
|
+
return snapshotEditionTarget(EditionChannel.Develop, distributionId, pins, { registry, engineImage, sourceImages, imageBuildRecordDigest });
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
function realpathOfPlanned(path) {
|
|
@@ -19,6 +19,7 @@ import { developImageRef } from './develop-image-coordinate.mjs';
|
|
|
19
19
|
import { notBuiltYetError } from './develop-image-probe.mjs';
|
|
20
20
|
import { EditionChannel } from './edition-channel.mjs';
|
|
21
21
|
import { editionsRoot } from './edition-workspace.mjs';
|
|
22
|
+
import { imageSubjectIdentity } from './image-subject-identity.mjs';
|
|
22
23
|
import { validateImmutableImageLock, loadKernelDistributionSchemas, } from './lib.mjs';
|
|
23
24
|
import { isNotPublishedYet, planDigestSeal } from './seal-image-digests.mjs';
|
|
24
25
|
import { imageSubjectKey, pinnedManifestDigests, thirdPartyImagePins } from './third-party-image-pins.mjs';
|
|
@@ -86,7 +87,7 @@ export async function sealImageSubjects({
|
|
|
86
87
|
const digests = new Map([...writes, ...unchanged].map(({ key, digest }) => [key, digest]));
|
|
87
88
|
return channelSubjects.map((subject) => ({
|
|
88
89
|
...subject,
|
|
89
|
-
imageDigest: digests.get(
|
|
90
|
+
imageDigest: digests.get(imageSubjectIdentity(subject)),
|
|
90
91
|
}));
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -26,28 +26,17 @@
|
|
|
26
26
|
|
|
27
27
|
import { realpathSync } from 'node:fs';
|
|
28
28
|
|
|
29
|
+
import { RefusalError } from './collected-refusals.mjs';
|
|
29
30
|
import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
30
31
|
|
|
31
32
|
/** The edition protocols this library interprets: version -> content digest. A protocol is listed only
|
|
32
|
-
* while every kernel published under it exports what this library imports (`kernel-export-coverage.test.mjs`)
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* v36 is the kernel's JobFailureReason workflow closure addition (XEMA-2282);
|
|
38
|
-
* v37 is the kernel's DistributionLock.catalogueContentDigest and effective-timeout wording (XEMA-2254, XEMA-2282); v38 is the kernel's optionalPeers service-manifest field (XEMA-2315); v33 to v38 are not yet published: their identities and contracts are captured from the
|
|
39
|
-
* linked kernel), so v29 is not listed either. */
|
|
33
|
+
* while every kernel published under it exports what this library imports (`kernel-export-coverage.test.mjs`).
|
|
34
|
+
* Only v45 is listed (contracts 0.119.0: image-lock v5, build record `members`, and an idempotent
|
|
35
|
+
* `carryForwardComposedArtifacts`). A v44 kernel's carry-forward bumps the generation on every call, so dist-check could never
|
|
36
|
+
* be green on its seals; v43 cannot carry build record members; v42 and older lack `DEFAULT_COMPOSED_IMAGE_CLASS` and
|
|
37
|
+
* `dedicatedServiceImageSubjects`. No legacy row is kept. */
|
|
40
38
|
export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
41
|
-
Object.freeze({ version:
|
|
42
|
-
Object.freeze({ version: 30, contentDigest: '91e3cdff255778b5aa94b64ca9316d031be549ead2267a9a9311ff06d4f1db2c' }),
|
|
43
|
-
Object.freeze({ version: 31, contentDigest: 'b2026847fdd202e7b9e05815cdc2c9819b60be7007c789cc5b5f291c9c682fdd' }),
|
|
44
|
-
Object.freeze({ version: 32, contentDigest: 'a4ca824486052ca7f61b9a68f759b798defb620b6b3e9cbaab2c701bee2c6700' }),
|
|
45
|
-
Object.freeze({ version: 33, contentDigest: 'b070713e7abeeefad6da3a9053843b0f7420a466f6d504f60aaa00ef196b2b05' }),
|
|
46
|
-
Object.freeze({ version: 34, contentDigest: '370ceef5956d66035265ddbf7b9c46aa686cd10c1e95171c806c0d590ef5f49c' }),
|
|
47
|
-
Object.freeze({ version: 35, contentDigest: '3e616e532c45ecbd6277dde31c0d9c12a12139abe06cc0f56329d1ffe9813f8d' }),
|
|
48
|
-
Object.freeze({ version: 36, contentDigest: 'b6b0c8ec3fc4182c6c3b623685af6293cfed1321f8d7c1891a83652d864c3df0' }),
|
|
49
|
-
Object.freeze({ version: 37, contentDigest: 'f10adca1ca5b6f51f3971756261032eeddd60e372ebfcff06a2a9d8a796d0fef' }),
|
|
50
|
-
Object.freeze({ version: 38, contentDigest: 'f670cb922c989fd40ac03703a612e7a6df2937233a1a20a394a859ec835d47d4' }),
|
|
39
|
+
Object.freeze({ version: 45, contentDigest: '54eb2f7d40e966e6efd5bc2ef0f4751b1121786300f4c50066e8add10c3ecb6a' }),
|
|
51
40
|
]);
|
|
52
41
|
|
|
53
42
|
/** Where a kernel package publishes its identity and its contract, package-relative. */
|
|
@@ -86,7 +75,7 @@ export const EditionProtocolMismatchReason = Object.freeze({
|
|
|
86
75
|
NotOnDevelop: 'not-on-develop',
|
|
87
76
|
});
|
|
88
77
|
|
|
89
|
-
export class EditionProtocolMismatchError extends
|
|
78
|
+
export class EditionProtocolMismatchError extends RefusalError {
|
|
90
79
|
constructor(reason, message) {
|
|
91
80
|
super(message);
|
|
92
81
|
this.name = 'EditionProtocolMismatchError';
|
|
@@ -20,6 +20,7 @@ import { certificateConstraintArgs } from './cosign-certificate-constraints.mjs'
|
|
|
20
20
|
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
21
21
|
import { effectiveRepositoryPins, manifestSourcePins } from './edition-source-pins.mjs';
|
|
22
22
|
import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
23
|
+
import { evidenceSubjectKey } from './image-subject-identity.mjs';
|
|
23
24
|
import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
|
|
24
25
|
import {
|
|
25
26
|
compareStrings,
|
|
@@ -165,7 +166,7 @@ export function assertProducerTtlCeiling({ issuedAt, expiresAt, maxTtlSeconds, l
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export function sourceIdentity(subject) {
|
|
168
|
-
return
|
|
169
|
+
return evidenceSubjectKey(subject);
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
/** Root of the committed evidence tree, in every producing repository. */
|
|
@@ -424,7 +425,10 @@ export function validateProducerStatementBinding({
|
|
|
424
425
|
export function compareCompletenessImages(left, right) {
|
|
425
426
|
return (
|
|
426
427
|
compareStrings(left.subjectKind, right.subjectKind) ||
|
|
427
|
-
compareStrings(left.subjectId, right.subjectId)
|
|
428
|
+
compareStrings(left.subjectId, right.subjectId) ||
|
|
429
|
+
// The generations of one composed artifact share kind and id: they order by image, then manifest.
|
|
430
|
+
compareStrings(left.imageRepository, right.imageRepository) ||
|
|
431
|
+
compareStrings(left.manifestDigest, right.manifestDigest)
|
|
428
432
|
);
|
|
429
433
|
}
|
|
430
434
|
|
|
@@ -13,14 +13,14 @@ function isOwnedByBiome(subject, biomeId, enums) {
|
|
|
13
13
|
/**
|
|
14
14
|
* The subject carrying the image of `kind:id` for the biome `biomeId`, or undefined.
|
|
15
15
|
* A service is served by (a) a Service subject with that id owned by the biome, or (b) a subject owned by the biome
|
|
16
|
-
* whose `members[]` names the service. The member-to-subject step is the kernel's `
|
|
17
|
-
* (`enums.
|
|
16
|
+
* whose `members[]` names the service. The member-to-subject step is the kernel's `dedicatedServiceImageSubjects`
|
|
17
|
+
* (`enums.dedicatedServiceImageSubjects`), applied to the lock's `schemaVersion` and the biome's subjects; the same rule the launcher's `loadImageLock` applies; this file adds only the
|
|
18
18
|
* biome-owner filter. The biome's own `services` list supplies `id`.
|
|
19
19
|
*/
|
|
20
|
-
export function subjectServingBiome(subjects, biomeId, kind, id, enums) {
|
|
20
|
+
export function subjectServingBiome(schemaVersion, subjects, biomeId, kind, id, enums) {
|
|
21
21
|
const owned = subjects.filter((subject) => isOwnedByBiome(subject, biomeId, enums));
|
|
22
22
|
if (kind !== enums.RuntimeImageSubjectKind.Service) {
|
|
23
23
|
return owned.find((subject) => subject.kind === kind && subject.id === id);
|
|
24
24
|
}
|
|
25
|
-
return enums.
|
|
25
|
+
return enums.dedicatedServiceImageSubjects({ schemaVersion, subjects: owned }).get(id);
|
|
26
26
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The identity of a subject of an images lock, as the kernel's image-lock v5 uniqueness rule defines it
|
|
2
|
+
// (`parseImageLock`): a subject is `kind:id`, except a composed-process ARTIFACT, which is `imageRef:contentHash` — the
|
|
3
|
+
// generations of one composed artifact share one id and differ by that coordinate. Every map keyed by subject (digest
|
|
4
|
+
// plans, the seal, the lock validation) uses this one function, so two live generations never collapse onto one key.
|
|
5
|
+
|
|
6
|
+
const COMPOSED_PROCESS_KIND = 'composed-process';
|
|
7
|
+
|
|
8
|
+
/** The printable identity of a lock subject (or of a verification entry carrying `kind`, `id`, `imageRef`, `contentHash`). */
|
|
9
|
+
export function imageSubjectIdentity({ kind, id, imageRef, contentHash }) {
|
|
10
|
+
return kind === COMPOSED_PROCESS_KIND ? `${kind}:${imageRef}:${contentHash}` : `${kind}:${id}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The key of a subject in the evidence lane: `kind\0id` for every kind but a composed-process artifact, which is its
|
|
15
|
+
* `imageSubjectIdentity` (so two live generations of one composed artifact get distinct producer stems and index keys).
|
|
16
|
+
*/
|
|
17
|
+
export function evidenceSubjectKey({ kind, id, imageRef, contentHash }) {
|
|
18
|
+
return kind === COMPOSED_PROCESS_KIND ? imageSubjectIdentity({ kind, id, imageRef, contentHash }) : `${kind}\0${id}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The key of one image of a completeness statement, which names its image by repository and manifest digest, not by content hash. */
|
|
22
|
+
export function completenessImageKey({ subjectKind, subjectId, imageRepository, manifestDigest }) {
|
|
23
|
+
return subjectKind === COMPOSED_PROCESS_KIND ? `${subjectKind}:${imageRepository}@${manifestDigest}` : `${subjectKind}\0${subjectId}`;
|
|
24
|
+
}
|