@xemahq/xema 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edition/verify-image-evidence.bundle.mjs +977 -892
- package/dist/edition/verify-image-evidence.bundle.provenance.json +479 -473
- package/edition/README.md +1 -1
- package/edition/build-runtime-image-source-index.mjs +5 -2
- package/edition/edition-protocol.mjs +6 -19
- package/edition/kernel-contract-authority.mjs +62 -18
- package/edition/kernel-contract-source-path-set.mjs +94 -0
- package/edition/oci-image-source-evidence.mjs +15 -1
- package/package.json +3 -3
package/edition/README.md
CHANGED
|
@@ -143,7 +143,7 @@ The edition library builds, validates and verifies evidence statements; it never
|
|
|
143
143
|
|
|
144
144
|
## Kernel protocol and pins
|
|
145
145
|
|
|
146
|
-
The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports protocols
|
|
146
|
+
The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports protocols v28, v30 and v31 (no published kernel declares v29; a CLI lists a protocol only while every kernel published under it exports what the CLI imports; v27 and older kernels lack `serviceImageSubjects`, so a data root whose pinned kernel speaks v27 or older stays on an earlier CLI), pinned to its digest and vendored, and REFUSES an identity with `provisional: true`; every loader that executes the kernel, `xema edition check` first, and the evidence verifier compare only that identity and refuse an unsupported one, naming the supported and the declared versions. A protocol change needs a CLI release first; only then does the data root repin its kernel authority.
|
|
147
147
|
|
|
148
148
|
A develop edition does not take the kernel's develop head blindly: `xema edition develop choose-kernel --kernel <package dir> [--kernel-pin <40-hex>] --out <file>` reads the declared protocol of each commit on the checkout's first-parent history (from source, unbuilt) and chooses the newest one this CLI supports. It writes `{chosenSha, headSha, held, reason}` (`reason`: `head-supported` | `head-ahead` | `explicit-pin`; `held` is `head-ahead`), and when held prints `kernel held at v<N> (<sha12>), head at v<M> (<sha12>): raise the CLI` (stdout only, never persisted). The head is the checkout's `refs/remotes/origin/develop` (a full-history checkout). An explicit pin wins but is still checked, and must be on that head's first-parent history (`not-on-develop` otherwise); when no commit is supported it refuses (`EditionProtocolMismatchError`, `no-supported-commit`; the walk ends at the first commit with no declaration file) and never falls back to the head; a declaration that exists but does not parse, at the head or any walked commit, is refused by name (`unparseable`), never skipped. `freeze-graph` applies the same check to `--kernel-sha`.
|
|
149
149
|
|
|
@@ -758,8 +758,11 @@ export const REPIN_CONTRACT_AUTHORITY_FOLLOW_UPS = Object.freeze([
|
|
|
758
758
|
* states (`kernelCommit`, required; HEAD must equal it), and write it back.
|
|
759
759
|
*
|
|
760
760
|
* The document is read as raw strict JSON and the field replaced BEFORE it is
|
|
761
|
-
* parsed
|
|
762
|
-
*
|
|
761
|
+
* parsed, so a stale index cannot block its own repair (the wire schema
|
|
762
|
+
* checks the shape of `sourcePaths` only, from contracts 0.110.3).
|
|
763
|
+
* The new authority's `sourcePaths` are the set the stated commit declares,
|
|
764
|
+
* and `buildTrustedKernelContractAuthority` requires this CLI's bundled set to
|
|
765
|
+
* equal it: a repin is trusted generation at HEAD.
|
|
763
766
|
* Every repository pin and subject is kept byte for byte. Repository coverage
|
|
764
767
|
* is not asserted: this repin produces no pin, and requiring coverage would
|
|
765
768
|
* make a stale index unrepairable.
|
|
@@ -28,27 +28,14 @@ import { realpathSync } from 'node:fs';
|
|
|
28
28
|
|
|
29
29
|
import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
30
30
|
|
|
31
|
-
/** The edition protocols this library interprets: version -> content digest.
|
|
32
|
-
*
|
|
31
|
+
/** 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
|
+
* v27 and older kernels lack `serviceImageSubjects` (first exported by the v28 kernel), so they are not listed.
|
|
34
|
+
* No published kernel declares v29 (0.110.2 is v28, 0.110.3 is v30, 0.111.0 is v31), so v29 is not listed either. */
|
|
33
35
|
export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
34
|
-
Object.freeze({ version: 9, contentDigest: '7cd6d760e7e7caaec3634a08c739debbdeeeaac4f1bad6791246a12f4f248e37' }),
|
|
35
|
-
Object.freeze({ version: 10, contentDigest: '660129065650a8300f7c0ad54f957b4d43507e1a44e093c9812d2b1bbf143104' }),
|
|
36
|
-
Object.freeze({ version: 11, contentDigest: '600451d2253c470c7d21bafe87260dfc3bfd43b6ad3eee2ef4d8b1f7ad08eb6e' }),
|
|
37
|
-
Object.freeze({ version: 12, contentDigest: 'cff51ea06b6ba7d95d02dcd6305b6a55061b428da48ae74dcbff8a5c8107ce41' }),
|
|
38
|
-
Object.freeze({ version: 13, contentDigest: '5f69fe73d1dea1650ef1757204a53ccd4030ade78bfa91959c2d01c937fc0e16' }),
|
|
39
|
-
Object.freeze({ version: 14, contentDigest: '403b6a491b5922df1a423ba8a2d6ae3d3900bb1caff4e8c0caa48de90a47eb9b' }),
|
|
40
|
-
Object.freeze({ version: 15, contentDigest: 'c66191703abfd9b532d8b6c15476bede2a5752af5bea5032e839fb9d6303a2d1' }),
|
|
41
|
-
Object.freeze({ version: 16, contentDigest: '2e65c280a399304ebb438e4e9829bcb4688f36a3c6a5e8b35b679cf69c689f55' }),
|
|
42
|
-
Object.freeze({ version: 19, contentDigest: '66dab3dcf18c3a587f073808cc91165cc513eabb237b35439ff6cfe2b312b127' }),
|
|
43
|
-
Object.freeze({ version: 20, contentDigest: '22c0e4ba6ee2771e91516ea78806117b5e3e60b76e085b9a9b98e993c1de79f8' }),
|
|
44
|
-
Object.freeze({ version: 21, contentDigest: 'f6364cf4830dcd15f82fb316ec9c0f0c11ffbe45262d97d8621242117d02f2b8' }),
|
|
45
|
-
Object.freeze({ version: 22, contentDigest: '566a0c37ee8b25c22bb9a5979190508e479c87593790b4a9e481ab1db8f7bcd1' }),
|
|
46
|
-
Object.freeze({ version: 23, contentDigest: '96735bd71236b4d0a2cef8a59c4a8d4f6919c6b5e54654c4b2b01e22b9880e04' }),
|
|
47
|
-
Object.freeze({ version: 24, contentDigest: '6ae70f55db789f95e194f623525b30ffedc21478ec17d246336b9a2783b6c0db' }),
|
|
48
|
-
Object.freeze({ version: 25, contentDigest: '79916e652836c7f6c31a7284c083c4f11249c6f21ef0b1bf79efc9dc23684d4a' }),
|
|
49
|
-
Object.freeze({ version: 26, contentDigest: '8d1dcfca5c59ff7b5eee6ea84fec3d9d62a48f3260f4ddb27b6665c25056f968' }),
|
|
50
|
-
Object.freeze({ version: 27, contentDigest: 'd4807f6464508c1abb32f7de9754f2e1f12cd7f2be5fbf939b6a5175c01dd90b' }),
|
|
51
36
|
Object.freeze({ version: 28, contentDigest: 'f7795478b1bfe9a41bcbea9efe496011fcd209a56c90294029a08801e84cf92c' }),
|
|
37
|
+
Object.freeze({ version: 30, contentDigest: '91e3cdff255778b5aa94b64ca9316d031be549ead2267a9a9311ff06d4f1db2c' }),
|
|
38
|
+
Object.freeze({ version: 31, contentDigest: 'b2026847fdd202e7b9e05815cdc2c9819b60be7007c789cc5b5f291c9c682fdd' }),
|
|
52
39
|
]);
|
|
53
40
|
|
|
54
41
|
/** Where a kernel package publishes its identity and its contract, package-relative. */
|
|
@@ -4,6 +4,11 @@ import { join } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { kernelRepoDir } from './edition-workspace.mjs';
|
|
6
6
|
import { COMMIT_PATTERN } from './evidence-grammars.mjs';
|
|
7
|
+
import {
|
|
8
|
+
assertSourcePathsEqualSet,
|
|
9
|
+
KERNEL_CONTRACT_SOURCE_PATH_SET_FILE,
|
|
10
|
+
parseKernelContractSourcePathSet,
|
|
11
|
+
} from './kernel-contract-source-path-set.mjs';
|
|
7
12
|
|
|
8
13
|
const GIT_ENV = Object.freeze({
|
|
9
14
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
@@ -171,6 +176,28 @@ function gitBlobsAtCommit(repositoryDirectory, sourceCommit, paths) {
|
|
|
171
176
|
return sources;
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The source-path set the kernel declares AT `sourceCommit`, read from that
|
|
181
|
+
* commit's own declaration blob: the same tree the digest is recomputed from,
|
|
182
|
+
* never the list of the contracts version this CLI bundles.
|
|
183
|
+
*/
|
|
184
|
+
export function sourcePathSetAtCommit(repositoryDirectory, sourceCommit) {
|
|
185
|
+
const [declaration] = gitBlobsAtCommit(repositoryDirectory, sourceCommit, [
|
|
186
|
+
KERNEL_CONTRACT_SOURCE_PATH_SET_FILE,
|
|
187
|
+
]);
|
|
188
|
+
return parseKernelContractSourcePathSet(declaration.blob.toString('utf8'), sourceCommit);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Refuses `sourcePaths` unless they equal the source-path set at `sourceCommit`
|
|
193
|
+
* (KernelContractSourcePathSetMismatchError names the missing and extra paths).
|
|
194
|
+
* The wire schema checks shape only; this is the check that the set means
|
|
195
|
+
* something.
|
|
196
|
+
*/
|
|
197
|
+
export function assertSourcePathsMatchCommit(repositoryDirectory, sourceCommit, sourcePaths) {
|
|
198
|
+
assertSourcePathsEqualSet(sourcePaths, sourcePathSetAtCommit(repositoryDirectory, sourceCommit), sourceCommit);
|
|
199
|
+
}
|
|
200
|
+
|
|
174
201
|
function exactAuthorityJson(value) {
|
|
175
202
|
return JSON.stringify(value);
|
|
176
203
|
}
|
|
@@ -194,11 +221,12 @@ export async function readKernelContractAuthorityAtCommit({
|
|
|
194
221
|
}
|
|
195
222
|
exactOrigin(repositoryDirectory, schemas);
|
|
196
223
|
assertCommitExists(repositoryDirectory, sourceCommit);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
224
|
+
// The set is the commit's own (XEMA-2010): an authority at a commit whose
|
|
225
|
+
// contracts declare 446 paths is read, digested and stamped over those 446,
|
|
226
|
+
// whatever this CLI bundles. The digest's validateSources checks shape only
|
|
227
|
+
// (contracts 0.110.3 and later).
|
|
228
|
+
const sourcePaths = sourcePathSetAtCommit(repositoryDirectory, sourceCommit);
|
|
229
|
+
const sources = gitBlobsAtCommit(repositoryDirectory, sourceCommit, sourcePaths);
|
|
202
230
|
const contentDigest =
|
|
203
231
|
await schemas.computeKernelContractAuthorityContentDigest(sources);
|
|
204
232
|
return {
|
|
@@ -206,7 +234,7 @@ export async function readKernelContractAuthorityAtCommit({
|
|
|
206
234
|
generatorIdentity: schemas.XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR,
|
|
207
235
|
repositoryUrl: schemas.XEMA_KERNEL_CONTRACT_REPOSITORY_URL,
|
|
208
236
|
sourceCommit,
|
|
209
|
-
sourcePaths
|
|
237
|
+
sourcePaths,
|
|
210
238
|
contentHashAlgorithm:
|
|
211
239
|
schemas.KernelContractContentHashAlgorithm.LengthFramedGitPathModeBlobV1,
|
|
212
240
|
contentDigest,
|
|
@@ -254,14 +282,11 @@ export function assertExactCleanKernelContractCheckout({
|
|
|
254
282
|
const path = record.slice(record.indexOf('\t') + 1);
|
|
255
283
|
indexByPath.set(path, [...(indexByPath.get(path) ?? []), record]);
|
|
256
284
|
}
|
|
285
|
+
const sourcePaths = sourcePathSetAtCommit(repositoryDirectory, sourceCommit);
|
|
257
286
|
const committed = new Map(
|
|
258
|
-
gitBlobsAtCommit(
|
|
259
|
-
repositoryDirectory,
|
|
260
|
-
sourceCommit,
|
|
261
|
-
schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS,
|
|
262
|
-
).map((source) => [source.path, source]),
|
|
287
|
+
gitBlobsAtCommit(repositoryDirectory, sourceCommit, sourcePaths).map((source) => [source.path, source]),
|
|
263
288
|
);
|
|
264
|
-
for (const path of
|
|
289
|
+
for (const path of sourcePaths) {
|
|
265
290
|
const indexRecords = indexByPath.get(path) ?? [];
|
|
266
291
|
const indexMatch =
|
|
267
292
|
indexRecords.length === 1
|
|
@@ -323,6 +348,16 @@ export async function buildTrustedKernelContractAuthority({
|
|
|
323
348
|
sourceCommit,
|
|
324
349
|
schemas,
|
|
325
350
|
});
|
|
351
|
+
// Trusted generation at HEAD ONLY: a NEW pin is minted by a CLI whose bundled
|
|
352
|
+
// contracts declare exactly the set at that commit, so the CLI that writes
|
|
353
|
+
// the pin understands every source it binds. Reading or validating an
|
|
354
|
+
// existing pin never makes this comparison.
|
|
355
|
+
assertSourcePathsEqualSet(
|
|
356
|
+
schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS,
|
|
357
|
+
sourcePathSetAtCommit(repositoryDirectory, sourceCommit),
|
|
358
|
+
sourceCommit,
|
|
359
|
+
"this CLI's bundled kernel contract source paths",
|
|
360
|
+
);
|
|
326
361
|
const authority = await readKernelContractAuthorityAtCommit({
|
|
327
362
|
repositoryDirectory,
|
|
328
363
|
sourceCommit,
|
|
@@ -401,12 +436,19 @@ async function assessAuthorityLineage({ authority, repositoryDirectory, schemas
|
|
|
401
436
|
return { head, changedPaths: [] };
|
|
402
437
|
}
|
|
403
438
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
439
|
+
// The two commits may declare different sets: a path added or dropped since
|
|
440
|
+
// the pin has moved as surely as one whose blob changed.
|
|
441
|
+
const pinnedTree = gitTreeAtCommit(repositoryDirectory, authority.sourceCommit);
|
|
442
|
+
const currentTree = gitTreeAtCommit(repositoryDirectory, head);
|
|
443
|
+
const pinnedSet = new Set(authority.sourcePaths);
|
|
444
|
+
const currentSet = new Set(atHead.sourcePaths);
|
|
445
|
+
const changedPaths = [...new Set([...pinnedSet, ...currentSet])]
|
|
446
|
+
.sort()
|
|
447
|
+
.filter(
|
|
448
|
+
(path) =>
|
|
449
|
+
pinnedSet.has(path) !== currentSet.has(path) ||
|
|
450
|
+
pinnedTree.get(path)?.objectId !== currentTree.get(path)?.objectId,
|
|
451
|
+
);
|
|
410
452
|
return { head, changedPaths };
|
|
411
453
|
}
|
|
412
454
|
|
|
@@ -451,6 +493,8 @@ export async function validateKernelContractAuthorityFromGit({
|
|
|
451
493
|
schemas,
|
|
452
494
|
});
|
|
453
495
|
}
|
|
496
|
+
assertCommitExists(repositoryDirectory, authority.sourceCommit);
|
|
497
|
+
assertSourcePathsMatchCommit(repositoryDirectory, authority.sourceCommit, authority.sourcePaths);
|
|
454
498
|
const expected = await readKernelContractAuthorityAtCommit({
|
|
455
499
|
repositoryDirectory,
|
|
456
500
|
sourceCommit: authority.sourceCommit,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kernel contract authority's source-path set, as the kernel commit itself
|
|
3
|
+
* declares it. The set belongs to the commit an authority is pinned at, never
|
|
4
|
+
* to the contracts package the CLI happens to bundle: a bundled constant is the
|
|
5
|
+
* set of ONE contracts version, and comparing a pin made under another version
|
|
6
|
+
* against it is the cross-version defect this check replaces.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The file in the kernel repository that declares the source-path set. */
|
|
10
|
+
export const KERNEL_CONTRACT_SOURCE_PATH_SET_FILE =
|
|
11
|
+
'packages/kernel/kernel-contracts/src/distribution/lib/kernel-contract-source-paths.ts';
|
|
12
|
+
|
|
13
|
+
const DECLARATION_START = 'export const XEMA_KERNEL_CONTRACT_SOURCE_PATHS = Object.freeze([';
|
|
14
|
+
const DECLARATION_END = '] as const);';
|
|
15
|
+
const PATH_LINE_PATTERN = /^ {2}'([^'\\\s]+)',$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A source-path list is not the source-path set at `sourceCommit`. `subject`
|
|
19
|
+
* names the list that was compared: a pinned authority's `sourcePaths`, or the
|
|
20
|
+
* set the CLI's bundled contracts declare (trusted generation only).
|
|
21
|
+
*/
|
|
22
|
+
export class KernelContractSourcePathSetMismatchError extends Error {
|
|
23
|
+
constructor({ sourceCommit, missing, extra, subject = 'kernel contract authority sourcePaths' }) {
|
|
24
|
+
super(
|
|
25
|
+
`${subject} are not the source-path set at ${sourceCommit}: ` +
|
|
26
|
+
`${missing.length} missing (${missing.join(', ') || 'none'}), ` +
|
|
27
|
+
`${extra.length} extra (${extra.join(', ') || 'none'})`,
|
|
28
|
+
);
|
|
29
|
+
this.name = 'KernelContractSourcePathSetMismatchError';
|
|
30
|
+
this.sourceCommit = sourceCommit;
|
|
31
|
+
this.missing = missing;
|
|
32
|
+
this.extra = extra;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The ordered path list declared by the text of
|
|
38
|
+
* {@link KERNEL_CONTRACT_SOURCE_PATH_SET_FILE}. Refuses any other shape, a list
|
|
39
|
+
* that is empty, not strictly ascending (which also makes it unique), or that
|
|
40
|
+
* does not name the declaration file itself: the declaration is one of the
|
|
41
|
+
* digested sources, so the digest commits to the set it was taken over.
|
|
42
|
+
*/
|
|
43
|
+
export function parseKernelContractSourcePathSet(text, sourceCommit) {
|
|
44
|
+
const lines = text.split('\n');
|
|
45
|
+
const start = lines.indexOf(DECLARATION_START);
|
|
46
|
+
const end = lines.indexOf(DECLARATION_END, start + 1);
|
|
47
|
+
if (start === -1 || end === -1) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} does not declare XEMA_KERNEL_CONTRACT_SOURCE_PATHS in the expected form`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const paths = lines.slice(start + 1, end).map((line) => {
|
|
53
|
+
const match = PATH_LINE_PATTERN.exec(line);
|
|
54
|
+
if (!match || match[1].split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} has a line that is not one quoted relative path: ${JSON.stringify(line)}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return match[1];
|
|
60
|
+
});
|
|
61
|
+
if (paths.length === 0) {
|
|
62
|
+
throw new Error(`${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} declares no source paths`);
|
|
63
|
+
}
|
|
64
|
+
for (let index = 1; index < paths.length; index += 1) {
|
|
65
|
+
if (!(paths[index - 1] < paths[index])) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} is not in strictly ascending order at ${JSON.stringify(paths[index])}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!paths.includes(KERNEL_CONTRACT_SOURCE_PATH_SET_FILE)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} does not declare itself, so the digest would not commit to the set`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return paths;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Refuses `sourcePaths` unless it equals `expected`, the set at `sourceCommit`
|
|
81
|
+
* (same paths, same order); names the paths `sourcePaths` lacks (missing) and
|
|
82
|
+
* the ones it has beyond the set (extra).
|
|
83
|
+
*/
|
|
84
|
+
export function assertSourcePathsEqualSet(sourcePaths, expected, sourceCommit, subject = undefined) {
|
|
85
|
+
const have = new Set(sourcePaths);
|
|
86
|
+
const want = new Set(expected);
|
|
87
|
+
const missing = expected.filter((path) => !have.has(path));
|
|
88
|
+
const extra = sourcePaths.filter((path) => !want.has(path));
|
|
89
|
+
const sameOrder =
|
|
90
|
+
sourcePaths.length === expected.length && sourcePaths.every((path, index) => path === expected[index]);
|
|
91
|
+
if (missing.length > 0 || extra.length > 0 || !sameOrder) {
|
|
92
|
+
throw new KernelContractSourcePathSetMismatchError({ sourceCommit, missing, extra, subject });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -204,7 +204,8 @@ export function writeInvalidReferrerReport(skipped) {
|
|
|
204
204
|
* `getBlob` verifies every digest again, independently of the manifest.
|
|
205
205
|
* An artifact that is not valid evidence under `expectedAuthorityCommit`
|
|
206
206
|
* (a manifest that is not the exact subject's, RAW statements stating another authority commit, a
|
|
207
|
-
* failed strict parse
|
|
207
|
+
* failed strict parse, a statement whose contract authority is not exactly
|
|
208
|
+
* `expectedContractAuthority`, which is required) is refused with `InvalidEvidenceReferrerError`; a
|
|
208
209
|
* registry that cannot answer, or bytes that do not match their digests
|
|
209
210
|
* (`EvidenceIntegrityError`, a tampering signal), still throw.
|
|
210
211
|
*/
|
|
@@ -215,8 +216,12 @@ export async function readImageSourceEvidenceArtifact({
|
|
|
215
216
|
artifactDigest,
|
|
216
217
|
schemas,
|
|
217
218
|
expectedAuthorityCommit,
|
|
219
|
+
expectedContractAuthority,
|
|
218
220
|
}) {
|
|
219
221
|
assertExpectedAuthorityCommit(expectedAuthorityCommit);
|
|
222
|
+
if (!expectedContractAuthority) {
|
|
223
|
+
throw new Error('the expected kernel contract authority is required');
|
|
224
|
+
}
|
|
220
225
|
const fetched = await client.getManifestByDigest(repository, artifactDigest);
|
|
221
226
|
const invalid = (error) =>
|
|
222
227
|
new InvalidEvidenceReferrerError({
|
|
@@ -264,6 +269,14 @@ export async function readImageSourceEvidenceArtifact({
|
|
|
264
269
|
subjectDigest,
|
|
265
270
|
schemas,
|
|
266
271
|
});
|
|
272
|
+
// The wire schema checks the shape of an authority's sourcePaths only
|
|
273
|
+
// (contracts 0.110.3), so a statement naming the expected commit with
|
|
274
|
+
// another path set or digest parses: it is not evidence under the pin.
|
|
275
|
+
for (const record of evidence.records) {
|
|
276
|
+
if (canonicalJson(record.statement.predicate.contractAuthority) !== canonicalJson(expectedContractAuthority)) {
|
|
277
|
+
throw new Error(`${record.key} statement's kernel contract authority is not exactly the expected one at ${expectedAuthorityCommit}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
267
280
|
return { ...evidence, artifactDigest, created };
|
|
268
281
|
} catch (error) {
|
|
269
282
|
if (error instanceof InvalidEvidenceReferrerError || error instanceof EvidenceIntegrityError) {
|
|
@@ -327,6 +340,7 @@ export async function resolveImageSourceEvidenceReferrer({
|
|
|
327
340
|
artifactDigest: descriptor.digest,
|
|
328
341
|
schemas,
|
|
329
342
|
expectedAuthorityCommit,
|
|
343
|
+
expectedContractAuthority,
|
|
330
344
|
}),
|
|
331
345
|
);
|
|
332
346
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/xema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/xema-dev/xema-cli.git",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@xemahq/distribution-source-hash": "^0.2.2",
|
|
55
55
|
"@xemahq/dsl": "^0.24.0",
|
|
56
56
|
"@xemahq/identity-client": "^0.16.0",
|
|
57
|
-
"@xemahq/contracts": "^0.
|
|
57
|
+
"@xemahq/contracts": "^0.111.0",
|
|
58
58
|
"@xemahq/kernel-state-runtime": "^0.8.0",
|
|
59
59
|
"@xemahq/license-verifier": "^0.2.0",
|
|
60
60
|
"@xemahq/service-registry-nest": "^0.12.2",
|
|
@@ -92,6 +92,6 @@
|
|
|
92
92
|
"typecheck": "tsc --noEmit"
|
|
93
93
|
},
|
|
94
94
|
"xemaRelease": {
|
|
95
|
-
"sourceDigest": "sha256:
|
|
95
|
+
"sourceDigest": "sha256:5f6bd75aa0d1bdc9a141a6f9f31be0957f7a8e5aa967695362db0ced936cac5a"
|
|
96
96
|
}
|
|
97
97
|
}
|