@blamejs/pki 0.2.16 → 0.2.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.
@@ -40,6 +40,9 @@ var crl = require("./schema-crl");
40
40
  var ocsp = require("./schema-ocsp");
41
41
  var guard = require("./guard-all");
42
42
  var constants = require("./constants");
43
+ var validator = require("./validator-all");
44
+ var compositeSig = require("./composite-sig");
45
+ var edwardsPoint = require("./edwards-point");
43
46
 
44
47
  var PathError = errors.PathError;
45
48
  function E(code, message, cause) { return new PathError(code, message, cause); }
@@ -114,6 +117,10 @@ function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
114
117
  var entry = { verify: verify, imp: imp, params: params };
115
118
  if (ecdsa) entry.ecdsa = true;
116
119
  if (sameKeyOid) entry.sameKeyOid = true;
120
+ // EdDSA descriptors carry the Edwards curve id (6 = Ed25519, 7 = Ed448) so the verify path
121
+ // validates the issuer point through the shared gate without re-branching on the algorithm name.
122
+ if (verify.name === "Ed25519") entry.eddsa = 6;
123
+ else if (verify.name === "Ed448") entry.eddsa = 7;
117
124
  SIG_ALGS[oid.byName(name)] = entry;
118
125
  }
119
126
  // RSASSA-PKCS1-v1_5 -- parameters MUST be NULL.
@@ -151,13 +158,8 @@ HASH_BY_OID[oid.byName("sha256")] = "SHA-256";
151
158
  HASH_BY_OID[oid.byName("sha384")] = "SHA-384";
152
159
  HASH_BY_OID[oid.byName("sha512")] = "SHA-512";
153
160
 
154
- var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
155
- // Curve group orders n -- a valid ECDSA signature has r,s in [1, n-1].
156
- var CURVE_ORDER = {
157
- "P-256": BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),
158
- "P-384": BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"),
159
- "P-521": BigInt("0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409"),
160
- };
161
+ // The order-aware ECDSA DER->P1363 converter + its CURVE_FIELD_BYTES / CURVE_ORDER tables now
162
+ // live in validator-sig.js (validator.sig.ecdsaDerToP1363), shared with the composite engine.
161
163
 
162
164
  // The algorithm OID of an AlgorithmIdentifier SEQUENCE { algorithm OID,
163
165
  // parameters OPTIONAL }. STRICT: a universal SEQUENCE with the OID and AT MOST
@@ -187,6 +189,9 @@ function hashAlgOid(seq) {
187
189
  }
188
190
  // The hash OID inside an EXPLICIT [n] wrapper around a hash AlgorithmIdentifier.
189
191
  function explicitHashAlgOid(wrapper) {
192
+ // Coverage residual -- unreachable: the sole caller (resolveRsaPss) already
193
+ // asserts the EXPLICIT wrapper carries exactly one child before calling this, so
194
+ // this identical inner check cannot fire; it is a local defense-in-depth backstop.
190
195
  if (!wrapper.children || wrapper.children.length !== 1) throw E("path/unsupported-algorithm", "malformed EXPLICIT hash AlgorithmIdentifier");
191
196
  return hashAlgOid(wrapper.children[0]);
192
197
  }
@@ -258,16 +263,11 @@ function isDerNull(p) { return p && p.length === 2 && p[0] === 0x05 && p[1] ===
258
263
 
259
264
  function resolveDescriptor(sigAlg) {
260
265
  if (sigAlg.oid === OID_RSA_PSS) return resolveRsaPss(sigAlg.parameters);
261
- var comp = COMPOSITE_ALGS[sigAlg.oid];
262
- if (comp) {
263
- // draft-ietf-lamps-pq-composite-sigs sec. 5.3: parameters MUST be absent.
264
- if (sigAlg.parameters !== null && sigAlg.parameters !== undefined) {
265
- throw E("path/unsupported-algorithm", "composite signature algorithm parameters must be absent (draft-ietf-lamps-pq-composite-sigs sec. 5.3)");
266
- }
267
- // The composite public-key algorithm OID equals the signature OID; enforce
268
- // the RFC 9814-style key<->signature consistency structurally (sameKeyOid).
269
- return { composite: comp, sameKeyOid: true };
270
- }
266
+ // Composite ML-DSA: the OID-keyed registry + the parameters-absent check
267
+ // (draft-ietf-lamps-pq-composite-sigs sec. 5.3) live in composite-sig.js, shared with
268
+ // CMS. sameKeyOid enforces the RFC 9814 sec. 4 key<->signature OID consistency.
269
+ var comp = compositeSig.resolveCompositeDescriptor(sigAlg, PathError, "path/unsupported-algorithm");
270
+ if (comp) return comp;
271
271
  var d = SIG_ALGS[sigAlg.oid];
272
272
  if (!d) throw E("path/unsupported-algorithm", "no verify descriptor for signature algorithm " + (sigAlg.name || sigAlg.oid));
273
273
  // The signatureAlgorithm's parameters MUST match the algorithm's fixed shape:
@@ -297,169 +297,15 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
297
297
  }
298
298
  }
299
299
 
