@xemahq/xema 0.27.0 → 0.29.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.d.ts.map +1 -1
- package/dist/commands/edition.js +8 -7
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +0 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +231 -50
- package/dist/edition/verify-image-evidence.bundle.provenance.json +55 -43
- 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/runner/chart-set-lock.d.ts +4 -1
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +39 -4
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/edition/README.md +76 -41
- package/edition/build-biome-index.mjs +1 -1
- package/edition/build-platform-service-index.mjs +1 -1
- package/edition/build-runtime-image-source-index.mjs +64 -20
- package/edition/catalogue-fetch.mjs +36 -9
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +11 -0
- package/edition/chart-set-source-commit.mjs +17 -7
- package/edition/checkout-indexed-source-repositories.mjs +17 -17
- package/edition/customer-flags.mjs +0 -1
- package/edition/develop-customer-edition.mjs +23 -5
- package/edition/develop-edition.mjs +147 -48
- package/edition/develop-freeze.mjs +69 -25
- package/edition/develop-seal.mjs +39 -25
- package/edition/dist-check.mjs +1 -1
- package/edition/edition-artifact.mjs +79 -18
- package/edition/edition-biome-manifests.mjs +2 -3
- package/edition/edition-chart-set.mjs +59 -6
- package/edition/edition-config.mjs +45 -21
- package/edition/edition-constants.mjs +10 -4
- package/edition/edition-contract-parse.mjs +52 -0
- package/edition/edition-contract.json +21 -5
- package/edition/edition-workspace.mjs +23 -9
- package/edition/evidence-checkout.mjs +22 -1
- package/edition/evidence-trust-policy.mjs +142 -78
- package/edition/external-source-walk.mjs +48 -61
- package/edition/git-source-auth.mjs +104 -25
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence.mjs +21 -3
- package/edition/kernel-contract-authority.mjs +23 -1
- package/edition/lib.mjs +16 -24
- package/edition/oras.mjs +1 -1
- package/edition/pin-source.mjs +6 -1
- package/edition/produce-image-delivery-entitlement-evidence.mjs +12 -12
- package/edition/registry-credentials.mjs +11 -1
- package/edition/release-channel-guard.mjs +11 -1
- package/edition/release-edition-artifact.mjs +5 -6
- package/edition/resolve-chart-set.mjs +50 -30
- package/edition/resolve-customer-edition.mjs +63 -7
- package/edition/resolve-distribution.mjs +1 -1
- package/edition/resolve-edition.mjs +12 -11
- package/edition/resolve-images.mjs +36 -15
- package/edition/seal-image-digests.mjs +31 -7
- package/edition/source-closure.mjs +1 -2
- package/edition/trust-policy-arming-verb.mjs +68 -0
- package/edition/verify-image-evidence.mjs +40 -12
- package/package.json +3 -3
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
// git-source-auth — the ONE place the edition library turns a read token into git
|
|
2
2
|
// authentication for a repository URL, shared by every script that fetches or
|
|
3
|
-
// lists a repository (first-party
|
|
3
|
+
// lists a repository (first-party and customer sources alike).
|
|
4
4
|
//
|
|
5
5
|
// The credential reaches git through GIT_CONFIG_* environment variables, never
|
|
6
6
|
// argv, a file or a clone's config, and it is scoped to the ORIGIN of the URL it
|
|
7
7
|
// is for: `http.<origin>/.extraHeader` is applied by git only to URLs under that
|
|
8
|
-
// origin,
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// origin, and redirects are not followed for a credentialed fetch. Source
|
|
9
|
+
// credentials are declared only by the data root's edition-workspace.json
|
|
10
|
+
// (`sourceRepositoryUrlPrefix` + `sourceAuthUsername`), the token comes from
|
|
11
|
+
// SOURCE_READ_TOKEN_ENV, and `holdSource` / `withSourceGitEnv` are the hold and the
|
|
12
|
+
// environment every network git call on a source passes: a definition never names
|
|
13
|
+
// a credential, a URL outside the prefix is fetched with none, and git runs
|
|
14
|
+
// isolated from the user's own git configuration (global and system config,
|
|
15
|
+
// ~/.netrc, credential helpers, URL rewrites).
|
|
11
16
|
//
|
|
12
17
|
// Why this is not `gitEnvWithAuth` of the CLI's runtime (src/runtime/sources/kinds/git.ts):
|
|
13
18
|
// the edition library is plain ESM that `node` runs straight from this directory
|
|
@@ -16,37 +21,111 @@
|
|
|
16
21
|
// on purpose (Basic `<declared username>:<token>`, `Authorization` header, origin
|
|
17
22
|
// scope; the username is a required argument, never a built-in default), so there is exactly ONE implementation per runtime.
|
|
18
23
|
|
|
24
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
import { canonicalPublicHttpsRepositoryUrl } from './lib.mjs';
|
|
29
|
+
|
|
30
|
+
/** The fixed environment variable holding the source read token. */
|
|
31
|
+
export const SOURCE_READ_TOKEN_ENV = 'EVIDENCE_REPOSITORIES_TOKEN';
|
|
32
|
+
|
|
33
|
+
/** Whether `canonicalUrl` names a repository under the workspace's `sourceRepositoryUrlPrefix`. */
|
|
34
|
+
export function isUnderSourcePrefix(canonicalUrl, prefix) {
|
|
35
|
+
return canonicalUrl.startsWith(prefix) && canonicalUrl.length > prefix.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A definition does not declare source credentials; the data root does. */
|
|
39
|
+
export function assertSourceDeclaresNoCredential(source) {
|
|
40
|
+
for (const key of ['authTokenEnv', 'authUsername']) {
|
|
41
|
+
if (source[key] !== undefined) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`source "${source.name}" declares "${key}". A definition does not declare source credentials; ` +
|
|
44
|
+
'the data root\'s edition-workspace.json does (`sourceRepositoryUrlPrefix` + `sourceAuthUsername`, ' +
|
|
45
|
+
`token from ${SOURCE_READ_TOKEN_ENV}). Remove the key.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The source's url, which must already be written in its canonical public https form. */
|
|
52
|
+
export function assertCanonicalSourceUrl(source) {
|
|
53
|
+
const context = `source "${source.name}" url`;
|
|
54
|
+
const canonical = canonicalPublicHttpsRepositoryUrl(source.url, context);
|
|
55
|
+
if (canonical !== source.url) {
|
|
56
|
+
throw new Error(`${context} ${JSON.stringify(source.url)} is not in its canonical form (${canonical})`);
|
|
57
|
+
}
|
|
58
|
+
return canonical;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const INHERITED_GIT_VARIABLES = /^(GIT_CONFIG_(COUNT|KEY_\d+|VALUE_\d+|PARAMETERS|GLOBAL|NOSYSTEM)|GIT_ASKPASS|SSH_ASKPASS|GIT_SSL_NO_VERIFY|HOME|XDG_CONFIG_HOME)$/;
|
|
62
|
+
|
|
19
63
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* fall-back to the ambient credential helper.
|
|
64
|
+
* THE HOLD, pure: refuses a source that declares a credential or whose URL is not canonical, and
|
|
65
|
+
* decides whether the workspace credential covers it. A covered source needs the token in
|
|
66
|
+
* SOURCE_READ_TOKEN_ENV (unset is refused, never replaced by an ambient credential). Spawns nothing.
|
|
24
67
|
*/
|
|
25
|
-
export function
|
|
68
|
+
export function holdSource(source, workspace, env = process.env) {
|
|
69
|
+
assertSourceDeclaresNoCredential(source);
|
|
70
|
+
const url = assertCanonicalSourceUrl(source);
|
|
71
|
+
if (!isUnderSourcePrefix(url, workspace.sourceRepositoryUrlPrefix)) {return { url, credential: null };}
|
|
72
|
+
const token = env[SOURCE_READ_TOKEN_ENV];
|
|
73
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`source "${source.name}" is under the workspace's sourceRepositoryUrlPrefix ${workspace.sourceRepositoryUrlPrefix}, ` +
|
|
76
|
+
`which needs a read token, but ${SOURCE_READ_TOKEN_ENV} is unset or empty`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const username = workspace.sourceAuthUsername;
|
|
26
80
|
if (typeof username !== 'string' || username.length === 0 || username.includes(':')) {
|
|
27
81
|
throw new Error(
|
|
28
|
-
`a git username is required to authenticate git against ${
|
|
29
|
-
'it is declared, never defaulted: `sourceAuthUsername` in edition-workspace.json
|
|
30
|
-
'the source\'s own `authUsername` for a customer git source',
|
|
82
|
+
`a git username is required to authenticate git against ${url} (non-empty, no ":"); ` +
|
|
83
|
+
'it is declared, never defaulted: `sourceAuthUsername` in edition-workspace.json',
|
|
31
84
|
);
|
|
32
85
|
}
|
|
33
|
-
|
|
34
|
-
|
|
86
|
+
return { url, credential: { origin: new URL(url).origin, username, token } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isolatedGitEnv({ credential }, env, home) {
|
|
90
|
+
const base = {};
|
|
91
|
+
for (const [key, value] of Object.entries(env)) {
|
|
92
|
+
if (!INHERITED_GIT_VARIABLES.test(key)) {base[key] = value;}
|
|
35
93
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
94
|
+
const entries = [
|
|
95
|
+
['credential.helper', ''],
|
|
96
|
+
['http.extraHeader', ''],
|
|
97
|
+
];
|
|
98
|
+
if (credential !== null) {
|
|
99
|
+
const basic = Buffer.from(`${credential.username}:${credential.token}`, 'utf8').toString('base64');
|
|
100
|
+
entries.push(['http.followRedirects', 'false'], [`http.${credential.origin}/.extraHeader`, `Authorization: Basic ${basic}`]);
|
|
43
101
|
}
|
|
44
|
-
const
|
|
102
|
+
const config = { GIT_CONFIG_COUNT: String(entries.length) };
|
|
103
|
+
entries.forEach(([key, value], index) => {
|
|
104
|
+
config[`GIT_CONFIG_KEY_${index}`] = key;
|
|
105
|
+
config[`GIT_CONFIG_VALUE_${index}`] = value;
|
|
106
|
+
});
|
|
45
107
|
return {
|
|
46
108
|
...base,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
109
|
+
...config,
|
|
110
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
111
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
112
|
+
HOME: home,
|
|
113
|
+
XDG_CONFIG_HOME: home,
|
|
50
114
|
GIT_TERMINAL_PROMPT: '0',
|
|
51
115
|
};
|
|
52
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Run `fn(gitEnv)` with the git environment for one declared source; the hold comes first, before
|
|
120
|
+
* anything is created or spawned. The environment points HOME at an empty directory that exists
|
|
121
|
+
* only for the duration of `fn`.
|
|
122
|
+
*/
|
|
123
|
+
export function withSourceGitEnv(source, { workspace, env = process.env }, fn) {
|
|
124
|
+
const decision = holdSource(source, workspace, env);
|
|
125
|
+
const home = mkdtempSync(join(tmpdir(), 'xema-git-home-'));
|
|
126
|
+
try {
|
|
127
|
+
return fn(isolatedGitEnv(decision, env, home));
|
|
128
|
+
} finally {
|
|
129
|
+
rmSync(home, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// `entitlement.json`: the file `xema edition evidence statement entitlement
|
|
2
|
+
// accept` writes beside the signed statement, and the ONE reader of it
|
|
3
|
+
// (`verify-evidence --entitlement`). Writer and reader share this module so the
|
|
4
|
+
// shape is stated once.
|
|
5
|
+
//
|
|
6
|
+
// Every field but `evidence` is an UNAUTHENTICATED convenience for the operator
|
|
7
|
+
// and for `verify-evidence`'s early refusals: the signed statement stays the
|
|
8
|
+
// authority (`validateEntitlementStatementBinding`). The file names no workflow
|
|
9
|
+
// SHA: the commit the entitlement's certificate is held to is stated by the
|
|
10
|
+
// caller of the verifier (`--entitlement-workflow-sha`), never by the evidence.
|
|
11
|
+
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const ENTITLEMENT_DESCRIPTOR_FILE = 'entitlement.json';
|
|
15
|
+
export const ENTITLEMENT_DESCRIPTOR_SCHEMA_VERSION = 2;
|
|
16
|
+
|
|
17
|
+
const ENTITLEMENT_DESCRIPTOR_KEYS = Object.freeze([
|
|
18
|
+
'schemaVersion',
|
|
19
|
+
'distributionId',
|
|
20
|
+
'customerId',
|
|
21
|
+
'mode',
|
|
22
|
+
'targetRegistry',
|
|
23
|
+
'idempotencyKey',
|
|
24
|
+
'distributionStatementDigest',
|
|
25
|
+
'evidence',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** The document `accept` writes. `evidence` is the kernel Sigstore evidence descriptor of the signed statement. */
|
|
29
|
+
export function entitlementDescriptorDocument({
|
|
30
|
+
distributionId,
|
|
31
|
+
customerId,
|
|
32
|
+
mode,
|
|
33
|
+
targetRegistry,
|
|
34
|
+
idempotencyKey,
|
|
35
|
+
distributionStatementDigest,
|
|
36
|
+
evidence,
|
|
37
|
+
}) {
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: ENTITLEMENT_DESCRIPTOR_SCHEMA_VERSION,
|
|
40
|
+
distributionId,
|
|
41
|
+
customerId,
|
|
42
|
+
mode,
|
|
43
|
+
targetRegistry,
|
|
44
|
+
idempotencyKey,
|
|
45
|
+
distributionStatementDigest,
|
|
46
|
+
evidence,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse an `entitlement.json`: exactly the keys `accept` writes, at the one
|
|
52
|
+
* schema version, with `evidence` parsed by the kernel's Sigstore evidence
|
|
53
|
+
* descriptor schema. Any other shape is refused.
|
|
54
|
+
*/
|
|
55
|
+
export function parseEntitlementDescriptorDocument(raw, schemas) {
|
|
56
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
57
|
+
throw new Error(`${ENTITLEMENT_DESCRIPTOR_FILE} must be a JSON object`);
|
|
58
|
+
}
|
|
59
|
+
if (raw.schemaVersion !== ENTITLEMENT_DESCRIPTOR_SCHEMA_VERSION) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${ENTITLEMENT_DESCRIPTOR_FILE} has schemaVersion ${JSON.stringify(raw.schemaVersion)}; ` +
|
|
62
|
+
`this verifier reads schemaVersion ${ENTITLEMENT_DESCRIPTOR_SCHEMA_VERSION} only (accept the entitlement again with this CLI)`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const keys = Object.keys(raw).sort();
|
|
66
|
+
const expected = [...ENTITLEMENT_DESCRIPTOR_KEYS].sort();
|
|
67
|
+
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`${ENTITLEMENT_DESCRIPTOR_FILE} must hold exactly the keys ${ENTITLEMENT_DESCRIPTOR_KEYS.join(', ')}; it holds ${keys.join(', ')}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
for (const key of ['distributionId', 'customerId', 'mode', 'idempotencyKey', 'distributionStatementDigest']) {
|
|
73
|
+
if (typeof raw[key] !== 'string' || raw[key] === '') {
|
|
74
|
+
throw new Error(`${ENTITLEMENT_DESCRIPTOR_FILE} "${key}" must be a non-empty string`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (raw.targetRegistry !== null && (typeof raw.targetRegistry !== 'string' || raw.targetRegistry === '')) {
|
|
78
|
+
throw new Error(`${ENTITLEMENT_DESCRIPTOR_FILE} "targetRegistry" must be null or a non-empty string`);
|
|
79
|
+
}
|
|
80
|
+
return { ...raw, evidence: schemas.SigstoreEvidenceDescriptorSchema.parse(raw.evidence) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Refuse an `entitlement.json` that states another request than the one being
|
|
85
|
+
* verified. `expected` holds the caller's flags and the completeness statement
|
|
86
|
+
* digest the verifier has just verified.
|
|
87
|
+
*/
|
|
88
|
+
export function assertEntitlementDescriptorStates(document, expected) {
|
|
89
|
+
for (const key of ['distributionId', 'customerId', 'mode', 'targetRegistry', 'distributionStatementDigest']) {
|
|
90
|
+
if (document[key] !== expected[key]) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${ENTITLEMENT_DESCRIPTOR_FILE} states ${key} ${JSON.stringify(document[key])}, ` +
|
|
93
|
+
`not the ${JSON.stringify(expected[key])} this verification was asked for`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The evidence descriptor's paths are relative to the directory `accept` sealed
|
|
101
|
+
* (the one holding `entitlement.json`), which lies inside the evidence root the
|
|
102
|
+
* descriptor file itself was read from.
|
|
103
|
+
*/
|
|
104
|
+
export function entitlementEvidenceRoot(evidenceRoot, entitlementDescriptorPath) {
|
|
105
|
+
return dirname(join(evidenceRoot, entitlementDescriptorPath));
|
|
106
|
+
}
|
|
@@ -600,9 +600,27 @@ function assertEvidenceChannel(channel) {
|
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
/**
|
|
603
|
-
* The
|
|
604
|
-
*
|
|
605
|
-
*
|
|
603
|
+
* The policy rows that authorize `predicateType` on `channel`: a filter on the
|
|
604
|
+
* row's `channel` and `predicateTypes`, and, when `imageRepository` is given
|
|
605
|
+
* (producer evidence), on the row's `imageRepositories` holding that exact
|
|
606
|
+
* repository (no prefix match). It reads no certificate and proves nothing by
|
|
607
|
+
* itself.
|
|
608
|
+
*
|
|
609
|
+
* What ties a row to its channel is the kernel policy schema the policy was
|
|
610
|
+
* parsed by: a row's `githubWorkflow.ref` must be the ref of its `channel`
|
|
611
|
+
* (`EVIDENCE_CHANNEL_REFS`) and its `certificateIdentity` must be exactly
|
|
612
|
+
* `https://github.com/<repository>/<path>@<ref>`. One ROW is therefore one
|
|
613
|
+
* channel; the same workflow file may still be authorized on both channels, as
|
|
614
|
+
* two rows naming the two refs. The caller then runs cosign once per returned
|
|
615
|
+
* row with that row's `certificateIdentity`, `oidcIssuer`, workflow repository
|
|
616
|
+
* and ref (and the exact workflow commit), so evidence signed from the other
|
|
617
|
+
* channel's ref matches no row selected here and fails verification; an
|
|
618
|
+
* edition that states a channel with no row for the predicate is refused
|
|
619
|
+
* before cosign runs ("authorizes no signer").
|
|
620
|
+
*
|
|
621
|
+
* The channel an edition DESCRIPTOR states is unauthenticated input: it only
|
|
622
|
+
* selects which policy rows are tried; what authenticates a channel is the
|
|
623
|
+
* certificate matching a row of that channel.
|
|
606
624
|
*/
|
|
607
625
|
function policyCandidates(policy, predicateType, channel, imageRepository) {
|
|
608
626
|
return policy.policies.filter(
|
|
@@ -291,11 +291,33 @@ export function assertExactCleanKernelContractCheckout({
|
|
|
291
291
|
return head;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/** The flag by which a caller STATES the kernel commit a new contract authority is taken from. */
|
|
295
|
+
export const KERNEL_COMMIT_FLAG = '--kernel-commit';
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The contract authority of the kernel checkout, held to the commit the caller
|
|
299
|
+
* STATES. A clean checkout at the kernel's origin URL proves only which
|
|
300
|
+
* repository the directory claims to be, never which commit somebody meant to
|
|
301
|
+
* pin: `kernelCommit` is that decision, it is required, and the checkout's HEAD
|
|
302
|
+
* must equal it. It is the `sourceCommit` the authority records.
|
|
303
|
+
*/
|
|
294
304
|
export async function buildTrustedKernelContractAuthority({
|
|
295
305
|
repositoryDirectory = kernelRepoDir(),
|
|
296
306
|
schemas,
|
|
307
|
+
kernelCommit,
|
|
297
308
|
}) {
|
|
298
|
-
|
|
309
|
+
if (typeof kernelCommit !== 'string' || !COMMIT_PATTERN.test(kernelCommit)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`${KERNEL_COMMIT_FLAG} <40-hex> is required: a new kernel contract authority is pinned to the commit the caller states, never to whatever the checkout happens to hold`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const head = exactHeadCommit(repositoryDirectory);
|
|
315
|
+
if (head !== kernelCommit) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`kernel contract repository HEAD ${head} is not the stated ${KERNEL_COMMIT_FLAG} ${kernelCommit}: check out the stated commit, or state the commit you mean to pin`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const sourceCommit = kernelCommit;
|
|
299
321
|
assertExactCleanKernelContractCheckout({
|
|
300
322
|
repositoryDirectory,
|
|
301
323
|
sourceCommit,
|
package/edition/lib.mjs
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
kernelDistPath,
|
|
25
25
|
kernelPackageDir,
|
|
26
26
|
kernelRepoDir,
|
|
27
|
-
sourceHashPackageDir,
|
|
28
27
|
sourcePath,
|
|
29
28
|
} from './edition-workspace.mjs';
|
|
30
29
|
import { SHA256_HEX_PATTERN } from './evidence-grammars.mjs';
|
|
@@ -149,38 +148,28 @@ export const BIOME_WORKLOAD_KINDS = new Set(['service', 'runtime-component']);
|
|
|
149
148
|
export const DEFAULT_BIOME_WORKLOAD_KIND = 'service';
|
|
150
149
|
|
|
151
150
|
/**
|
|
152
|
-
* The
|
|
153
|
-
*
|
|
151
|
+
* The `@xemahq/distribution-source-hash` package (CJS dist), a caret (0.x minor-locked)
|
|
152
|
+
* dependency of the CLI, loaded by name on first use.
|
|
154
153
|
*
|
|
155
154
|
* That package is the SINGLE owner of `xema-source-tree-sha256-v2` — the same
|
|
156
155
|
* implementation every producing repository's CI uses to derive the
|
|
157
156
|
* `<contentHash>` release tag. This tooling must never carry a second copy: a
|
|
158
157
|
* divergence would put a lock and its images at coordinates that silently
|
|
159
|
-
* disagree.
|
|
160
|
-
*
|
|
161
|
-
*
|
|
158
|
+
* disagree. The evidence verifier never hashes a source tree, so its bundle
|
|
159
|
+
* does not reach this package; only the entries that resolve, build, index or
|
|
160
|
+
* seal load it.
|
|
162
161
|
*/
|
|
163
162
|
const requireFromLib = createRequire(import.meta.url);
|
|
164
163
|
|
|
165
|
-
|
|
164
|
+
let loadedSourceHash;
|
|
166
165
|
|
|
167
166
|
function distributionSourceHash() {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
mod = createRequire(join(directory, 'package.json'))(directory);
|
|
174
|
-
} catch (err) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Cannot load the built distribution-source-hash package from ${directory}. ` +
|
|
177
|
-
`Build it first (\`pnpm install --frozen-lockfile && pnpm run build\` in its workspace). ` +
|
|
178
|
-
`Underlying error: ${err.message}`,
|
|
179
|
-
);
|
|
167
|
+
if (loadedSourceHash === undefined) {
|
|
168
|
+
const mod = requireFromLib('@xemahq/distribution-source-hash');
|
|
169
|
+
assertSourceHashMatchesKernel(mod);
|
|
170
|
+
loadedSourceHash = mod;
|
|
180
171
|
}
|
|
181
|
-
|
|
182
|
-
loadedSourceHash.set(directory, mod);
|
|
183
|
-
return mod;
|
|
172
|
+
return loadedSourceHash;
|
|
184
173
|
}
|
|
185
174
|
|
|
186
175
|
/**
|
|
@@ -214,9 +203,9 @@ function requireAlgorithmMember(enumObject, enumName, member) {
|
|
|
214
203
|
const value = enumObject?.[member];
|
|
215
204
|
if (typeof value !== 'string' || value.length === 0) {
|
|
216
205
|
throw new Error(
|
|
217
|
-
|
|
206
|
+
`@xemahq/distribution-source-hash does not declare ` +
|
|
218
207
|
`${enumName}.${member}. The installed package is `+
|
|
219
|
-
`stale or the member was renamed;
|
|
208
|
+
`stale or the member was renamed; change the pinned @xemahq/distribution-source-hash version and, if the `+
|
|
220
209
|
`rename was deliberate, move every reader in the same change.`,
|
|
221
210
|
);
|
|
222
211
|
}
|
|
@@ -722,6 +711,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
722
711
|
parseDistributionImageEvidenceSet,
|
|
723
712
|
parseDistributionImageReplayState,
|
|
724
713
|
parseSigstoreOfflineTrustPolicy,
|
|
714
|
+
SigstoreEvidenceDescriptorSchema,
|
|
725
715
|
parsePlatformServiceSourceIndex,
|
|
726
716
|
parseRuntimeImageSourceIndex,
|
|
727
717
|
resolveRuntimeImagePlatforms,
|
|
@@ -798,6 +788,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
798
788
|
typeof parseDistributionImageEvidenceSet !== 'function' ||
|
|
799
789
|
typeof parseDistributionImageReplayState !== 'function' ||
|
|
800
790
|
typeof parseSigstoreOfflineTrustPolicy !== 'function' ||
|
|
791
|
+
typeof SigstoreEvidenceDescriptorSchema?.parse !== 'function' ||
|
|
801
792
|
typeof parsePlatformServiceSourceIndex !== 'function' ||
|
|
802
793
|
typeof parseRuntimeImageSourceIndex !== 'function' ||
|
|
803
794
|
typeof resolveRuntimeImagePlatforms !== 'function' ||
|
|
@@ -892,6 +883,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
892
883
|
parseDistributionImageEvidenceSet,
|
|
893
884
|
parseDistributionImageReplayState,
|
|
894
885
|
parseSigstoreOfflineTrustPolicy,
|
|
886
|
+
SigstoreEvidenceDescriptorSchema,
|
|
895
887
|
parsePlatformServiceSourceIndex,
|
|
896
888
|
parseRuntimeImageSourceIndex,
|
|
897
889
|
resolveRuntimeImagePlatforms,
|
package/edition/oras.mjs
CHANGED
|
@@ -173,7 +173,7 @@ function assertOrasVersion(bin, home) {
|
|
|
173
173
|
/**
|
|
174
174
|
* The oras digests the configured data root trusts: `trustedOrasBinaryDigests`
|
|
175
175
|
* of its evidence-trust pin. `null` when the data root declares no evidence
|
|
176
|
-
* trust at all (`evidenceTrustPolicy:
|
|
176
|
+
* trust at all (`evidenceTrustPolicy: null`):
|
|
177
177
|
* such a data root has no reviewed pin to hold a binary to.
|
|
178
178
|
*/
|
|
179
179
|
export function configuredOrasTrust() {
|
package/edition/pin-source.mjs
CHANGED
|
@@ -65,13 +65,18 @@ export function sourceNameOfPinKey(key) {
|
|
|
65
65
|
return key.slice(SOURCE_PIN_PREFIX.length);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** Whether `key` is the snapshot key of a declared source (`sources/<name>`); pure, it reads no workspace section. */
|
|
69
|
+
export function isSourcePinKey(key) {
|
|
70
|
+
return key.startsWith(SOURCE_PIN_PREFIX) && key.length > SOURCE_PIN_PREFIX.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
68
73
|
/** Which closed namespace a snapshot key belongs to; refuses a key in none. */
|
|
69
74
|
export function snapshotPinNamespace(key) {
|
|
70
75
|
if (key === dataPinKey()) {return SnapshotPinNamespace.Distributions;}
|
|
71
76
|
if (key === kernelPinKey()) {return SnapshotPinNamespace.Kernel;}
|
|
72
77
|
if (key === deployPinKey()) {return SnapshotPinNamespace.ChartSource;}
|
|
73
78
|
if (key === EDITIONS_ROOT_PIN_KEY) {return SnapshotPinNamespace.EditionsRoot;}
|
|
74
|
-
if (
|
|
79
|
+
if (isSourcePinKey(key)) {
|
|
75
80
|
return SnapshotPinNamespace.Source;
|
|
76
81
|
}
|
|
77
82
|
if (GRAPH_PIN_PATTERN.test(key)) {return SnapshotPinNamespace.Graph;}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
dataRoot,
|
|
29
29
|
isEntryModule,
|
|
30
30
|
} from './edition-workspace.mjs';
|
|
31
|
+
import { ENTITLEMENT_DESCRIPTOR_FILE, entitlementDescriptorDocument } from './image-delivery-entitlement-descriptor.mjs';
|
|
31
32
|
import { canonicalJsonBytes, readCanonicalJsonFromRoot } from './image-evidence.mjs';
|
|
32
33
|
import { serializeJson } from './lib.mjs';
|
|
33
34
|
import { githubWorkflowSigner } from './produce-distribution-completeness-evidence.mjs';
|
|
@@ -38,7 +39,6 @@ import {
|
|
|
38
39
|
} from './produce-image-source-evidence.mjs';
|
|
39
40
|
import { assertStagedExactly, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
|
|
40
41
|
|
|
41
|
-
const ENTITLEMENT_DESCRIPTOR_FILE = 'entitlement.json';
|
|
42
42
|
const ENTITLEMENT_STATEMENT_PATH = 'entitlement/statement.json';
|
|
43
43
|
const ENTITLEMENT_BUNDLE_PATH = 'entitlement/bundle.sigstore.json';
|
|
44
44
|
|
|
@@ -176,17 +176,17 @@ export function acceptImageDeliveryEntitlementEvidence(options) {
|
|
|
176
176
|
const descriptor = acceptSignedEvidenceStatement({ ...signing, sealingRoot: sealing, runCosign });
|
|
177
177
|
writeFileSync(
|
|
178
178
|
join(sealing, ENTITLEMENT_DESCRIPTOR_FILE),
|
|
179
|
-
canonicalJsonBytes(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
canonicalJsonBytes(
|
|
180
|
+
entitlementDescriptorDocument({
|
|
181
|
+
distributionId: options.distributionId,
|
|
182
|
+
customerId: options.customerId,
|
|
183
|
+
mode: options.mode,
|
|
184
|
+
targetRegistry: options.targetRegistry,
|
|
185
|
+
idempotencyKey: options.idempotencyKey,
|
|
186
|
+
distributionStatementDigest: completenessStatementDigest,
|
|
187
|
+
evidence: descriptor,
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
190
|
{ flag: 'wx', mode: 0o644 },
|
|
191
191
|
);
|
|
192
192
|
return { outputDirectory, statement, descriptor, completenessStatementDigest };
|
|
@@ -117,7 +117,17 @@ export function loadRegistryCredentialDeclarations(
|
|
|
117
117
|
distributionsRoot = dataRoot(),
|
|
118
118
|
) {
|
|
119
119
|
const path = join(distributionsRoot, REGISTRY_CREDENTIALS_FILE);
|
|
120
|
-
|
|
120
|
+
let text;
|
|
121
|
+
try {
|
|
122
|
+
text = readFileSync(path, 'utf8');
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code !== 'ENOENT') {throw error;}
|
|
125
|
+
throw new Error(
|
|
126
|
+
`${REGISTRY_CREDENTIALS_FILE} does not exist in ${distributionsRoot}: the editions root declares the registry credentials its images are verified with`,
|
|
127
|
+
{ cause: error },
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const raw = JSON.parse(text);
|
|
121
131
|
const keys = Object.keys(raw ?? {}).sort().join(',');
|
|
122
132
|
if (
|
|
123
133
|
typeof raw !== 'object' ||
|
|
@@ -60,8 +60,18 @@ export function assertReleaseEditionTree(dir) {
|
|
|
60
60
|
*/
|
|
61
61
|
export const RELEASE_COMPANION_FILES = Object.freeze([EDITION_CHART_SET_FILE]);
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The release locks an edition directory holds (the distribution and the
|
|
65
|
+
* authorization lock). A directory holding either IS a release edition: a
|
|
66
|
+
* develop resolve writes nothing into an editions tree, so a lock committed
|
|
67
|
+
* there was written by the release resolve.
|
|
68
|
+
*/
|
|
69
|
+
export function heldReleaseLocks(dir) {
|
|
70
|
+
return [EditionLockFile.Distribution, EditionLockFile.Authorization].filter((file) => existsSync(join(dir, file)));
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
function assertReleaseLockSet(dir) {
|
|
64
|
-
const held =
|
|
74
|
+
const held = heldReleaseLocks(dir);
|
|
65
75
|
if (held.length === 0) {return;}
|
|
66
76
|
const missing = RELEASE_COMPANION_FILES.filter((file) => !existsSync(join(dir, file)));
|
|
67
77
|
if (missing.length > 0) {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// release-edition-artifact — write the OCI artifact of a RELEASE edition from
|
|
3
3
|
// the committed, sealed locks of the editions root.
|
|
4
4
|
//
|
|
5
|
-
// artifact <id> --channel release --registry <
|
|
5
|
+
// artifact <id> --channel release --registry <root> --coordinate <tag>
|
|
6
6
|
// --engine-image <repository>@sha256:<digest> --out <dir> [--local]
|
|
7
7
|
//
|
|
8
8
|
// The artifact has the layout of every edition (edition-artifact.mjs): the
|
|
9
9
|
// descriptor plus the four locks and the offering's catalogue.json, written to <dir>, and the target
|
|
10
|
-
// `<
|
|
10
|
+
// `<root>/<release namespace segment>/xema-edition-<id>:<coordinate>` is printed. The workflow pushes
|
|
11
11
|
// <dir> with oras and signs the manifest DIGEST with cosign; the tag is for
|
|
12
12
|
// humans. A lock set that is not sealed is refused: an unsealed images lock names
|
|
13
13
|
// no image digests, so nothing could be admitted against it.
|
|
@@ -128,13 +128,12 @@ export async function readCommittedEdition(id, root = editionsRoot()) {
|
|
|
128
128
|
|
|
129
129
|
async function run(argv, env) {
|
|
130
130
|
const { id, options } = parseReleaseArtifactArgs(argv);
|
|
131
|
-
const target = editionTarget(id, options['--registry'], options['--coordinate']);
|
|
131
|
+
const target = editionTarget(id, options['--registry'], EditionChannel.Release, options['--coordinate']);
|
|
132
132
|
const workflow = options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env);
|
|
133
133
|
const locks = await readCommittedEdition(id);
|
|
134
|
+
// Refuses (EvidenceTrustRequiredError) unless the policy is ARMED on the
|
|
135
|
+
// release channel: a release artifact is never written not-armed.
|
|
134
136
|
const evidenceTrustPolicy = await descriptorEvidenceTrust({ channel: EditionChannel.Release });
|
|
135
|
-
if (evidenceTrustPolicy === 'not-armed') {
|
|
136
|
-
console.warn('[edition-artifact] evidence trust policy: NOT ARMED — the descriptor records not-armed.');
|
|
137
|
-
}
|
|
138
137
|
const outDir = resolve(options['--out']);
|
|
139
138
|
writeEditionArtifact({
|
|
140
139
|
outDir,
|