@xemahq/xema 0.29.1 → 0.30.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 +60 -389
- package/dist/commands/edition.js +3 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +8715 -807
- package/dist/edition/verify-image-evidence.bundle.provenance.json +873 -405
- package/edition/README.md +53 -273
- package/edition/adapter-bundled-versions.mjs +107 -0
- package/edition/adapter-host-versions.mjs +41 -0
- package/edition/authorization-base.mjs +18 -9
- package/edition/build-runtime-image-source-index.mjs +77 -4
- package/edition/catalogue.mjs +15 -12
- package/edition/customer-definition.mjs +3 -20
- package/edition/develop-customer-edition.mjs +2 -2
- package/edition/develop-edition-resolve.mjs +10 -2
- package/edition/develop-edition.mjs +56 -62
- package/edition/develop-freeze.mjs +14 -3
- package/edition/dist-check.mjs +1 -0
- package/edition/distribution-evidence-set.mjs +32 -5
- package/edition/edition-channel.mjs +15 -1
- package/edition/edition-locks.mjs +16 -6
- package/edition/edition-protocol.mjs +30 -8
- package/edition/edition-workspace.mjs +8 -0
- package/edition/image-evidence.mjs +133 -25
- package/edition/kernel-package.mjs +193 -0
- package/edition/lib.mjs +73 -1
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/plan-image-evidence-refresh.mjs +2 -2
- package/edition/produce-distribution-completeness-evidence.mjs +5 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
- package/edition/release-catalogue.mjs +10 -3
- package/edition/resolve-chart-set.mjs +1 -1
- package/edition/resolve-customer-edition.mjs +29 -33
- package/edition/resolve-distribution.mjs +5 -1
- package/edition/resolve-edition.mjs +3 -2
- package/edition/seal-image-digests.mjs +16 -4
- package/edition/trust-policy-arming-verb.mjs +2 -10
- package/edition/verify-image-evidence.mjs +1 -0
- package/examples/README.md +3 -2
- package/package.json +3 -3
|
@@ -112,6 +112,7 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
|
|
|
112
112
|
* @param {string} request.kernelSha the commit the kernel checkout is at
|
|
113
113
|
* @param {string} request.deploySha the commit the deploy checkout is at
|
|
114
114
|
* @param {string} [request.editionsRootUrl] https URL of the editions root; absent = this repository
|
|
115
|
+
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source; runs for ALL graph sources before the first head
|
|
115
116
|
*/
|
|
116
117
|
export function freezeGraph({
|
|
117
118
|
runtimeSourceIndex,
|
|
@@ -121,6 +122,7 @@ export function freezeGraph({
|
|
|
121
122
|
editionsRootUrl,
|
|
122
123
|
overrides = {},
|
|
123
124
|
resolveHead,
|
|
125
|
+
holdSource,
|
|
124
126
|
}) {
|
|
125
127
|
assertNoToolingOverride(overrides);
|
|
126
128
|
assertOverrides(overrides, GRAPH_PHASE);
|
|
@@ -137,6 +139,12 @@ export function freezeGraph({
|
|
|
137
139
|
{ key: deployPinKey(), sha: deploySha, flag: '--deploy-sha', what: 'the deploy checkout' },
|
|
138
140
|
];
|
|
139
141
|
for (const { key, sha, flag, what } of checkedOut) {assertCheckedOutPin({ key, sha, flag, what, overrides });}
|
|
142
|
+
const toResolve = runtimeSourceIndex.repositoryPins.map((pin) => ({ name: pin.repo, url: pin.repositoryUrl }));
|
|
143
|
+
if (editionsRootUrl !== undefined) {
|
|
144
|
+
toResolve.push({ name: EDITIONS_ROOT_PIN_KEY, url: canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url') });
|
|
145
|
+
}
|
|
146
|
+
// Every source is held before the first head is asked: a refusal of the last leaves no git call behind.
|
|
147
|
+
for (const source of toResolve) {holdSource(source);}
|
|
140
148
|
if (editionsRootUrl === undefined && overrides[EDITIONS_ROOT_PIN_KEY] !== undefined) {
|
|
141
149
|
throw new Error(`override names "${EDITIONS_ROOT_PIN_KEY}", but the editions root is this repository`);
|
|
142
150
|
}
|
|
@@ -155,8 +163,9 @@ export function freezeGraph({
|
|
|
155
163
|
* @param {object} request
|
|
156
164
|
* @param {Record<string,string>} request.pins the graph phase's pins
|
|
157
165
|
* @param {object[]} request.declaredSources every `sources[]` entry in the chain (`{ name, url, ... }`)
|
|
166
|
+
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source (`holdSource` of git-source-auth with the workspace and env); runs for ALL sources before the first head
|
|
158
167
|
*/
|
|
159
|
-
export function freezeSources({ pins, declaredSources, overrides = {}, resolveHead }) {
|
|
168
|
+
export function freezeSources({ pins, declaredSources, overrides = {}, resolveHead, holdSource }) {
|
|
160
169
|
assertOverrides(overrides, SOURCE_PHASE);
|
|
161
170
|
const head = memoizedHeads(resolveHead);
|
|
162
171
|
const sourceByName = new Map();
|
|
@@ -172,6 +181,8 @@ export function freezeSources({ pins, declaredSources, overrides = {}, resolveHe
|
|
|
172
181
|
throw new Error(`override names "${key}", but the chain declares no such source`);
|
|
173
182
|
}
|
|
174
183
|
}
|
|
184
|
+
// Every source is held before the first head is asked: a refusal of the last leaves no git call behind.
|
|
185
|
+
for (const source of sourceByName.values()) {holdSource(source);}
|
|
175
186
|
const frozen = { ...pins };
|
|
176
187
|
for (const [name, source] of sourceByName) {
|
|
177
188
|
frozen[sourcePinKey(name)] = overrides[sourcePinKey(name)] ?? head(source);
|
|
@@ -207,11 +218,11 @@ export function assertCustomerSourceUrls(sources, sourceRepositoryUrlPrefix) {
|
|
|
207
218
|
* each the head of refs/heads/develop of that source's repository, resolved once. Every URL is held by
|
|
208
219
|
* `assertCustomerSourceUrls` before the first head is asked.
|
|
209
220
|
*/
|
|
210
|
-
export function freezeCustomerSources({ definition, resolveHead, sourceRepositoryUrlPrefix }) {
|
|
221
|
+
export function freezeCustomerSources({ definition, resolveHead, holdSource, sourceRepositoryUrlPrefix }) {
|
|
211
222
|
const sources = definition?.sources ?? [];
|
|
212
223
|
if (!Array.isArray(sources) || sources.some((source) => typeof source?.name !== 'string' || typeof source?.url !== 'string')) {
|
|
213
224
|
throw new Error('the definition\'s sources must be an array of {name, url}');
|
|
214
225
|
}
|
|
215
226
|
assertCustomerSourceUrls(sources, sourceRepositoryUrlPrefix);
|
|
216
|
-
return freezeSources({ pins: {}, declaredSources: sources, resolveHead });
|
|
227
|
+
return freezeSources({ pins: {}, declaredSources: sources, resolveHead, holdSource });
|
|
217
228
|
}
|
package/edition/dist-check.mjs
CHANGED
|
@@ -194,6 +194,7 @@ async function checkAuthorizationLock(id, kernel, index) {
|
|
|
194
194
|
distributionId: id,
|
|
195
195
|
editionDir,
|
|
196
196
|
lockBiomes: lock.biomes,
|
|
197
|
+
biomeOrigins: (await loadKernelDistributionSchemas()).biomeOriginsOfLock(lock),
|
|
197
198
|
index,
|
|
198
199
|
kernel,
|
|
199
200
|
serialize: serializeJson,
|
|
@@ -464,6 +464,7 @@ export async function resolveProducerEvidence({
|
|
|
464
464
|
schemas,
|
|
465
465
|
expectedContractAuthority,
|
|
466
466
|
registryClientFactory,
|
|
467
|
+
channel,
|
|
467
468
|
}) {
|
|
468
469
|
assertCanonicalDistributionId(distributionId);
|
|
469
470
|
assertAbsolutePath(dataRoot, 'data root');
|
|
@@ -472,7 +473,7 @@ export async function resolveProducerEvidence({
|
|
|
472
473
|
throw new Error('the expected kernel contract authority is required');
|
|
473
474
|
}
|
|
474
475
|
const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex } =
|
|
475
|
-
await loadValidatedLocks(distributionId, dataRoot, schemas, { editionsRoot });
|
|
476
|
+
await loadValidatedLocks(distributionId, dataRoot, schemas, { editionsRoot, channel });
|
|
476
477
|
assertKernelContractAuthorityEqual(
|
|
477
478
|
runtimeSourceIndex.contractAuthority,
|
|
478
479
|
expectedContractAuthority,
|
|
@@ -581,6 +582,22 @@ export async function resolveProducerEvidence({
|
|
|
581
582
|
// Statement shapes
|
|
582
583
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
583
584
|
|
|
585
|
+
/**
|
|
586
|
+
* The evidence channel a signer signs for: the one whose kernel-assigned ref (`EVIDENCE_CHANNEL_REFS`) is the
|
|
587
|
+
* signer's workflow ref. The kernel's policy schema ties each policy row to exactly that ref, so the signer's ref
|
|
588
|
+
* IS the channel the verifying row will state. A ref no channel owns is refused.
|
|
589
|
+
*/
|
|
590
|
+
export function evidenceChannelOfSigner(signer, schemas) {
|
|
591
|
+
const ref = signer?.githubWorkflow?.ref;
|
|
592
|
+
const channel = schemas.EVIDENCE_CHANNELS.find((candidate) => schemas.EVIDENCE_CHANNEL_REFS[candidate] === ref);
|
|
593
|
+
if (channel === undefined) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`the signer workflow ref "${String(ref)}" is the ref of no evidence channel (${schemas.EVIDENCE_CHANNELS.map((candidate) => `${candidate}: ${schemas.EVIDENCE_CHANNEL_REFS[candidate]}`).join(', ')})`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return channel;
|
|
599
|
+
}
|
|
600
|
+
|
|
584
601
|
/**
|
|
585
602
|
* The distribution-completeness statement: "these EXACT images, produced by
|
|
586
603
|
* these EXACT bundles, are the complete image set of this edition at this
|
|
@@ -590,6 +607,7 @@ export function buildDistributionImagesStatement({
|
|
|
590
607
|
distributionLock,
|
|
591
608
|
imageLock,
|
|
592
609
|
images,
|
|
610
|
+
channel,
|
|
593
611
|
releaseSequence,
|
|
594
612
|
releaseSourceCommit,
|
|
595
613
|
runId,
|
|
@@ -599,7 +617,7 @@ export function buildDistributionImagesStatement({
|
|
|
599
617
|
maxTtlSeconds,
|
|
600
618
|
schemas,
|
|
601
619
|
}) {
|
|
602
|
-
if (!schemas?.parseDistributionImagesStatement) {
|
|
620
|
+
if (!schemas?.parseDistributionImagesStatement || typeof schemas.distributionLockDigest !== 'function') {
|
|
603
621
|
throw new Error('trusted kernel distribution schemas are required');
|
|
604
622
|
}
|
|
605
623
|
if (
|
|
@@ -664,7 +682,9 @@ export function buildDistributionImagesStatement({
|
|
|
664
682
|
],
|
|
665
683
|
predicateType: schemas.DISTRIBUTION_IMAGES_PREDICATE_TYPE,
|
|
666
684
|
predicate: {
|
|
667
|
-
schemaVersion:
|
|
685
|
+
schemaVersion: 2,
|
|
686
|
+
channel,
|
|
687
|
+
lockDigest: schemas.distributionLockDigest(distributionLock),
|
|
668
688
|
distributionId: imageLock.distributionId,
|
|
669
689
|
distributionInputHash: distributionLock.inputHash,
|
|
670
690
|
normalizedImageLockDigest: normalizedLockDigest,
|
|
@@ -706,6 +726,7 @@ export function buildDistributionImagesStatement({
|
|
|
706
726
|
* decision that already has an owner.
|
|
707
727
|
*/
|
|
708
728
|
export function buildImageDeliveryEntitlementStatement({
|
|
729
|
+
distributionLock,
|
|
709
730
|
distributionId,
|
|
710
731
|
completenessStatementDigest,
|
|
711
732
|
customerId,
|
|
@@ -719,10 +740,13 @@ export function buildImageDeliveryEntitlementStatement({
|
|
|
719
740
|
maxTtlSeconds,
|
|
720
741
|
schemas,
|
|
721
742
|
}) {
|
|
722
|
-
if (!schemas?.parseImageDeliveryEntitlementStatement) {
|
|
743
|
+
if (!schemas?.parseImageDeliveryEntitlementStatement || typeof schemas.distributionLockDigest !== 'function') {
|
|
723
744
|
throw new Error('trusted kernel distribution schemas are required');
|
|
724
745
|
}
|
|
725
746
|
assertCanonicalDistributionId(distributionId);
|
|
747
|
+
if (distributionLock?.distributionId !== distributionId) {
|
|
748
|
+
throw new Error(`delivery entitlement for ${String(customerId)} requires the distribution lock of ${distributionId}`);
|
|
749
|
+
}
|
|
726
750
|
const label = `delivery entitlement for ${String(customerId)}`;
|
|
727
751
|
if (!SHA256_DIGEST_PATTERN.test(completenessStatementDigest ?? '')) {
|
|
728
752
|
throw new Error(
|
|
@@ -751,9 +775,12 @@ export function buildImageDeliveryEntitlementStatement({
|
|
|
751
775
|
],
|
|
752
776
|
predicateType: schemas.IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE,
|
|
753
777
|
predicate: {
|
|
754
|
-
schemaVersion:
|
|
778
|
+
schemaVersion: 2,
|
|
755
779
|
customerId,
|
|
756
780
|
distributionId,
|
|
781
|
+
lockDigest: schemas.distributionLockDigest(distributionLock),
|
|
782
|
+
// Present-and-nullable: null states the lock carries no catalogue; the lock digest binds the lock either way.
|
|
783
|
+
catalogueDigest: distributionLock.catalogueDigest ?? null,
|
|
757
784
|
distributionStatementDigest: completenessStatementDigest,
|
|
758
785
|
mode,
|
|
759
786
|
targetRegistry,
|
|
@@ -2,5 +2,19 @@
|
|
|
2
2
|
// validator (which a channel selects) and edition-constants (which imports the
|
|
3
3
|
// chart-set file name) do not import each other.
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Where an edition was produced. Recorded in the descriptor's provenance. verify-evidence selects its policy row by the
|
|
7
|
+
* descriptor's channel (a predicate channel mismatch is still refused after the signature check); lock readers require one.
|
|
8
|
+
*/
|
|
6
9
|
export const EditionChannel = Object.freeze({ Develop: 'develop', Release: 'release' });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The channel of `--channel <channel>`, which must be all of `rest`; anything
|
|
13
|
+
* else is a usage error. Shared by every verb that operates on one channel.
|
|
14
|
+
*/
|
|
15
|
+
export function parseChannelArgs(rest) {
|
|
16
|
+
if (rest.length !== 2 || rest[0] !== '--channel' || !Object.values(EditionChannel).includes(rest[1])) {
|
|
17
|
+
throw new Error(`--channel <${Object.values(EditionChannel).join('|')}> is required and nothing else is accepted (got ${rest.length === 0 ? 'nothing' : JSON.stringify(rest.join(' '))})`);
|
|
18
|
+
}
|
|
19
|
+
return { channel: rest[1] };
|
|
20
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `xema edition locks <id>`: one edition's validated locks, as JSON.
|
|
3
3
|
//
|
|
4
|
-
// usage: xema edition locks <id> --data-root <dir> --editions-root <dir> --kernel <dir>
|
|
4
|
+
// usage: xema edition locks <id> --channel <release|develop> --data-root <dir> --editions-root <dir> --kernel <dir>
|
|
5
5
|
//
|
|
6
6
|
// The locks are read and validated exactly as the evidence verifier reads
|
|
7
7
|
// them: `images.lock.json` and `distribution.lock.json` under the kernel
|
|
8
8
|
// schema, immutable coordinates only, and the image lock's kernel contract
|
|
9
|
-
// authority equal to the data root's runtime image source index.
|
|
9
|
+
// authority equal to the data root's runtime image source index. The
|
|
10
|
+
// distribution lock is read for `--channel`: an unsealed lock on develop only,
|
|
11
|
+
// a sealed one on every other channel. A consumer
|
|
10
12
|
// reads this output instead of parsing or validating a lock itself.
|
|
11
13
|
|
|
12
14
|
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
15
|
+
import { EditionChannel, parseChannelArgs } from './edition-channel.mjs';
|
|
13
16
|
import {
|
|
14
17
|
WorkspaceRoot,
|
|
15
18
|
configureEditionWorkspaceFromArgv,
|
|
@@ -21,12 +24,12 @@ import { loadValidatedLocks } from './image-evidence.mjs';
|
|
|
21
24
|
import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
22
25
|
|
|
23
26
|
const USAGE =
|
|
24
|
-
|
|
27
|
+
`usage: xema edition locks <id> --channel <${Object.values(EditionChannel).join('|')}> --data-root <dir> --editions-root <dir> --kernel <dir>`;
|
|
25
28
|
|
|
26
|
-
export async function readEditionLocks({ distributionId, dataRoot: data, editionsRoot: editions, schemas }) {
|
|
29
|
+
export async function readEditionLocks({ distributionId, channel, dataRoot: data, editionsRoot: editions, schemas }) {
|
|
27
30
|
assertCanonicalDistributionId(distributionId);
|
|
28
31
|
const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex } =
|
|
29
|
-
await loadValidatedLocks(distributionId, data, schemas, { editionsRoot: editions });
|
|
32
|
+
await loadValidatedLocks(distributionId, data, schemas, { editionsRoot: editions, channel });
|
|
30
33
|
return {
|
|
31
34
|
schemaVersion: 1,
|
|
32
35
|
distributionId,
|
|
@@ -45,14 +48,21 @@ async function main() {
|
|
|
45
48
|
WorkspaceRoot.Editions,
|
|
46
49
|
WorkspaceRoot.Kernel,
|
|
47
50
|
]);
|
|
48
|
-
if (rest.length
|
|
51
|
+
if (rest.length === 0 || rest[0].startsWith('--')) {
|
|
49
52
|
throw new Error(USAGE);
|
|
50
53
|
}
|
|
54
|
+
let channel;
|
|
55
|
+
try {
|
|
56
|
+
({ channel } = parseChannelArgs(rest.slice(1)));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`${error.message}\n${USAGE}`);
|
|
59
|
+
}
|
|
51
60
|
const schemas = await loadKernelDistributionSchemas();
|
|
52
61
|
process.stdout.write(
|
|
53
62
|
serializeJson(
|
|
54
63
|
await readEditionLocks({
|
|
55
64
|
distributionId: rest[0],
|
|
65
|
+
channel,
|
|
56
66
|
dataRoot: dataRoot(),
|
|
57
67
|
editionsRoot: editionsRoot(),
|
|
58
68
|
schemas,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// it too.
|
|
3
3
|
//
|
|
4
4
|
// The kernel (`@xemahq/contracts/edition-protocol`) exports the identity of the
|
|
5
|
-
// code this library executes as DATA: `{contentDigest, version}` in
|
|
5
|
+
// code this library executes as DATA: `{contentDigest, provisional, version}` in
|
|
6
6
|
// `dist/edition-protocol/edition-protocol.json`, and the edition contract in
|
|
7
7
|
// `dist/edition-protocol/edition-contract.schema.json` (the identity, the
|
|
8
8
|
// hashing convention, the entry subpaths this library may load, and every
|
|
@@ -31,8 +31,8 @@ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
|
31
31
|
/** The edition protocols this library interprets: version -> content digest. */
|
|
32
32
|
export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
33
33
|
Object.freeze({
|
|
34
|
-
version:
|
|
35
|
-
contentDigest: '
|
|
34
|
+
version: 3,
|
|
35
|
+
contentDigest: '8d8ec9160f78ab46e50efe9da90633d9a906af7cdb0d7837ed3b1f9e885716a4',
|
|
36
36
|
}),
|
|
37
37
|
]);
|
|
38
38
|
|
|
@@ -52,8 +52,10 @@ const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
|
52
52
|
export const EditionProtocolMismatchReason = Object.freeze({
|
|
53
53
|
/** The kernel publishes no edition protocol identity. */
|
|
54
54
|
Undeclared: 'undeclared',
|
|
55
|
-
/** The identity file is not exactly `{contentDigest, version}`. */
|
|
55
|
+
/** The identity file is not exactly `{contentDigest, provisional, version}`. */
|
|
56
56
|
Malformed: 'malformed',
|
|
57
|
+
/** The identity states `provisional: true`: a branch-local working identity, never pinned. */
|
|
58
|
+
Provisional: 'provisional',
|
|
57
59
|
/** This library supports no protocol of that version. */
|
|
58
60
|
UnsupportedVersion: 'unsupported-version',
|
|
59
61
|
/** The version is supported, but pinned to a different digest. */
|
|
@@ -77,7 +79,20 @@ const describe = ({ version, contentDigest }) => `edition protocol v${version} (
|
|
|
77
79
|
const supportedList = () =>
|
|
78
80
|
SUPPORTED_EDITION_PROTOCOLS.map((protocol) => `v${protocol.version}`).join(', ');
|
|
79
81
|
|
|
80
|
-
/**
|
|
82
|
+
/** Refuse an identity that states `provisional: true`; nothing provisional is ever read as a protocol. */
|
|
83
|
+
function assertFinalIdentity(identity, label) {
|
|
84
|
+
if (identity.provisional !== false) {
|
|
85
|
+
throw new EditionProtocolMismatchError(
|
|
86
|
+
EditionProtocolMismatchReason.Provisional,
|
|
87
|
+
`${label} declares ${describe(identity)} as provisional (a branch-local working identity); a provisional edition protocol is never pinned`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse identity bytes strictly: exactly `{contentDigest, provisional, version}`, a positive integer version, a
|
|
94
|
+
* lowercase SHA-256 hex digest and a boolean `provisional`. An identity stating `provisional: true` is refused.
|
|
95
|
+
*/
|
|
81
96
|
export function parseEditionProtocolIdentity(bytes, label) {
|
|
82
97
|
let identity;
|
|
83
98
|
try {
|
|
@@ -92,7 +107,8 @@ export function parseEditionProtocolIdentity(bytes, label) {
|
|
|
92
107
|
identity === null ||
|
|
93
108
|
typeof identity !== 'object' ||
|
|
94
109
|
Array.isArray(identity) ||
|
|
95
|
-
Object.keys(identity).sort().join() !== 'contentDigest,version' ||
|
|
110
|
+
Object.keys(identity).sort().join() !== 'contentDigest,provisional,version' ||
|
|
111
|
+
typeof identity.provisional !== 'boolean' ||
|
|
96
112
|
!Number.isSafeInteger(identity.version) ||
|
|
97
113
|
identity.version < 1 ||
|
|
98
114
|
typeof identity.contentDigest !== 'string' ||
|
|
@@ -100,10 +116,15 @@ export function parseEditionProtocolIdentity(bytes, label) {
|
|
|
100
116
|
) {
|
|
101
117
|
throw new EditionProtocolMismatchError(
|
|
102
118
|
EditionProtocolMismatchReason.Malformed,
|
|
103
|
-
`${label} must be exactly {"contentDigest":"<64 lowercase hex>","version":<positive integer>}`,
|
|
119
|
+
`${label} must be exactly {"contentDigest":"<64 lowercase hex>","provisional":<boolean>,"version":<positive integer>}`,
|
|
104
120
|
);
|
|
105
121
|
}
|
|
106
|
-
|
|
122
|
+
assertFinalIdentity(identity, label);
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
contentDigest: identity.contentDigest,
|
|
125
|
+
provisional: identity.provisional,
|
|
126
|
+
version: identity.version,
|
|
127
|
+
});
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
/** The `{version, contentDigest}` a contract file states, or undefined. */
|
|
@@ -216,6 +237,7 @@ export function assertKernelEditionProtocol(kernelPackageDir) {
|
|
|
216
237
|
* it, so the kernel the workspace states must speak exactly that protocol.
|
|
217
238
|
*/
|
|
218
239
|
export function assertKernelSpeaksBundledProtocol({ kernelPackageDir, bundled }) {
|
|
240
|
+
assertFinalIdentity(bundled, 'the edition protocol compiled into this verifier');
|
|
219
241
|
const stated = readKernelEditionProtocol(kernelPackageDir);
|
|
220
242
|
if (stated.version !== bundled.version || stated.contentDigest !== bundled.contentDigest) {
|
|
221
243
|
throw new EditionProtocolMismatchError(
|
|
@@ -70,6 +70,14 @@ export function configureEditionWorkspace(roots) {
|
|
|
70
70
|
return configured;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* State the Kernel root AFTER configuration, for a customer verb that takes its kernel from the catalogue
|
|
75
|
+
* (kernel-package.mjs) rather than from a flag. Every other configured root is kept; there is still no default.
|
|
76
|
+
*/
|
|
77
|
+
export function stateKernelPackageDir(dir) {
|
|
78
|
+
configured = Object.freeze({ ...configured, [WorkspaceRoot.Kernel]: assertDirectory(WorkspaceRoot.Kernel, dir) });
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
function root(name) {
|
|
74
82
|
const value = configured[name];
|
|
75
83
|
if (value === undefined) {
|
|
@@ -593,6 +593,13 @@ export function validateEntitlementStatementBinding({
|
|
|
593
593
|
return statement;
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
+
/** Throw the kernel's refusal code, verbatim, when a kernel predicate check returned one. */
|
|
597
|
+
function assertKernelPredicateAccepted(label, refusal) {
|
|
598
|
+
if (refusal !== undefined) {
|
|
599
|
+
throw new Error(`${label} is refused: ${refusal}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
596
603
|
function assertEvidenceChannel(channel) {
|
|
597
604
|
if (typeof channel !== 'string' || channel === '') {
|
|
598
605
|
throw new Error('evidence verification requires the channel of the edition being verified');
|
|
@@ -670,6 +677,7 @@ export function verifySigstoreEvidenceDescriptor({
|
|
|
670
677
|
parseStatement,
|
|
671
678
|
policyRoot = evidenceRoot,
|
|
672
679
|
trustedCosign,
|
|
680
|
+
holdProducerWorkflow,
|
|
673
681
|
}) {
|
|
674
682
|
assertEvidenceChannel(channel);
|
|
675
683
|
const statementFile = readExactDescriptorFile(
|
|
@@ -744,6 +752,7 @@ export function verifySigstoreEvidenceDescriptor({
|
|
|
744
752
|
statement.predicate?.producerRun?.producerWorkflowSha;
|
|
745
753
|
let verified = false;
|
|
746
754
|
let verifiedIdentityPolicy;
|
|
755
|
+
let verifiedCandidateWorkflowSha;
|
|
747
756
|
const verificationErrors = [];
|
|
748
757
|
for (const candidate of candidates) {
|
|
749
758
|
if (!verifiedWorkflowSha) {
|
|
@@ -806,6 +815,7 @@ export function verifySigstoreEvidenceDescriptor({
|
|
|
806
815
|
});
|
|
807
816
|
verified = true;
|
|
808
817
|
verifiedIdentityPolicy = candidate;
|
|
818
|
+
verifiedCandidateWorkflowSha = verifiedWorkflowSha;
|
|
809
819
|
break;
|
|
810
820
|
} catch (error) {
|
|
811
821
|
verificationErrors.push(
|
|
@@ -826,6 +836,16 @@ export function verifySigstoreEvidenceDescriptor({
|
|
|
826
836
|
verificationErrors.join('; '),
|
|
827
837
|
);
|
|
828
838
|
}
|
|
839
|
+
// The certificate's workflow commit is held to the data root's operator-stated pin for that workflow's repository.
|
|
840
|
+
if (holdProducerWorkflow !== undefined) {
|
|
841
|
+
assertKernelPredicateAccepted(
|
|
842
|
+
`${predicateType} signer ${verifiedIdentityPolicy.githubWorkflow.repository}`,
|
|
843
|
+
holdProducerWorkflow({
|
|
844
|
+
repository: verifiedIdentityPolicy.githubWorkflow.repository,
|
|
845
|
+
workflowCommit: verifiedCandidateWorkflowSha,
|
|
846
|
+
}),
|
|
847
|
+
);
|
|
848
|
+
}
|
|
829
849
|
return {
|
|
830
850
|
statement,
|
|
831
851
|
statementPath: statementFile.path,
|
|
@@ -874,10 +894,12 @@ async function verifyDistributionImageEvidenceSetWithCosign({
|
|
|
874
894
|
distributionLock,
|
|
875
895
|
imageLock,
|
|
876
896
|
repositoryPins,
|
|
877
|
-
runtimeSourceIndex
|
|
897
|
+
runtimeSourceIndex,
|
|
878
898
|
} = await loadValidatedLocks(distributionId, dataRoot, schemas, {
|
|
879
899
|
editionsRoot,
|
|
900
|
+
channel,
|
|
880
901
|
});
|
|
902
|
+
const expectedContractAuthority = runtimeSourceIndex.contractAuthority;
|
|
881
903
|
if (evidenceSet.distributionId !== distributionId) {
|
|
882
904
|
throw new Error(
|
|
883
905
|
`evidence set "${evidenceSet.distributionId}" does not match requested distribution "${distributionId}"`,
|
|
@@ -922,6 +944,7 @@ async function verifyDistributionImageEvidenceSetWithCosign({
|
|
|
922
944
|
parseStatement: schemas.parseImageProducerStatement,
|
|
923
945
|
policyRoot,
|
|
924
946
|
trustedCosign,
|
|
947
|
+
holdProducerWorkflow: (producer) => schemas.producerWorkflowPinRefusal(runtimeSourceIndex, producer),
|
|
925
948
|
});
|
|
926
949
|
validateProducerStatementBinding({
|
|
927
950
|
statement,
|
|
@@ -943,7 +966,7 @@ async function verifyDistributionImageEvidenceSetWithCosign({
|
|
|
943
966
|
}
|
|
944
967
|
}
|
|
945
968
|
|
|
946
|
-
const { statement: completenessStatement } = verifySigstoreEvidenceDescriptor(
|
|
969
|
+
const { statement: completenessStatement, identityPolicy: completenessPolicy } = verifySigstoreEvidenceDescriptor(
|
|
947
970
|
{
|
|
948
971
|
evidenceRoot,
|
|
949
972
|
descriptor: evidenceSet.completeness,
|
|
@@ -956,6 +979,16 @@ async function verifyDistributionImageEvidenceSetWithCosign({
|
|
|
956
979
|
trustedCosign,
|
|
957
980
|
},
|
|
958
981
|
);
|
|
982
|
+
// After the signature verified: the channel the predicate states is the verifying policy row's, and the
|
|
983
|
+
// predicate completes THIS lock. A kernel refusal is terminal and is surfaced by its code.
|
|
984
|
+
assertKernelPredicateAccepted(
|
|
985
|
+
`distribution completeness for ${imageLock.distributionId}`,
|
|
986
|
+
schemas.distributionImagesPredicateChannelRefusal(completenessStatement.predicate, completenessPolicy),
|
|
987
|
+
);
|
|
988
|
+
assertKernelPredicateAccepted(
|
|
989
|
+
`distribution completeness for ${imageLock.distributionId}`,
|
|
990
|
+
schemas.distributionImagesPredicateLockRefusal(completenessStatement.predicate, distributionLock),
|
|
991
|
+
);
|
|
959
992
|
validateCompletenessStatementBinding({
|
|
960
993
|
statement: completenessStatement,
|
|
961
994
|
distributionLock,
|
|
@@ -1002,6 +1035,7 @@ async function verifyImageDeliveryEntitlementWithCosign({
|
|
|
1002
1035
|
completenessStatementDigest,
|
|
1003
1036
|
customerId,
|
|
1004
1037
|
distributionId,
|
|
1038
|
+
distributionLock,
|
|
1005
1039
|
channel,
|
|
1006
1040
|
mode,
|
|
1007
1041
|
targetRegistry,
|
|
@@ -1023,6 +1057,10 @@ async function verifyImageDeliveryEntitlementWithCosign({
|
|
|
1023
1057
|
policyRoot,
|
|
1024
1058
|
trustedCosign,
|
|
1025
1059
|
});
|
|
1060
|
+
assertKernelPredicateAccepted(
|
|
1061
|
+
`delivery entitlement for ${customerId}`,
|
|
1062
|
+
schemas.imageDeliveryEntitlementRefusal(statement.predicate, distributionLock),
|
|
1063
|
+
);
|
|
1026
1064
|
validateEntitlementStatementBinding({
|
|
1027
1065
|
statement,
|
|
1028
1066
|
completenessStatementDigest,
|
|
@@ -1124,20 +1162,57 @@ export function externalSourceRepositoryPins({
|
|
|
1124
1162
|
* tolerant read-only posture `parseDistributionLockForReading` gives a
|
|
1125
1163
|
* runtime, without depending on which kernel the bundle was built from.
|
|
1126
1164
|
*
|
|
1127
|
-
*
|
|
1128
|
-
* `
|
|
1129
|
-
*
|
|
1165
|
+
* Two grammars exist, and which one is accepted is decided by `channel`, the
|
|
1166
|
+
* kernel evidence channel (`EVIDENCE_CHANNELS`) the caller operates on; it is
|
|
1167
|
+
* required and has no default. An UNSEALED lock (it carries `unsealedSchema`;
|
|
1168
|
+
* every edition lock on develop is one by design) is accepted ONLY on the
|
|
1169
|
+
* develop channel, read through the kernel's `parseUnsealedDistributionLock`.
|
|
1170
|
+
* Every other channel requires a SEALED lock: images sealed over an unsealed
|
|
1171
|
+
* lock is the crash state every sealed reader refuses, and a release statement
|
|
1172
|
+
* must never verify over it. A SEALED lock must have `schemaVersion` 5 (the
|
|
1173
|
+
* kernel's `DISTRIBUTION_LOCK_SCHEMA_VERSIONS`): a pre-v5 lock is refused, with
|
|
1174
|
+
* no compatibility path. Both carry `distributionId` and `inputHash`, and
|
|
1175
|
+
* sealing preserves them, so either way the binding is the same two fields.
|
|
1130
1176
|
*/
|
|
1131
|
-
|
|
1177
|
+
/** The one evidence channel an unsealed distribution lock is read on. */
|
|
1178
|
+
const UNSEALED_LOCK_CHANNEL = 'develop';
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* The channel a distribution lock is read for: one of the kernel's
|
|
1182
|
+
* `EVIDENCE_CHANNELS`, stated by the caller. Missing or unknown is refused.
|
|
1183
|
+
*/
|
|
1184
|
+
function assertLockReadingChannel(channel, schemas) {
|
|
1185
|
+
if (!schemas) {
|
|
1186
|
+
throw new Error('trusted kernel distribution schemas are required');
|
|
1187
|
+
}
|
|
1188
|
+
if (!schemas.EVIDENCE_CHANNELS.includes(channel)) {
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
`a distribution lock is read for one evidence channel (${schemas.EVIDENCE_CHANNELS.join(' | ')}); got ${JSON.stringify(channel)}`,
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
export function readDistributionLockBinding(raw, schemas, channel) {
|
|
1196
|
+
assertLockReadingChannel(channel, schemas);
|
|
1132
1197
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1133
1198
|
throw new Error('distribution.lock.json is not a JSON object');
|
|
1134
1199
|
}
|
|
1135
|
-
|
|
1136
|
-
if (
|
|
1200
|
+
let source = raw;
|
|
1201
|
+
if ('unsealedSchema' in raw) {
|
|
1202
|
+
if (channel !== UNSEALED_LOCK_CHANNEL) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
`distribution.lock.json is unsealed; the ${channel} channel accepts only a sealed lock (schemaVersion 5) — ` +
|
|
1205
|
+
`an unsealed lock is read on the ${UNSEALED_LOCK_CHANNEL} channel only`,
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
source = schemas.parseUnsealedDistributionLock(raw);
|
|
1209
|
+
} else if (raw.schemaVersion !== 5) {
|
|
1137
1210
|
throw new Error(
|
|
1138
|
-
`distribution.lock.json
|
|
1211
|
+
`distribution.lock.json is neither an unsealed lock (carrying unsealedSchema) ` +
|
|
1212
|
+
`nor a sealed lock (schemaVersion 5); it has schemaVersion ${String(raw.schemaVersion)} (regenerate the lock)`,
|
|
1139
1213
|
);
|
|
1140
1214
|
}
|
|
1215
|
+
const { distributionId, inputHash } = source;
|
|
1141
1216
|
// The id is format-checked only. Which edition it must equal is decided by
|
|
1142
1217
|
// the bindings that USE it — the completeness predicate and the evidence set
|
|
1143
1218
|
// compare it to the image lock and the signed statement — exactly as before
|
|
@@ -1149,21 +1224,63 @@ export function readDistributionLockBinding(raw) {
|
|
|
1149
1224
|
'expected 64 lowercase sha256 hex characters',
|
|
1150
1225
|
);
|
|
1151
1226
|
}
|
|
1152
|
-
return Object.freeze({
|
|
1227
|
+
return Object.freeze({ distributionId, inputHash });
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* The edition's sealed `distribution.lock.json`, parsed by the kernel's strict lock parser: the document the
|
|
1232
|
+
* predicates' `lockDigest` is computed over. Held to the binding rules of {@link readDistributionLockBinding} first.
|
|
1233
|
+
*/
|
|
1234
|
+
export function readEditionDistributionLock(editionsRoot, distributionId, schemas, channel) {
|
|
1235
|
+
assertLockReadingChannel(channel, schemas);
|
|
1236
|
+
const raw = JSON.parse(
|
|
1237
|
+
readFileSync(join(editionsRoot, distributionId, 'distribution.lock.json'), 'utf8'),
|
|
1238
|
+
);
|
|
1239
|
+
readDistributionLockBinding(raw, schemas, channel);
|
|
1240
|
+
// The WHOLE lock in its own grammar, never the two-field binding: the
|
|
1241
|
+
// predicate and entitlement refusals recompute distributionLockDigest over
|
|
1242
|
+
// what this returns, so an unsealed develop lock is digested as itself.
|
|
1243
|
+
return 'unsealedSchema' in raw ? schemas.parseUnsealedDistributionLock(raw) : schemas.parseDistributionLock(raw);
|
|
1153
1244
|
}
|
|
1154
1245
|
|
|
1155
1246
|
/**
|
|
1156
1247
|
* Read and validate an edition's two locks plus the runtime image source index
|
|
1157
|
-
* they must agree with.
|
|
1248
|
+
* they must agree with. The distribution lock is read for `channel`
|
|
1249
|
+
* ({@link readDistributionLockBinding}); the rest is {@link loadValidatedImageLocks}.
|
|
1250
|
+
*/
|
|
1251
|
+
export async function loadValidatedLocks(
|
|
1252
|
+
distributionId,
|
|
1253
|
+
dataRoot,
|
|
1254
|
+
schemas,
|
|
1255
|
+
{ editionsRoot, channel },
|
|
1256
|
+
) {
|
|
1257
|
+
if (!schemas) {
|
|
1258
|
+
throw new Error('trusted kernel distribution schemas are required');
|
|
1259
|
+
}
|
|
1260
|
+
const distributionLock = readEditionDistributionLock(
|
|
1261
|
+
editionsRoot,
|
|
1262
|
+
distributionId,
|
|
1263
|
+
schemas,
|
|
1264
|
+
channel,
|
|
1265
|
+
);
|
|
1266
|
+
return {
|
|
1267
|
+
distributionLock,
|
|
1268
|
+
...(await loadValidatedImageLocks(distributionId, dataRoot, schemas, { editionsRoot })),
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Read and validate an edition's image lock plus the runtime image source index
|
|
1274
|
+
* it must agree with; the distribution lock is not read.
|
|
1158
1275
|
*
|
|
1159
1276
|
* `dataRoot` holds the runtime image source index, read from it always.
|
|
1160
|
-
* `editionsRoot` is where `<id>/
|
|
1161
|
-
* `<id>/
|
|
1162
|
-
*
|
|
1277
|
+
* `editionsRoot` is where `<id>/images.lock.json` and
|
|
1278
|
+
* `<id>/xema-distribution.json` are read from. The manifest extends the
|
|
1279
|
+
* repository pins with the edition's own declared sources
|
|
1163
1280
|
* ({@link externalSourceRepositoryPins}); the verifier grades the editions
|
|
1164
|
-
* checkout so
|
|
1281
|
+
* checkout so the files it reads are HEAD-bound.
|
|
1165
1282
|
*/
|
|
1166
|
-
export async function
|
|
1283
|
+
export async function loadValidatedImageLocks(
|
|
1167
1284
|
distributionId,
|
|
1168
1285
|
dataRoot,
|
|
1169
1286
|
schemas,
|
|
@@ -1172,14 +1289,6 @@ export async function loadValidatedLocks(
|
|
|
1172
1289
|
if (!schemas) {
|
|
1173
1290
|
throw new Error('trusted kernel distribution schemas are required');
|
|
1174
1291
|
}
|
|
1175
|
-
const distributionLock = readDistributionLockBinding(
|
|
1176
|
-
JSON.parse(
|
|
1177
|
-
readFileSync(
|
|
1178
|
-
join(editionsRoot, distributionId, 'distribution.lock.json'),
|
|
1179
|
-
'utf8',
|
|
1180
|
-
),
|
|
1181
|
-
),
|
|
1182
|
-
);
|
|
1183
1292
|
const imageLock = validateImmutableImageLock(
|
|
1184
1293
|
schemas.parseImageLock(
|
|
1185
1294
|
JSON.parse(
|
|
@@ -1235,7 +1344,6 @@ export async function loadValidatedLocks(
|
|
|
1235
1344
|
}
|
|
1236
1345
|
return {
|
|
1237
1346
|
schemas,
|
|
1238
|
-
distributionLock,
|
|
1239
1347
|
imageLock,
|
|
1240
1348
|
runtimeSourceIndex,
|
|
1241
1349
|
repositoryPins,
|