300
- // DER Ecdsa-Sig-Value SEQUENCE { r, s } -> fixed-width r||s (P1363), rejecting
301
- // r or s outside [1, n-1] (CVE-2022-21449 "Psychic Signatures").
302
- function ecdsaDerToP1363(der, curve) {
303
- var width = CURVE_FIELD_BYTES[curve];
304
- var order = CURVE_ORDER[curve];
305
- if (!width || !order) throw E("path/bad-signature", "unsupported ECDSA curve " + curve);
306
- var n;
307
- try { n = asn1.decode(der); }
308
- catch (e) { throw E("path/bad-signature", "ECDSA signature is not a DER SEQUENCE(r,s)", e); }
309
- if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children || n.children.length !== 2) {
310
- throw E("path/bad-signature", "ECDSA signature must be a SEQUENCE of exactly two INTEGERs");
311
- }
312
- var r = asn1.read.integer(n.children[0]);
313
- var s = asn1.read.integer(n.children[1]);
314
- if (r < 1n || s < 1n || r >= order || s >= order) {
315
- throw E("path/bad-signature", "ECDSA signature component out of range [1, n-1] (CVE-2022-21449)");
316
- }
317
- function pad(v) {
318
- var hex = v.toString(16);
319
- if (hex.length % 2) hex = "0" + hex;
320
- var buf = Buffer.from(hex, "hex");
321
- if (buf.length > width) throw E("path/bad-signature", "ECDSA signature component wider than the curve field");
322
- var out = Buffer.alloc(width);
323
- buf.copy(out, width - buf.length);
324
- return out;
325
- }
326
- return Buffer.concat([pad(r), pad(s)]);
327
- }
300
+ // ecdsaDerToP1363 relocated to validator-sig.js; the composite trad-ECDSA and classical ECDSA
301
+ // paths call validator.sig.ecdsaDerToP1363(sig, curve, PathError, "path/bad-signature").
328
302
 
329
303
  // ---- composite ML-DSA signatures (draft-ietf-lamps-pq-composite-sigs) -------
