@xemahq/xema 0.57.1 → 0.59.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.
@@ -2126,12 +2126,21 @@ export async function resolvePreviousSeal({ distributionId, previousLockPath, pr
2126
2126
  : `--previous-lock and --previous-lock-sha256 go together (one was given); give ${PREVIOUS_SEAL_USAGE}`,
2127
2127
  );
2128
2128
  }
2129
- const bytes = readFileSync(previousLockPath);
2129
+ const bytes = readPreviousLockBytes(previousLockPath, previousLockSha256);
2130
+ return parsePreviousLock(distributionId, parseStrictJson(bytes.toString('utf8'), previousLockPath), '--previous-lock');
2131
+ }
2132
+
2133
+ /**
2134
+ * The bytes of a previous lock file, held to the sha256 (hex) they were given with: the one check of the pair
2135
+ * (`flags` names the pair in the refusal). The parse, with the kernel and against the edition's id, is the caller's.
2136
+ */
2137
+ export function readPreviousLockBytes(path, sha256, flags = { lock: '--previous-lock', sha256: '--previous-lock-sha256' }) {
2138
+ const bytes = readFileSync(path);
2130
2139
  const actual = createHash('sha256').update(bytes).digest('hex');
2131
- if (actual !== previousLockSha256) {
2132
- throw new Error(`--previous-lock ${previousLockPath} has sha256 ${actual}, not the given --previous-lock-sha256 ${previousLockSha256}`);
2140
+ if (actual !== sha256) {
2141
+ throw new Error(`${flags.lock} ${path} has sha256 ${actual}, not the given ${flags.sha256} ${sha256}`);
2133
2142
  }
2134
- return parsePreviousLock(distributionId, readStrictJsonFile(previousLockPath), '--previous-lock');
2143
+ return bytes;
2135
2144
  }
2136
2145
 
2137
2146
  /** The in-repo lane's previous lock: the committed file at `outPath`, parsed before it is overwritten; null (first seal) when there is none. */
@@ -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,14 +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.run('verify-attestation', ['--type', COSIGN_SLSA_PROVENANCE_ALIAS, ...common, 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]);
176
215
  }
177
216
 
178
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.57.1",
3
+ "version": "0.59.0",
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:d587d9025bbf46bc076d6d88075347bd45fa0ca644c804a32a463157091f5bfa",
104
- "sha": "286425dc5a0bf4ee9b7a3b3c0b53271f3ec661ed"
103
+ "sourceDigest": "sha256:bdd50b327d2da70bdc17c4cdcfce8ed38500fbd68aea8f23029c53d45ddbcb86",
104
+ "sha": "9e648fd62243cb0fc9df74680ce6c2a110a901ad"
105
105
  }
106
106
  }