@blamejs/exceptd-skills 0.13.10 → 0.13.12

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.
@@ -162,21 +162,50 @@ function checkSbomCurrency(root) {
162
162
  );
163
163
  continue;
164
164
  }
165
- const sha256Entry = (comp.hashes || []).find(
166
- (h) => h && h.alg === "SHA-256",
167
- );
165
+ // v0.13.12: verify SHA-256 AND SHA3-512 when present. SHA-256 is
166
+ // the universal-tool contract (CycloneDX 1.6 default, Anchore /
167
+ // Trivy / Dependency-Track / GitHub Dependency Graph). SHA3-512
168
+ // is the SHA-3 family hedge, matching the existing key-fingerprint
169
+ // pattern (lib/verify.js). Both must agree with the live bytes;
170
+ // a mismatch on either fires the same drift error. A missing
171
+ // SHA-256 is a hard error (the universal contract is the floor);
172
+ // a missing SHA3-512 surfaces as a downgrade-attack warning so an
173
+ // operator who intentionally strips the second hash from an
174
+ // SBOM (post-quantum posture relaxation) sees it in the gate
175
+ // output, not in the JSON downstream.
176
+ const sha256Entry = (comp.hashes || []).find((h) => h && h.alg === "SHA-256");
177
+ const sha3Entry = (comp.hashes || []).find((h) => h && h.alg === "SHA3-512");
168
178
  if (!sha256Entry || typeof sha256Entry.content !== "string") {
169
179
  errors.push(
170
180
  `SBOM file component "${relPath}" lacks a SHA-256 hash entry`
171
181
  );
172
182
  continue;
173
183
  }
174
- const live = crypto.createHash("sha256").update(fs.readFileSync(absPath)).digest("hex");
175
- if (live !== sha256Entry.content) {
184
+ const fileBytes = fs.readFileSync(absPath);
185
+ const liveSha256 = crypto.createHash("sha256").update(fileBytes).digest("hex");
186
+ if (liveSha256 !== sha256Entry.content) {
176
187
  errors.push(
177
- `SBOM file component "${relPath}" hash drift: recorded ${sha256Entry.content.slice(0, 12)}…, live ${live.slice(0, 12)}… — re-sign skills (\`node $(exceptd path)/lib/sign.js sign-all\` from a contributor checkout) and then \`npm run refresh-sbom\`, in that order (sbom must regenerate AFTER the final sign).`,
188
+ `SBOM file component "${relPath}" SHA-256 drift: recorded ${sha256Entry.content.slice(0, 12)}…, live ${liveSha256.slice(0, 12)}… — re-sign skills (\`node $(exceptd path)/lib/sign.js sign-all\` from a contributor checkout) and then \`npm run refresh-sbom\`, in that order (sbom must regenerate AFTER the final sign).`,
178
189
  );
179
190
  }
191
+ // Codex P1 on PR #52: the dual-hash contract requires SHA3-512 to be
192
+ // PRESENT, not just verified when present. An attacker (or a careless
193
+ // sbom-generator regression) that strips the SHA3-512 column would
194
+ // silently pass the gate under an `if (sha3Entry)` guard, defeating
195
+ // the downgrade defense the dual-hash design is supposed to provide.
196
+ // Refuse absence as a hard error.
197
+ if (!sha3Entry || typeof sha3Entry.content !== "string") {
198
+ errors.push(
199
+ `SBOM file component "${relPath}" lacks a SHA3-512 hash entry — the dual-hash contract (SHA-256 + SHA3-512) requires both algorithms on every file: component. Regenerate via \`npm run refresh-sbom\` (v0.13.12+).`
200
+ );
201
+ } else {
202
+ const liveSha3 = crypto.createHash("sha3-512").update(fileBytes).digest("hex");
203
+ if (liveSha3 !== sha3Entry.content) {
204
+ errors.push(
205
+ `SBOM file component "${relPath}" SHA3-512 drift: recorded ${sha3Entry.content.slice(0, 12)}…, live ${liveSha3.slice(0, 12)}… — same remediation as SHA-256 drift (re-sign then refresh-sbom).`,
206
+ );
207
+ }
208
+ }
180
209
  fileComponentsChecked++;
181
210
  }
182
211
 
@@ -185,6 +185,29 @@ function sha256File(absPath) {
185
185
  .digest('hex');
186
186
  }
187
187
 
188
+ // v0.13.12 — emit SHA3-512 alongside SHA-256 for every file: component.
189
+ // CycloneDX 1.6 supports multiple hash entries per component. Rationale
190
+ // mirrors the existing key-fingerprint emission in lib/verify.js:
191
+ //
192
+ // - SHA-256 stays as the universal-tool contract (Anchore / Trivy /
193
+ // Dependency-Track / GitHub Dependency Graph all parse it).
194
+ // - SHA3-512 is the SHA-3 family (Keccak / sponge), different
195
+ // mathematical foundation. Hedges against future SHA-2 weaknesses
196
+ // and aligns with the project's PQ posture (ML-KEM / ML-DSA both
197
+ // internally hash with SHA-3).
198
+ //
199
+ // check-sbom-currency.js verifies BOTH when present and refuses if a
200
+ // SHA3-512 entry is recorded but its content drifts from the live
201
+ // bytes — so a downgrade attack that drops SHA3-512 from the recorded
202
+ // SBOM (leaving only SHA-256) is observable as a missing-hash error,
203
+ // not a silent acceptance.
204
+ function sha3_512File(absPath) {
205
+ return crypto
206
+ .createHash('sha3-512')
207
+ .update(fs.readFileSync(absPath))
208
+ .digest('hex');
209
+ }
210
+
188
211
  function fileComponents(allowlist) {
189
212
  const rels = expandAllowlist(allowlist);
190
213
  const out = [];
@@ -194,7 +217,10 @@ function fileComponents(allowlist) {
194
217
  'bom-ref': `file:${rel}`,
195
218
  type: 'file',
196
219
  name: rel,
197
- hashes: [{ alg: 'SHA-256', content: sha256File(abs) }],
220
+ hashes: [
221
+ { alg: 'SHA-256', content: sha256File(abs) },
222
+ { alg: 'SHA3-512', content: sha3_512File(abs) },
223
+ ],
198
224
  });
199
225
  }
200
226
  return out;