330
- // A composite signature pairs a post-quantum ML-DSA with a traditional RSA /
331
- // ECDSA / EdDSA. The public key is the raw concatenation mldsaPK || tradPK
332
- // (sec. 4.1) in the SPKI BIT STRING; the signature is mldsaSig || tradSig
333
- // (sec. 4.3) in the signatureValue BIT STRING. Both are surfaced RAW by the
334
- // x509 parser (no sub-walk). Verification (sec. 2): reconstruct
335
- // M' = Prefix || Label || len(ctx) || ctx || PH(M),
336
- // verify the ML-DSA component over M' with ctx = the composite Label, verify the
337
- // traditional component over M' under its own hash, and accept IFF BOTH pass
338
- // (THREAT-MODEL: all components must verify -- never an AND-to-OR downgrade). The
339
- // ML-DSA component is the fixed-length FIRST half; the split point is its length.
340
- var COMPOSITE_PREFIX = Buffer.from("CompositeAlgorithmSignatures2025", "ascii");
341
- var MLDSA_COMPONENT = {
342
- "ML-DSA-44": { pk: 1312, sig: 2420, oid: "id-ml-dsa-44" },
343
- "ML-DSA-65": { pk: 1952, sig: 3309, oid: "id-ml-dsa-65" },
344
- "ML-DSA-87": { pk: 2592, sig: 4627, oid: "id-ml-dsa-87" },
345
- };
346
- var EC_CURVE_OID = { "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1" };
347
- var COMPOSITE_ALGS = {};
348
- // _comp(name, mldsa, ph, label, trad). `trad` is exactly one component shape:
349
- // { ec, hash } | { eddsa } | { rsaPss, hash, salt } | { rsaPkcs1, hash } |
350
- // { unsupported } for the arms Node's WebCrypto surface cannot verify (brainpool
351
- // curves; the SHAKE256/64 pre-hash) -- registered + params-guarded, deferred at
352
- // verify to path/unsupported-algorithm rather than silently accepted.
353
- function _comp(name, mldsa, ph, label, trad) {
354
- var sz = MLDSA_COMPONENT[mldsa];
355
- COMPOSITE_ALGS[oid.byName(name)] = {
356
- name: name, mldsa: mldsa, mldsaPk: sz.pk, mldsaSig: sz.sig, mldsaOid: sz.oid,
357
- ph: ph, label: Buffer.from(label, "ascii"), trad: trad,
358
- };
359
- }
360
- _comp("id-MLDSA44-RSA2048-PSS-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-RSA2048-PSS-SHA256", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 2048 });
361
- _comp("id-MLDSA44-RSA2048-PKCS15-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-RSA2048-PKCS15-SHA256", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 2048 });
362
- _comp("id-MLDSA44-Ed25519-SHA512", "ML-DSA-44", "SHA-512", "COMPSIG-MLDSA44-Ed25519-SHA512", { eddsa: "Ed25519" });
363
- _comp("id-MLDSA44-ECDSA-P256-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-ECDSA-P256-SHA256", { ec: "P-256", hash: "SHA-256" });
364
- _comp("id-MLDSA65-RSA3072-PSS-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA3072-PSS-SHA512", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 3072 });
365
- _comp("id-MLDSA65-RSA3072-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA3072-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 3072 });
366
- _comp("id-MLDSA65-RSA4096-PSS-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA4096-PSS-SHA512", { rsaPss: true, hash: "SHA-384", salt: 48, rsaBits: 4096 });
367
- _comp("id-MLDSA65-RSA4096-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA4096-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-384", rsaBits: 4096 });
368
- _comp("id-MLDSA65-ECDSA-P256-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P256-SHA512", { ec: "P-256", hash: "SHA-256" });
369
- _comp("id-MLDSA65-ECDSA-P384-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
370
- _comp("id-MLDSA65-ECDSA-brainpoolP256r1-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-BP256-SHA512", { unsupported: "brainpoolP256r1 is not in the WebCrypto ECDSA curve set" });
371
- _comp("id-MLDSA65-Ed25519-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-Ed25519-SHA512", { eddsa: "Ed25519" });
372
- _comp("id-MLDSA87-ECDSA-P384-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
373
- _comp("id-MLDSA87-ECDSA-brainpoolP384r1-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-BP384-SHA512", { unsupported: "brainpoolP384r1 is not in the WebCrypto ECDSA curve set" });
374
- _comp("id-MLDSA87-Ed448-SHAKE256", "ML-DSA-87", "SHAKE256", "COMPSIG-MLDSA87-Ed448-SHAKE256", { unsupported: "the SHAKE256/64 pre-hash is not in the WebCrypto digest set" });
375
- _comp("id-MLDSA87-RSA3072-PSS-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-RSA3072-PSS-SHA512", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 3072 });
376
- _comp("id-MLDSA87-RSA4096-PSS-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-RSA4096-PSS-SHA512", { rsaPss: true, hash: "SHA-384", salt: 48, rsaBits: 4096 });
377
- _comp("id-MLDSA87-ECDSA-P521-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P521-SHA512", { ec: "P-521", hash: "SHA-512" });
378
-
379
- var _b = asn1.build;
380
- // Wrap a raw component public key in the SPKI its WebCrypto import expects, so
381
- // each half is verified through the SAME import + verify seam the classical path
382
- // uses (no second parallel verify path). tradPK for RSA is the RSAPublicKey DER;
383
- // for EC the uncompressed point; for EdDSA the raw public key.
384
- function _spkiFor(algNode, keyBytes) { return _b.sequence([algNode, _b.bitString(keyBytes, 0)]); }
385
- function _mldsaSpki(mldsaOid, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(mldsaOid))]), raw); }
386
- function _ecSpki(curve, point) { return _spkiFor(_b.sequence([_b.oid(oid.byName("ecPublicKey")), _b.oid(oid.byName(EC_CURVE_OID[curve]))]), point); }
387
- function _edSpki(name, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(name))]), raw); }
388
- function _rsaSpki(rsaPub) { return _spkiFor(_b.sequence([_b.oid(oid.byName("rsaEncryption")), _b.nullValue()]), rsaPub); }
389
-
390
- // The exact modulus bit length of an RSAPublicKey (SEQUENCE { modulus, exponent }).
391
- function _rsaModulusBits(rsaPubDer) {
392
- return asn1.read.integer(asn1.decode(rsaPubDer).children[0]).toString(2).length;
393
- }
394
-
395
- function _verifyTradComponent(trad, tradPK, tradSig, mprime) {
396
- if (trad.ec) {
397
- // draft sec. 5.1: the EC point MUST be the uncompressed X9.62 form (leading 0x04).
398
- // A compressed / hybrid point is a non-conforming composite component encoding.
399
- if (tradPK.length < 1 || tradPK[0] !== 0x04) return Promise.resolve(false);
400
- return subtle.importKey("spki", _ecSpki(trad.ec, tradPK), { name: "ECDSA" }, false, ["verify"]).then(function (k) {
401
- // The traditional ECDSA signature is DER Ecdsa-Sig-Value; convert to P1363
402
- // through the shared reader that also rejects r/s outside [1,n-1] (CVE-2022-21449).
403
- var p1363 = ecdsaDerToP1363(tradSig, trad.ec);
404
- return subtle.verify({ name: "ECDSA", hash: trad.hash }, k, p1363, mprime);
405
- });
406
- }
407
- if (trad.eddsa) {
408
- return subtle.importKey("spki", _edSpki(trad.eddsa, tradPK), { name: trad.eddsa }, false, ["verify"])
409
- .then(function (k) { return subtle.verify({ name: trad.eddsa }, k, tradSig, mprime); });
410
- }
411
- if (trad.rsaPss || trad.rsaPkcs1) {
412
- // The composite OID fixes the RSA modulus size: a downgraded or mismatched modulus
413
- // under the declared OID (an id-MLDSA44-RSA2048-* whose component is really 1024-bit)
414
- // is rejected BEFORE verify, so a weak RSA component cannot satisfy an arm that
415
- // promises 2048/3072/4096 bits. A malformed RSAPublicKey rejects the same way.
416
- var bits;
417
- try { bits = _rsaModulusBits(tradPK); }
418
- catch (_e) { return Promise.resolve(false); }
419
- if (bits !== trad.rsaBits) return Promise.resolve(false);
420
- if (trad.rsaPss) {
421
- return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSA-PSS", hash: trad.hash }, false, ["verify"])
422
- .then(function (k) { return subtle.verify({ name: "RSA-PSS", saltLength: trad.salt }, k, tradSig, mprime); });
423
- }
424
- return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"])
425
- .then(function (k) { return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, k, tradSig, mprime); });
426
- }
427
- return Promise.resolve(false);
428
- }
429
-
430
- // Verify a composite signature: `spkiBytes` the issuer SPKI DER, `sigBytes` the
431
- // raw signatureValue (mldsaSig || tradSig), `message` the signed region (tbsBytes).
432
- function compositeVerify(spkiBytes, sigBytes, message, d) {
433
- if (d.trad.unsupported) {
434
- return Promise.resolve({ ok: false, code: "path/unsupported-algorithm",
435
- error: E("path/unsupported-algorithm", "composite " + d.name + ": " + d.trad.unsupported) });
436
- }
437
- var rawKey;
438
- try {
439
- var bs = asn1.read.bitString(asn1.decode(spkiBytes).children[1]);
440
- // The composite subjectPublicKey is an octet-aligned concatenation (no unused
441
- // bits); a non-zero unused-bit count is malformed.
442
- if (bs.unusedBits !== 0) throw E("path/bad-signature", "composite subjectPublicKey has unused bits");
443
- rawKey = bs.bytes;
444
- } catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/bad-signature"), error: e }); }
445
- // The ML-DSA half is fixed-length and FIRST; the traditional half is the
446
- // remainder. Both must be non-empty for a well-formed composite.
447
- if (rawKey.length <= d.mldsaPk || sigBytes.length <= d.mldsaSig) {
448
- return Promise.resolve({ ok: false, code: "path/bad-signature",
449
- error: E("path/bad-signature", "composite key/signature shorter than the fixed ML-DSA component") });
450
- }
451
- var mldsaPK = rawKey.subarray(0, d.mldsaPk), tradPK = rawKey.subarray(d.mldsaPk);
452
- var mldsaSig = sigBytes.subarray(0, d.mldsaSig), tradSig = sigBytes.subarray(d.mldsaSig);
453
- return subtle.digest({ name: d.ph }, message).then(function (phBuf) {
454
- // M' = Prefix || Label || len(ctx)=0 || ctx="" || PH(M). In X.509 path
455
- // validation the application context is empty (a caller ctx is out of scope).
456
- var mprime = Buffer.concat([COMPOSITE_PREFIX, d.label, Buffer.from([0]), Buffer.from(phBuf)]);
457
- var mldsaP = subtle.importKey("spki", _mldsaSpki(d.mldsaOid, mldsaPK), { name: d.mldsa }, false, ["verify"])
458
- .then(function (mk) { return subtle.verify({ name: d.mldsa, context: d.label }, mk, mldsaSig, mprime); });
459
- var tradP = _verifyTradComponent(d.trad, tradPK, tradSig, mprime);
460
- return Promise.all([mldsaP, tradP]).then(function (r) { return { ok: r[0] === true && r[1] === true }; });
461
- }).catch(function (e) { return { ok: false, code: pathCode(e, "path/bad-signature"), error: e }; });
462
- }
304
+ // The composite verify/sign engine + the COMPOSITE_ALGS OID-keyed registry live in
305
+ // composite-sig.js (shared with CMS composite SignerInfo). Path validation composes it:
306
+ // resolveDescriptor (above) delegates to compositeSig.resolveCompositeDescriptor, the
307
+ // certificate + OCSP verify paths to compositeSig.compositeVerify, and
308
+ // compositeKeyUsageCheck (below) enforces the sec. 5.2 signature-only keyUsage restriction.
463
309
 
