@xemahq/xema 0.25.1 → 0.27.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/README.md +70 -37
- package/dist/assets/appliance-base/images.lock.json +126 -126
- package/dist/assets/appliance-lean/images.lock.json +123 -123
- package/dist/assets/cloud/images.lock.json +126 -126
- package/dist/assets/core/images.lock.json +120 -120
- package/dist/assets/distribution-assets.manifest.json +15 -16
- package/dist/assets/minimal/images.lock.json +108 -108
- package/dist/assets/oss/images.lock.json +126 -126
- package/dist/assets/starter/images.lock.json +123 -123
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +6 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
- package/dist/runtime/backing-registration.d.ts.map +1 -1
- package/dist/runtime/backing-registration.js +7 -2
- package/dist/runtime/backing-registration.js.map +1 -1
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +3 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +20 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +3 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +37 -8
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +3 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +2 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +315 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +270 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +207 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +23 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +176 -0
- package/edition/evidence-checkout.mjs +418 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +256 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1225 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3018 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +99 -0
- package/edition/release-edition-artifact.mjs +158 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -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-image-evidence.mjs +430 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +18 -7
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -154
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The image-source producer statement layer: build the canonical statement
|
|
3
|
+
// bytes a producing workflow signs, and accept the signed bundles back.
|
|
4
|
+
//
|
|
5
|
+
// `xema edition evidence statement producer stage|accept`. This module never
|
|
6
|
+
// signs: it holds no key and requests no OIDC token. `stage` writes the exact
|
|
7
|
+
// bytes to sign and prints the signing plan; the caller runs
|
|
8
|
+
// `cosign attest-blob`; `accept` re-derives the statements from the same
|
|
9
|
+
// input, refuses staged bytes that differ, validates each bundle, verifies it
|
|
10
|
+
// with cosign against the declared signer, and seals the evidence directory.
|
|
11
|
+
//
|
|
12
|
+
// The kernel contract authority every statement binds is the one the data
|
|
13
|
+
// root's evidence-trust pin records (`evidence-trust-pin.mjs`): the data root's
|
|
14
|
+
// `runtime-image-source-index.json` states it, and it is used only once it is
|
|
15
|
+
// the pinned one. The verifier holds producer statements to the same pin.
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
29
|
+
import {
|
|
30
|
+
WorkspaceRoot,
|
|
31
|
+
configureEditionWorkspaceFromArgv,
|
|
32
|
+
dataRoot,
|
|
33
|
+
isEntryModule,
|
|
34
|
+
kernelDistPath,
|
|
35
|
+
kernelRepoDir,
|
|
36
|
+
} from './edition-workspace.mjs';
|
|
37
|
+
import { COMMIT_PATTERN, GITHUB_REF_PATTERN, GITHUB_REPOSITORY_PATTERN, SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
38
|
+
import {
|
|
39
|
+
RUNTIME_IMAGE_SOURCE_INDEX_FILE,
|
|
40
|
+
loadPinnedDeclaredTrustPolicy,
|
|
41
|
+
readEvidenceTrustPin,
|
|
42
|
+
readPinnedKernelContractAuthority,
|
|
43
|
+
} from './evidence-trust-pin.mjs';
|
|
44
|
+
import {
|
|
45
|
+
canonicalJsonBytes,
|
|
46
|
+
decodeExactUtf8,
|
|
47
|
+
parseCanonicalJsonBytes,
|
|
48
|
+
sha256Digest,
|
|
49
|
+
} from './image-evidence.mjs';
|
|
50
|
+
import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
|
|
51
|
+
import { serializeJson } from './lib.mjs';
|
|
52
|
+
import { assertStagedExactly, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
|
|
53
|
+
|
|
54
|
+
export const BUNDLE_MEDIA_TYPE =
|
|
55
|
+
'application/vnd.dev.sigstore.bundle.v0.3+json';
|
|
56
|
+
const STAGED_STATEMENTS_DIRECTORY = 'statements';
|
|
57
|
+
const STAGED_BUNDLES_DIRECTORY = 'bundles';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Git, run under a fully sandboxed environment: no user/system config, no
|
|
61
|
+
* replace-objects rewriting, no credential prompt, no locale-dependent output.
|
|
62
|
+
*
|
|
63
|
+
* Exported because the producer-input assembler runs git against the SAME
|
|
64
|
+
* checkouts under the SAME guarantees — a second copy of this environment is a
|
|
65
|
+
* second security posture, and the two would drift silently (the assembler's
|
|
66
|
+
* copy WAS byte-for-byte identical, which is exactly how nobody noticed).
|
|
67
|
+
*/
|
|
68
|
+
export function sandboxedGit(repositoryRoot, args) {
|
|
69
|
+
return execFileSync('/usr/bin/git', ['-C', repositoryRoot, ...args], {
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
env: {
|
|
72
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
73
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
74
|
+
GIT_NO_REPLACE_OBJECTS: '1',
|
|
75
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
76
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
77
|
+
LANG: 'C',
|
|
78
|
+
LC_ALL: 'C',
|
|
79
|
+
PATH: '/usr/bin:/bin',
|
|
80
|
+
},
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
}).trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The data root's runtime image source index, unparsed: where the authority is read before any schema is loaded. */
|
|
86
|
+
function readRawContractAuthority(root) {
|
|
87
|
+
if (!isAbsolute(root)) {
|
|
88
|
+
throw new Error('the data root must be an absolute path');
|
|
89
|
+
}
|
|
90
|
+
const authority = JSON.parse(
|
|
91
|
+
readFileSync(join(root, RUNTIME_IMAGE_SOURCE_INDEX_FILE), 'utf8'),
|
|
92
|
+
)?.contractAuthority;
|
|
93
|
+
if (
|
|
94
|
+
typeof authority?.repositoryUrl !== 'string' ||
|
|
95
|
+
!COMMIT_PATTERN.test(authority?.sourceCommit ?? '')
|
|
96
|
+
) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`${RUNTIME_IMAGE_SOURCE_INDEX_FILE} at ${root} carries no kernel contract authority`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return authority;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The kernel contract authority evidence of a data root is held to: the one its
|
|
106
|
+
* evidence-trust pin records. The runtime image source index states it; a
|
|
107
|
+
* stated authority that is not the pinned one, or a data root with no pin, is
|
|
108
|
+
* refused (`KernelContractAuthorityPinError`). `declaration` is the data root's
|
|
109
|
+
* `evidenceTrustPolicy`; it defaults to the configured workspace's.
|
|
110
|
+
*/
|
|
111
|
+
export function readPinnedContractAuthority({
|
|
112
|
+
dataRoot: root,
|
|
113
|
+
schemas,
|
|
114
|
+
declaration = editionWorkspaceConfig().evidenceTrustPolicy,
|
|
115
|
+
}) {
|
|
116
|
+
return readPinnedKernelContractAuthority({ dataRoot: root, declaration, schemas });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The kernel schemas, loaded from a kernel checkout that is clean, has the
|
|
121
|
+
* authority's repository as its origin and is at exactly the PINNED authority
|
|
122
|
+
* commit. The pin is read before any kernel code is imported, so a checkout at
|
|
123
|
+
* a commit the data root did not pin is never loaded. Returns the schemas and
|
|
124
|
+
* the pinned authority they were held to.
|
|
125
|
+
*/
|
|
126
|
+
export async function loadProducerKernelSchemas({
|
|
127
|
+
dataRoot: root,
|
|
128
|
+
kernelRepositoryDirectory = kernelRepoDir(),
|
|
129
|
+
kernelDistributionModule = kernelDistPath('distribution', 'index.js'),
|
|
130
|
+
declaration = editionWorkspaceConfig().evidenceTrustPolicy,
|
|
131
|
+
}) {
|
|
132
|
+
if (declaration.policy === null) {
|
|
133
|
+
// The coded refusal of a data root with no pin.
|
|
134
|
+
readPinnedKernelContractAuthority({ dataRoot: root, declaration, schemas: {} });
|
|
135
|
+
}
|
|
136
|
+
const expected = readRawContractAuthority(root);
|
|
137
|
+
const pinned = readEvidenceTrustPin({ dataRoot: root, declaration }).kernelContractAuthority;
|
|
138
|
+
if (expected.sourceCommit !== pinned.sourceCommit) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${RUNTIME_IMAGE_SOURCE_INDEX_FILE} at ${root} states kernel contract authority commit ` +
|
|
141
|
+
`${expected.sourceCommit} but the evidence-trust pin records ${pinned.sourceCommit}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!isAbsolute(kernelRepositoryDirectory)) {
|
|
145
|
+
throw new Error('the kernel contracts checkout must be an absolute path');
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
sandboxedGit(kernelRepositoryDirectory, ['remote', 'get-url', 'origin']) !==
|
|
149
|
+
expected.repositoryUrl ||
|
|
150
|
+
sandboxedGit(kernelRepositoryDirectory, ['rev-parse', '--verify', 'HEAD^{commit}']) !==
|
|
151
|
+
expected.sourceCommit ||
|
|
152
|
+
sandboxedGit(kernelRepositoryDirectory, [
|
|
153
|
+
'status',
|
|
154
|
+
'--porcelain=v1',
|
|
155
|
+
'--untracked-files=all',
|
|
156
|
+
]) !== ''
|
|
157
|
+
) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'kernel contracts checkout must be clean, canonical, and at the evidence authority commit',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
let schemas;
|
|
163
|
+
try {
|
|
164
|
+
schemas = await import(pathToFileURL(kernelDistributionModule));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`cannot load built kernel evidence schemas at ${expected.sourceCommit}: ${error.message}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
schemas,
|
|
172
|
+
contractAuthority: readPinnedContractAuthority({ dataRoot: root, schemas, declaration }),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function exactKeys(value, keys, label) {
|
|
177
|
+
if (
|
|
178
|
+
typeof value !== 'object' ||
|
|
179
|
+
value === null ||
|
|
180
|
+
Array.isArray(value) ||
|
|
181
|
+
JSON.stringify(Object.keys(value).sort()) !==
|
|
182
|
+
JSON.stringify([...keys].sort())
|
|
183
|
+
) {
|
|
184
|
+
throw new Error(`${label} does not have the exact required fields`);
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function subjectKey(subject) {
|
|
190
|
+
return `${subject.kind}:${subject.id}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function evidenceNonce(input, subject) {
|
|
194
|
+
return createHash('sha256')
|
|
195
|
+
.update('xema.image-source-evidence.nonce.v2\0')
|
|
196
|
+
.update(input.runId)
|
|
197
|
+
.update('\0')
|
|
198
|
+
.update(String(input.runAttempt))
|
|
199
|
+
.update('\0')
|
|
200
|
+
.update(subjectKey(subject))
|
|
201
|
+
.update('\0')
|
|
202
|
+
.update(subject.manifestDigest)
|
|
203
|
+
.digest('base64url');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function artifactStem(subject) {
|
|
207
|
+
return createHash('sha256').update(subjectKey(subject)).digest('hex');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function assertSigner(signer) {
|
|
211
|
+
exactKeys(
|
|
212
|
+
signer,
|
|
213
|
+
['certificateIdentity', 'oidcIssuer', 'githubWorkflow'],
|
|
214
|
+
'image-source producer signer',
|
|
215
|
+
);
|
|
216
|
+
exactKeys(
|
|
217
|
+
signer.githubWorkflow,
|
|
218
|
+
['repository', 'ref', 'path'],
|
|
219
|
+
'image-source producer GitHub workflow',
|
|
220
|
+
);
|
|
221
|
+
const { repository, ref, path } = signer.githubWorkflow;
|
|
222
|
+
if (
|
|
223
|
+
!GITHUB_REPOSITORY_PATTERN.test(repository) ||
|
|
224
|
+
!GITHUB_REF_PATTERN.test(ref) ||
|
|
225
|
+
typeof path !== 'string' ||
|
|
226
|
+
!path.startsWith('.github/workflows/') ||
|
|
227
|
+
path.split('/').some((segment) => !segment || segment === '..')
|
|
228
|
+
) {
|
|
229
|
+
throw new Error('image-source producer GitHub workflow is not canonical');
|
|
230
|
+
}
|
|
231
|
+
const expectedIdentity = `https://github.com/${repository}/${path}@${ref}`;
|
|
232
|
+
if (
|
|
233
|
+
signer.certificateIdentity !== expectedIdentity ||
|
|
234
|
+
signer.oidcIssuer !== 'https://token.actions.githubusercontent.com'
|
|
235
|
+
) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
'image-source producer signer identity or OIDC issuer is not exact',
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return signer;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function assertGitHubProducerContext(input, context) {
|
|
244
|
+
exactKeys(
|
|
245
|
+
context,
|
|
246
|
+
['repository', 'ref', 'sha', 'workflowRef'],
|
|
247
|
+
'GitHub producer context',
|
|
248
|
+
);
|
|
249
|
+
const signer = assertSigner(input.signer);
|
|
250
|
+
const expectedWorkflowRef =
|
|
251
|
+
`${signer.githubWorkflow.repository}/${signer.githubWorkflow.path}` +
|
|
252
|
+
`@${signer.githubWorkflow.ref}`;
|
|
253
|
+
if (
|
|
254
|
+
context.repository !== signer.githubWorkflow.repository ||
|
|
255
|
+
context.ref !== signer.githubWorkflow.ref ||
|
|
256
|
+
context.sha !== input.producerWorkflowSha ||
|
|
257
|
+
context.workflowRef !== expectedWorkflowRef
|
|
258
|
+
) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'GitHub producer context does not exactly match the input signer and workflow SHA',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return context;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The producer input, exact. `expectedContractAuthority` is the data root's
|
|
268
|
+
* pinned authority ({@link readPinnedContractAuthority}); an input that binds any
|
|
269
|
+
* other authority is refused here, before a statement is built from it.
|
|
270
|
+
*/
|
|
271
|
+
export function assertProducerInput(input, expectedContractAuthority) {
|
|
272
|
+
if (!expectedContractAuthority) {
|
|
273
|
+
throw new Error('the expected kernel contract authority is required');
|
|
274
|
+
}
|
|
275
|
+
exactKeys(
|
|
276
|
+
input,
|
|
277
|
+
[
|
|
278
|
+
'schemaVersion',
|
|
279
|
+
'contractAuthority',
|
|
280
|
+
'sourceHashAlgorithm',
|
|
281
|
+
'producerWorkflowSha',
|
|
282
|
+
'signer',
|
|
283
|
+
'builder',
|
|
284
|
+
'runId',
|
|
285
|
+
'runAttempt',
|
|
286
|
+
'issuedAt',
|
|
287
|
+
'expiresAt',
|
|
288
|
+
'expectedSubjects',
|
|
289
|
+
'subjects',
|
|
290
|
+
],
|
|
291
|
+
'image-source producer input',
|
|
292
|
+
);
|
|
293
|
+
if (input.schemaVersion !== 1) {
|
|
294
|
+
throw new Error('image-source producer input requires schemaVersion 1');
|
|
295
|
+
}
|
|
296
|
+
assertKernelContractAuthorityEqual(
|
|
297
|
+
input.contractAuthority,
|
|
298
|
+
expectedContractAuthority,
|
|
299
|
+
'image-source producer input',
|
|
300
|
+
);
|
|
301
|
+
if (!COMMIT_PATTERN.test(input.producerWorkflowSha)) {
|
|
302
|
+
throw new Error('producerWorkflowSha must be an exact 40-hex commit');
|
|
303
|
+
}
|
|
304
|
+
assertSigner(input.signer);
|
|
305
|
+
exactKeys(
|
|
306
|
+
input.builder,
|
|
307
|
+
['builderId', 'buildType', 'buildDefinitionDigest', 'parametersDigest'],
|
|
308
|
+
'image-source producer builder',
|
|
309
|
+
);
|
|
310
|
+
if (
|
|
311
|
+
!SHA256_DIGEST_PATTERN.test(input.builder.buildDefinitionDigest) ||
|
|
312
|
+
!SHA256_DIGEST_PATTERN.test(input.builder.parametersDigest)
|
|
313
|
+
) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
'producer build definition and parameters require sha256 digests',
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (
|
|
319
|
+
!Array.isArray(input.expectedSubjects) ||
|
|
320
|
+
input.expectedSubjects.length === 0
|
|
321
|
+
) {
|
|
322
|
+
throw new Error('image-source producer expected subject set is empty');
|
|
323
|
+
}
|
|
324
|
+
if (!Array.isArray(input.subjects) || input.subjects.length === 0) {
|
|
325
|
+
throw new Error('image-source producer subject set is empty');
|
|
326
|
+
}
|
|
327
|
+
for (const subject of input.subjects) {
|
|
328
|
+
exactKeys(
|
|
329
|
+
subject,
|
|
330
|
+
[
|
|
331
|
+
'kind',
|
|
332
|
+
'id',
|
|
333
|
+
'repositoryUrl',
|
|
334
|
+
'repo',
|
|
335
|
+
'path',
|
|
336
|
+
'sourceCommit',
|
|
337
|
+
'sourceManifestHash',
|
|
338
|
+
'sourceTreeHash',
|
|
339
|
+
'imageRepository',
|
|
340
|
+
'manifestDigest',
|
|
341
|
+
'manifestMediaType',
|
|
342
|
+
'platforms',
|
|
343
|
+
'sbomDigest',
|
|
344
|
+
'provenanceDigest',
|
|
345
|
+
],
|
|
346
|
+
`image-source producer subject ${String(subject?.kind)}:${String(subject?.id)}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const expected = [...input.expectedSubjects];
|
|
350
|
+
const actual = input.subjects.map(subjectKey);
|
|
351
|
+
const sortedExpected = [...new Set(expected)].sort();
|
|
352
|
+
const sortedActual = [...new Set(actual)].sort();
|
|
353
|
+
if (
|
|
354
|
+
JSON.stringify(expected) !== JSON.stringify(sortedExpected) ||
|
|
355
|
+
JSON.stringify(actual) !== JSON.stringify(sortedActual) ||
|
|
356
|
+
JSON.stringify(expected) !== JSON.stringify(actual)
|
|
357
|
+
) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`image-source producer requires the exact complete subject set; expected=${expected.join(',')} actual=${actual.join(',')}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function buildImageSourceStatements(input, schemas, expectedContractAuthority) {
|
|
365
|
+
assertProducerInput(input, expectedContractAuthority);
|
|
366
|
+
return input.subjects.map((subject) => {
|
|
367
|
+
const statement = {
|
|
368
|
+
_type: 'https://in-toto.io/Statement/v1',
|
|
369
|
+
subject: [
|
|
370
|
+
{
|
|
371
|
+
name: subject.imageRepository,
|
|
372
|
+
digest: { sha256: subject.manifestDigest.slice('sha256:'.length) },
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
predicateType: schemas.IMAGE_PRODUCER_PREDICATE_TYPE,
|
|
376
|
+
predicate: {
|
|
377
|
+
schemaVersion: 2,
|
|
378
|
+
contractAuthority: input.contractAuthority,
|
|
379
|
+
source: {
|
|
380
|
+
kind: subject.kind,
|
|
381
|
+
id: subject.id,
|
|
382
|
+
repositoryUrl: subject.repositoryUrl,
|
|
383
|
+
repo: subject.repo,
|
|
384
|
+
path: subject.path,
|
|
385
|
+
sourceCommit: subject.sourceCommit,
|
|
386
|
+
sourceManifestHash: subject.sourceManifestHash,
|
|
387
|
+
sourceHashAlgorithm: input.sourceHashAlgorithm,
|
|
388
|
+
sourceTreeHash: subject.sourceTreeHash,
|
|
389
|
+
},
|
|
390
|
+
build: {
|
|
391
|
+
...input.builder,
|
|
392
|
+
platforms: subject.platforms,
|
|
393
|
+
},
|
|
394
|
+
artifact: {
|
|
395
|
+
imageRepository: subject.imageRepository,
|
|
396
|
+
manifestDigest: subject.manifestDigest,
|
|
397
|
+
manifestMediaType: subject.manifestMediaType,
|
|
398
|
+
sbom: {
|
|
399
|
+
predicateType: 'https://spdx.dev/Document',
|
|
400
|
+
digest: subject.sbomDigest,
|
|
401
|
+
},
|
|
402
|
+
provenance: {
|
|
403
|
+
predicateType: 'https://slsa.dev/provenance/v1',
|
|
404
|
+
digest: subject.provenanceDigest,
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
producerRun: {
|
|
408
|
+
producerWorkflowSha: input.producerWorkflowSha,
|
|
409
|
+
runId: input.runId,
|
|
410
|
+
runAttempt: input.runAttempt,
|
|
411
|
+
nonce: evidenceNonce(input, subject),
|
|
412
|
+
issuedAt: input.issuedAt,
|
|
413
|
+
expiresAt: input.expiresAt,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
return schemas.parseImageProducerStatement(statement);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function validateProducedBundle(statementBytes, bundleBytes) {
|
|
422
|
+
const bundle = parseCanonicalJsonBytes(
|
|
423
|
+
bundleBytes,
|
|
424
|
+
'produced Sigstore bundle',
|
|
425
|
+
);
|
|
426
|
+
if (
|
|
427
|
+
bundle.mediaType !== BUNDLE_MEDIA_TYPE ||
|
|
428
|
+
bundle.messageSignature !== undefined ||
|
|
429
|
+
typeof bundle.dsseEnvelope !== 'object' ||
|
|
430
|
+
bundle.dsseEnvelope === null ||
|
|
431
|
+
bundle.dsseEnvelope.payloadType !== 'application/vnd.in-toto+json' ||
|
|
432
|
+
!Array.isArray(bundle.dsseEnvelope.signatures) ||
|
|
433
|
+
bundle.dsseEnvelope.signatures.length !== 1 ||
|
|
434
|
+
typeof bundle.dsseEnvelope.payload !== 'string'
|
|
435
|
+
) {
|
|
436
|
+
throw new Error('produced bundle is not one exact DSSE in-toto signature');
|
|
437
|
+
}
|
|
438
|
+
const payload = Buffer.from(bundle.dsseEnvelope.payload, 'base64');
|
|
439
|
+
if (
|
|
440
|
+
payload.toString('base64') !== bundle.dsseEnvelope.payload ||
|
|
441
|
+
!payload.equals(statementBytes)
|
|
442
|
+
) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
'produced bundle payload does not exactly match its statement',
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return bundle;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** The cosign that verifies is the version the data root's trust policy pins; the library names none. */
|
|
451
|
+
function exactCosignVersion(output, policyCosignVersion) {
|
|
452
|
+
if (typeof policyCosignVersion !== 'string' || !/^v\d+\.\d+\.\d+$/.test(policyCosignVersion)) {
|
|
453
|
+
throw new Error("the trust policy's cosignVersion is required to accept signed evidence");
|
|
454
|
+
}
|
|
455
|
+
const match = /\bGitVersion:\s*(v\d+\.\d+\.\d+)\b/.exec(output);
|
|
456
|
+
if (!match || match[1] !== policyCosignVersion) {
|
|
457
|
+
throw new Error(`cosign version does not match pinned ${policyCosignVersion}`);
|
|
458
|
+
}
|
|
459
|
+
return match[1];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* A cosign runner that can read the version and VERIFY, and nothing else: the
|
|
464
|
+
* edition library never signs, so any other subcommand is refused here. It
|
|
465
|
+
* passes no OIDC request variables to the child.
|
|
466
|
+
*/
|
|
467
|
+
export function createCosignVerifier(cosignBinary) {
|
|
468
|
+
if (typeof cosignBinary !== 'string' || !isAbsolute(cosignBinary)) {
|
|
469
|
+
throw new Error('cosign binary must be an absolute path');
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
readCosignVersion: () =>
|
|
473
|
+
execFileSync(cosignBinary, ['version'], {
|
|
474
|
+
encoding: 'utf8',
|
|
475
|
+
env: { LANG: 'C', LC_ALL: 'C', PATH: '/usr/bin:/bin', TZ: 'UTC' },
|
|
476
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
477
|
+
timeout: 10_000,
|
|
478
|
+
}),
|
|
479
|
+
runCosign: (args) => {
|
|
480
|
+
if (args[0] !== 'verify-blob-attestation') {
|
|
481
|
+
throw new Error(
|
|
482
|
+
`the edition library only verifies with cosign; it does not run "cosign ${String(args[0])}"`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return execFileSync(cosignBinary, args, {
|
|
486
|
+
env: {
|
|
487
|
+
HOME: process.env.HOME,
|
|
488
|
+
LANG: 'C',
|
|
489
|
+
LC_ALL: 'C',
|
|
490
|
+
PATH: '/usr/bin:/bin',
|
|
491
|
+
TZ: 'UTC',
|
|
492
|
+
},
|
|
493
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
494
|
+
timeout: 180_000,
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** The verification every accepted bundle must pass: the declared signer, at the exact workflow commit. */
|
|
501
|
+
export function cosignVerifyAttestationArgs({ bundlePath, signer, certificateWorkflowSha, predicateType }) {
|
|
502
|
+
return [
|
|
503
|
+
'verify-blob-attestation',
|
|
504
|
+
'--check-claims=false',
|
|
505
|
+
'--new-bundle-format',
|
|
506
|
+
'--bundle',
|
|
507
|
+
bundlePath,
|
|
508
|
+
'--certificate-identity',
|
|
509
|
+
signer.certificateIdentity,
|
|
510
|
+
'--certificate-oidc-issuer',
|
|
511
|
+
signer.oidcIssuer,
|
|
512
|
+
'--certificate-github-workflow-repository',
|
|
513
|
+
signer.githubWorkflow.repository,
|
|
514
|
+
'--certificate-github-workflow-ref',
|
|
515
|
+
signer.githubWorkflow.ref,
|
|
516
|
+
'--certificate-github-workflow-sha',
|
|
517
|
+
certificateWorkflowSha,
|
|
518
|
+
'--type',
|
|
519
|
+
predicateType,
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** The canonical bytes of a cosign-produced bundle, held to its statement. Reads; writes nothing. */
|
|
524
|
+
export function canonicalSignedBundleBytes({ statementBytes, bundlePath }) {
|
|
525
|
+
let producedBundle;
|
|
526
|
+
try {
|
|
527
|
+
producedBundle = JSON.parse(
|
|
528
|
+
decodeExactUtf8(readFileSync(bundlePath), 'cosign-produced bundle'),
|
|
529
|
+
);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`cosign-produced bundle is not exact UTF-8 JSON: ${error.message}`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const bundleBytes = canonicalJsonBytes(producedBundle);
|
|
536
|
+
validateProducedBundle(statementBytes, bundleBytes);
|
|
537
|
+
return bundleBytes;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function stagedPaths(statement) {
|
|
541
|
+
const stem = artifactStem(statement.predicate.source);
|
|
542
|
+
return {
|
|
543
|
+
statementPath: `${STAGED_STATEMENTS_DIRECTORY}/${stem}.json`,
|
|
544
|
+
bundlePath: `${STAGED_BUNDLES_DIRECTORY}/${stem}.sigstore.json`,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Write the canonical statement bytes to sign into a NEW staging directory and
|
|
550
|
+
* return the signing plan: what to sign, where each bundle must be written,
|
|
551
|
+
* and the identity each bundle will be verified against.
|
|
552
|
+
*/
|
|
553
|
+
export function stageImageSourceEvidence({
|
|
554
|
+
input,
|
|
555
|
+
stagingDirectory,
|
|
556
|
+
schemas,
|
|
557
|
+
expectedContractAuthority,
|
|
558
|
+
}) {
|
|
559
|
+
if (!isAbsolute(stagingDirectory)) {
|
|
560
|
+
throw new Error('the staging directory must be an absolute path');
|
|
561
|
+
}
|
|
562
|
+
if (existsSync(stagingDirectory)) {
|
|
563
|
+
throw new Error(`evidence staging directory already exists: ${stagingDirectory}`);
|
|
564
|
+
}
|
|
565
|
+
const statements = buildImageSourceStatements(input, schemas, expectedContractAuthority);
|
|
566
|
+
mkdirSync(join(stagingDirectory, STAGED_STATEMENTS_DIRECTORY), { recursive: true, mode: 0o700 });
|
|
567
|
+
mkdirSync(join(stagingDirectory, STAGED_BUNDLES_DIRECTORY), { mode: 0o700 });
|
|
568
|
+
const planned = [];
|
|
569
|
+
for (const statement of statements) {
|
|
570
|
+
const paths = stagedPaths(statement);
|
|
571
|
+
writeFileSync(join(stagingDirectory, paths.statementPath), canonicalJsonBytes(statement), {
|
|
572
|
+
flag: 'wx',
|
|
573
|
+
mode: 0o600,
|
|
574
|
+
});
|
|
575
|
+
planned.push({
|
|
576
|
+
subjectKind: statement.predicate.source.kind,
|
|
577
|
+
subjectId: statement.predicate.source.id,
|
|
578
|
+
...paths,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
schemaVersion: 1,
|
|
583
|
+
kind: 'producer',
|
|
584
|
+
root: stagingDirectory,
|
|
585
|
+
predicateType: schemas.IMAGE_PRODUCER_PREDICATE_TYPE,
|
|
586
|
+
signer: input.signer,
|
|
587
|
+
certificateWorkflowSha: input.producerWorkflowSha,
|
|
588
|
+
statements: planned,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Accept the signed bundles of a staged directory and seal `outputDirectory`.
|
|
594
|
+
*
|
|
595
|
+
* The output is BUILT, never renamed from the staging: each statement is
|
|
596
|
+
* re-derived from `input` (a staged statement that is not byte-identical is
|
|
597
|
+
* refused, so nothing but what this library built can have been signed), each
|
|
598
|
+
* bundle is canonicalized and verified, and only those derived files are
|
|
599
|
+
* written. The staging must hold exactly what stage recorded.
|
|
600
|
+
*
|
|
601
|
+
* The staging does not outlive accept. A refused accept removes it together
|
|
602
|
+
* with the partial output, so no signed-but-unaccepted bundle is left for a
|
|
603
|
+
* later step to pick up; a successful accept removes it too. Only the argument
|
|
604
|
+
* refusals above the first read leave it in place.
|
|
605
|
+
*
|
|
606
|
+
* `cosignVersion` is the trust policy's pinned version.
|
|
607
|
+
*/
|
|
608
|
+
export function acceptImageSourceEvidence({
|
|
609
|
+
input,
|
|
610
|
+
stagingDirectory,
|
|
611
|
+
outputDirectory,
|
|
612
|
+
schemas,
|
|
613
|
+
expectedContractAuthority,
|
|
614
|
+
cosignVersion,
|
|
615
|
+
readCosignVersion,
|
|
616
|
+
runCosign,
|
|
617
|
+
}) {
|
|
618
|
+
if (!isAbsolute(stagingDirectory) || !isAbsolute(outputDirectory)) {
|
|
619
|
+
throw new Error('the staging and output directories must be absolute paths');
|
|
620
|
+
}
|
|
621
|
+
if (typeof readCosignVersion !== 'function' || typeof runCosign !== 'function') {
|
|
622
|
+
throw new Error('cosign seams are required');
|
|
623
|
+
}
|
|
624
|
+
const sealing = reserveSealing(outputDirectory, 'evidence');
|
|
625
|
+
return sealAccepted({
|
|
626
|
+
stagingDirectory,
|
|
627
|
+
sealing,
|
|
628
|
+
outputDirectory,
|
|
629
|
+
build: () => {
|
|
630
|
+
exactCosignVersion(readCosignVersion(), cosignVersion);
|
|
631
|
+
const statements = buildImageSourceStatements(input, schemas, expectedContractAuthority);
|
|
632
|
+
assertStagedExactly(
|
|
633
|
+
stagingDirectory,
|
|
634
|
+
statements.flatMap((statement) => {
|
|
635
|
+
const { statementPath, bundlePath } = stagedPaths(statement);
|
|
636
|
+
return [statementPath, bundlePath];
|
|
637
|
+
}),
|
|
638
|
+
);
|
|
639
|
+
mkdirSync(join(sealing, STAGED_STATEMENTS_DIRECTORY), { recursive: true, mode: 0o755 });
|
|
640
|
+
mkdirSync(join(sealing, STAGED_BUNDLES_DIRECTORY), { mode: 0o755 });
|
|
641
|
+
const producerEvidence = [];
|
|
642
|
+
for (const statement of statements) {
|
|
643
|
+
const { statementPath, bundlePath } = stagedPaths(statement);
|
|
644
|
+
const statementBytes = canonicalJsonBytes(statement);
|
|
645
|
+
if (!readFileSync(join(stagingDirectory, statementPath)).equals(statementBytes)) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`staged statement ${statementPath} is not the statement this producer input builds`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
const bundleBytes = canonicalSignedBundleBytes({
|
|
651
|
+
statementBytes,
|
|
652
|
+
bundlePath: join(stagingDirectory, bundlePath),
|
|
653
|
+
});
|
|
654
|
+
writeFileSync(join(sealing, statementPath), statementBytes, { flag: 'wx', mode: 0o644 });
|
|
655
|
+
writeFileSync(join(sealing, bundlePath), bundleBytes, { flag: 'wx', mode: 0o644 });
|
|
656
|
+
// Verified where it is sealed: the bytes cosign accepts are the bytes kept.
|
|
657
|
+
runCosign(
|
|
658
|
+
cosignVerifyAttestationArgs({
|
|
659
|
+
bundlePath: join(sealing, bundlePath),
|
|
660
|
+
signer: input.signer,
|
|
661
|
+
certificateWorkflowSha: input.producerWorkflowSha,
|
|
662
|
+
predicateType: schemas.IMAGE_PRODUCER_PREDICATE_TYPE,
|
|
663
|
+
}),
|
|
664
|
+
);
|
|
665
|
+
producerEvidence.push({
|
|
666
|
+
subjectKind: statement.predicate.source.kind,
|
|
667
|
+
subjectId: statement.predicate.source.id,
|
|
668
|
+
evidence: {
|
|
669
|
+
statementPath,
|
|
670
|
+
statementDigest: sha256Digest(statementBytes),
|
|
671
|
+
bundlePath,
|
|
672
|
+
bundleDigest: sha256Digest(bundleBytes),
|
|
673
|
+
bundleMediaType: BUNDLE_MEDIA_TYPE,
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
writeFileSync(
|
|
678
|
+
join(sealing, 'producer-evidence.json'),
|
|
679
|
+
serializeJson({ schemaVersion: 1, producerEvidence }),
|
|
680
|
+
{ flag: 'wx', mode: 0o644 },
|
|
681
|
+
);
|
|
682
|
+
return producerEvidence;
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const USAGE =
|
|
688
|
+
'usage: xema edition evidence statement producer <stage|accept> --input <json> --staging <absolute-directory> ' +
|
|
689
|
+
'--github-repository <owner/repo> --github-ref <ref> --github-sha <commit> ' +
|
|
690
|
+
'--github-workflow-ref <owner/repo/path@ref> [accept: --output <absolute-directory> --cosign <absolute-binary>] ' +
|
|
691
|
+
'--data-root <dir> --kernel <dir>';
|
|
692
|
+
|
|
693
|
+
function parseFlags(args, known) {
|
|
694
|
+
const flags = new Map();
|
|
695
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
696
|
+
const flag = args[index];
|
|
697
|
+
const value = args[index + 1];
|
|
698
|
+
if (typeof flag !== 'string' || !flag.startsWith('--') || !known.includes(flag.slice(2))) {
|
|
699
|
+
throw new Error(USAGE);
|
|
700
|
+
}
|
|
701
|
+
if (value === undefined || value.startsWith('--') || flags.has(flag.slice(2))) {
|
|
702
|
+
throw new Error(USAGE);
|
|
703
|
+
}
|
|
704
|
+
flags.set(flag.slice(2), value);
|
|
705
|
+
}
|
|
706
|
+
for (const name of known) {
|
|
707
|
+
if (!flags.has(name)) {throw new Error(USAGE);}
|
|
708
|
+
}
|
|
709
|
+
return flags;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function main() {
|
|
713
|
+
const [phase, ...args] = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
|
|
714
|
+
WorkspaceRoot.Data,
|
|
715
|
+
WorkspaceRoot.Kernel,
|
|
716
|
+
]);
|
|
717
|
+
const context = ['github-repository', 'github-ref', 'github-sha', 'github-workflow-ref'];
|
|
718
|
+
if (phase !== 'stage' && phase !== 'accept') {
|
|
719
|
+
throw new Error(USAGE);
|
|
720
|
+
}
|
|
721
|
+
const flags = parseFlags(
|
|
722
|
+
args,
|
|
723
|
+
phase === 'stage' ? ['input', 'staging', ...context] : ['input', 'staging', 'output', 'cosign', ...context],
|
|
724
|
+
);
|
|
725
|
+
const input = JSON.parse(readFileSync(resolve(flags.get('input')), 'utf8'));
|
|
726
|
+
const { schemas, contractAuthority } = await loadProducerKernelSchemas({ dataRoot: dataRoot() });
|
|
727
|
+
assertProducerInput(input, contractAuthority);
|
|
728
|
+
assertGitHubProducerContext(input, {
|
|
729
|
+
repository: flags.get('github-repository'),
|
|
730
|
+
ref: flags.get('github-ref'),
|
|
731
|
+
sha: flags.get('github-sha'),
|
|
732
|
+
workflowRef: flags.get('github-workflow-ref'),
|
|
733
|
+
});
|
|
734
|
+
if (phase === 'stage') {
|
|
735
|
+
const plan = stageImageSourceEvidence({
|
|
736
|
+
input,
|
|
737
|
+
stagingDirectory: flags.get('staging'),
|
|
738
|
+
schemas,
|
|
739
|
+
expectedContractAuthority: contractAuthority,
|
|
740
|
+
});
|
|
741
|
+
process.stdout.write(serializeJson(plan));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const evidence = acceptImageSourceEvidence({
|
|
745
|
+
input,
|
|
746
|
+
stagingDirectory: flags.get('staging'),
|
|
747
|
+
outputDirectory: flags.get('output'),
|
|
748
|
+
schemas,
|
|
749
|
+
expectedContractAuthority: contractAuthority,
|
|
750
|
+
cosignVersion: loadPinnedDeclaredTrustPolicy({
|
|
751
|
+
dataRoot: dataRoot(),
|
|
752
|
+
declaration: editionWorkspaceConfig().evidenceTrustPolicy,
|
|
753
|
+
schemas,
|
|
754
|
+
}).cosignVersion,
|
|
755
|
+
...createCosignVerifier(flags.get('cosign')),
|
|
756
|
+
});
|
|
757
|
+
process.stdout.write(serializeJson({ schemaVersion: 1, kind: 'producer', outputDirectory: flags.get('output'), producerEvidence: evidence }));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (isEntryModule(import.meta.url)) {
|
|
761
|
+
main().catch((error) => {
|
|
762
|
+
console.error(`[image-source-evidence] ${error.message}`);
|
|
763
|
+
process.exitCode = 1;
|
|
764
|
+
});
|
|
765
|
+
}
|