@blamejs/exceptd-skills 0.12.18 → 0.12.20

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.
@@ -58,6 +58,65 @@ function normalizeSkillBytes(buf) {
58
58
  return Buffer.from(s.replace(/\r\n/g, "\n"), "utf8");
59
59
  }
60
60
 
61
+ // Audit O P1-C: in-line manifest-signature verifier for the extracted
62
+ // tarball. Kept here (rather than imported) for the same defense-in-depth
63
+ // reasoning as normalizeSkillBytes: a bug in lib/verify.js's verifier
64
+ // should not also disable this gate (we want at least one independent
65
+ // check). The canonical-bytes computation MUST stay in lockstep with
66
+ // lib/sign.js + lib/verify.js + lib/refresh-network.js — enforced by
67
+ // tests/normalize-contract.test.js.
68
+ function canonicalizeForTarball(value) {
69
+ if (Array.isArray(value)) return value.map(canonicalizeForTarball);
70
+ if (value && typeof value === "object") {
71
+ const out = {};
72
+ for (const key of Object.keys(value).sort()) {
73
+ out[key] = canonicalizeForTarball(value[key]);
74
+ }
75
+ return out;
76
+ }
77
+ return value;
78
+ }
79
+ function canonicalManifestBytesForTarball(manifest) {
80
+ const clone = Object.assign({}, manifest);
81
+ delete clone.manifest_signature;
82
+ const cryptoMod = require("crypto"); // eslint-disable-line no-unused-vars
83
+ const json = JSON.stringify(canonicalizeForTarball(clone), null, 2);
84
+ return normalizeSkillBytes(Buffer.from(json, "utf8"));
85
+ }
86
+ function verifyExtractedManifestSignature(manifest, publicKeyPem) {
87
+ const cryptoMod = require("crypto");
88
+ const sig = manifest && manifest.manifest_signature;
89
+ if (!sig || typeof sig !== "object") return { status: "missing" };
90
+ if (typeof sig.signature_base64 !== "string") {
91
+ return { status: "invalid", reason: "manifest_signature.signature_base64 missing or not a string" };
92
+ }
93
+ if (sig.algorithm !== "Ed25519") {
94
+ return { status: "invalid", reason: `manifest_signature.algorithm must be 'Ed25519' (got ${JSON.stringify(sig.algorithm)})` };
95
+ }
96
+ let signatureBytes;
97
+ try { signatureBytes = Buffer.from(sig.signature_base64, "base64"); }
98
+ catch (e) { return { status: "invalid", reason: `malformed base64: ${e.message}` }; }
99
+ const bytes = canonicalManifestBytesForTarball(manifest);
100
+ let ok = false;
101
+ try {
102
+ ok = cryptoMod.verify(null, bytes, {
103
+ key: publicKeyPem,
104
+ dsaEncoding: "ieee-p1363",
105
+ }, signatureBytes);
106
+ } catch (e) {
107
+ return { status: "invalid", reason: `crypto.verify threw: ${e.message}` };
108
+ }
109
+ return ok ? { status: "valid" } : { status: "invalid", reason: "Ed25519 manifest signature did not verify against extracted public.pem" };
110
+ }
111
+
112
+ // Audit P P1-A: exported so tests/normalize-contract.test.js can assert
113
+ // byte-identical normalize() behavior across all four implementations.
114
+ module.exports = {
115
+ normalizeSkillBytes,
116
+ verifyExtractedManifestSignature,
117
+ canonicalManifestBytesForTarball,
118
+ };
119
+
61
120
  const ROOT = path.resolve(__dirname, "..");
62
121
 
63
122
  function emit(msg) { process.stdout.write(`[verify-shipped-tarball] ${msg}\n`); }
@@ -66,6 +125,16 @@ function fail(msg, code = 1) {
66
125
  process.exit(code);
67
126
  }
68
127
 
128
+ // Audit P P1-A: gate the script body behind require.main === module so
129
+ // tests can `require()` this file to load the exported helpers (notably
130
+ // normalizeSkillBytes for the byte-stability contract test) without
131
+ // invoking npm pack as a side effect of import.
132
+ if (require.main !== module) {
133
+ // Loaded as a library (e.g. by tests/normalize-contract.test.js).
134
+ // Skip the script body; consumers use the module.exports surface above.
135
+ return;
136
+ }
137
+
69
138
  const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "verify-shipped-"));
70
139
  try {
71
140
  emit(`packing into ${tmpRoot} ...`);
@@ -178,6 +247,26 @@ try {
178
247
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
179
248
  const pubPem = fs.readFileSync(pubKeyPath, "utf8");
180
249
  const pubKey = crypto.createPublicKey(pubPem);
250
+
251
+ // Audit O P1-C: verify the top-level manifest_signature on the
252
+ // EXTRACTED manifest.json. Per-skill signatures only sign the skill body
253
+ // bytes — they do not sign skill.name / skill.path / skill.atlas_refs or
254
+ // any other manifest envelope metadata. A tarball whose body bytes are
255
+ // signed but whose manifest envelope was rewritten (re-routing a skill
256
+ // path, renaming a skill, changing atlas refs) would pass per-skill
257
+ // verification but fail this gate. v0.12.17+ shipped tarballs always
258
+ // include manifest_signature, so a missing signature here is also a
259
+ // refusal (the audit's stricter posture vs. the post-install warn-and-
260
+ // continue path, which tolerates legacy v0.12.16-and-earlier installs).
261
+ const manifestSigStatus = verifyExtractedManifestSignature(manifest, pubPem);
262
+ if (manifestSigStatus.status !== "valid") {
263
+ fail(
264
+ `tarball manifest_signature ${manifestSigStatus.status} — refusing to publish. ` +
265
+ `reason=${manifestSigStatus.reason || "(none)"}`,
266
+ 1
267
+ );
268
+ }
269
+ emit(`manifest envelope signature: valid (Ed25519, signed by extracted public.pem)`);
181
270
  const pubFp = crypto.createHash("sha256")
182
271
  .update(pubKey.export({ type: "spki", format: "der" }))
183
272
  .digest("base64");