@xemahq/xema 0.51.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/edition.js +1 -1
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +957 -852
- package/dist/edition/verify-image-evidence.bundle.provenance.json +490 -490
- package/dist/runtime/edition/verb-map.d.ts +1 -0
- 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/edition/README.md +4 -1
- package/edition/assemble-producer-input.mjs +244 -83
- package/edition/assemble-third-party-input.mjs +336 -0
- package/edition/edition-protocol.mjs +4 -1
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/image-evidence.mjs +19 -1
- package/edition/kernel-contract-authority.mjs +10 -2
- package/edition/kernel-package.mjs +13 -0
- package/edition/lib.mjs +51 -37
- package/edition/oci-evidence-verbs.mjs +21 -6
- package/edition/operation-requirements.mjs +11 -0
- package/edition/oras.mjs +5 -4
- package/edition/platform-service-class.mjs +4 -2
- package/edition/produce-image-source-evidence.mjs +33 -17
- package/edition/registry-credentials.mjs +24 -2
- package/edition/third-party-source-evidence.mjs +8 -1
- package/edition/third-party-source-flags.mjs +1 -1
- package/package.json +3 -3
package/edition/lib.mjs
CHANGED
|
@@ -1524,6 +1524,53 @@ export function signedBiomeDeclaredCapabilities(indexBiome, xema) {
|
|
|
1524
1524
|
return [...new Set([...shipped, ...requiredForService])].sort(compareStrings);
|
|
1525
1525
|
}
|
|
1526
1526
|
|
|
1527
|
+
/**
|
|
1528
|
+
* The components of a biome manifest that are image workloads: a service or worker with its own `oci-image`, or a
|
|
1529
|
+
* worker running its sibling service's image with its own entry module (`shared-oci-image`). Such a worker is a
|
|
1530
|
+
* standing workload like any other image component (rostered, with process facts, task queues and peers); it just
|
|
1531
|
+
* builds no image of its own, and `workerImages` in {@link manifestToIndexEntry} records whose it runs.
|
|
1532
|
+
*/
|
|
1533
|
+
export function biomeImageComponents(components) {
|
|
1534
|
+
return components.filter(
|
|
1535
|
+
(component) =>
|
|
1536
|
+
component !== null &&
|
|
1537
|
+
typeof component === 'object' &&
|
|
1538
|
+
(component.kind === 'service' || component.kind === 'worker') &&
|
|
1539
|
+
(component.artifact?.kind === 'oci-image' ||
|
|
1540
|
+
(component.kind === 'worker' && component.artifact?.kind === 'shared-oci-image')),
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* The image name of one image component of biome `id` (manifest `rel`, `target`), claimed in `taken` (the names the
|
|
1546
|
+
* manifest's earlier image components claimed). A `service` registers under `protocol.serviceName`; a `worker` never
|
|
1547
|
+
* registers with the service registry, so its image name is the component key. Same rule the producing repo's build
|
|
1548
|
+
* plane applies: the image name is the SERVICE build arg either way. Refused: no image name, a web biome owning a
|
|
1549
|
+
* deployable image, and one image name declared twice.
|
|
1550
|
+
*/
|
|
1551
|
+
export function claimBiomeImageName(component, { rel, id, target, taken }) {
|
|
1552
|
+
const protocol = component.protocol !== null && typeof component.protocol === 'object' ? component.protocol : {};
|
|
1553
|
+
const name = component.kind === 'service' ? protocol.serviceName : component.key;
|
|
1554
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
`${rel}: xema.components[] kind "${component.kind}" component ` +
|
|
1557
|
+
`"${String(component.key)}" has no image name — a service declares ` +
|
|
1558
|
+
`protocol.serviceName, a worker a non-empty key`,
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
if (target !== 'server') {
|
|
1562
|
+
throw new Error(`${rel}: web biome "${id}" cannot own deployable image "${name}"`);
|
|
1563
|
+
}
|
|
1564
|
+
if (taken.has(name)) {
|
|
1565
|
+
throw new Error(`${rel}: xema.components[] declares image "${name}" more than once`);
|
|
1566
|
+
}
|
|
1567
|
+
taken.add(name);
|
|
1568
|
+
return name;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/** An image component launched per allocation (`scheduler: "runtime"`): a runtime component, never a rostered service. */
|
|
1572
|
+
export const isRuntimeImageComponent = (component) => component.scheduler === 'runtime';
|
|
1573
|
+
|
|
1527
1574
|
export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, path, authorizationModule }) {
|
|
1528
1575
|
const xema = raw?.xema;
|
|
1529
1576
|
if (!xema || typeof xema !== 'object') {
|
|
@@ -1691,19 +1738,7 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1691
1738
|
}
|
|
1692
1739
|
bundleSource = parsed.data;
|
|
1693
1740
|
}
|
|
1694
|
-
const imageComponents = components
|
|
1695
|
-
(component) =>
|
|
1696
|
-
component !== null &&
|
|
1697
|
-
typeof component === 'object' &&
|
|
1698
|
-
(component.kind === 'service' || component.kind === 'worker') &&
|
|
1699
|
-
(component.artifact?.kind === 'oci-image' ||
|
|
1700
|
-
// A worker running its sibling service's image with its own entry
|
|
1701
|
-
// module is a standing workload like any other image component — it is
|
|
1702
|
-
// rostered, has process facts, task queues and peers — it just builds
|
|
1703
|
-
// no image of its own. `workerImages` below records whose it runs.
|
|
1704
|
-
(component.kind === 'worker' &&
|
|
1705
|
-
component.artifact?.kind === 'shared-oci-image')),
|
|
1706
|
-
);
|
|
1741
|
+
const imageComponents = biomeImageComponents(components);
|
|
1707
1742
|
// A worker is a standing workload that runs an image: its own oci-image or a
|
|
1708
1743
|
// sibling service's (shared-oci-image, validated by `sharedImageWorker`).
|
|
1709
1744
|
// A worker with neither would be rostered by nothing and ship no image.
|
|
@@ -1807,34 +1842,13 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1807
1842
|
// service's image — see `sharedImageWorker`.
|
|
1808
1843
|
const workerImages = {};
|
|
1809
1844
|
if (imageComponents.length > 0) {
|
|
1845
|
+
const imageNames = new Set();
|
|
1810
1846
|
for (const component of imageComponents) {
|
|
1811
1847
|
const protocol =
|
|
1812
1848
|
component.protocol !== null && typeof component.protocol === 'object'
|
|
1813
1849
|
? component.protocol
|
|
1814
1850
|
: {};
|
|
1815
|
-
|
|
1816
|
-
// registers with the service registry, so its image name is the component
|
|
1817
|
-
// key. Same rule the producing repo's build plane applies — the image name
|
|
1818
|
-
// is the SERVICE build arg either way.
|
|
1819
|
-
const name =
|
|
1820
|
-
component.kind === 'service' ? protocol.serviceName : component.key;
|
|
1821
|
-
if (typeof name !== 'string' || name.length === 0) {
|
|
1822
|
-
throw new Error(
|
|
1823
|
-
`${rel}: xema.components[] kind "${component.kind}" component ` +
|
|
1824
|
-
`"${String(component.key)}" has no image name — a service declares ` +
|
|
1825
|
-
`protocol.serviceName, a worker a non-empty key`,
|
|
1826
|
-
);
|
|
1827
|
-
}
|
|
1828
|
-
if (target !== 'server') {
|
|
1829
|
-
throw new Error(
|
|
1830
|
-
`${rel}: web biome "${id}" cannot own deployable image "${name}"`,
|
|
1831
|
-
);
|
|
1832
|
-
}
|
|
1833
|
-
if (services.includes(name) || runtimeComponents.includes(name)) {
|
|
1834
|
-
throw new Error(
|
|
1835
|
-
`${rel}: xema.components[] declares image "${name}" more than once`,
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1851
|
+
const name = claimBiomeImageName(component, { rel, id, target, taken: imageNames });
|
|
1838
1852
|
const where = `${rel}: xema.components[] "${name}"`;
|
|
1839
1853
|
const requirements = parseComponentResourceDeclaration(
|
|
1840
1854
|
'ResourceRequirementsSchema',
|
|
@@ -1847,7 +1861,7 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1847
1861
|
`${where} protocol.providesResources`,
|
|
1848
1862
|
);
|
|
1849
1863
|
const nexus = componentNexusDeclarations(component, name, protocol, where);
|
|
1850
|
-
if (component
|
|
1864
|
+
if (isRuntimeImageComponent(component)) {
|
|
1851
1865
|
// A Nexus binding names the provider COMPONENT the deploy plane points
|
|
1852
1866
|
// an endpoint at, which must be a rostered image; a component launched
|
|
1853
1867
|
// per allocation is not one. Refused rather than dropped.
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// [--input <file> --cosign <abs path> [--now <epoch-ms>]]
|
|
8
8
|
// backfill-plan --distribution <id>… --producer-repository-root <abs dir>…
|
|
9
9
|
//
|
|
10
|
+
// attach-plan and validate take --data-root <dir> --kernel <dir> (vendor mode) or --contract-authority <file>
|
|
11
|
+
// (public-input mode: a publisher's run with the kernel this CLI bundles); backfill-plan is vendor mode only.
|
|
12
|
+
//
|
|
10
13
|
// The library reads and decides; it never writes to a registry. `attach-plan`
|
|
11
14
|
// validates a produced evidence directory and prints the exact `oras attach`
|
|
12
15
|
// operands; `validate` holds a pulled artifact to its manifest (and, with
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
isEntryModule,
|
|
33
36
|
} from './edition-workspace.mjs';
|
|
34
37
|
import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
38
|
+
import { EvidenceInputMode, configureEvidenceInputMode } from './evidence-input-mode.mjs';
|
|
35
39
|
import { canonicalJson, loadValidatedImageLocks } from './image-evidence.mjs';
|
|
36
40
|
import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
37
41
|
import {
|
|
@@ -320,12 +324,24 @@ const readJsonFile = (path, label) => {
|
|
|
320
324
|
}
|
|
321
325
|
};
|
|
322
326
|
|
|
327
|
+
/**
|
|
328
|
+
* `attach-plan` and `validate` in one of the two input modes: the vendor's pinned authority and kernel checkout, or
|
|
329
|
+
* a publisher's `--contract-authority` file with the kernel this CLI bundles.
|
|
330
|
+
*/
|
|
331
|
+
async function ociInputMode(argv, operation) {
|
|
332
|
+
const mode = await configureEvidenceInputMode(argv, { verb: `oci ${operation}` });
|
|
333
|
+
if (mode.mode === EvidenceInputMode.PublicInput) {
|
|
334
|
+
return { rest: mode.rest, schemas: mode.schemas, expectedContractAuthority: mode.contractAuthority };
|
|
335
|
+
}
|
|
336
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
337
|
+
return { rest: mode.rest, schemas, expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }) };
|
|
338
|
+
}
|
|
339
|
+
|
|
323
340
|
async function main() {
|
|
324
341
|
const [operation, ...argv] = process.argv.slice(2);
|
|
325
342
|
if (operation === 'attach-plan') {
|
|
326
|
-
const rest =
|
|
343
|
+
const { rest, schemas, expectedContractAuthority } = await ociInputMode(argv, operation);
|
|
327
344
|
const flags = parseEvidenceCliFlags(rest, { single: ['evidence-directory', 'input'] });
|
|
328
|
-
const schemas = await loadKernelDistributionSchemas();
|
|
329
345
|
const input = flags.optional('input');
|
|
330
346
|
process.stdout.write(
|
|
331
347
|
serializeJson(
|
|
@@ -333,18 +349,17 @@ async function main() {
|
|
|
333
349
|
evidenceDirectory: flags.require('evidence-directory'),
|
|
334
350
|
input: input === undefined ? undefined : readJsonFile(input, 'producer input'),
|
|
335
351
|
schemas,
|
|
336
|
-
expectedContractAuthority
|
|
352
|
+
expectedContractAuthority,
|
|
337
353
|
}),
|
|
338
354
|
),
|
|
339
355
|
);
|
|
340
356
|
return;
|
|
341
357
|
}
|
|
342
358
|
if (operation === 'validate') {
|
|
343
|
-
const rest =
|
|
359
|
+
const { rest, schemas, expectedContractAuthority } = await ociInputMode(argv, operation);
|
|
344
360
|
const flags = parseEvidenceCliFlags(rest, {
|
|
345
361
|
single: ['evidence-directory', 'manifest', 'subject-digest', 'input', 'cosign', 'now'],
|
|
346
362
|
});
|
|
347
|
-
const schemas = await loadKernelDistributionSchemas();
|
|
348
363
|
const input = flags.optional('input');
|
|
349
364
|
const cosign = flags.optional('cosign');
|
|
350
365
|
const now = flags.optional('now');
|
|
@@ -362,7 +377,7 @@ async function main() {
|
|
|
362
377
|
subjectDigest: flags.require('subject-digest'),
|
|
363
378
|
schemas,
|
|
364
379
|
input: input === undefined ? undefined : readJsonFile(input, 'producer input'),
|
|
365
|
-
expectedContractAuthority
|
|
380
|
+
expectedContractAuthority,
|
|
366
381
|
runCosign: cosign === undefined ? undefined : createCosignVerifier(cosign).runCosign,
|
|
367
382
|
now: now === undefined ? Date.now() : Number(now),
|
|
368
383
|
}),
|
|
@@ -60,6 +60,7 @@ function loadKernelAudienceGrammars() {
|
|
|
60
60
|
kernelAudienceGrammars = {
|
|
61
61
|
ExactAudienceSchema: elementSchema('requiresAudiences'),
|
|
62
62
|
AudienceNamespaceSchema: elementSchema('ownsAudienceNamespaces'),
|
|
63
|
+
PeerServiceNameSchema: elementSchema('optionalPeers'),
|
|
63
64
|
};
|
|
64
65
|
return kernelAudienceGrammars;
|
|
65
66
|
}
|
|
@@ -162,6 +163,16 @@ export function componentOwnedAudienceNamespaces(component, where) {
|
|
|
162
163
|
return declaredPrivilegeList(component, where, 'ownsAudienceNamespaces', AudienceNamespaceSchema, 'audience namespace');
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
/**
|
|
167
|
+
* The peer services one platform service uses only when they are served
|
|
168
|
+
* (`optionalPeers`, edition protocol v38), each graded by the kernel's peer
|
|
169
|
+
* service name grammar. Declared INTENT only; `null` when undeclared.
|
|
170
|
+
*/
|
|
171
|
+
export function componentOptionalPeers(component, where) {
|
|
172
|
+
const { PeerServiceNameSchema } = loadKernelAudienceGrammars();
|
|
173
|
+
return declaredPrivilegeList(component, where, 'optionalPeers', PeerServiceNameSchema, 'peer service name');
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
/**
|
|
166
177
|
* The event subscription clauses one workload component declares
|
|
167
178
|
* (`subscribesEvents`), each graded by the kernel's clause grammar and returned
|
package/edition/oras.mjs
CHANGED
|
@@ -197,8 +197,9 @@ const noticesGiven = new WeakSet();
|
|
|
197
197
|
/**
|
|
198
198
|
* Prepare the oras binary for use. With `trustedDigests` (an array) the binary
|
|
199
199
|
* must hash to one of them and a private copy of exactly those bytes is what
|
|
200
|
-
* runs; an empty list trusts no binary. With `trustedDigests: null` (
|
|
201
|
-
*
|
|
200
|
+
* runs; an empty list trusts no binary. With `trustedDigests: null` (no
|
|
201
|
+
* evidence trust in force: a publisher's run with no data root, or a data root
|
|
202
|
+
* that declares none) the binary is held to its version only,
|
|
202
203
|
* and the run says so once.
|
|
203
204
|
*
|
|
204
205
|
* @returns {{version: string, pinned: boolean, path: () => string, dispose: () => void}}
|
|
@@ -215,8 +216,8 @@ export function prepareTrustedOras({ env = process.env, trustedDigests, notice =
|
|
|
215
216
|
if (!noticesGiven.has(notice)) {
|
|
216
217
|
noticesGiven.add(notice);
|
|
217
218
|
notice(
|
|
218
|
-
`[oras] ${located} is held to its version (${version}) only:
|
|
219
|
-
'so there are no trustedOrasBinaryDigests to hold it to.',
|
|
219
|
+
`[oras] ${located} is held to its version (${version}) only: no evidence-trust pin is in force ` +
|
|
220
|
+
'(a publisher run, or a data root that declares none), so there are no trustedOrasBinaryDigests to hold it to.',
|
|
220
221
|
);
|
|
221
222
|
}
|
|
222
223
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isKernelCapabilityDomain } from './kernel-capability-domain.mjs';
|
|
2
|
-
import { componentAudienceRequirements, componentOperationRequirements, componentOwnedAudienceNamespaces } from './operation-requirements.mjs';
|
|
2
|
+
import { componentAudienceRequirements, componentOperationRequirements, componentOptionalPeers, componentOwnedAudienceNamespaces } from './operation-requirements.mjs';
|
|
3
3
|
import { compareStrings } from './string-order.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -58,7 +58,7 @@ export function sourceOwnedCapabilityDomains(sourceManifest, where) {
|
|
|
58
58
|
/**
|
|
59
59
|
* What a platform service declares it requires and owns, as the index carries it, read from the
|
|
60
60
|
* same declaration as {@link sourceOwnedCapabilityDomains} (the `xema` block of its source manifest):
|
|
61
|
-
* `requiresOperations`, `requiresAudiences` and `
|
|
61
|
+
* `requiresOperations`, `requiresAudiences`, `ownsAudienceNamespaces` and `optionalPeers`. Each is sorted, unique and
|
|
62
62
|
* non-empty, or no key at all. Declared INTENT: the lock admits or refuses each
|
|
63
63
|
* (`admitOperationGrants`, `admitExactAudiences`).
|
|
64
64
|
*
|
|
@@ -70,10 +70,12 @@ export function sourceDeclaredRequirements(sourceManifest, where) {
|
|
|
70
70
|
const requiresOperations = componentOperationRequirements(xema, where);
|
|
71
71
|
const requiresAudiences = componentAudienceRequirements(xema, where);
|
|
72
72
|
const ownsAudienceNamespaces = componentOwnedAudienceNamespaces(xema, where);
|
|
73
|
+
const optionalPeers = componentOptionalPeers(xema, where);
|
|
73
74
|
return {
|
|
74
75
|
...(requiresOperations === null ? {} : { requiresOperations }),
|
|
75
76
|
...(requiresAudiences === null ? {} : { requiresAudiences }),
|
|
76
77
|
...(ownsAudienceNamespaces === null ? {} : { ownsAudienceNamespaces }),
|
|
78
|
+
...(optionalPeers === null ? {} : { optionalPeers }),
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -28,14 +28,13 @@ import { pathToFileURL } from 'node:url';
|
|
|
28
28
|
import { certificateConstraintArgs } from './cosign-certificate-constraints.mjs';
|
|
29
29
|
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
30
30
|
import {
|
|
31
|
-
WorkspaceRoot,
|
|
32
|
-
configureEditionWorkspaceFromArgv,
|
|
33
31
|
dataRoot,
|
|
34
32
|
isEntryModule,
|
|
35
33
|
kernelDistPath,
|
|
36
34
|
kernelRepoDir,
|
|
37
35
|
} from './edition-workspace.mjs';
|
|
38
36
|
import { COMMIT_PATTERN, GITHUB_REF_PATTERN, GITHUB_REPOSITORY_PATTERN, SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
37
|
+
import { EvidenceInputMode, configureEvidenceInputMode } from './evidence-input-mode.mjs';
|
|
39
38
|
import {
|
|
40
39
|
KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE,
|
|
41
40
|
KernelContractAuthorityPinError,
|
|
@@ -610,7 +609,12 @@ export function stageImageSourceEvidence({
|
|
|
610
609
|
* later step to pick up; a successful accept removes it too. Only the argument
|
|
611
610
|
* refusals above the first read leave it in place.
|
|
612
611
|
*
|
|
613
|
-
* `
|
|
612
|
+
* `inputMode` (EvidenceInputMode) decides the cosign version check. In vendor
|
|
613
|
+
* mode `cosignVersion` is the trust policy's pinned version and the cosign that
|
|
614
|
+
* verifies must be it. In public-input mode there is no vendor trust policy and
|
|
615
|
+
* no version is held (`cosignVersion` must be null): the vendor's admission
|
|
616
|
+
* re-verifies every bundle with its own pinned cosign, and that is the
|
|
617
|
+
* authority. The signature verification itself runs in both modes.
|
|
614
618
|
*/
|
|
615
619
|
export function acceptImageSourceEvidence({
|
|
616
620
|
input,
|
|
@@ -618,6 +622,7 @@ export function acceptImageSourceEvidence({
|
|
|
618
622
|
outputDirectory,
|
|
619
623
|
schemas,
|
|
620
624
|
expectedContractAuthority,
|
|
625
|
+
inputMode,
|
|
621
626
|
cosignVersion,
|
|
622
627
|
readCosignVersion,
|
|
623
628
|
runCosign,
|
|
@@ -625,6 +630,12 @@ export function acceptImageSourceEvidence({
|
|
|
625
630
|
if (!isAbsolute(stagingDirectory) || !isAbsolute(outputDirectory)) {
|
|
626
631
|
throw new Error('the staging and output directories must be absolute paths');
|
|
627
632
|
}
|
|
633
|
+
if (!Object.values(EvidenceInputMode).includes(inputMode)) {
|
|
634
|
+
throw new Error(`accept needs an input mode, one of ${Object.values(EvidenceInputMode).join(', ')} (got ${String(inputMode)})`);
|
|
635
|
+
}
|
|
636
|
+
if (inputMode === EvidenceInputMode.PublicInput && cosignVersion !== null) {
|
|
637
|
+
throw new Error('public-input accept holds no cosign version: the vendor admission re-verifies with its pinned cosign');
|
|
638
|
+
}
|
|
628
639
|
if (typeof readCosignVersion !== 'function' || typeof runCosign !== 'function') {
|
|
629
640
|
throw new Error('cosign seams are required');
|
|
630
641
|
}
|
|
@@ -634,7 +645,9 @@ export function acceptImageSourceEvidence({
|
|
|
634
645
|
sealing,
|
|
635
646
|
outputDirectory,
|
|
636
647
|
build: () => {
|
|
637
|
-
|
|
648
|
+
if (inputMode === EvidenceInputMode.Vendor) {
|
|
649
|
+
exactCosignVersion(readCosignVersion(), cosignVersion);
|
|
650
|
+
}
|
|
638
651
|
const statements = buildImageSourceStatements(input, schemas, expectedContractAuthority);
|
|
639
652
|
assertStagedExactly(
|
|
640
653
|
stagingDirectory,
|
|
@@ -695,7 +708,7 @@ const USAGE =
|
|
|
695
708
|
'usage: xema edition evidence statement producer <stage|accept> --input <json> --staging <absolute-directory> ' +
|
|
696
709
|
'--github-repository <owner/repo> --github-ref <ref> --github-sha <commit> ' +
|
|
697
710
|
'--github-workflow-ref <owner/repo/path@ref> [accept: --output <absolute-directory> --cosign <absolute-binary>] ' +
|
|
698
|
-
'--data-root <dir> --kernel <dir>';
|
|
711
|
+
'(--data-root <dir> --kernel <dir> | --contract-authority <file>)';
|
|
699
712
|
|
|
700
713
|
function parseFlags(args, known) {
|
|
701
714
|
const flags = new Map();
|
|
@@ -717,20 +730,19 @@ function parseFlags(args, known) {
|
|
|
717
730
|
}
|
|
718
731
|
|
|
719
732
|
async function main() {
|
|
720
|
-
const [phase, ...
|
|
721
|
-
WorkspaceRoot.Data,
|
|
722
|
-
WorkspaceRoot.Kernel,
|
|
723
|
-
]);
|
|
724
|
-
const context = ['github-repository', 'github-ref', 'github-sha', 'github-workflow-ref'];
|
|
733
|
+
const [phase, ...argv] = process.argv.slice(2);
|
|
725
734
|
if (phase !== 'stage' && phase !== 'accept') {
|
|
726
735
|
throw new Error(USAGE);
|
|
727
736
|
}
|
|
737
|
+
const mode = await configureEvidenceInputMode(argv, { verb: `statement producer ${phase}` });
|
|
738
|
+
const context = ['github-repository', 'github-ref', 'github-sha', 'github-workflow-ref'];
|
|
728
739
|
const flags = parseFlags(
|
|
729
|
-
|
|
740
|
+
mode.rest,
|
|
730
741
|
phase === 'stage' ? ['input', 'staging', ...context] : ['input', 'staging', 'output', 'cosign', ...context],
|
|
731
742
|
);
|
|
732
743
|
const input = JSON.parse(readFileSync(resolve(flags.get('input')), 'utf8'));
|
|
733
|
-
const { schemas, contractAuthority } =
|
|
744
|
+
const { schemas, contractAuthority } =
|
|
745
|
+
mode.mode === EvidenceInputMode.PublicInput ? mode : await loadProducerKernelSchemas({ dataRoot: dataRoot() });
|
|
734
746
|
assertProducerInput(input, contractAuthority);
|
|
735
747
|
assertGitHubProducerContext(input, {
|
|
736
748
|
repository: flags.get('github-repository'),
|
|
@@ -754,11 +766,15 @@ async function main() {
|
|
|
754
766
|
outputDirectory: flags.get('output'),
|
|
755
767
|
schemas,
|
|
756
768
|
expectedContractAuthority: contractAuthority,
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
769
|
+
inputMode: mode.mode,
|
|
770
|
+
cosignVersion:
|
|
771
|
+
mode.mode === EvidenceInputMode.PublicInput
|
|
772
|
+
? null
|
|
773
|
+
: loadPinnedDeclaredTrustPolicy({
|
|
774
|
+
dataRoot: dataRoot(),
|
|
775
|
+
declaration: editionWorkspaceConfig().evidenceTrustPolicy,
|
|
776
|
+
schemas,
|
|
777
|
+
}).cosignVersion,
|
|
762
778
|
...createCosignVerifier(flags.get('cosign')),
|
|
763
779
|
});
|
|
764
780
|
process.stdout.write(serializeJson({ schemaVersion: 1, kind: 'producer', outputDirectory: flags.get('output'), producerEvidence: evidence }));
|
|
@@ -127,6 +127,28 @@ export function loadRegistryCredentialDeclarations(
|
|
|
127
127
|
{ cause: error },
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
|
+
return parseRegistryCredentialDeclarations(text, REGISTRY_CREDENTIALS_FILE);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A registry credential declaration file the CALLER names, in the same strict
|
|
135
|
+
* schema: a third-party publisher states its own registries this way
|
|
136
|
+
* (`evidence assemble-third-party-input --registry-credentials <file>`). A
|
|
137
|
+
* missing file is refused; there is no default declaration.
|
|
138
|
+
*/
|
|
139
|
+
export function loadRegistryCredentialDeclarationsFile(path) {
|
|
140
|
+
let text;
|
|
141
|
+
try {
|
|
142
|
+
text = readFileSync(path, 'utf8');
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error?.code !== 'ENOENT') {throw error;}
|
|
145
|
+
throw new Error(`the registry credential declaration ${path} does not exist`, { cause: error });
|
|
146
|
+
}
|
|
147
|
+
return parseRegistryCredentialDeclarations(text, path);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The declarations of one credential declaration file's text, strict schema v1, by authority. */
|
|
151
|
+
function parseRegistryCredentialDeclarations(text, label) {
|
|
130
152
|
const raw = JSON.parse(text);
|
|
131
153
|
const keys = Object.keys(raw ?? {}).sort().join(',');
|
|
132
154
|
if (
|
|
@@ -141,14 +163,14 @@ export function loadRegistryCredentialDeclarations(
|
|
|
141
163
|
(!Array.isArray(raw.$comment) ||
|
|
142
164
|
raw.$comment.some((line) => typeof line !== 'string')))
|
|
143
165
|
) {
|
|
144
|
-
throw new Error(`${
|
|
166
|
+
throw new Error(`${label} is not strict schema v1`);
|
|
145
167
|
}
|
|
146
168
|
const byAuthority = new Map();
|
|
147
169
|
for (const [index, entry] of raw.registries.entries()) {
|
|
148
170
|
assertCredentialDeclaration(entry, index);
|
|
149
171
|
if (byAuthority.has(entry.authority)) {
|
|
150
172
|
throw new Error(
|
|
151
|
-
`${
|
|
173
|
+
`${label} declares "${entry.authority}" more than once`,
|
|
152
174
|
);
|
|
153
175
|
}
|
|
154
176
|
byAuthority.set(entry.authority, entry);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// evidence/sha256-<image manifest hex>/producer-evidence.json (+ statements/, bundles/)
|
|
11
11
|
// the image-source/v2 evidence, stored the way a first-party producer commits it.
|
|
12
12
|
|
|
13
|
-
import { decodeExactUtf8, evidenceDirectoryForDigest, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
|
|
13
|
+
import { assertProducerTtlCeiling, decodeExactUtf8, evidenceDirectoryForDigest, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
|
|
14
14
|
import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
|
|
15
15
|
import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
16
16
|
import { PRODUCER_EVIDENCE_INDEX_FILE } from './producer-evidence-index-file.mjs';
|
|
@@ -234,6 +234,13 @@ function verifiedImageEvidence({ source, image, evidenceRoot, context, hold, bun
|
|
|
234
234
|
throw new Error(`${label}: ${error.message}`);
|
|
235
235
|
}
|
|
236
236
|
assertImageStatementBindsPin({ statement: verified.statement, source, image, expectedContractAuthority: context.expectedContractAuthority, label });
|
|
237
|
+
// The ceiling is held here, at the publisher's door, as well as by verify-evidence: a too-long lifetime is never pinned.
|
|
238
|
+
assertProducerTtlCeiling({
|
|
239
|
+
issuedAt: verified.statement.predicate.producerRun.issuedAt,
|
|
240
|
+
expiresAt: verified.statement.predicate.producerRun.expiresAt,
|
|
241
|
+
maxTtlSeconds: context.policy.maxProducerTtlSeconds,
|
|
242
|
+
label,
|
|
243
|
+
});
|
|
237
244
|
const bundleBytes = readEvidenceFile(evidenceRoot, descriptor.bundlePath, `${label} bundle`, 16 * 1024 * 1024).bytes;
|
|
238
245
|
assertCertificateExtensions({ bundle: JSON.parse(decodeExactUtf8(bundleBytes, `${label} bundle`)), source, label });
|
|
239
246
|
return verified.statement;
|
|
@@ -61,7 +61,7 @@ function matching(flag, value, pattern, expectation) {
|
|
|
61
61
|
return value;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const OCI_REPOSITORY_AT_DIGEST = /^[^@\s]+@sha256:[a-f0-9]{64}$/u;
|
|
64
|
+
export const OCI_REPOSITORY_AT_DIGEST = /^[^@\s]+@sha256:[a-f0-9]{64}$/u;
|
|
65
65
|
const NUMERIC_ID = /^[1-9][0-9]*$/u;
|
|
66
66
|
|
|
67
67
|
function parseImage(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/xema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/xema-dev/xema-cli.git",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@xemahq/distribution-source-hash": "^0.2.2",
|
|
58
58
|
"@xemahq/dsl": "^0.28.0",
|
|
59
59
|
"@xemahq/identity-client": "^0.36.0",
|
|
60
|
-
"@xemahq/contracts": "^0.
|
|
60
|
+
"@xemahq/contracts": "^0.116.0",
|
|
61
61
|
"@xemahq/kernel-state-runtime": "^0.8.0",
|
|
62
62
|
"@xemahq/license-verifier": "^0.2.0",
|
|
63
63
|
"@xemahq/release-policy": "^17.6.0",
|
|
@@ -96,6 +96,6 @@
|
|
|
96
96
|
"typecheck": "tsc --noEmit"
|
|
97
97
|
},
|
|
98
98
|
"xemaRelease": {
|
|
99
|
-
"sourceDigest": "sha256:
|
|
99
|
+
"sourceDigest": "sha256:86b6ce1a0758e2ed6ab8e9a2cd84c29c52f72582ff30c670e16e8e5982038894"
|
|
100
100
|
}
|
|
101
101
|
}
|