464
310
  // draft-ietf-lamps-pq-composite-sigs sec. 5.2: a certificate whose SubjectPublicKeyInfo
465
311
  // carries a composite ML-DSA OID, IF it has a keyUsage extension, MUST assert at least
@@ -485,6 +331,19 @@ function compositeKeyUsageCheck(cert) {
485
331
  return { ok: true };
486
332
  }
487
333
 
334
+ // Import a descriptor's verification key, validating an EdDSA point FIRST: node/OpenSSL import a
335
+ // low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without complaint and such a key
336
+ // verifies a forged signature. This is the ONE seam both the certificate path and the revocation
337
+ // (CRL / OCSP-response) path import through, so neither can skip the point gate -- a low-order
338
+ // issuer / responder key fails the caller closed (a rejected promise the caller maps to a bad
339
+ // verdict), never verifying a forged chain or a forged revocation.
340
+ function _importVerifyKey(spkiBytes, d) {
341
+ try {
342
+ if (d.eddsa) edwardsPoint.validateSpki(spkiBytes, d.eddsa, PathError, "path/bad-signature");
343
+ } catch (e) { return Promise.reject(e); }
344
+ return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]);
345
+ }
346
+
488
347
  // Verify cert.signatureValue over cert.tbsBytes with the working public key.
