@blamejs/pki 0.1.20 → 0.1.21

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.1.21 — 2026-07-09
8
+
9
+ SLH-DSA signatures verify in certification-path validation, and the post-quantum / EdDSA parameters-absent rule is enforced across every format.
10
+
11
+ ### Added
12
+
13
+ - SLH-DSA signature verification in pki.path.validate — all twelve FIPS 205 parameter sets (id-slh-dsa-sha2-{128,192,256}{s,f} and the SHAKE sets). A certificate or CRL signed with SLH-DSA now verifies by importing the issuer's SLH-DSA public key and checking the one-shot signature over the raw signed region; the same rows serve both the certificate signature check and the CRL revocation checker. ML-DSA and the classical RSA / ECDSA / EdDSA set were already wired.
14
+ - pki.oid.paramsMustBeAbsent(oid) — a predicate that reports whether an AlgorithmIdentifier bearing the given OID must encode its parameters field as absent (the ML-DSA and SLH-DSA families and the RFC 8410 Edwards / Montgomery curves). It is the single source the shared AlgorithmIdentifier decoder consults.
15
+ - The OID registry now names the twelve pre-hash HashSLH-DSA identifiers (id-hash-slh-dsa-*, RFC 9909 §3), so a certificate or CRL that carries a HashSLH-DSA algorithm resolves to a name and is covered by the parameters-absent rule.
16
+
17
+ ### Fixed
18
+
19
+ - The shared AlgorithmIdentifier decoder now rejects a present parameters field on the algorithms whose parameters MUST be absent — ML-DSA, SLH-DSA, Ed25519, Ed448, X25519, and X448 (RFC 9909 §3, RFC 9814 §4, RFC 9881 §2, RFC 8410 §3) — failing closed with a <format>/bad-algorithm-parameters code. Previously a stray explicit NULL or arbitrary bytes in that field were surfaced raw. The rule is enforced once in the shared decoder, so every format that names an algorithm inherits it; a conforming identifier, which omits the field, is unaffected.
20
+ - Certification-path validation now enforces issuer-key / signature-algorithm consistency for the one-shot families whose public-key OID equals the signature OID — EdDSA, ML-DSA, and SLH-DSA — rejecting a mismatch with a path/algorithm-mismatch reason (RFC 9814 §4). Because the underlying WebCrypto import binds a public key of a different type to the requested algorithm name and verifies with the real key, a certificate or CRL signed by one key type but labelling its signatureAlgorithm as another one-shot type could otherwise validate; the check closes that algorithm-confusion path for both the certificate signature and the CRL revocation checker.
21
+
7
22
  ## v0.1.20 — 2026-07-09
8
23
 
9
24
  An RFC 6962 Certificate Transparency SCT-list parser joins the toolkit.
