@xemahq/edition-tooling 0.0.0-stage → 0.1.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/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- package/README.md +0 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
import { parseStrictJson } from './strict-json.mjs';
|
|
5
|
+
|
|
6
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
7
|
+
|
|
8
|
+
let loadedSourceHash;
|
|
9
|
+
|
|
10
|
+
/** Git-object primitives of `@xemahq/distribution-source-hash`; no kernel is consulted. */
|
|
11
|
+
function gitObjectReader() {
|
|
12
|
+
loadedSourceHash ??= requireFromHere('@xemahq/distribution-source-hash');
|
|
13
|
+
return loadedSourceHash;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read + parse a JSON file out of an exact Git commit (never the work tree).
|
|
18
|
+
*
|
|
19
|
+
* This is the seam every authored source file crosses on its way into a lock
|
|
20
|
+
* or an index — `xema-biome.json`, a platform service's `package.json`,
|
|
21
|
+
* `cross-repo-build-inputs.json`, the committed generated biome index — so it
|
|
22
|
+
* parses STRICTLY: a duplicated object key is refused, naming the commit, the
|
|
23
|
+
* path and the JSON pointer of the object. `JSON.parse` would keep the last
|
|
24
|
+
* occurrence, which is how a `resources` declaration was dropped from a sealed
|
|
25
|
+
* lock with every reader agreeing (see `strict-json.mjs`).
|
|
26
|
+
*
|
|
27
|
+
* The commit check, the buffer ceiling and the exact-UTF-8 decode are the
|
|
28
|
+
* source-hash package's own (loaded WITHOUT the kernel check hashing needs, so
|
|
29
|
+
* the source checkout, which runs before any kernel is built, can read a record
|
|
30
|
+
* too); only the parse differs from its
|
|
31
|
+
* `readGitJsonAtCommit`, which is lenient and is therefore not delegated to.
|
|
32
|
+
*/
|
|
33
|
+
export function readGitJsonAtCommit(repoDir, repoRelativePath, commit) {
|
|
34
|
+
const sourceHash = gitObjectReader();
|
|
35
|
+
sourceHash.assertGitCommitExists(repoDir, commit);
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = execFileSync(
|
|
39
|
+
'git',
|
|
40
|
+
['-C', repoDir, 'show', `${commit}:${repoRelativePath}`],
|
|
41
|
+
{
|
|
42
|
+
encoding: 'buffer',
|
|
43
|
+
maxBuffer: sourceHash.GIT_OBJECT_MAX_BUFFER_BYTES,
|
|
44
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`cannot read "${repoRelativePath}" from Git commit "${commit}"`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const context = `Git object "${commit}:${repoRelativePath}"`;
|
|
53
|
+
return parseStrictJson(sourceHash.decodeExactUtf8(raw, context), context);
|
|
54
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// git-source-auth — the ONE place the edition library turns a read token into git
|
|
2
|
+
// authentication for a repository URL, shared by every script that fetches or
|
|
3
|
+
// lists a repository (first-party and customer sources alike).
|
|
4
|
+
//
|
|
5
|
+
// The credential reaches git through GIT_CONFIG_* environment variables, never
|
|
6
|
+
// argv, a file or a clone's config, and it is scoped to the ORIGIN of the URL it
|
|
7
|
+
// is for: `http.<origin>/.extraHeader` is applied by git only to URLs under that
|
|
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).
|
|
16
|
+
//
|
|
17
|
+
// Why this is not `gitEnvWithAuth` of the CLI's runtime (src/runtime/sources/kinds/git.ts):
|
|
18
|
+
// the edition library is plain ESM that `node` runs straight from this directory
|
|
19
|
+
// and that the evidence verifier bundle is built from, so it cannot import the CLI's
|
|
20
|
+
// TypeScript sources or its compiled CommonJS output. The shape is the same
|
|
21
|
+
// on purpose (Basic `<declared username>:<token>`, `Authorization` header, origin
|
|
22
|
+
// scope; the username is a required argument, never a built-in default), so there is exactly ONE implementation per runtime.
|
|
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 './canonical-repository-url.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
|
+
|
|
63
|
+
/**
|
|
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.
|
|
67
|
+
*/
|
|
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;
|
|
80
|
+
if (typeof username !== 'string' || username.length === 0 || username.includes(':')) {
|
|
81
|
+
throw new Error(
|
|
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',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
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;}
|
|
93
|
+
}
|
|
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}`]);
|
|
101
|
+
}
|
|
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
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
...base,
|
|
109
|
+
...config,
|
|
110
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
111
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
112
|
+
HOME: home,
|
|
113
|
+
XDG_CONFIG_HOME: home,
|
|
114
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
115
|
+
};
|
|
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
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants as fsConstants,
|
|
5
|
+
fstatSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
canonicalJsonBytes,
|
|
18
|
+
parseCanonicalJsonBytes,
|
|
19
|
+
} from './image-evidence.mjs';
|
|
20
|
+
|
|
21
|
+
const MAX_REPLAY_STATE_BYTES = 64 * 1024;
|
|
22
|
+
|
|
23
|
+
function openRegularFileNoFollow(path) {
|
|
24
|
+
const descriptor = openSync(
|
|
25
|
+
path,
|
|
26
|
+
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW,
|
|
27
|
+
);
|
|
28
|
+
const metadata = fstatSync(descriptor);
|
|
29
|
+
if (!metadata.isFile()) {
|
|
30
|
+
closeSync(descriptor);
|
|
31
|
+
throw new Error(`replay state "${path}" is not a regular file`);
|
|
32
|
+
}
|
|
33
|
+
if ((metadata.mode & 0o777) !== 0o600) {
|
|
34
|
+
closeSync(descriptor);
|
|
35
|
+
throw new Error(`replay state "${path}" must have exact mode 0600`);
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
typeof process.getuid === 'function' &&
|
|
39
|
+
metadata.uid !== process.getuid()
|
|
40
|
+
) {
|
|
41
|
+
closeSync(descriptor);
|
|
42
|
+
throw new Error(`replay state "${path}" must be owned by the verifier`);
|
|
43
|
+
}
|
|
44
|
+
return descriptor;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertTrustedStateDirectory(path) {
|
|
48
|
+
const descriptor = openSync(
|
|
49
|
+
path,
|
|
50
|
+
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW,
|
|
51
|
+
);
|
|
52
|
+
try {
|
|
53
|
+
const metadata = fstatSync(descriptor);
|
|
54
|
+
if (!metadata.isDirectory()) {
|
|
55
|
+
throw new Error(`replay state parent "${path}" is not a directory`);
|
|
56
|
+
}
|
|
57
|
+
if ((metadata.mode & 0o777) !== 0o700) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`replay state parent "${path}" must have exact mode 0700`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
typeof process.getuid === 'function' &&
|
|
64
|
+
metadata.uid !== process.getuid()
|
|
65
|
+
) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`replay state parent "${path}" must be owned by the verifier`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
closeSync(descriptor);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeDurableReplacement(path, value) {
|
|
76
|
+
const directory = realpathSync(dirname(path));
|
|
77
|
+
const temporaryPath = join(
|
|
78
|
+
directory,
|
|
79
|
+
`.${basename(path)}.${randomUUID()}.tmp`,
|
|
80
|
+
);
|
|
81
|
+
let descriptor;
|
|
82
|
+
try {
|
|
83
|
+
descriptor = openSync(
|
|
84
|
+
temporaryPath,
|
|
85
|
+
fsConstants.O_CREAT |
|
|
86
|
+
fsConstants.O_EXCL |
|
|
87
|
+
fsConstants.O_WRONLY |
|
|
88
|
+
fsConstants.O_NOFOLLOW,
|
|
89
|
+
0o600,
|
|
90
|
+
);
|
|
91
|
+
writeFileSync(descriptor, canonicalJsonBytes(value));
|
|
92
|
+
fsyncSync(descriptor);
|
|
93
|
+
closeSync(descriptor);
|
|
94
|
+
descriptor = undefined;
|
|
95
|
+
renameSync(temporaryPath, path);
|
|
96
|
+
const directoryDescriptor = openSync(directory, fsConstants.O_RDONLY);
|
|
97
|
+
try {
|
|
98
|
+
fsyncSync(directoryDescriptor);
|
|
99
|
+
} finally {
|
|
100
|
+
closeSync(directoryDescriptor);
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
if (descriptor !== undefined) {closeSync(descriptor);}
|
|
104
|
+
removeIfPresent(temporaryPath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Remove `path`; only its absence is tolerated. */
|
|
109
|
+
function removeIfPresent(path) {
|
|
110
|
+
try {
|
|
111
|
+
unlinkSync(path);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error?.code !== 'ENOENT') {throw error;}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function verifyAndAdvanceReplayState({
|
|
118
|
+
statePath,
|
|
119
|
+
distributionId,
|
|
120
|
+
trustPolicyDigest,
|
|
121
|
+
parseReplayState,
|
|
122
|
+
verify,
|
|
123
|
+
}) {
|
|
124
|
+
if (!isAbsolute(statePath)) {
|
|
125
|
+
throw new Error('replay state path must be absolute');
|
|
126
|
+
}
|
|
127
|
+
const stateDirectory = realpathSync(dirname(statePath));
|
|
128
|
+
if (stateDirectory !== dirname(statePath)) {
|
|
129
|
+
throw new Error('replay state parent path must be canonical');
|
|
130
|
+
}
|
|
131
|
+
assertTrustedStateDirectory(stateDirectory);
|
|
132
|
+
const canonicalStatePath = join(stateDirectory, basename(statePath));
|
|
133
|
+
const lockPath = `${canonicalStatePath}.lock`;
|
|
134
|
+
let lockDescriptor;
|
|
135
|
+
try {
|
|
136
|
+
lockDescriptor = openSync(
|
|
137
|
+
lockPath,
|
|
138
|
+
fsConstants.O_CREAT |
|
|
139
|
+
fsConstants.O_EXCL |
|
|
140
|
+
fsConstants.O_WRONLY |
|
|
141
|
+
fsConstants.O_NOFOLLOW,
|
|
142
|
+
0o600,
|
|
143
|
+
);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error?.code === 'EEXIST') {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`replay state "${canonicalStatePath}" is locked by another verifier; explicit operator reconciliation is required for a stale lock`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
writeFileSync(
|
|
155
|
+
lockDescriptor,
|
|
156
|
+
`${process.pid} ${new Date().toISOString()}\n`,
|
|
157
|
+
);
|
|
158
|
+
fsyncSync(lockDescriptor);
|
|
159
|
+
const stateDescriptor = openRegularFileNoFollow(canonicalStatePath);
|
|
160
|
+
let state;
|
|
161
|
+
try {
|
|
162
|
+
const before = fstatSync(stateDescriptor);
|
|
163
|
+
if (before.size > MAX_REPLAY_STATE_BYTES) {
|
|
164
|
+
throw new Error(`replay state exceeds ${MAX_REPLAY_STATE_BYTES} bytes`);
|
|
165
|
+
}
|
|
166
|
+
const stateBytes = Buffer.alloc(before.size);
|
|
167
|
+
let offset = 0;
|
|
168
|
+
while (offset < stateBytes.length) {
|
|
169
|
+
const count = readSync(
|
|
170
|
+
stateDescriptor,
|
|
171
|
+
stateBytes,
|
|
172
|
+
offset,
|
|
173
|
+
stateBytes.length - offset,
|
|
174
|
+
offset,
|
|
175
|
+
);
|
|
176
|
+
if (count === 0) {
|
|
177
|
+
throw new Error('replay state was truncated while reading');
|
|
178
|
+
}
|
|
179
|
+
offset += count;
|
|
180
|
+
}
|
|
181
|
+
if (readSync(stateDescriptor, Buffer.alloc(1), 0, 1, offset) !== 0) {
|
|
182
|
+
throw new Error('replay state grew while reading');
|
|
183
|
+
}
|
|
184
|
+
const after = fstatSync(stateDescriptor);
|
|
185
|
+
for (const field of ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs']) {
|
|
186
|
+
if (after[field] !== before[field]) {
|
|
187
|
+
throw new Error('replay state changed while reading');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
state = parseReplayState(
|
|
191
|
+
parseCanonicalJsonBytes(stateBytes, 'distribution image replay state'),
|
|
192
|
+
);
|
|
193
|
+
} finally {
|
|
194
|
+
closeSync(stateDescriptor);
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
state.distributionId !== distributionId ||
|
|
198
|
+
state.trustPolicyDigest !== trustPolicyDigest
|
|
199
|
+
) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'replay state does not match the selected distribution and committed trust policy',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const verified = await verify(state);
|
|
206
|
+
const completeness = verified.completenessStatement.predicate;
|
|
207
|
+
if (completeness.releaseSequence <= state.lastReleaseSequence) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
'verified completeness evidence does not advance releaseSequence',
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (completeness.nonce === state.lastCompletenessNonce) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
'verified completeness evidence reuses the last accepted nonce',
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
verified.completenessStatementDigest ===
|
|
219
|
+
state.lastCompletenessStatementDigest
|
|
220
|
+
) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
'verified completeness evidence reuses the last accepted statement',
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const nextState = parseReplayState({
|
|
226
|
+
schemaVersion: 1,
|
|
227
|
+
distributionId,
|
|
228
|
+
trustPolicyDigest,
|
|
229
|
+
lastReleaseSequence: completeness.releaseSequence,
|
|
230
|
+
lastCompletenessStatementDigest: verified.completenessStatementDigest,
|
|
231
|
+
lastCompletenessNonce: completeness.nonce,
|
|
232
|
+
});
|
|
233
|
+
writeDurableReplacement(canonicalStatePath, nextState);
|
|
234
|
+
return { verified, replayState: nextState };
|
|
235
|
+
} finally {
|
|
236
|
+
try {
|
|
237
|
+
closeSync(lockDescriptor);
|
|
238
|
+
} finally {
|
|
239
|
+
unlinkSync(lockPath);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|