489
348
  function builtinVerify(state, cert) {
490
349
  var d;
@@ -498,12 +357,12 @@ function builtinVerify(state, cert) {
498
357
  // A composite signature verifies its ML-DSA and traditional halves and accepts
499
358
  // IFF both pass -- delegated to the composite combinator (which reuses this
500
359
  // file's ECDSA range-check + the same import/verify seam).
501
- if (d.composite) return compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite);
360
+ if (d.composite) return compositeSig.compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature");
502
361
  var key;
503
- return subtle.importKey("spki", state.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
362
+ return _importVerifyKey(state.workingPublicKey, d).then(function (k) {
504
363
  key = k;
505
364
  var sig = cert.signatureValue.bytes;
506
- if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
365
+ if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
507
366
  return subtle.verify(d.verify, key, sig, cert.tbsBytes);
508
367
  }).then(function (ok) {
509
368
  return { ok: ok === true };
@@ -714,6 +573,9 @@ function certNameForms(cert) {
714
573
  // constraint check fails such a form closed (name-constraint-unsupported).
715
574
  san.value.names.forEach(function (nm) {
716
575
  if (nm.tagNumber === 1) hasRfc822San = true; // an rfc822Name SAN carries the email identity
576
+ // Coverage residual -- the `undefined -> null` arm is unreachable: the SAN
577
+ // decoder (schema-pkix altName, decodeValue:true) routes through generalName,
578
+ // which already maps an undecoded value to null, so nm.value is never undefined.
717
579
  forms.push({ tag: nm.tagNumber, value: nm.value === undefined ? null : nm.value });
718
580
  });
719
581
  }
@@ -808,6 +670,8 @@ function treeWithoutParent(node) {
808
670
  }
809
671
  function leavesAt(tree, depth) {
810
672
  var out = [];
673
+ // Coverage residual -- unreachable: every call site guards state.validPolicyTree
674
+ // truthy before calling leavesAt; the null-tree early return is a backstop.
811
675
  if (!tree) return out; // a pruned-empty tree has no nodes
812
676
  (function walk(node) {
813
677
  if (node.depth === depth) { out.push(node); return; }
@@ -1135,6 +999,10 @@ function applyPolicyMappings(state, mappings, i) {
1135
999
  // extension may have already emptied the tree -- stop if it is gone.
1136
1000
  var mappedSet = {};
1137
1001
  mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
1002
+ // Coverage residual -- unreachable today: applyPolicyMappings is invoked once per
1003
+ // certificate under an `if (state.validPolicyTree)` guard and does not null the
1004
+ // tree before this point. Retained as a cheap correctness backstop for a future
1005
+ // per-mapping-batch refactor; do not remove.
1138
1006
  if (!state.validPolicyTree) return;
1139
1007
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
1140
1008
  if (mappedSet[node.validPolicy] && node.parent) {
@@ -1341,7 +1209,7 @@ async function validate(path, opts) {
1341
1209
  // draft-ietf-lamps-pq-composite-sigs sec. 5.2: a composite-keyed certificate's
1342
1210
  // keyUsage must be signature-only (no dual-usage). Runs for the target AND every
1343
1211
  // intermediate whose own subject key is composite.
1344
- if (COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
1212
+ if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
1345
1213
  var cku = compositeKeyUsageCheck(cert);
1346
1214
  checks.push({ name: "compositeKeyUsage", ok: cku.ok, code: cku.ok ? undefined : cku.code });
1347
1215
  if (!cku.ok) failed = true;
@@ -1773,6 +1641,8 @@ function crlChecker(crls) {
1773
1641
  // process (anything but reasonCode) makes the CRL unusable for ANY
1774
1642
  // certificate, not just the entry that carries it.
1775
1643
  for (var ry = 0; ry < theCrl.revokedCertificates.length && !unhandledCritical; ry++) {
1644
+ // Coverage residual -- the `|| []` fallback is unreachable: schema-crl sets
1645
+ // crlEntryExtensions to [] when absent, so it is always an array.
1776
1646
  var ees = theCrl.revokedCertificates[ry].crlEntryExtensions || [];
1777
1647
  for (var ex = 0; ex < ees.length; ex++) {
1778
1648
  // Key on the stable OID only -- a display name is registry-dependent
@@ -1893,6 +1763,8 @@ function crlChecker(crls) {
1893
1763
  // number), or null when absent.
1894
1764
  var OID_REASON_CODE = oid.byName("reasonCode");
1895
1765
  function crlEntryReason(entry) {
1766
+ // Coverage residual -- the `|| []` fallback is unreachable: schema-crl guarantees
1767
+ // crlEntryExtensions is an array (empty when absent).
1896
1768
  var exts = entry.crlEntryExtensions || [];
1897
1769
  for (var i = 0; i < exts.length; i++) {
1898
1770
  if (exts[i].oid === OID_REASON_CODE) return exts[i].value; // stable OID, not the display name
@@ -1918,10 +1790,10 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
1918
1790
  // A composite-signed CRL / OCSP response verifies through the same combinator
1919
1791
  // (both halves must pass) that the certificate path uses -- one composite verify,
1920
1792
  // never a second parallel one.
1921
- if (d.composite) return compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite).then(function (r) { return r.ok === true; });
1922
- return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]).then(function (key) {
1793
+ if (d.composite) return compositeSig.compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature").then(function (r) { return r.ok === true; });
1794
+ return _importVerifyKey(spkiBytes, d).then(function (key) {
1923
1795
  var sig = rawSig;
1924
- if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
1796
+ if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
1925
1797
  return subtle.verify(d.verify, key, sig, tbsBytes);
1926
1798
  }).then(function (ok) { return ok === true; }, function () { return false; });
1927
1799
  }
@@ -1963,6 +1835,9 @@ function ocspResponderSpki(rc, issuer) {
1963
1835
  var keyAlg = rc.subjectPublicKeyInfo.algorithm;
1964
1836
  if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
1965
1837
  var issuerOid, issuerParams;
1838
+ // Coverage residual -- the catch is unreachable: this function runs only after the
1839
+ // delegate's signature verified under issuer.workingPublicKey, so that SPKI already
1840
+ // imported and decodes cleanly here. (The children[1] ? : null false side IS covered.)
1966
1841
  try {
1967
1842
  var alg = asn1.decode(issuer.workingPublicKey).children[0];
1968
1843
  issuerOid = asn1.read.oid(alg.children[0]);
@@ -2082,7 +1957,7 @@ async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits
2082
1957
  // A composite-keyed delegate is an out-of-path signer cert and gets the same
2083
1958
  // composite keyUsage gate the path certificates do (draft sec. 5.2): a dual-usage
2084
1959
  // composite responder key (a forbidden encryption bit set) is not authorized.
2085
- if (COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
1960
+ if (compositeSig.COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
2086
1961
  return ocspResponderSpki(rc, issuer);
2087
1962
  }
2088
1963
  return null;
@@ -2149,6 +2024,9 @@ function ocspChecker(responses) {
2149
2024
  var resp = parsed[k];
2150
2025
  if (resp.responseStatus.code !== 0) continue; // non-successful -> conveys no status
2151
2026
  var br = resp.basicResponse;
2027
+ // Coverage residual -- unreachable for a parsed response: schema-ocsp enforces
2028
+ // the successful-status <-> responseBytes biconditional, so a code-0 response
2029
+ // always carries a basicResponse. Backstop for a hand-built object.
2152
2030
  if (!br) continue;
2153
2031
  var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
2154
2032
  if (!signerSpki) continue; // unauthorized responder
package/lib/schema-cms.js CHANGED
@@ -146,6 +146,9 @@ function _checkAttrPlacement(attrs, place) {
146
146
  for (var i = 0; i < attrs.length; i++) {
147
147
  var row = ATTR_FORBIDDEN_IN[attrs[i].type];
148
148
  if (row && row[place]) {
149
+ // Coverage residual -- reached only when the OID is a registry entry (the guard above
150
+ // guarantees it), so oid.name() never returns undefined and this fallback is a
151
+ // defensive belt, not selectable through the public parse path.
149
152
  throw NS.E("cms/misplaced-attr", "the " + (oid.name(attrs[i].type) || attrs[i].type) +
150
153
  " attribute must not be " + ATTR_PLACE_LABELS[place] + " (RFC 5652 sec. 11)");
151
154
  }
@@ -250,6 +253,10 @@ function _validateAeadParams(alg, macLen) {
250
253
  // sec. 3.1/sec. 3.2: the parameters field MUST be present and carry the Parameters SEQUENCE.
251
254
  if (alg.parameters === null) throw NS.E("cms/bad-aead-params", "an AES-" + K + " content-encryption algorithm MUST carry its parameters (RFC 5084 sec. 3." + (kind === "gcm" ? "2" : "1") + ")");
252
255
  var node;
256
+ // Coverage residual -- alg.parameters is an AlgorithmIdentifier child already fully
257
+ // validated by the eager-recursive asn1.decode during the enclosing SEQUENCE parse;
258
+ // re-decoding the same TLV cannot fault, so this catch is an unreachable fail-closed
259
+ // belt (malformed parameters are rejected as asn1/* before _validateAeadParams runs).
253
260
  try { node = asn1.decode(alg.parameters); }
254
261
  catch (e) { throw NS.E("cms/bad-aead-params", "malformed AES-" + K + " parameters", e); }
255
262
  if (node.tagClass !== "universal" || node.tagNumber !== T.SEQUENCE || !node.children || node.children.length < 1 || node.children.length > 2) {
@@ -739,12 +746,18 @@ var KEM_RECIPIENT_INFO = schema.seq([
739
746
  // recognized AES key-wrap pins the exact KEK size (registry, not switch).
740
747
  var wrapLen = WRAP_KEK_LENGTHS[wrap.oid];
741
748
  if (wrapLen !== undefined && kekLength !== wrapLen) {
749
+ // Coverage residual -- reached only when the OID is a registry entry (the guard above
750
+ // guarantees it), so oid.name() never returns undefined and this fallback is a
751
+ // defensive belt, not selectable through the public parse path.
742
752
  throw NS.E("cms/kek-length-mismatch", "kekLength " + kekLength + " does not match the " + (oid.name(wrap.oid) || wrap.oid) + " KEK size " + wrapLen + " (RFC 9629 sec. 3)");
743
753
  }
744
754
  // FIPS 203 -- a recognized ML-KEM kem produces a fixed-size ciphertext; any
745
755
  // other kemct length can never decapsulate.
746
756
  var ctLen = KEM_CT_LENGTHS[kem.oid];
747
757
  if (ctLen !== undefined && kemct.length !== ctLen) {
758
+ // Coverage residual -- reached only when the OID is a registry entry (the guard above
759
+ // guarantees it), so oid.name() never returns undefined and this fallback is a
760
+ // defensive belt, not selectable through the public parse path.
748
761
  throw NS.E("cms/bad-kem-ciphertext", "the " + (oid.name(kem.oid) || kem.oid) + " kemct must be exactly " + ctLen + " octets (FIPS 203)");
749
762
  }
750
763
  return {
@@ -1049,6 +1062,9 @@ var CONTENT_INFO = schema.seq([
1049
1062
  else if (ct === OID_AUTH_ENVELOPED_DATA) inner = AUTH_ENVELOPED_DATA;
1050
1063
  if (inner === null) {
1051
1064
  if (DEFERRED.has(ct)) {
1065
+ // Coverage residual -- reached only when the OID is a registry entry (the guard above
1066
+ // guarantees it), so oid.name() never returns undefined and this fallback is a
1067
+ // defensive belt, not selectable through the public parse path.
1052
1068
  throw NS.E("cms/unsupported-content-type", (ctx.oid.name(ct) || ct) + " is recognized but not parsed by this build");
1053
1069
  }
1054
1070
  throw NS.E("cms/unknown-content-type", "unrecognized ContentInfo content type " + ct);
@@ -1058,6 +1074,9 @@ var CONTENT_INFO = schema.seq([
1058
1074
  // a consumer branches on contentTypeName instead of duck-typing the shape.
1059
1075
  var result = schema.walk(inner, m.fields.content.value, ctx).result;
1060
1076
  result.contentType = ct;
1077
+ // Coverage residual -- reached only when the OID is a registry entry (the guard above
1078
+ // guarantees it), so oid.name() never returns undefined and this fallback is a
1079
+ // defensive belt, not selectable through the public parse path.
1061
1080
  result.contentTypeName = ctx.oid.name(ct) || null;
1062
1081
  return result;
1063
1082
  },
package/lib/sigstore.js CHANGED
@@ -205,6 +205,7 @@ function _sha256(buf) { return nodeCrypto.createHash("sha256").update(buf).diges
205
205
  // A validFor bound is an epoch-ms number, a Date, or an ISO-8601 string (the form
206
206
  // a Sigstore trusted_root carries). Returns null for absent/unparseable.
207
207
  function _toMs(x) {
208
+ // Coverage residual -- _inWindow gates every _toMs call behind a != null check; the null guard is dead defensive code.
208
209
  if (x == null) return null;
209
210
  if (typeof x === "number") return Number.isFinite(x) ? x : null;
210
211
  if (x instanceof Date) { var d = x.getTime(); return isNaN(d) ? null : d; }
@@ -418,6 +419,7 @@ async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
418
419
  if (res.valid) return;
419
420
  lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain is not valid as of the log time");
420
421
  } catch (e) {
422
+ // Coverage residual -- pathValidate.validate is fail-safe (builtinVerify never throws; every decodeExt wrapped), so no raw throw escapes for a well-formed path; the catch is a defensive re-type.
421
423
  lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain failed validation: " + e.message, e);
422
424
  }
423
425
  }
@@ -439,6 +441,7 @@ function _identity(leaf) {
439
441
  // malformed certificate and fails closed with a named reason (never a silent
440
442
  // drop that would hide an identity claim a caller policy relies on).
441
443
  try {
444
+ // Coverage residual -- x509.parse always surfaces extensions as an array.
442
445
  (leaf.extensions || []).forEach(function (ext) {
443
446
  if (ext.oid === SAN_OID) { out.san = _sanValue(ext); return; }
444
447
  if (ext.oid.indexOf(FULCIO_PREFIX) === 0) {
@@ -458,6 +461,7 @@ var GN_TYPE = { 1: "rfc822Name", 2: "dNSName", 6: "uri" };
458
461
  function _sanValue(ext) {
459
462
  var seq = asn1.decode(ext.value);
460
463
  var names = [];
464
+ // Coverage residual -- path.validate's GeneralName gate (context-only, SEQUENCE SAN, strict otherName [0]) rejects the shapes that would trigger these fallbacks before _identity; asn1.decode guarantees a children array on any constructed node.
461
465
  for (var i = 0; i < (seq.children || []).length; i++) {
462
466
  var n = seq.children[i];
463
467
  if (n.tagClass !== "context") continue;
@@ -470,6 +474,7 @@ function _sanValue(ext) {
470
474
  // machine identities here (type-id .1.7, value a UTF8String).
471
475
  var inner = (n.children[1].children || [])[0] || n.children[1];
472
476
  var v;
477
+ // Coverage residual -- asn1.decode gives every node a (truthy) Buffer content, so the || [] fallback is unreachable; the catch itself runs for a non-string otherName inner value.
473
478
  try { v = asn1.read.string(inner); } catch (_e) { v = Buffer.from(inner.content || []).toString("utf8"); }
474
479
  val = { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
475
480
  }
@@ -482,6 +487,7 @@ function _sanValue(ext) {
482
487
  return names[0] || null;
483
488
  }
484
489
  function _fulcioExtValue(ext, leafArc) {
490
+ // Coverage residual -- ext.value is read.octetString output, always a Buffer.
485
491
  if (!Buffer.isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
486
492
  // The raw-vs-DER split by member (Fulcio oid-info): .1-.6 legacy are raw UTF-8
487
493
  // strings; .8+ are DER UTF8String -- the arc is open-ended past .22. An
@@ -66,6 +66,9 @@ var ALG_PROFILE = {
66
66
 
67
67
  // The value node of an integer-labelled COSE_Key parameter (labels are ints), or null.
68
68
  function _mapGetInt(node, key) {
69
+ // Coverage residual -- credentialKey validates node is a majorType-5 map (before any
70
+ // _mapGetInt call), and cbor-det always gives a map a (truthy) children array, so this
71
+ // guard cannot fire.
69
72
  if (!node || node.majorType !== 5 || !node.children) return null;
70
73
  for (var i = 0; i < node.children.length; i++) {
71
74
  var k = node.children[i][0];
@@ -146,6 +149,9 @@ function toSpki(key, E, code) {
146
149
  function bad(msg) { return new E(code, msg); }
147
150
  if (key.kty === 2 && key.x && key.y) {
148
151
  var curveOid = EC2_CRV_OID[key.crv];
152
+ // Coverage residual -- toSpki only receives a credentialKey-validated key; EC2 crv is
153
+ // already gated to {1,2,3} by EC2_CRV_LEN, the exact EC2_CRV_OID keyset, so curveOid is
154
+ // always defined.
149
155
  if (!curveOid) throw bad("unsupported EC2 curve " + key.crv);
150
156
  var b = asn1.build;
151
157
  return b.sequence([
@@ -164,6 +170,8 @@ function toSpki(key, E, code) {
164
170
  var eb = asn1.build;
165
171
  return eb.sequence([eb.sequence([eb.oid(oid.byName(OKP_CRV[key.crv].oid))]), eb.bitString(key.x)]);
166
172
  }
173
+ // Coverage residual -- a credentialKey-validated key always matches one of the three forms
174
+ // above; this fallthrough is defensive depth.
167
175
  throw bad("cannot build an SPKI for this COSE key type");
168
176
  }
169
177
 
@@ -71,4 +71,51 @@ function rawToEcdsaDer(raw, coordLen) {
71
71
  return asn1.build.sequence([asn1.build.integer(r), asn1.build.integer(s)]);
72
72
  }
73
73
 
74
- module.exports = { ecdsaSigToRaw: ecdsaSigToRaw, rawToEcdsaDer: rawToEcdsaDer };
74
+ // The curve group orders n -- a valid ECDSA signature has r, s in [1, n-1].
75
+ var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
76
+ var CURVE_ORDER = {
77
+ "P-256": BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),
78
+ "P-384": BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"),
79
+ "P-521": BigInt("0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409"),
80
+ };
81
+
82
+ // ecdsaDerToP1363(der, curve, E, code) -> the DER ECDSA-Sig-Value converted to raw r||s (P1363),
83
+ // each coordinate left-padded to the curve field width, rejecting r or s outside [1, n-1] against
84
+ // the curve ORDER (CVE-2022-21449 "Psychic Signatures" -- the r/s = 0 case AND the >= n upper
85
+ // bound). `curve` is a WebCrypto namedCurve (P-256/384/521). This is the ORDER-AWARE gate a
86
+ // verifier that knows the curve order MUST use; it is STRICTER than ecdsaSigToRaw above, which
87
+ // bounds r/s only by the field SIZE (>= 1, <= coordLen bytes) and does not know the order. A
88
+ // signature whose r or s is >= n is rejected here but passes ecdsaSigToRaw -- do not conflate them.
89
+ // @enforced-by behavioral -- the RED conformance vectors (r/s = 0, r/s >= n, non-minimal, over-size)
90
+ // and the composite-signature + path-validation KATs are the guard.
91
+ function ecdsaDerToP1363(der, curve, E, code) {
92
+ var width = CURVE_FIELD_BYTES[curve];
93
+ var order = CURVE_ORDER[curve];
94
+ if (!width || !order) throw new E(code, "unsupported ECDSA curve " + curve);
95
+ var n;
96
+ try { n = asn1.decode(der); }
97
+ catch (e) { throw new E(code, "ECDSA signature is not a DER SEQUENCE(r,s)", e); }
98
+ if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children || n.children.length !== 2) {
99
+ throw new E(code, "ECDSA signature must be a SEQUENCE of exactly two INTEGERs");
100
+ }
101
+ var r = asn1.read.integer(n.children[0]);
102
+ var s = asn1.read.integer(n.children[1]);
103
+ if (r < 1n || s < 1n || r >= order || s >= order) {
104
+ throw new E(code, "ECDSA signature component out of range [1, n-1] (CVE-2022-21449)");
105
+ }
106
+ function pad(v) {
107
+ var hex = v.toString(16);
108
+ if (hex.length % 2) hex = "0" + hex;
109
+ var buf = Buffer.from(hex, "hex");
110
+ // Coverage residual -- unreachable: a component needing more than `width` bytes is >=
111
+ // 2^(8*width) > n (the curve order n < 2^(8*width) for P-256/384/521), so it is already
112
+ // rejected by the r/s >= order check above; this width guard is a defense-in-depth backstop.
113
+ if (buf.length > width) throw new E(code, "ECDSA signature component wider than the curve field");
114
+ var out = Buffer.alloc(width);
115
+ buf.copy(out, width - buf.length);
116
+ return out;
117
+ }
118
+ return Buffer.concat([pad(r), pad(s)]);
119
+ }
120
+
121
+ module.exports = { ecdsaSigToRaw: ecdsaSigToRaw, rawToEcdsaDer: rawToEcdsaDer, ecdsaDerToP1363: ecdsaDerToP1363 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
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:171a9606-6760-469e-abba-719538a2f2ab",
5
+ "serialNumber": "urn:uuid:bec63d3e-7332-4a32-bcc8-ac8614e7402a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-14T02:42:50.555Z",
8
+ "timestamp": "2026-07-14T07:20:40.274Z",
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.2.16",
22
+ "bom-ref": "@blamejs/pki@0.2.18",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.16",
25
+ "version": "0.2.18",
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.2.16",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.18",
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.2.16",
57
+ "ref": "@blamejs/pki@0.2.18",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]