package/lib/oid.js CHANGED
@@ -217,9 +217,10 @@ var FAMILIES = {
217
217
  sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
218
218
 
219
219
  // NIST signature algorithms — FIPS 204 ML-DSA + FIPS 205 SLH-DSA share the
220
- // signature arc 2.16.840.1.101.3.4.3. The 12 Pure SLH-DSA parameter sets are
221
- // assigned sequentially (RFC 9909 §3): the SHA-2 sets .20-.25, then the SHAKE
222
- // sets .26-.31.
220
+ // signature arc 2.16.840.1.101.3.4.3. RFC 9909 §3 assigns the 12 Pure SLH-DSA
221
+ // sets .20-.31 (SHA-2 .20-.25, SHAKE .26-.31) and the 12 pre-hash HashSLH-DSA
222
+ // sets .35-.46, each pairing a parameter set with its message-digest hash. The
223
+ // parameters MUST be absent for every one of them (enforced via paramsMustBeAbsent).
223
224
  nistSig: { base: [2, 16, 840, 1, 101, 3, 4, 3], of: {
224
225
  "id-ml-dsa-44": 17, "id-ml-dsa-65": 18, "id-ml-dsa-87": 19,
225
226
  "id-slh-dsa-sha2-128s": 20, "id-slh-dsa-sha2-128f": 21,
@@ -227,7 +228,13 @@ var FAMILIES = {
227
228
  "id-slh-dsa-sha2-256s": 24, "id-slh-dsa-sha2-256f": 25,
228
229
  "id-slh-dsa-shake-128s": 26, "id-slh-dsa-shake-128f": 27,
229
230
  "id-slh-dsa-shake-192s": 28, "id-slh-dsa-shake-192f": 29,
230
- "id-slh-dsa-shake-256s": 30, "id-slh-dsa-shake-256f": 31 } },
231
+ "id-slh-dsa-shake-256s": 30, "id-slh-dsa-shake-256f": 31,
232
+ "id-hash-slh-dsa-sha2-128s-with-sha256": 35, "id-hash-slh-dsa-sha2-128f-with-sha256": 36,
233
+ "id-hash-slh-dsa-sha2-192s-with-sha512": 37, "id-hash-slh-dsa-sha2-192f-with-sha512": 38,
234
+ "id-hash-slh-dsa-sha2-256s-with-sha512": 39, "id-hash-slh-dsa-sha2-256f-with-sha512": 40,
235
+ "id-hash-slh-dsa-shake-128s-with-shake128": 41, "id-hash-slh-dsa-shake-128f-with-shake128": 42,
236
+ "id-hash-slh-dsa-shake-192s-with-shake256": 43, "id-hash-slh-dsa-shake-192f-with-shake256": 44,
237
+ "id-hash-slh-dsa-shake-256s-with-shake256": 45, "id-hash-slh-dsa-shake-256f-with-shake256": 46 } },
231
238
 
232
239
  // NIST KEM — FIPS 203 ML-KEM (arc 2.16.840.1.101.3.4.4).
