@xemahq/xema 0.58.0 → 0.59.1

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.
@@ -3,9 +3,12 @@
3
3
  // verified by cosign (`third-party-source-evidence.mjs`) before it is committed.
4
4
 
5
5
  import { execFileSync } from 'node:child_process';
6
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
6
9
 
7
10
  import { certificateConstraintArgs, certificateIdentityArgs } from './cosign-certificate-constraints.mjs';
8
- import { COSIGN_ENV, decodeExactUtf8, sha256Digest, validatePinnedTrustedRoot } from './image-evidence.mjs';
11
+ import { COSIGN_ENV, classicCosignTrustMaterial, decodeExactUtf8, sha256Digest, validatePinnedTrustedRoot } from './image-evidence.mjs';
9
12
  import { splitImageRef } from './registry-credentials.mjs';
10
13
  import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
11
14
  import { SLSA_PROVENANCE_V1, decodeJsonStatement, digestOfOciRef } from './third-party-source-evidence.mjs';
@@ -137,7 +140,13 @@ export async function bundleDigestRef({ clientFor, ociRef }) {
137
140
 
138
141
  const tail = (error) => String(error?.stderr ?? error?.message ?? error).trim().split('\n').slice(-4).join(' | ').slice(0, 1024);
139
142
 
140
- /** The cosign run of a registry verification: the trusted binary, the operator's registry credential (HOME / DOCKER_CONFIG). */
143
+ /**
144
+ * The cosign run of a registry verification: the trusted binary under a CLEAN environment (`COSIGN_ENV` and the
145
+ * operator's registry credential, HOME / DOCKER_CONFIG; no `SIGSTORE_*`, `COSIGN_*` or `TUF_*` of the operator is ever
146
+ * inherited). `run` takes `--offline --trusted-root <pinned>`; `runClassic` instead pins cosign's classic trust inputs
147
+ * to the keys derived from the same pinned root (`classicCosignTrustMaterial`), in a private directory removed after the
148
+ * run, with an empty private TUF_ROOT so no live TUF material is ever read.
149
+ */
141
150
  function registryCosign({ trustedCosign, policy, policyRoot, row, ociRef, env }) {
142
151
  const root = validatePinnedTrustedRoot(policyRoot, policy);
143
152
  const environment = {
@@ -145,18 +154,41 @@ function registryCosign({ trustedCosign, policy, policyRoot, row, ociRef, env })
145
154
  ...(env.HOME === undefined ? {} : { HOME: env.HOME }),
146
155
  ...(env.DOCKER_CONFIG === undefined ? {} : { DOCKER_CONFIG: env.DOCKER_CONFIG }),
147
156
  };
157
+ const exec = (command, args, runEnv) => {
158
+ try {
159
+ return execFileSync(assertPreparedExecutable(trustedCosign, 'cosign'), [command, ...args], {
160
+ env: runEnv,
161
+ encoding: 'utf8',
162
+ stdio: ['ignore', 'pipe', 'pipe'],
163
+ timeout: REGISTRY_COSIGN_TIMEOUT_MS,
164
+ });
165
+ } catch (error) {
166
+ throw new Error(`cosign ${command} refused ${ociRef} under row "${row.policyId}": ${tail(error)}`);
167
+ }
168
+ };
148
169
  return {
149
170
  trustArgs: ['--offline', '--trusted-root', root.path],
150
- run(command, args) {
171
+ run: (command, args) => exec(command, args, environment),
172
+ runClassic(command, args) {
173
+ const material = classicCosignTrustMaterial(root.bytes);
174
+ const dir = mkdtempSync(join(tmpdir(), 'xema-cosign-trust-'));
151
175
  try {
152
- return execFileSync(assertPreparedExecutable(trustedCosign, 'cosign'), [command, ...args], {
153
- env: environment,
154
- encoding: 'utf8',
155
- stdio: ['ignore', 'pipe', 'pipe'],
156
- timeout: REGISTRY_COSIGN_TIMEOUT_MS,
176
+ const file = (name, content) => {
177
+ const path = join(dir, name);
178
+ writeFileSync(path, content, { mode: 0o600, flag: 'wx' });
179
+ return path;
180
+ };
181
+ const tufRoot = join(dir, 'tuf');
182
+ mkdirSync(tufRoot, { mode: 0o700 });
183
+ return exec(command, ['--offline', ...args], {
184
+ ...environment,
185
+ SIGSTORE_ROOT_FILE: file('fulcio.pem', material.fulcioPem),
186
+ SIGSTORE_REKOR_PUBLIC_KEY: file('rekor.pub', material.rekorPem),
187
+ SIGSTORE_CT_LOG_PUBLIC_KEY_FILE: file('ctlog.pub', material.ctLogPem),
188
+ TUF_ROOT: tufRoot,
157
189
  });
158
- } catch (error) {
159
- throw new Error(`cosign ${command} refused ${ociRef} under row "${row.policyId}": ${tail(error)}`);
190
+ } finally {
191
+ rmSync(dir, { recursive: true, force: true });
160
192
  }
161
193
  },
162
194
  };
@@ -165,16 +197,21 @@ function registryCosign({ trustedCosign, policy, policyRoot, row, ociRef, env })
165
197
  /**
166
198
  * (a) against the registry: keyless `cosign verify` and `cosign verify-attestation --type slsaprovenance1` of the bundle
167
199
  * artifact under EXACTLY the signer row's identity and issuer, the row's workflow repository, ref and the stated
168
- * commit, the pinned trusted root and the trusted cosign binary. `--offline` verifies the tlog inclusion proof carried
169
- * with the signature; the tlog is never ignored. The registry credential is the operator's (HOME / DOCKER_CONFIG).
200
+ * commit, and the trusted cosign binary, both `--offline`: the tlog entry carried with the signature is verified, the
201
+ * tlog is never ignored. The registry credential is the operator's (HOME / DOCKER_CONFIG).
202
+ *
203
+ * Publishers write the CLASSIC cosign format (`sha256-<d>.sig` / `.att` tags; `cosign sign` / `cosign attest`), not
204
+ * referrer Sigstore bundles. `verify` takes the pinned trusted root as is (`--trusted-root`, classic or bundle form).
205
+ * cosign 2.6 `verify-attestation` takes `--trusted-root` only with `--new-bundle-format`, which finds no classic
206
+ * attestation, so it runs without either, against the Fulcio, Rekor v1 and CT keys derived from the SAME pinned root
207
+ * and passed in cosign's classic trust variables (`registryCosign().runClassic`). A publisher switching to referrer
208
+ * bundles is refused loudly (cosign then requires a trusted root), never verified against anything unpinned.
170
209
  */
171
210
  export function verifyBundleOnRegistry({ trustedCosign, policy, policyRoot, row, workflowSha, ociRef, env = process.env }) {
172
211
  const cosign = registryCosign({ trustedCosign, policy, policyRoot, row, ociRef, env });
173
- const common = [...cosign.trustArgs, ...certificateConstraintArgs(row, workflowSha)];
174
- cosign.run('verify', [...common, ociRef]);
175
- // cosign refuses `verify-attestation --trusted-root` without `--new-bundle-format`; plain `verify` takes it as is.
176
- const [offline, trustedRoot, rootPath, ...claims] = common;
177
- cosign.run('verify-attestation', [offline, '--new-bundle-format', trustedRoot, rootPath, '--type', COSIGN_SLSA_PROVENANCE_ALIAS, ...claims, ociRef]);
212
+ const claims = certificateConstraintArgs(row, workflowSha);
213
+ cosign.run('verify', [...cosign.trustArgs, ...claims, ociRef]);
214
+ cosign.runClassic('verify-attestation', ['--type', COSIGN_SLSA_PROVENANCE_ALIAS, ...claims, ociRef]);
178
215
  }
179
216
 
180
217
  const WORKFLOW_SHA = /^[0-9a-f]{40}$/u;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.58.0",
3
+ "version": "0.59.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -100,7 +100,7 @@
100
100
  "typecheck": "tsc --noEmit"
101
101
  },
102
102
  "xemaRelease": {
103
- "sourceDigest": "sha256:90788d0401718a3e8bfab84c8818ff21bde3b1218931ab71aedd032894bc346a",
104
- "sha": "931d0b103540e14e34d6c726a5167640aa7a1c6e"
103
+ "sourceDigest": "sha256:f02866c7d95e9f1f32bbbf2879720aa6c6a4ff30ea4c7ac306047775483f31bd",
104
+ "sha": "36535b2452d6413f38a32eafc2544ba463c41f62"
105
105
  }
106
106
  }