@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,166 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
closeSync,
|
|
5
|
+
constants as fsConstants,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const PREPARED_EXECUTABLES = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function sha256Digest(bytes) {
|
|
20
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validatePreparedExecutable(record, label) {
|
|
24
|
+
const stats = lstatSync(record.path);
|
|
25
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`prepared trusted ${label} must be a no-follow regular file`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (record.kind === 'root-owned') {
|
|
31
|
+
for (const field of ['dev', 'ino', 'uid', 'mode', 'size']) {
|
|
32
|
+
if (stats[field] !== record.snapshot[field]) {
|
|
33
|
+
throw new Error(`prepared trusted ${label} file identity changed`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let current = record.path;
|
|
37
|
+
while (true) {
|
|
38
|
+
const currentStats = lstatSync(current);
|
|
39
|
+
if (currentStats.uid !== 0 || (currentStats.mode & 0o022) !== 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${label} and every ancestor must be root-owned and not group/world writable`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (current === '/') {break;}
|
|
45
|
+
current = dirname(current);
|
|
46
|
+
}
|
|
47
|
+
if ((stats.mode & 0o777) !== 0o755) {
|
|
48
|
+
throw new Error(`prepared trusted ${label} must have exact mode 0755`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (sha256Digest(readFileSync(record.path)) !== record.digest) {
|
|
52
|
+
throw new Error(`prepared trusted ${label} bytes changed`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function prepareDigestPinnedExecutable({
|
|
57
|
+
executablePath,
|
|
58
|
+
trustedDigests,
|
|
59
|
+
label,
|
|
60
|
+
verifyVersion,
|
|
61
|
+
}) {
|
|
62
|
+
if (!isAbsolute(executablePath)) {
|
|
63
|
+
throw new Error(`${label} executable path must be absolute`);
|
|
64
|
+
}
|
|
65
|
+
const sourceStats = lstatSync(executablePath);
|
|
66
|
+
if (!sourceStats.isFile() || sourceStats.isSymbolicLink()) {
|
|
67
|
+
throw new Error(`${label} executable must be a no-follow regular file`);
|
|
68
|
+
}
|
|
69
|
+
const bytes = readFileSync(executablePath);
|
|
70
|
+
const digest = sha256Digest(bytes);
|
|
71
|
+
if (!trustedDigests.includes(digest)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`${label} executable digest ${digest} is not trusted by policy`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const directory = mkdtempSync(join(tmpdir(), `xema-trusted-${label}-`));
|
|
77
|
+
const privatePath = join(directory, basename(executablePath));
|
|
78
|
+
try {
|
|
79
|
+
let descriptor;
|
|
80
|
+
try {
|
|
81
|
+
descriptor = openSync(
|
|
82
|
+
privatePath,
|
|
83
|
+
fsConstants.O_CREAT |
|
|
84
|
+
fsConstants.O_EXCL |
|
|
85
|
+
fsConstants.O_WRONLY |
|
|
86
|
+
fsConstants.O_NOFOLLOW,
|
|
87
|
+
0o500,
|
|
88
|
+
);
|
|
89
|
+
writeFileSync(descriptor, bytes);
|
|
90
|
+
fsyncSync(descriptor);
|
|
91
|
+
} finally {
|
|
92
|
+
if (descriptor !== undefined) {closeSync(descriptor);}
|
|
93
|
+
}
|
|
94
|
+
verifyVersion(privatePath);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
rmSync(directory, { recursive: true, force: true });
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const handle = {
|
|
100
|
+
path: privatePath,
|
|
101
|
+
dispose() {
|
|
102
|
+
rmSync(directory, { recursive: true, force: true });
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
PREPARED_EXECUTABLES.set(handle, {
|
|
106
|
+
kind: 'private-copy',
|
|
107
|
+
path: privatePath,
|
|
108
|
+
digest,
|
|
109
|
+
});
|
|
110
|
+
return handle;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function prepareRootOwnedPinnedExecutable({
|
|
114
|
+
executablePath,
|
|
115
|
+
trustedDigests,
|
|
116
|
+
label,
|
|
117
|
+
verifyVersion,
|
|
118
|
+
}) {
|
|
119
|
+
if (!isAbsolute(executablePath)) {
|
|
120
|
+
throw new Error(`${label} executable path must be absolute`);
|
|
121
|
+
}
|
|
122
|
+
const initialStats = lstatSync(executablePath);
|
|
123
|
+
if (!initialStats.isFile() || initialStats.isSymbolicLink()) {
|
|
124
|
+
throw new Error(`${label} executable must be a no-follow regular file`);
|
|
125
|
+
}
|
|
126
|
+
const handle = { path: executablePath, dispose() {} };
|
|
127
|
+
const record = {
|
|
128
|
+
kind: 'root-owned',
|
|
129
|
+
path: executablePath,
|
|
130
|
+
digest: sha256Digest(readFileSync(executablePath)),
|
|
131
|
+
snapshot: {
|
|
132
|
+
dev: initialStats.dev,
|
|
133
|
+
ino: initialStats.ino,
|
|
134
|
+
uid: initialStats.uid,
|
|
135
|
+
mode: initialStats.mode,
|
|
136
|
+
size: initialStats.size,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
PREPARED_EXECUTABLES.set(handle, record);
|
|
140
|
+
validatePreparedExecutable(record, label);
|
|
141
|
+
if (!trustedDigests.includes(record.digest)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`${label} executable digest ${record.digest} is not trusted by policy`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
verifyVersion(executablePath);
|
|
147
|
+
validatePreparedExecutable(record, label);
|
|
148
|
+
return handle;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function assertPreparedExecutable(handle, label) {
|
|
152
|
+
const record = PREPARED_EXECUTABLES.get(handle);
|
|
153
|
+
if (!record) {
|
|
154
|
+
throw new Error(`a prepared trusted ${label} executable is required`);
|
|
155
|
+
}
|
|
156
|
+
validatePreparedExecutable(record, label);
|
|
157
|
+
return record.path;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function exactExecutableOutput(path, args, options = {}) {
|
|
161
|
+
return execFileSync(path, args, {
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
164
|
+
...options,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// What the running evidence verifier is held to before it trusts anything:
|
|
2
|
+
// a clean launch environment, an install nobody else can write, and the
|
|
3
|
+
// data-root pin of its own bytes. (The kernel the workspace states is held to
|
|
4
|
+
// the bundled edition protocol by `edition-protocol.mjs`.)
|
|
5
|
+
|
|
6
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { assertProtectedPathAncestry } from './evidence-checkout.mjs';
|
|
10
|
+
import { VERIFIER_PROVENANCE_FILE } from './verifier-files.mjs';
|
|
11
|
+
import { parseVerifierProvenance } from './verifier-provenance.mjs';
|
|
12
|
+
|
|
13
|
+
/** The launch environment carries no variable that changes what code runs. */
|
|
14
|
+
export function assertSanitizedOperatorEnvironment(env) {
|
|
15
|
+
for (const key of Object.keys(env)) {
|
|
16
|
+
if (
|
|
17
|
+
key === 'NODE_OPTIONS' ||
|
|
18
|
+
key === 'NODE_PATH' ||
|
|
19
|
+
key.startsWith('DYLD_') ||
|
|
20
|
+
key.startsWith('LD_') ||
|
|
21
|
+
key.startsWith('GIT_')
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`trusted verifier launch refuses inherited control variable ${key}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The verifier file that is running (the realpath of `bundlePath`) and its
|
|
32
|
+
* provenance beside it, after refusing an install anyone else can write or
|
|
33
|
+
* that a foreign user owns, and a provenance that does not describe this
|
|
34
|
+
* bundle. Returns the bytes the pin is compared with.
|
|
35
|
+
*/
|
|
36
|
+
export function readRunningVerifier(bundlePath) {
|
|
37
|
+
const realBundle = realpathSync(bundlePath);
|
|
38
|
+
assertProtectedPathAncestry(realBundle, 'the evidence verifier install');
|
|
39
|
+
const bundleBytes = readFileSync(realBundle);
|
|
40
|
+
const provenanceBytes = readFileSync(
|
|
41
|
+
join(dirname(realBundle), VERIFIER_PROVENANCE_FILE),
|
|
42
|
+
);
|
|
43
|
+
parseVerifierProvenance({ provenanceBytes, bundleBytes });
|
|
44
|
+
return { bundleBytes, provenanceBytes };
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// verifier-digest-pin — whether the running evidence verifier is held to the trust pin's recorded verifier digests. One
|
|
2
|
+
// predicate, used by every site that compares the verifier's digests with the pin's.
|
|
3
|
+
|
|
4
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether the verifier on `channel` must be byte-identical to the one the trust pin records. False only on the develop
|
|
8
|
+
* channel: a deployment we manage installs the moving develop CLI, and the running CLI is trusted as installed
|
|
9
|
+
* (TEAM-159), so no pin of its identity is kept. Only the pinned-digest equality is lifted; the policy digest, the
|
|
10
|
+
* kernel contract authority, the third-party source pins and the evidence signatures are checked on every channel.
|
|
11
|
+
*/
|
|
12
|
+
export function isVerifierDigestPinned(channel) {
|
|
13
|
+
if (!Object.values(EditionChannel).includes(channel)) {
|
|
14
|
+
throw new Error(`unknown edition channel ${JSON.stringify(channel)}`);
|
|
15
|
+
}
|
|
16
|
+
return channel !== EditionChannel.Develop;
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The names and place of the built evidence verifier. Pure constants, shared by
|
|
2
|
+
// the build, the gate and the verifier itself (which is bundled and so must not
|
|
3
|
+
// derive anything from where it sits).
|
|
4
|
+
|
|
5
|
+
export const VERIFIER_BUNDLE_FILE = 'verify-image-evidence.bundle.mjs';
|
|
6
|
+
export const VERIFIER_PROVENANCE_FILE =
|
|
7
|
+
'verify-image-evidence.bundle.provenance.json';
|
|
8
|
+
export const VERIFIER_DIST_SUBPATH = Object.freeze(['dist', 'edition']);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
2
|
+
import { kernelPackageDir } from './edition-workspace.mjs';
|
|
3
|
+
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The ONE place the evidence verifier obtains the kernel distribution schemas,
|
|
7
|
+
* and the edition protocol they implement.
|
|
8
|
+
*
|
|
9
|
+
* From source it loads the built kernel the workspace states with `--kernel`,
|
|
10
|
+
* whose protocol is then trivially the stated one. The verifier build
|
|
11
|
+
* (`scripts/build-evidence-verifier.mjs`) replaces this module with the
|
|
12
|
+
* `@xemahq/contracts` distribution schemas and edition protocol identity
|
|
13
|
+
* bundled from the CLI's own dependency, so the bundle never executes a schema
|
|
14
|
+
* from outside itself; the entry then holds `--kernel`'s declared protocol
|
|
15
|
+
* equal to the bundled one.
|
|
16
|
+
*/
|
|
17
|
+
export async function loadVerifierKernelSchemas() {
|
|
18
|
+
return {
|
|
19
|
+
schemas: await loadKernelDistributionSchemas(),
|
|
20
|
+
editionProtocol: assertKernelEditionProtocol(kernelPackageDir()),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The provenance record written beside the evidence verifier bundle, and the
|
|
2
|
+
// one reader of it. The build writes it (`scripts/build-evidence-verifier.mjs`);
|
|
3
|
+
// the data root pins its digest beside the bundle's.
|
|
4
|
+
//
|
|
5
|
+
// Deterministic by construction: no timestamps, no host paths. Every bundled
|
|
6
|
+
// input is {package, version, path relative to ITS package root, sha256}, so the
|
|
7
|
+
// record does not depend on how the dependencies were installed.
|
|
8
|
+
|
|
9
|
+
import { sha256Digest } from './image-evidence.mjs';
|
|
10
|
+
import { compareStrings } from './string-order.mjs';
|
|
11
|
+
|
|
12
|
+
export const VERIFIER_PROVENANCE_SCHEMA_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
const PROVENANCE_KEYS = Object.freeze([
|
|
15
|
+
'artifactSha256',
|
|
16
|
+
'esbuild',
|
|
17
|
+
'inputs',
|
|
18
|
+
'schemaVersion',
|
|
19
|
+
]);
|
|
20
|
+
const INPUT_KEYS = Object.freeze(['package', 'path', 'sha256', 'version']);
|
|
21
|
+
const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
22
|
+
|
|
23
|
+
const exactKeys = (value, keys) =>
|
|
24
|
+
value !== null &&
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
!Array.isArray(value) &&
|
|
27
|
+
Object.keys(value).sort().join() === keys.join();
|
|
28
|
+
|
|
29
|
+
/** Total order of inputs: package, then path. */
|
|
30
|
+
export const compareProvenanceInputs = (a, b) =>
|
|
31
|
+
compareStrings(a.package, b.package) || compareStrings(a.path, b.path);
|
|
32
|
+
|
|
33
|
+
/** Parse provenance bytes strictly, binding them to the bundle they describe. */
|
|
34
|
+
export function parseVerifierProvenance({ provenanceBytes, bundleBytes }) {
|
|
35
|
+
const provenance = JSON.parse(provenanceBytes.toString('utf8'));
|
|
36
|
+
if (
|
|
37
|
+
!exactKeys(provenance, PROVENANCE_KEYS) ||
|
|
38
|
+
provenance.schemaVersion !== VERIFIER_PROVENANCE_SCHEMA_VERSION ||
|
|
39
|
+
!exactKeys(provenance.esbuild, ['version']) ||
|
|
40
|
+
typeof provenance.esbuild.version !== 'string' ||
|
|
41
|
+
!Array.isArray(provenance.inputs) ||
|
|
42
|
+
provenance.inputs.length === 0
|
|
43
|
+
) {
|
|
44
|
+
throw new Error('verifier provenance has an invalid structure');
|
|
45
|
+
}
|
|
46
|
+
provenance.inputs.forEach((input, index) => {
|
|
47
|
+
if (
|
|
48
|
+
!exactKeys(input, INPUT_KEYS) ||
|
|
49
|
+
INPUT_KEYS.some((key) => typeof input[key] !== 'string' || input[key] === '') ||
|
|
50
|
+
!SHA256_DIGEST.test(input.sha256) ||
|
|
51
|
+
input.path.startsWith('/') ||
|
|
52
|
+
input.path.split('/').includes('..') ||
|
|
53
|
+
(index > 0 && compareProvenanceInputs(provenance.inputs[index - 1], input) >= 0)
|
|
54
|
+
) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`verifier provenance input ${index} is malformed or out of order`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (provenance.artifactSha256 !== sha256Digest(bundleBytes)) {
|
|
61
|
+
throw new Error('verifier provenance does not describe this bundle');
|
|
62
|
+
}
|
|
63
|
+
return provenance;
|
|
64
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// verify-catalogue-evidence — the `catalogue-evidence` mode of the evidence verifier bundle.
|
|
2
|
+
//
|
|
3
|
+
// <bundle> catalogue-evidence arming --channel <release|develop> --data-root <dir>
|
|
4
|
+
// <bundle> catalogue-evidence verify --channel <c> --data-root <dir> --evidence-root <abs dir>
|
|
5
|
+
// --statement-digest <sha256:..> --bundle-digest <sha256:..> --workflow-sha <40hex> --offering-id <id>
|
|
6
|
+
// --subject-digest <sha256:..> --catalogue-layer-digest <sha256:..> --lock-layer-digest <sha256:..>
|
|
7
|
+
// --cosign <abs path>
|
|
8
|
+
//
|
|
9
|
+
// A customer verifies the E4 statement of a release catalogue BEFORE the catalogue's kernel is adopted, so there is no
|
|
10
|
+
// kernel package to load schemas from yet. The only kernel-dependent parts of the judgement are the policy schema
|
|
11
|
+
// (parseSigstoreOfflineTrustPolicy) and the predicate-type/channel constants; before adoption they come from the pinned
|
|
12
|
+
// bundle's own embedded kernel, through `loadVerifierKernelSchemas` and nowhere else. This mode never takes `--kernel`.
|
|
13
|
+
//
|
|
14
|
+
// The anchors are the customer's pinned policy and the pinned verifier itself (the bundle refuses to run unless its own
|
|
15
|
+
// bytes are the ones the data-root pin records). There is NO git grading in this mode: it reads no checkout of an editions
|
|
16
|
+
// root, only the declared policy and pin of the data root, both of which the pin binds by digest.
|
|
17
|
+
//
|
|
18
|
+
// ONE arming rule (trust-policy-arming.mjs): the policy must be ARMED on the channel with the E4 row required
|
|
19
|
+
// (`consumesCatalogue: true`); `arming` stops there. `verify` then holds the statement to that policy and to the exact
|
|
20
|
+
// manifest, catalogue layer and lock layer the caller read, with the kernel's own `editionCatalogueRefusal`.
|
|
21
|
+
|
|
22
|
+
import { editionWorkspaceConfig, resetEditionWorkspaceConfig } from './edition-config.mjs';
|
|
23
|
+
import { EDITION_CONTRACT } from './edition-constants.mjs';
|
|
24
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot } from './edition-workspace.mjs';
|
|
25
|
+
import {
|
|
26
|
+
assertPinnedPolicy,
|
|
27
|
+
assertPinnedThirdPartySourcePins,
|
|
28
|
+
assertPinnedVerifier,
|
|
29
|
+
loadDeclaredTrustPolicy,
|
|
30
|
+
readEvidenceTrustPin,
|
|
31
|
+
verifierDigests,
|
|
32
|
+
} from './evidence-trust-pin.mjs';
|
|
33
|
+
import { prepareTrustedCosignBinary, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
|
|
34
|
+
import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
|
|
35
|
+
import { TrustPolicyArmingState, assessTrustPolicyArming } from './trust-policy-arming.mjs';
|
|
36
|
+
import { assertSanitizedOperatorEnvironment, readRunningVerifier } from './verifier-binding.mjs';
|
|
37
|
+
import { loadVerifierKernelSchemas } from './verifier-kernel-schemas.mjs';
|
|
38
|
+
|
|
39
|
+
/** The first argument that selects this mode of the bundle. */
|
|
40
|
+
export const CATALOGUE_EVIDENCE_MODE = 'catalogue-evidence';
|
|
41
|
+
export const CATALOGUE_EVIDENCE_USAGE =
|
|
42
|
+
'usage: catalogue-evidence arming --channel <release|develop> --data-root <dir> | ' +
|
|
43
|
+
'catalogue-evidence verify --channel <c> --data-root <dir> --evidence-root <abs dir> --statement-digest <sha256:..> ' +
|
|
44
|
+
'--bundle-digest <sha256:..> --workflow-sha <40-hex> --offering-id <id> --subject-digest <sha256:..> ' +
|
|
45
|
+
'--catalogue-layer-digest <sha256:..> --lock-layer-digest <sha256:..> --cosign <abs path>';
|
|
46
|
+
|
|
47
|
+
export const CATALOGUE_EVIDENCE_STATEMENT_FILE = 'statement.json';
|
|
48
|
+
export const CATALOGUE_EVIDENCE_BUNDLE_FILE = 'bundle.sigstore.json';
|
|
49
|
+
/** The code of the refusal that the data root's policy is not armed (also the code of the library's error). */
|
|
50
|
+
export const CATALOGUE_EVIDENCE_UNARMED_CODE = EDITION_CONTRACT.evidenceTrust.refusal.code;
|
|
51
|
+
|
|
52
|
+
export const CatalogueEvidenceRefusalCode = Object.freeze({
|
|
53
|
+
SubjectMismatch: 'catalogue_evidence_subject_mismatch',
|
|
54
|
+
OfferingMismatch: 'catalogue_evidence_offering_mismatch',
|
|
55
|
+
LockDigestMismatch: 'catalogue_evidence_lock_digest_mismatch',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
59
|
+
const COMMIT = /^[a-f0-9]{40}$/;
|
|
60
|
+
const ARMING_FLAGS = Object.freeze(['--channel']);
|
|
61
|
+
const VERIFY_FLAGS = Object.freeze([
|
|
62
|
+
'--channel',
|
|
63
|
+
'--evidence-root',
|
|
64
|
+
'--statement-digest',
|
|
65
|
+
'--bundle-digest',
|
|
66
|
+
'--workflow-sha',
|
|
67
|
+
'--offering-id',
|
|
68
|
+
'--subject-digest',
|
|
69
|
+
'--catalogue-layer-digest',
|
|
70
|
+
'--lock-layer-digest',
|
|
71
|
+
'--cosign',
|
|
72
|
+
]);
|
|
73
|
+
const ABSOLUTE_FLAGS = Object.freeze(['--evidence-root', '--cosign']);
|
|
74
|
+
const DIGEST_FLAGS = Object.freeze(['--statement-digest', '--bundle-digest', '--subject-digest', '--catalogue-layer-digest', '--lock-layer-digest']);
|
|
75
|
+
|
|
76
|
+
/** A bad command line: exit 2. */
|
|
77
|
+
export class CatalogueEvidenceUsageError extends Error {
|
|
78
|
+
constructor(message) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = 'CatalogueEvidenceUsageError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The data root's policy is not armed (or declared) for a catalogue consumer on the channel. Carries the stable code. */
|
|
85
|
+
export class CatalogueEvidenceUnarmedError extends Error {
|
|
86
|
+
constructor(message) {
|
|
87
|
+
super(`${CATALOGUE_EVIDENCE_UNARMED_CODE}: ${message}`);
|
|
88
|
+
this.name = 'EvidenceTrustRequiredError';
|
|
89
|
+
this.code = CATALOGUE_EVIDENCE_UNARMED_CODE;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The statement does not bind what the caller read; `code` is the refusal's stable name. */
|
|
94
|
+
export class CatalogueEvidenceRefusal extends Error {
|
|
95
|
+
constructor(code, message) {
|
|
96
|
+
super(`${code}: ${message}`);
|
|
97
|
+
this.name = 'CatalogueEvidenceRefusal';
|
|
98
|
+
this.code = code;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hexOf = (digest) => digest.slice('sha256:'.length);
|
|
103
|
+
|
|
104
|
+
function parseFlags(args, allowed) {
|
|
105
|
+
const flags = new Map();
|
|
106
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
107
|
+
const flag = args[index];
|
|
108
|
+
const value = args[index + 1];
|
|
109
|
+
if (typeof flag !== 'string' || !allowed.includes(flag)) {
|
|
110
|
+
throw new CatalogueEvidenceUsageError(`unknown or unexpected argument ${JSON.stringify(flag)}`);
|
|
111
|
+
}
|
|
112
|
+
if (value === undefined || value.startsWith('--')) {throw new CatalogueEvidenceUsageError(`${flag} requires a value`);}
|
|
113
|
+
if (flags.has(flag)) {throw new CatalogueEvidenceUsageError(`${flag} was given twice`);}
|
|
114
|
+
flags.set(flag, value);
|
|
115
|
+
}
|
|
116
|
+
for (const flag of allowed) {
|
|
117
|
+
if (!flags.has(flag)) {throw new CatalogueEvidenceUsageError(`${flag} is required`);}
|
|
118
|
+
}
|
|
119
|
+
for (const flag of ABSOLUTE_FLAGS.filter((candidate) => flags.has(candidate))) {
|
|
120
|
+
if (!flags.get(flag).startsWith('/')) {throw new CatalogueEvidenceUsageError(`${flag} must be an absolute path`);}
|
|
121
|
+
}
|
|
122
|
+
for (const flag of DIGEST_FLAGS.filter((candidate) => flags.has(candidate))) {
|
|
123
|
+
if (!SHA256_DIGEST.test(flags.get(flag))) {throw new CatalogueEvidenceUsageError(`${flag} must be sha256:<64 lowercase hex>`);}
|
|
124
|
+
}
|
|
125
|
+
if (flags.has('--workflow-sha') && !COMMIT.test(flags.get('--workflow-sha'))) {
|
|
126
|
+
throw new CatalogueEvidenceUsageError('--workflow-sha must be a 40-character lowercase hexadecimal git commit SHA');
|
|
127
|
+
}
|
|
128
|
+
return flags;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The anchors, then the ONE arming rule: the sanitized launch, the running verifier, the declared policy held to the
|
|
133
|
+
* data-root pin of both the policy and THIS verifier, and the policy ARMED on `channel` with the E4 row required.
|
|
134
|
+
*/
|
|
135
|
+
async function armedAnchors({ channel, env, bundlePath }) {
|
|
136
|
+
assertSanitizedOperatorEnvironment(env);
|
|
137
|
+
const running = readRunningVerifier(bundlePath);
|
|
138
|
+
const { schemas } = await loadVerifierKernelSchemas();
|
|
139
|
+
resetEditionWorkspaceConfig();
|
|
140
|
+
const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
|
|
141
|
+
if (declaration.policy === null) {
|
|
142
|
+
throw new CatalogueEvidenceUnarmedError(`${dataRoot()} declares no evidenceTrustPolicy in edition-workspace.json`);
|
|
143
|
+
}
|
|
144
|
+
const { policy, policyDigest } = loadDeclaredTrustPolicy({ dataRoot: dataRoot(), declaration, schemas });
|
|
145
|
+
const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
|
|
146
|
+
assertPinnedPolicy({ pin, policyDigest });
|
|
147
|
+
assertPinnedThirdPartySourcePins({ pin, dataRoot: dataRoot() });
|
|
148
|
+
assertPinnedVerifier({ pin, actual: verifierDigests(running), channel });
|
|
149
|
+
const { state, unmet } = assessTrustPolicyArming(policy, schemas, channel, { consumesCatalogue: true });
|
|
150
|
+
if (state !== TrustPolicyArmingState.ARMED) {
|
|
151
|
+
throw new CatalogueEvidenceUnarmedError(
|
|
152
|
+
`the declared policy is ${state.toUpperCase()} on the ${channel} channel for a catalogue consumer: ${unmet.join('; ')}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return { schemas, policy };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Run the mode. `argv` is the bundle's arguments (`['catalogue-evidence', '<phase>', ...]`). Resolves to the JSON line to
|
|
160
|
+
* print on success; rejects with a named error otherwise.
|
|
161
|
+
*/
|
|
162
|
+
export async function runCatalogueEvidence({ argv, env, bundlePath }) {
|
|
163
|
+
const [mode, phase, ...tail] = argv;
|
|
164
|
+
if (mode !== CATALOGUE_EVIDENCE_MODE || (phase !== 'arming' && phase !== 'verify')) {
|
|
165
|
+
throw new CatalogueEvidenceUsageError(CATALOGUE_EVIDENCE_USAGE);
|
|
166
|
+
}
|
|
167
|
+
// `--kernel` is no root of this mode: the schemas are the bundle's own.
|
|
168
|
+
let rest;
|
|
169
|
+
try {
|
|
170
|
+
rest = configureEditionWorkspaceFromArgv(tail, [WorkspaceRoot.Data], { refuseOthers: true });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new CatalogueEvidenceUsageError(error.message);
|
|
173
|
+
}
|
|
174
|
+
const flags = parseFlags(rest, phase === 'arming' ? ARMING_FLAGS : VERIFY_FLAGS);
|
|
175
|
+
const channel = flags.get('--channel');
|
|
176
|
+
const { schemas, policy } = await armedAnchors({ channel, env, bundlePath });
|
|
177
|
+
if (phase === 'arming') {
|
|
178
|
+
return `${JSON.stringify({ channel, state: TrustPolicyArmingState.ARMED })}\n`;
|
|
179
|
+
}
|
|
180
|
+
const offeringId = flags.get('--offering-id');
|
|
181
|
+
const descriptor = {
|
|
182
|
+
statementPath: CATALOGUE_EVIDENCE_STATEMENT_FILE,
|
|
183
|
+
statementDigest: flags.get('--statement-digest'),
|
|
184
|
+
bundlePath: CATALOGUE_EVIDENCE_BUNDLE_FILE,
|
|
185
|
+
bundleDigest: flags.get('--bundle-digest'),
|
|
186
|
+
bundleMediaType: BUNDLE_MEDIA_TYPE,
|
|
187
|
+
};
|
|
188
|
+
const trustedCosign = prepareTrustedCosignBinary({ cosignBinary: flags.get('--cosign'), policy });
|
|
189
|
+
let statement;
|
|
190
|
+
try {
|
|
191
|
+
({ statement } = verifySigstoreEvidenceDescriptor({
|
|
192
|
+
evidenceRoot: flags.get('--evidence-root'),
|
|
193
|
+
descriptor,
|
|
194
|
+
policy,
|
|
195
|
+
predicateType: schemas.EDITION_CATALOGUE_PREDICATE_TYPE,
|
|
196
|
+
channel,
|
|
197
|
+
expectedWorkflowSha: flags.get('--workflow-sha'),
|
|
198
|
+
parseStatement: schemas.parseEditionCatalogueStatement,
|
|
199
|
+
policyRoot: dataRoot(),
|
|
200
|
+
trustedCosign,
|
|
201
|
+
}));
|
|
202
|
+
} finally {
|
|
203
|
+
trustedCosign.dispose();
|
|
204
|
+
}
|
|
205
|
+
const [subject] = statement.subject;
|
|
206
|
+
const expectedSubject = `xema-edition-${offeringId}`;
|
|
207
|
+
if (subject.digest.sha256 !== hexOf(flags.get('--subject-digest')) || subject.name !== expectedSubject) {
|
|
208
|
+
throw new CatalogueEvidenceRefusal(
|
|
209
|
+
CatalogueEvidenceRefusalCode.SubjectMismatch,
|
|
210
|
+
`the statement is over ${subject.name}@sha256:${subject.digest.sha256}, not ${expectedSubject}@${flags.get('--subject-digest')}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const refusal = schemas.editionCatalogueRefusal(statement.predicate, {
|
|
214
|
+
channel,
|
|
215
|
+
catalogueLayerDigest: flags.get('--catalogue-layer-digest'),
|
|
216
|
+
});
|
|
217
|
+
if (refusal !== undefined) {
|
|
218
|
+
throw new CatalogueEvidenceRefusal(
|
|
219
|
+
refusal,
|
|
220
|
+
`the statement states channel ${statement.predicate.channel} and catalogue ${statement.predicate.catalogueDigest}; ` +
|
|
221
|
+
`this catalogue is on ${channel} with layer ${flags.get('--catalogue-layer-digest')}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (statement.predicate.offeringId !== offeringId) {
|
|
225
|
+
throw new CatalogueEvidenceRefusal(
|
|
226
|
+
CatalogueEvidenceRefusalCode.OfferingMismatch,
|
|
227
|
+
`the statement is for offering ${statement.predicate.offeringId}, not ${offeringId}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (statement.predicate.lockDigest !== hexOf(flags.get('--lock-layer-digest'))) {
|
|
231
|
+
throw new CatalogueEvidenceRefusal(
|
|
232
|
+
CatalogueEvidenceRefusalCode.LockDigestMismatch,
|
|
233
|
+
`the statement states lock digest ${statement.predicate.lockDigest}, not that of the lock layer ${flags.get('--lock-layer-digest')}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return `${JSON.stringify({ verified: true, offeringId, channel, statementDigest: descriptor.statementDigest })}\n`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Run the mode as the bundle's entry: print its line, or the named refusal on stderr with exit 1 (usage: 2). */
|
|
240
|
+
export async function runCatalogueEvidenceEntry({ argv, env, bundlePath }) {
|
|
241
|
+
try {
|
|
242
|
+
process.stdout.write(await runCatalogueEvidence({ argv, env, bundlePath }));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
process.stderr.write(`catalogue-evidence: ${error.name}: ${error.message}\n`);
|
|
245
|
+
process.exitCode = error instanceof CatalogueEvidenceUsageError ? 2 : 1;
|
|
246
|
+
}
|
|
247
|
+
}
|