233
240
  nistKem: { base: [2, 16, 840, 1, 101, 3, 4, 4], of: {
@@ -402,10 +409,58 @@ function fromDER(input) {
402
409
  return asn1.read.oid(node);
403
410
  }
404
411
 
412
+ // Algorithm identifiers whose `parameters` field MUST be absent — no explicit
413
+ // NULL, no bytes. The FIPS 204 ML-DSA and FIPS 205 SLH-DSA signature families
414
+ // and the RFC 8410 Edwards / Montgomery curves each carry this MUST: RFC 9909 §3
415
+ // (SLH-DSA in X.509), RFC 9814 §4 (SLH-DSA in CMS), RFC 9881 §2 (ML-DSA in X.509),
416
+ // RFC 8410 §3 (Ed25519 / Ed448 / X25519 / X448). Built by NAME through the same
417
+ // registry every caller uses, so a typo fails closed at load rather than silently
418
+ // dropping a member. One set drives the single shared AlgorithmIdentifier reject
419
+ // (schema-pkix), so every format consumer inherits the rule with no per-format code.
420
+ var _PARAMS_ABSENT = new Set();
421
+ [
422
+ "id-ml-dsa-44", "id-ml-dsa-65", "id-ml-dsa-87",
423
+ "id-slh-dsa-sha2-128s", "id-slh-dsa-sha2-128f", "id-slh-dsa-sha2-192s",
424
+ "id-slh-dsa-sha2-192f", "id-slh-dsa-sha2-256s", "id-slh-dsa-sha2-256f",
425
+ "id-slh-dsa-shake-128s", "id-slh-dsa-shake-128f", "id-slh-dsa-shake-192s",
426
+ "id-slh-dsa-shake-192f", "id-slh-dsa-shake-256s", "id-slh-dsa-shake-256f",
427
+ // Pre-hash HashSLH-DSA sets (RFC 9909 §3) — parameters MUST be absent too.
428
+ "id-hash-slh-dsa-sha2-128s-with-sha256", "id-hash-slh-dsa-sha2-128f-with-sha256",
429
+ "id-hash-slh-dsa-sha2-192s-with-sha512", "id-hash-slh-dsa-sha2-192f-with-sha512",
430
+ "id-hash-slh-dsa-sha2-256s-with-sha512", "id-hash-slh-dsa-sha2-256f-with-sha512",
431
+ "id-hash-slh-dsa-shake-128s-with-shake128", "id-hash-slh-dsa-shake-128f-with-shake128",
432
+ "id-hash-slh-dsa-shake-192s-with-shake256", "id-hash-slh-dsa-shake-192f-with-shake256",
433
+ "id-hash-slh-dsa-shake-256s-with-shake256", "id-hash-slh-dsa-shake-256f-with-shake256",
434
+ "Ed25519", "Ed448", "X25519", "X448",
435
+ ].forEach(function (nm) { _PARAMS_ABSENT.add(byName(nm)); });
436
+
437
+ /**
438
+ * @primitive pki.oid.paramsMustBeAbsent
439
+ * @signature pki.oid.paramsMustBeAbsent(dotted) -> boolean
440
+ * @since 0.1.21
441
+ * @status experimental
442
+ * @spec RFC 9909, RFC 9814, RFC 9881, RFC 8410
443
+ * @related pki.oid.name, pki.oid.byName
444
+ *
445
+ * True when an AlgorithmIdentifier bearing this OID MUST encode its `parameters`
446
+ * field as ABSENT (not an explicit NULL, not any bytes): the FIPS 204 ML-DSA and
447
+ * FIPS 205 SLH-DSA signature families and the RFC 8410 Edwards / Montgomery
448
+ * curves. The shared AlgorithmIdentifier decoder consults this and fails closed
449
+ * on a present-parameters violation, so every format inherits the rule.
450
+ *
451
+ * @example
452
+ * pki.oid.paramsMustBeAbsent(pki.oid.byName("id-slh-dsa-sha2-128s")); // -> true
453
+ * pki.oid.paramsMustBeAbsent(pki.oid.byName("rsaEncryption")); // -> false
454
+ */
455
+ function paramsMustBeAbsent(dotted) {
456
+ return _PARAMS_ABSENT.has(dotted);
457
+ }
458
+
405
459
  module.exports = {
406
460
  name: name,
407
461
  byName: byName,
408
462
  has: has,
463
+ paramsMustBeAbsent: paramsMustBeAbsent,
409
464
  register: register,
410
465
  registerFamily: registerFamily,
411
466
  all: all,
@@ -86,9 +86,16 @@ var SIG_ALGS = {};
86
86
  // NULL must be present — RSASSA-PKCS1-v1_5, RFC 4055 §5) or "absent" (parameters
87
87
  // must be omitted — ECDSA/EdDSA/ML-DSA, RFC 5758/8410). A cert deviating from
88
88
  // its algorithm's required shape is malformed and rejected before verify.
89
- function _sig(name, verify, imp, params, ecdsa) {
89
+ // `sameKeyOid` marks the one-shot families whose PUBLIC-KEY algorithm OID is the
90
+ // SAME as the signature algorithm OID — EdDSA, ML-DSA, SLH-DSA. For these, Node's
91
+ // WebCrypto imports an SPKI of ANOTHER type under the requested name and verifies
92
+ // with the real key (it does NOT reject a mismatched SPKI the way it does for
93
+ // RSA/ECDSA), so the issuer-key <-> signature-algorithm consistency (RFC 9814 §4)
94
+ // must be checked structurally: the SPKI OID must equal the signature OID.
95
+ function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
90
96
  var entry = { verify: verify, imp: imp, params: params };
91
97
  if (ecdsa) entry.ecdsa = true;
98
+ if (sameKeyOid) entry.sameKeyOid = true;
92
99
  SIG_ALGS[oid.byName(name)] = entry;
93
100
  }
94
101
  // RSASSA-PKCS1-v1_5 — parameters MUST be NULL.
@@ -99,13 +106,22 @@ _sig("sha512WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
99
106
  _sig("ecdsaWithSHA256", { name: "ECDSA", hash: "SHA-256" }, { name: "ECDSA" }, "absent", true);
100
107
  _sig("ecdsaWithSHA384", { name: "ECDSA", hash: "SHA-384" }, { name: "ECDSA" }, "absent", true);
101
108
  _sig("ecdsaWithSHA512", { name: "ECDSA", hash: "SHA-512" }, { name: "ECDSA" }, "absent", true);
102
- // EdDSA (one-shot, no hash parameter) — params absent.
103
- _sig("Ed25519", { name: "Ed25519" }, { name: "Ed25519" }, "absent");
104
- _sig("Ed448", { name: "Ed448" }, { name: "Ed448" }, "absent");
105
- // ML-DSA (FIPS 204) — params absent.
106
- _sig("id-ml-dsa-44", { name: "ML-DSA-44" }, { name: "ML-DSA-44" }, "absent");
107
- _sig("id-ml-dsa-65", { name: "ML-DSA-65" }, { name: "ML-DSA-65" }, "absent");
108
- _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent");
109
+ // EdDSA (one-shot, no hash parameter) — params absent; key OID == sig OID.
110
+ _sig("Ed25519", { name: "Ed25519" }, { name: "Ed25519" }, "absent", false, true);
111
+ _sig("Ed448", { name: "Ed448" }, { name: "Ed448" }, "absent", false, true);
112
+ // ML-DSA (FIPS 204) — params absent; key OID == sig OID.
113
+ _sig("id-ml-dsa-44", { name: "ML-DSA-44" }, { name: "ML-DSA-44" }, "absent", false, true);
114
+ _sig("id-ml-dsa-65", { name: "ML-DSA-65" }, { name: "ML-DSA-65" }, "absent", false, true);
115
+ _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", false, true);
116
+ // SLH-DSA (FIPS 205) — params absent; key OID == sig OID. The twelve pure sets;
117
+ // the RFC 9909 §3 OID name maps to the WebCrypto set name by id-slh-dsa-<set> ->
118
+ // SLH-DSA-<SET> (the webcrypto SLH_DSA_NODE keys). One-shot verify like ML-DSA.
119
+ ["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
120
+ "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
121
+ ].forEach(function (set) {
122
+ var wc = "SLH-DSA-" + set.toUpperCase();
123
+ _sig("id-slh-dsa-" + set, { name: wc }, { name: wc }, "absent", false, true);
124
+ });
109
125
 
110
126
  // RSASSA-PSS resolves its hash + salt from the AlgorithmIdentifier parameters.
111
127
  var OID_RSA_PSS = oid.byName("rsassaPss");
@@ -238,6 +254,24 @@ function resolveDescriptor(sigAlg) {
238
254
  return d;
239
255
  }
240
256
 
257
+ // RFC 9814 §4 issuer-key <-> signature-algorithm consistency (algorithm-confusion
258
+ // defense). For the one-shot families whose public key shares the signature OID
259
+ // (EdDSA, ML-DSA, SLH-DSA), Node's WebCrypto imports an SPKI of a DIFFERENT type
260
+ // under the requested name and verifies with the real key — so an Ed25519-signed
261
+ // certificate labelled SLH-DSA would otherwise validate. Enforce structurally:
262
+ // the issuer SPKI's algorithm OID MUST equal the signature algorithm OID. (For
263
+ // RSA/ECDSA — different key vs signature OIDs — WebCrypto's import already rejects
264
+ // a mismatched key type, so `sameKeyOid` is not set and this is a no-op.)
265
+ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
266
+ if (!d || !d.sameKeyOid) return;
267
+ var keyOid;
268
+ try { keyOid = asn1.read.oid(asn1.decode(spkiBytes).children[0].children[0]); }
269
+ catch (e) { throw E("path/algorithm-mismatch", "cannot read the issuer public-key algorithm identifier", e); }
270
+ if (keyOid !== sigOid) {
271
+ throw E("path/algorithm-mismatch", "issuer public-key algorithm " + keyOid + " does not match the signature algorithm " + sigOid + " (RFC 9814 §4 - algorithm confusion)");
272
+ }
273
+ }
274
+
241
275
  // DER Ecdsa-Sig-Value SEQUENCE { r, s } -> fixed-width r||s (P1363), rejecting
242
276
  // r or s outside [1, n-1] (CVE-2022-21449 "Psychic Signatures").
243
277
  function ecdsaDerToP1363(der, curve) {
@@ -270,8 +304,10 @@ function ecdsaDerToP1363(der, curve) {
270
304
  // Verify cert.signatureValue over cert.tbsBytes with the working public key.
271
305
  function builtinVerify(state, cert) {
272
306
  var d;
273
- try { d = resolveDescriptor(cert.signatureAlgorithm); }
274
- catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/unsupported-algorithm"), error: e }); }
307
+ try {
308
+ d = resolveDescriptor(cert.signatureAlgorithm);
309
+ assertKeyMatchesSigAlg(state.workingPublicKey, cert.signatureAlgorithm.oid, d);
310
+ } catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/unsupported-algorithm"), error: e }); }
275
311
  // The signature is an octet-aligned BIT STRING (no unused bits) for every
276
312
  // supported algorithm; a non-zero unused-bit count is malformed.
277
313
  if (cert.signatureValue.unusedBits !== 0) return Promise.resolve({ ok: false, code: "path/bad-signature" });
@@ -1387,8 +1423,10 @@ function crlEntryReason(entry) {
1387
1423
 
1388
1424
  function verifyCrlSignature(theCrl, issuer) {
1389
1425
  var d;
1390
- try { d = resolveDescriptor(theCrl.signatureAlgorithm); }
1391
- catch (_e) { return Promise.resolve(false); }
1426
+ try {
1427
+ d = resolveDescriptor(theCrl.signatureAlgorithm);
1428
+ assertKeyMatchesSigAlg(issuer.workingPublicKey, theCrl.signatureAlgorithm.oid, d);
1429
+ } catch (_e) { return Promise.resolve(false); }
1392
1430
  if (theCrl.signatureValue.unusedBits !== 0) return Promise.resolve(false); // non-octet-aligned signature
1393
1431
  var key;
1394
1432
  return subtle.importKey("spki", issuer.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
@@ -156,6 +156,15 @@ function algorithmIdentifier(ns, opts) {
156
156
  arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
157
157
  build: function (m, ctx) {
158
158
  var dotted = m.fields.algorithm.value;
159
+ // RFC 9909 §3 / RFC 9814 §4 / RFC 9881 §2 / RFC 8410 §3: for the PQC
160
+ // signature families (ML-DSA, SLH-DSA) and the Edwards/Montgomery curves,
161
+ // the `parameters` field MUST be absent — no explicit NULL, no bytes.
162
+ // Enforced ONCE here so every consumer of the shared AlgorithmIdentifier
163
+ // inherits the rule; the guard-parity bug class is structurally impossible.
164
+ if (m.fields.parameters.present && ctx.oid.paramsMustBeAbsent(dotted)) {
165
+ throw ctx.E(ctx.prefix + "/bad-algorithm-parameters",
166
+ "the " + (ctx.oid.name(dotted) || dotted) + " AlgorithmIdentifier parameters field MUST be absent (RFC 9909 §3 / RFC 9814 §4 / RFC 9881 §2 / RFC 8410 §3)");
167
+ }
159
168
  return { oid: dotted, name: ctx.oid.name(dotted) || null, parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null };
160
169
  },
161
170
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:f23ca197-835e-4ec8-8d04-486fb3d9deac",
5
+ "serialNumber": "urn:uuid:bcaea9d8-cd0d-4a9f-8ac3-72b3efecc8be",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-09T14:17:20.162Z",
8
+ "timestamp": "2026-07-09T15:12:46.820Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.1.20",
22
+ "bom-ref": "@blamejs/pki@0.1.21",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.20",
25
+ "version": "0.1.21",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.1.20",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.21",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.1.20",
57
+ "ref": "@blamejs/pki@0.1.21",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]