@blamejs/exceptd-skills 0.12.15 → 0.12.18

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.
@@ -45,6 +45,19 @@ const path = require("path");
45
45
  const os = require("os");
46
46
  const { spawnSync } = require("child_process");
47
47
 
48
+ // v0.12.16 (audit I P1-1): mirror the byte-stability normalize() contract
49
+ // from lib/sign.js + lib/verify.js + lib/refresh-network.js. Duplicated
50
+ // (not require'd) to keep this script's dep surface minimal and to ensure
51
+ // a bug in the normalize() implementation in lib/ doesn't simultaneously
52
+ // disable both the source-tree-verify path AND the shipped-tarball-verify
53
+ // gate (we want at least one independent check). ANY change to normalize()
54
+ // in any of these four files must be mirrored in all of them.
55
+ function normalizeSkillBytes(buf) {
56
+ let s = Buffer.isBuffer(buf) ? buf.toString("utf8") : String(buf);
57
+ if (s.length > 0 && s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
58
+ return Buffer.from(s.replace(/\r\n/g, "\n"), "utf8");
59
+ }
60
+
48
61
  const ROOT = path.resolve(__dirname, "..");
49
62
 
50
63
  function emit(msg) { process.stdout.write(`[verify-shipped-tarball] ${msg}\n`); }
@@ -221,22 +234,38 @@ try {
221
234
  failures.push(`${s.name}: file not found at ${s.path}`);
222
235
  continue;
223
236
  }
224
- const content = fs.readFileSync(skillPath);
225
- const ok = crypto.verify(null, content, pubKey, Buffer.from(s.signature, "base64"));
237
+ // v0.12.16 (audit I P1-1): the prior code passed the raw file bytes
238
+ // directly to crypto.verify. lib/sign.js + lib/verify.js both NORMALIZE
239
+ // bytes (strip UTF-8 BOM, convert CRLF -> LF) before sign/verify, per
240
+ // the byte-stability contract in lib/verify.js's normalize() header.
241
+ // Without the same normalization here, this gate (which was added
242
+ // specifically to catch the v0.11.x signature regression class!) would
243
+ // itself report 0/38 on any tree where line-ending normalization
244
+ // touched the source between sign and pack — a Windows contributor
245
+ // with `core.autocrlf=true`, or a tool like Prettier between sign and
246
+ // pack. CLAUDE.md flags this as the recurring CRLF-bypass class.
247
+ const rawContent = fs.readFileSync(skillPath);
248
+ const normalizedContent = normalizeSkillBytes(rawContent);
249
+ const ok = crypto.verify(null, normalizedContent, pubKey, Buffer.from(s.signature, "base64"));
226
250
  if (ok) pass++;
227
251
  else {
228
252
  fail_count++;
229
253
  // Forensic detail: log size + sha256 of tarball-extracted content vs source-tree content
230
254
  // so we can pinpoint which bytes changed between npm pack and what was signed.
231
- const tarSha = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
255
+ // v0.12.16: forensic logging uses rawContent (pre-normalization
256
+ // bytes) so an operator inspecting failures sees the actual on-disk
257
+ // shape, but tarSha is computed over the NORMALIZED bytes that
258
+ // were actually fed to crypto.verify — making the comparison to
259
+ // sign-time bytes meaningful.
260
+ const tarSha = crypto.createHash("sha256").update(normalizedContent).digest("hex").slice(0, 16);
232
261
  let srcSha = "<missing>", srcSize = 0, srcContent;
233
262
  if (fs.existsSync(sourceSkillPath)) {
234
263
  srcContent = fs.readFileSync(sourceSkillPath);
235
264
  srcSize = srcContent.length;
236
- srcSha = crypto.createHash("sha256").update(srcContent).digest("hex").slice(0, 16);
265
+ srcSha = crypto.createHash("sha256").update(normalizeSkillBytes(srcContent)).digest("hex").slice(0, 16);
237
266
  }
238
- const equal = srcContent && content.equals(srcContent) ? "equal" : "DIFFER";
239
- failures.push(`${s.name}: signature did not verify (tarball size=${content.length} sha=${tarSha}; source size=${srcSize} sha=${srcSha}; bytes ${equal})`);
267
+ const equal = srcContent && rawContent.equals(srcContent) ? "equal" : "DIFFER";
268
+ failures.push(`${s.name}: signature did not verify (tarball size=${rawContent.length} sha-normalized=${tarSha}; source size=${srcSize} sha-normalized=${srcSha}; raw bytes ${equal})`);
240
269
  }
241
270
  }
242
271