@blamejs/pki 0.5.25 → 0.5.27

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.
@@ -738,6 +738,41 @@ function _notAnOptionsBag(v) {
738
738
  // error and says so. A Buffer is excluded because it is an object and is never an options bag --
739
739
  // passing one is an argument-order mistake, which is worth naming rather than treating as empty.
740
740
  //
741
+ // A built-in exotic whose data lives in internal slots rather than own enumerable keys, recognized by
742
+ // identity so a cross-realm instance is caught too. `util.types` covers each such kind; an array is not
743
+ // among them and is checked separately by the caller.
744
+ function _isBuiltinExotic(v) {
745
+ var t = util.types;
746
+ return t.isDate(v) || t.isMap(v) || t.isSet(v) || t.isWeakMap(v) || t.isWeakSet(v) || t.isRegExp(v) ||
747
+ t.isPromise(v) || t.isArrayBuffer(v) || t.isSharedArrayBuffer(v) || t.isArrayBufferView(v) ||
748
+ t.isBoxedPrimitive(v) || t.isNativeError(v) || t.isArgumentsObject(v) || t.isGeneratorObject(v);
749
+ }
750
+
751
+ // assertPlainRecord(v, E, code, label): v MUST be a plain data record -- a non-null object that is not an
752
+ // array, a Buffer, a Proxy, or a built-in exotic (a Date, Map, Set, boxed primitive, ...). A caller's
753
+ // `typeof v === "object"` gate admits an array or a Date, which then passes assertKnownKeys with no own
754
+ // key to reject and reads as a keyless record -- every field `undefined` and silently defaulted. This
755
+ // door refuses that shape, so a malformed container is a refusal rather than a request assembled from
756
+ // defaults. Composes BEFORE assertKnownKeys, which only
757
+ // judges the keys that are present, not whether the value is a record at all.
758
+ //
759
+ // The prototype is NOT consulted, so a record built by a caller's own class, or one from another realm
760
+ // (a vm context, a worker) whose prototype is a different Object.prototype, is accepted -- it holds its
761
+ // fields as own enumerable properties like any record. Only a built-in exotic that keeps its data in
762
+ // internal slots (a Date, Map, Set, boxed primitive, ...), an array (index-keyed), a Buffer, or a Proxy
763
+ // is refused, each recognized by identity through `util.types` / `Array.isArray` so a cross-realm
764
+ // instance is caught where an `instanceof` or a prototype check would miss it.
765
+ //
766
+ // @enforced-by behavioral -- a plain-record refusal has no single rename-proof code shape worth a
767
+ // tree-wide detector; the RED vectors (an array, a Date, and a Proxy at each routed entry point, and a
768
+ // cross-realm/subclass record that must still be ACCEPTED) are the guard.
769
+ function assertPlainRecord(v, E, code, label) {
770
+ _requireFactory(E);
771
+ if (v === null || typeof v !== "object" || _isNodeBuffer(v) || _isArray(v)) throw E(code, label + " must be a plain object");
772
+ _refuseUnenumerable(v, E, code, label); // a Proxy's reported keys need not match what it answers
773
+ if (_isBuiltinExotic(v)) throw E(code, label + " must be a plain object (a built-in exotic such as a Date or Map is refused)");
774
+ }
775
+
741
776
  // @enforced-by behavioral -- the defect is an ordering, which has no rename-proof code shape; the
742
777
  // RED vector (a verb handed `false` accepting it as `{}`) is the guard.
743
778
  function optionsObject(opts, E, code, label) {
@@ -856,6 +891,7 @@ function _settle(opts, E, code, label) {
856
891
  module.exports = intrinsic.freeze({
857
892
  assertCanonicalOid: assertCanonicalOid,
858
893
  assertKnownKeys: assertKnownKeys,
894
+ assertPlainRecord: assertPlainRecord,
859
895
  optionsObject: optionsObject,
860
896
  refuseAccessorFields: refuseAccessorFields,
861
897
  // Exported so a caller that forwards an options bag copies the same surface this module
package/lib/guard-name.js CHANGED
@@ -53,6 +53,12 @@ var _String = String;
53
53
  // matches, and it is captured for the same reason as the rest: what a guard does is settled before
54
54
  // a caller runs, or it is settled by the caller.
55
55
  var _isArray = Array.isArray;
56
+ // Byte identity and the shape test that gates it, both captured for the reason above: a
57
+ // distribution-point comparison asks whether two encodings are the same bytes, and an object
58
+ // supplying its own `equals` would be answering that question about itself.
59
+ var _isBuffer = intrinsic.isBuffer;
60
+ var _bufferEquals = intrinsic.bufferEquals;
61
+ var _getOwnPropertyDescriptor = intrinsic.getOwnPropertyDescriptor;
56
62
 
57
63
  // assertNoControlBytes(str, E, code, label) -> str | throws E(code, ...)
58
64
  // DirectoryString policy (a DN attribute value): reject NUL and C0 control bytes
@@ -185,6 +191,89 @@ function dnEqual(rdnsA, rdnsB, E, code, label) {
185
191
  return true;
186
192
  }
187
193
 
194
+ // dpnCorresponds(a, b, E, code, label) -> boolean. Do two DistributionPointNames name the
195
+ // same CRL distribution point? RFC 5280 sec. 6.3.3(b)(2)(i) asks it of a certificate's
196
+ // DistributionPoint against a shard CRL's issuingDistributionPoint, and RFC 9483 sec. 4.3.4
197
+ // asks it of the dpn a CMP crlUpdate requested against the IDP of the CRL that came back.
198
+ // The answer is "at least one name in common", compared by BYTE-IDENTICAL DER.
199
+ //
200
+ // The comparison key is pinned by sec. 5.2.5: "The identical encoding MUST be used in the
201
+ // distributionPoint fields of the certificate and the CRL". This is therefore the one name
202
+ // comparison in the toolkit that must NOT canonicalize. dnEqual's sec. 7.1 rule applied here
203
+ // would equate two encodings the spec keeps distinct, and a CRL would be accepted as covering
204
+ // a point it was never published for. Whole-set equality is the opposite error: a point
205
+ // published under several names (a URI beside a directoryName) shares one of them with a
206
+ // reference that lists only that one, and demanding all would reject its own CRL.
207
+ //
208
+ // Mixed forms never correspond, with fullName on one side and nameRelativeToCRLIssuer on the
209
+ // other. Deciding that would mean resolving an RDN fragment against a CRL issuer DN this
210
+ // comparison is not given, so the answer is no.
211
+ //
212
+ // @enforced-by behavioral -- the rule's identity is carried by the kind strings, which the
213
+ // pattern walk strips before matching, so it has no rename-proof code shape. The RED vectors
214
+ // in guard-name.test.js and the crlUpdate vectors in cmp-session.test.js are the guard.
215
+ function dpnCorresponds(a, b, E, code, label) {
216
+ var x = _reduceDpn(a, E, code, label), y = _reduceDpn(b, E, code, label);
217
+ if (x.kind !== y.kind) return false;
218
+ if (x.kind === "rdn") return _bufferEquals(x.bytes, y.bytes);
219
+ for (var i = 0; i < x.names.length; i++) {
220
+ for (var j = 0; j < y.names.length; j++) {
221
+ if (_bufferEquals(x.names[i], y.names[j])) return true;
222
+ }
223
+ }
224
+ return false;
225
+ }
226
+
227
+ // The comparand, checked and REDUCED to the values that were checked. The reason _assertSequence
228
+ // gives applies here in full: a comparison handed the wrong shape must refuse at the call site
229
+ // instead of answering. Reading `names` off a value that has none would run a loop over
230
+ // `i < undefined` zero times and report "no correspondence", which reads as a clean refusal of a
231
+ // CRL and hides that nothing was compared; reading it off a sparse array would walk to
232
+ // Array.prototype for the missing index and compare a name the reference never carried.
233
+ //
234
+ // Every field must be the comparand's OWN DATA property, and its value is taken FROM the property
235
+ // descriptor. That is one reflective read per field, so the value the shape check validated is the
236
+ // value the comparison is answered from: no second read is left for an accessor, or a Proxy trap,
237
+ // to answer differently, and a field the decoder never produces (inherited, or reached through a
238
+ // getter) is refused. Buffers, not caller byte-likes, for the same reason: the comparison is byte
239
+ // identity, and an object answering `equals` or `length` for itself would be deciding the question
240
+ // asked of it.
241
+ function _reduceDpn(d, E, code, label) {
242
+ function refuse(why) { return E(code, "cannot compare " + label + ": " + why); }
243
+ function ownValue(o, k) {
244
+ // FromPropertyDescriptor hands back a fresh ordinary object, so `value` here is the value the
245
+ // object itself reported for that key, including through a Proxy's own-descriptor trap.
246
+ var desc = _getOwnPropertyDescriptor(o, k);
247
+ if (!desc || !_hasOwn(desc, "value")) throw refuse("a distribution-point comparison requires " + k + " to be the comparand's own value (RFC 5280 sec. 6.3.3 compares decoded names)");
248
+ return desc.value;
249
+ }
250
+ if (!d || typeof d !== "object") {
251
+ throw refuse("a distribution-point comparison requires the decoded DistributionPointName on both sides (kind fullName or rdn)");
252
+ }
253
+ var kind = ownValue(d, "kind");
254
+ if (kind !== "fullName" && kind !== "rdn") throw refuse("a DistributionPointName is fullName or nameRelativeToCRLIssuer, and this is neither");
255
+ if (kind === "rdn") {
256
+ var bytes = ownValue(d, "bytes");
257
+ if (!_isBuffer(bytes)) throw refuse("a nameRelativeToCRLIssuer distribution point must carry its encoded bytes");
258
+ return { kind: kind, bytes: bytes };
259
+ }
260
+ var names = ownValue(d, "names");
261
+ if (!_isArray(names)) throw refuse("a fullName distribution point must carry a sequence of encoded GeneralNames");
262
+ // The length is read once, into the bound the copy runs to. A hole and an inherited element are
263
+ // the same refusal as any other missing own value: indexing past a hole walks to Array.prototype,
264
+ // where a planted numeric property supplies a name the reference never carried.
265
+ var len = names.length;
266
+ // Indexed assignment, never a push: the accumulator must not be built by a method a caller can
267
+ // replace, since what it collects is what the comparison is answered from.
268
+ var taken = [];
269
+ for (var i = 0; i < len; i++) {
270
+ var n = ownValue(names, i);
271
+ if (!_isBuffer(n)) throw refuse("every fullName GeneralName must be its encoded bytes");
272
+ taken[i] = n;
273
+ }
274
+ return { kind: kind, names: taken };
275
+ }
276
+
188
277
  // escapeControlBytes(str) -> str. The render-side sibling of assertNoControlBytes:
189
278
  // where a name string must still be DISPLAYED best-effort (a human-readable report,
190
279
  // a log line) instead of rejected, neutralize every C0 control byte and DEL by
@@ -316,7 +405,7 @@ function lowerAscii(s) {
316
405
  // call, so a writable export would let a caller replace the check rather than pass it.
317
406
  module.exports = intrinsic.freeze({
318
407
  assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5,
319
- dnEqual: dnEqual, rdnEqual: rdnEqual, emailEqual: emailEqual,
408
+ dnEqual: dnEqual, rdnEqual: rdnEqual, emailEqual: emailEqual, dpnCorresponds: dpnCorresponds,
320
409
  escapeControlBytes: escapeControlBytes, escapeDnValue: escapeDnValue,
321
410
  lowerAscii: lowerAscii,
322
411
  });
package/lib/oid.js CHANGED
@@ -234,8 +234,11 @@ var FAMILIES = {
234
234
  // id-pkip arc (id-pkix 5). id-regCtrl (id-pkip 1) names the control types a
235
235
  // CertRequest carries; id-regInfo (id-pkip 2) names the registration-info types.
236
236
  // The parser surfaces each control/info value raw, keyed by these names.
237
+ // altCertTemplate (7) and the two keySpec controls algId (11) / rsaKeyLen (12) are the RFC 9480
238
+ // sec. 2.16 additions; a CertReqTemplate response states its key requirements with the latter two.
237
239
  regCtrl: { base: [1, 3, 6, 1, 5, 5, 7, 5, 1], of: {
238
- regToken: 1, authenticator: 2, pkiPublicationInfo: 3, pkiArchiveOptions: 4, oldCertID: 5, protocolEncrKey: 6 } },
240
+ regToken: 1, authenticator: 2, pkiPublicationInfo: 3, pkiArchiveOptions: 4, oldCertID: 5, protocolEncrKey: 6,
241
+ altCertTemplate: 7, algId: 11, rsaKeyLen: 12 } },
239
242
  regInfo: { base: [1, 3, 6, 1, 5, 5, 7, 5, 2], of: { utf8Pairs: 1, certReq: 2 } },
240
243
 
241
244
  // PKIX extended key purposes (id-kp, RFC 5280 sec. 4.2.1.12). timeStamping is required,
@@ -376,6 +379,9 @@ var FAMILIES = {
376
379
  // SignerInfo carries a MAC-based proof instead. Registered so a decoder can
377
380
  // name it and refuse it deliberately.
378
381
  "id-alg-noSignature": 2,
382
+ // RSASSA-PSS with SHAKE (RFC 8692): RSA signature algorithms on the PKIX arc.
383
+ // Registered so a consumer can name them -- e.g. the CMP keySpec RSA exclusion.
384
+ "id-RSASSA-PSS-SHAKE128": 30, "id-RSASSA-PSS-SHAKE256": 31,
379
385
  "id-alg-xmss-hashsig": 34, "id-alg-xmssmt-hashsig": 35,
380
386
  // Composite ML-DSA signature algorithms (draft-ietf-lamps-pq-composite-sigs
381
387
  // sec. 6): a PQ ML-DSA paired with a traditional RSA / ECDSA / EdDSA so the
@@ -432,7 +438,49 @@ var FAMILIES = {
432
438
 
433
439
  // OIW Secsig SHA-1 (1.3.14.3.2.26) -- the most common OCSP CertID hashAlgorithm;
434
440
  // nistHash covers only SHA-256 and above.
435
- oiwSecsig: { base: [1, 3, 14, 3, 2], of: { sha1: 26 } },
441
+ // OIW Secsig arc (1.3.14.3.2). id-SHA1 (.26) plus every RSA identifier the OIW SIG assigned under the
442
+ // arc: md4WithRSA (.2), md5WithRSA (.3), md4WithRSAEncryption (.4), rsaSignature (.11, the ISO 9796 RSA
443
+ // scheme), mdc2WithRSASignature (.14), shaWithRSAEncryption (.15), rsaKeyTransport (.22), and the
444
+ // md2/md5/sha1-WithRSASignature set (.24/.25/.29) -- pre-PKCS#1 RSA algorithms still seen in old certs.
445
+ // This is the arc's complete RSA membership per the OIW SIG assignments. Registered so a consumer can
446
+ // name them -- e.g. the CMP keySpec RSA exclusion, which recognizes RSA wherever it is standardized.
447
+ // The arc is shared with DES, DSA, DH, and the hashes, so no prefix isolates RSA; each is named.
448
+ oiwSecsig: { base: [1, 3, 14, 3, 2], of: {
449
+ sha1: 26,
450
+ "md4WithRSA": 2, "md5WithRSA": 3, "md4WithRSAEncryption": 4, "rsaSignature": 11,
451
+ "mdc2WithRSASignature": 14, "shaWithRSAEncryption": 15, "rsaKeyTransport": 22,
452
+ "md2WithRSASignature": 24, "md5WithRSASignature": 25, "sha1WithRSASignature": 29 } },
453
+
454
+ // TeleTrusT (BSI) RSA signature arc (1.3.36.3.3.1) -- RSA signatures with SHA-1 and the RIPEMD family.
455
+ // The arc is RSA-dedicated, so a consumer can classify the whole family by its prefix; one member is
456
+ // registered here so that prefix can be derived from the registry rather than a dotted-decimal literal.
457
+ teletrustRsaSig: { base: [1, 3, 36, 3, 3, 1], of: { "rsaSignatureWithripemd160": 2 } },
458
+
459
+ // X.509 directory encryptionAlgorithm arc (2.5.8.1). id-ea-rsa (.1) is the X.509 `rsa` public-key
460
+ // identifier, distinct from PKCS#1 rsaEncryption. Registered so a consumer can name it -- e.g. the
461
+ // CMP keySpec RSA exclusion, which must recognize RSA wherever it is standardized.
462
+ x509EncAlg: { base: [2, 5, 8, 1], of: { "rsa": 1 } },
463
+
464
+ // X.500 directory signatureAlgorithm arc (2.5.8.3). Its RSA members are sqMod-nWithRSA (.1, the
465
+ // ITU-T X.509 / ISO 9594-8 square-mod-n-with-RSA signature) and mdc2WithRSA (.100, MDC-2-with-RSA).
466
+ // Registered so the CMP keySpec RSA exclusion can name them; the arc is not RSA-dedicated, so each
467
+ // member is named individually rather than prefix-matched.
468
+ x509SigAlg: { base: [2, 5, 8, 3], of: { "sqMod-nWithRSA": 1, "mdc2WithRSA": 100 } },
469
+
470
+ // Chinese GM/T (SCA) SM algorithm arc (1.2.156.10197.1). Its one RSA member is sm3WithRSAEncryption
471
+ // (.504, a PKCS#1 v1.5 RSA signature over an SM3 digest); sm2 (.301), sm3 (.401), and SM2-with-SM3
472
+ // (.501) on the arc are not RSA. Registered so the CMP keySpec RSA exclusion can name it -- the arc is
473
+ // mixed, so the RSA member is named individually rather than prefix-matched.
474
+ smAlg: { base: [1, 2, 156, 10197, 1], of: { "sm3WithRSAEncryption": 504 } },
475
+
476
+ // BSI TR-03110 EAC Terminal Authentication RSA arc (0.4.0.127.0.7.2.2.2.1, id-TA-RSA). Every child is an
477
+ // RSA signature scheme -- PKCS#1 v1.5 or PSS over SHA-1/256/512 -- and the sibling id-TA-ECDSA arc
478
+ // (0.4.0.127.0.7.2.2.2.2) is EC, so this arc is RSA-dedicated. The members are registered so a consumer
479
+ // can derive the arc prefix from the registry rather than a dotted-decimal literal and classify the
480
+ // whole family, including a variant BSI adds later, by that prefix.
481
+ bsiTaRsa: { base: [0, 4, 0, 127, 0, 7, 2, 2, 2, 1], of: {
482
+ "id-TA-RSA-v1-5-SHA-1": 1, "id-TA-RSA-v1-5-SHA-256": 2, "id-TA-RSA-PSS-SHA-1": 3,
483
+ "id-TA-RSA-PSS-SHA-256": 4, "id-TA-RSA-v1-5-SHA-512": 5, "id-TA-RSA-PSS-SHA-512": 6 } },
436
484
 
437
485
  // NIST AES content-encryption algorithms (arc 2.16.840.1.101.3.4.1) -- the
438
486
  // modern PBES2 / CMS content ciphers a PKCS#12 or EnvelopedData names.
@@ -451,6 +499,11 @@ var FAMILIES = {
451
499
  // sets .35-.46, each pairing a parameter set with its message-digest hash. The
452
500
  // parameters MUST be absent for every one of them (enforced via paramsMustBeAbsent).
453
501
  nistSig: { base: [2, 16, 840, 1, 101, 3, 4, 3], of: {
502
+ // RSASSA-PKCS1-v1_5 with SHA-3 (NIST CSOR): RSA signature algorithms on the NIST
503
+ // signature arc, alongside the ML-DSA / SLH-DSA sets. Registered so a consumer can
504
+ // name them -- e.g. the CMP keySpec check that must recognize RSA to exclude it.
505
+ "id-rsassa-pkcs1-v1_5-with-sha3-224": 13, "id-rsassa-pkcs1-v1_5-with-sha3-256": 14,
506
+ "id-rsassa-pkcs1-v1_5-with-sha3-384": 15, "id-rsassa-pkcs1-v1_5-with-sha3-512": 16,
454
507
  "id-ml-dsa-44": 17, "id-ml-dsa-65": 18, "id-ml-dsa-87": 19,
455
508
  "id-slh-dsa-sha2-128s": 20, "id-slh-dsa-sha2-128f": 21,
456
509
  "id-slh-dsa-sha2-192s": 22, "id-slh-dsa-sha2-192f": 23,
@@ -1695,38 +1695,23 @@ function decodeIdp(ext) {
1695
1695
  }
1696
1696
 
1697
1697
  // RFC 5280 sec. 6.3.3(b)(2)(i): find the certificate DistributionPoint that
1698
- // CORRESPONDS to a shard CRL's IDP distribution point name -- at least one
1699
- // name in common between the two DistributionPointNames, compared by
1700
- // BYTE-IDENTICAL DER encoding (Buffer.equals on the raw GeneralName / RDN
1701
- // TLVs). sec. 5.2.5 pins the comparison key: "The identical encoding MUST be
1702
- // used in the distributionPoint fields of the certificate and the CRL" -- so
1703
- // a canonicalized or semantic comparison (which would equate two
1704
- // differently-encoded names) is forbidden, and whole-set equality (which
1705
- // would falsely reject a legitimate multi-URI DP sharing one name) is
1706
- // over-strict. Mixed forms -- fullName on one side, nameRelativeToCRLIssuer
1707
- // on the other -- never correspond here: resolving an RDN fragment against
1708
- // the issuer DN is not attempted (fail closed). Per sec. 6.3.3(b)(1), a DP
1709
- // naming a cRLIssuer participates only when that cRLIssuer is the certificate
1710
- // issuer itself: the checker only consults CRLs issued BY the certificate
1711
- // issuer and rejects indirect CRLs, so a DP delegated to another CRL issuer
1712
- // is out of play. Returns the matched DP (its `reasons` feeds the coarse
1713
- // reason-mask rule) or null.
1698
+ // CORRESPONDS to a shard CRL's IDP distribution point name. The correspondence
1699
+ // rule itself -- at least one name in common, compared by BYTE-IDENTICAL DER,
1700
+ // mixed name forms never corresponding -- is guard.name.dpnCorresponds, which
1701
+ // is also what a CMP crlUpdate answer is held to, so one definition decides
1702
+ // whether a CRL speaks for a point wherever the question is asked. Per sec.
1703
+ // 6.3.3(b)(1), a DP naming a cRLIssuer participates only when that cRLIssuer
1704
+ // is the certificate issuer itself: the checker only consults CRLs issued BY
1705
+ // the certificate issuer and rejects indirect CRLs, so a DP delegated to
1706
+ // another CRL issuer is out of play. Returns the matched DP (its `reasons`
1707
+ // feeds the coarse reason-mask rule) or null.
1714
1708
  function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
1715
1709
  if (!idpDpn || !certDPs) return null;
1716
1710
  for (var i = 0; i < certDPs.length; i++) {
1717
1711
  var dp = certDPs[i];
1718
1712
  if (!dp.distributionPoint) continue;
1719
1713
  if (dp.cRLIssuer && !crlIssuerNamesIssuer(dp.cRLIssuer, issuerRdns)) continue;
1720
- var cdpn = dp.distributionPoint;
1721
- if (idpDpn.kind === "fullName" && cdpn.kind === "fullName") {
1722
- for (var a = 0; a < idpDpn.names.length; a++) {
1723
- for (var c = 0; c < cdpn.names.length; c++) {
1724
- if (idpDpn.names[a].equals(cdpn.names[c])) return dp;
1725
- }
1726
- }
1727
- } else if (idpDpn.kind === "rdn" && cdpn.kind === "rdn") {
1728
- if (idpDpn.bytes.equals(cdpn.bytes)) return dp;
1729
- }
1714
+ if (guard.name.dpnCorresponds(idpDpn, dp.distributionPoint, E, "path/bad-idp", "a CRL distribution point")) return dp;
1730
1715
  }
1731
1716
  return null;
1732
1717
  }
@@ -2332,7 +2317,10 @@ var ocspCore = ocspVerify.makeOcspVerify({
2332
2317
  // of it on the public pki.path surface (index.js exports the whole path-validate module).
2333
2318
  cmpVerify.setEngine({ verifyWithSpki: _verifyWithSpki, build: build, validate: validate });
2334
2319
  // pki.cmp.session validates the ISSUED leaf certificate (its signature + chain) through the same engine.
2335
- cmpSession.setEngine({ build: build, validate: validate, toAnchor: toAnchor, coerceCert: coerceCert });
2320
+ // `verifyWithSpki` so a root CA key update's rollover signatures are checked by the same signature
2321
+ // engine (with its EdDSA low-order-point and algorithm-confusion gates) that verifies a message's
2322
+ // protection, rather than a second, weaker check inside the session.
2323
+ cmpSession.setEngine({ build: build, validate: validate, toAnchor: toAnchor, coerceCert: coerceCert, verifyWithSpki: _verifyWithSpki });
2336
2324
  // pki.cms.verify chains a SignedData's signer certificate to the anchors the CALLER named, so its
2337
2325
  // `trusted` is decided by this one path engine rather than a second, weaker walk of its own.
2338
2326
  // `toAnchor` so cms.verify can validate the caller's anchors ONCE at entry, before any signer is
package/lib/schema-cmp.js CHANGED
@@ -836,6 +836,99 @@ var PKI_MESSAGE = schema.seq([
836
836
  },
837
837
  });
838
838
 
839
+ // ---- support-message infoValue structures (RFC 9480 sec. 2.14-2.17) -----
840
+ //
841
+ // A genm / genp carries its payload as an ANY, so the InfoTypeAndValue leaf above
842
+ // surfaces it as raw bytes. These are the strict decoders for the four values RFC 9483
843
+ // sec. 4.3 defines, each a standalone parser so a malformed payload is refused with the
844
+ // same caps, minimal-encoding and trailing-byte rules the message itself is held to.
845
+ // The CMP module is EXPLICIT TAGS, so every context tag here wraps its value.
846
+
847
+ // CACertsValue ::= SEQUENCE SIZE (1..MAX) OF CMPCertificate. An absent infoValue is how
848
+ // "no CA certificate is available" is said (sec. 4.3.1), so an EMPTY sequence is malformed
849
+ // rather than a second spelling of the same answer.
850
+ var CA_CERTS_VALUE = schema.seqOf(rawSequence("cmp/bad-info-value"), {
851
+ min: 1, code: "cmp/bad-info-value", what: "CACertsValue",
852
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
853
+ });
854
+
855
+ // RootCaKeyUpdateContent ::= SEQUENCE { newWithNew CMPCertificate,
856
+ // newWithOld [0] CMPCertificate OPTIONAL, oldWithNew [1] CMPCertificate OPTIONAL }
857
+ // (RFC 9480 sec. 2.15). Both optionals are OPTIONAL in the syntax; RFC 9483 sec. 4.3.2
858
+ // requires newWithOld of a response, which the caller of this decoder enforces.
859
+ var ROOT_CA_KEY_UPDATE = schema.seq([
860
+ schema.field("newWithNew", rawSequence("cmp/bad-info-value")),
861
+ schema.trailing([
862
+ { tag: 0, name: "newWithOld", schema: rawSequence("cmp/bad-info-value"), explicit: true, emptyCode: "cmp/bad-info-value" },
863
+ { tag: 1, name: "oldWithNew", schema: rawSequence("cmp/bad-info-value"), explicit: true, emptyCode: "cmp/bad-info-value" },
864
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "cmp/bad-info-value", orderCode: "cmp/bad-info-value" }),
865
+ ], {
866
+ assert: "sequence", code: "cmp/bad-info-value", what: "RootCaKeyUpdateContent",
867
+ build: function (m) {
868
+ return {
869
+ newWithNew: m.fields.newWithNew.value,
870
+ newWithOld: m.fields.newWithOld.present ? m.fields.newWithOld.value : null,
871
+ oldWithNew: m.fields.oldWithNew.present ? m.fields.oldWithNew.value : null,
872
+ };
873
+ },
874
+ });
875
+
876
+ // AttributeTypeAndValue ::= SEQUENCE { type OBJECT IDENTIFIER, value ANY }, the element of
877
+ // the Controls that carries a CertReqTemplate keySpec (RFC 9480 sec. 2.16). The value stays
878
+ // raw here: which syntax it must have depends on the type, which sec. 4.3.3 constrains.
879
+ var ATTR_TYPE_AND_VALUE = schema.seq([
880
+ schema.field("type", schema.oidLeaf()),
881
+ schema.field("value", schema.any()),
882
+ ], {
883
+ assert: "sequence", code: "cmp/bad-info-value", what: "AttributeTypeAndValue",
884
+ build: function (m, ctx) {
885
+ var t = m.fields.type.value;
886
+ return { type: t, name: ctx.oid.name(t) || null, value: m.fields.value.value.bytes };
887
+ },
888
+ });
889
+
890
+ // CertReqTemplateContent ::= SEQUENCE { certTemplate CertTemplate, keySpec Controls OPTIONAL }
891
+ // (RFC 9480 sec. 2.16). keySpec is untagged, so it is recognized by its universal SEQUENCE tag
892
+ // following the CertTemplate, which is itself IMPLICIT [tagged] by the CRMF module.
893
+ var CERT_REQ_TEMPLATE_VALUE = schema.seq([
894
+ schema.field("certTemplate", CERT_TEMPLATE_LEAF),
895
+ schema.optional("keySpec", schema.seqOf(ATTR_TYPE_AND_VALUE, {
896
+ min: 1, code: "cmp/bad-info-value", what: "CertReqTemplate keySpec",
897
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
898
+ }), { whenUniversal: [TAGS.SEQUENCE] }),
899
+ ], {
900
+ assert: "sequence", code: "cmp/bad-info-value", what: "CertReqTemplateValue",
901
+ build: function (m) {
902
+ return {
903
+ certTemplate: m.fields.certTemplate.value,
904
+ keySpec: m.fields.keySpec.present ? m.fields.keySpec.value.result : null,
905
+ };
906
+ },
907
+ });
908
+
909
+ // CRLsValue ::= SEQUENCE SIZE (1..MAX) OF CertificateList (RFC 9480 sec. 2.17).
910
+ var CRLS_VALUE = schema.seqOf(rawSequence("cmp/bad-info-value"), {
911
+ min: 1, code: "cmp/bad-info-value", what: "CRLsValue",
912
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
913
+ });
914
+
915
+ function _valueParser(what, topSchema) {
916
+ return pkix.makeParser({ pemLabel: "CMP", PemError: PemError, ErrorClass: CmpError, prefix: "cmp", what: what, topSchema: topSchema, ns: NS });
917
+ }
918
+ // @internal -- pki.cmp.session reads a support-message response through these.
919
+ var readCaCerts = _valueParser("CACertsValue", CA_CERTS_VALUE);
920
+ var readRootCaKeyUpdate = _valueParser("RootCaKeyUpdateContent", ROOT_CA_KEY_UPDATE);
921
+ var readCertReqTemplate = _valueParser("CertReqTemplateValue", CERT_REQ_TEMPLATE_VALUE);
922
+ var readCrls = _valueParser("CRLsValue", CRLS_VALUE);
923
+ // A CertReqTemplate keySpec states a key requirement as an AlgorithmIdentifier (RFC 9480 sec. 2.16),
924
+ // which is the shared sub-schema every format already reads one with: `SEQUENCE { algorithm OID,
925
+ // parameters ANY OPTIONAL }` and nothing after it. Reading it through the same definition is what
926
+ // keeps a third field from passing as a well-formed one.
927
+ var readAlgorithmIdentifier = _valueParser("AlgorithmIdentifier", ALG_ID);
928
+ // A bare Name, so a caller-encoded issuer can be read back into the RDN sequence that guard-name's
929
+ // canonical comparison takes. pki.cmp.session binds a returned CRL to the issuer it asked for.
930
+ var readName = _valueParser("Name", pkix.name(NS));
931
+
839
932
  /**
840
933
  * @primitive pki.schema.cmp.parse
841
934
  * @signature pki.schema.cmp.parse(input) -> pkiMessage
@@ -970,4 +1063,11 @@ module.exports = {
970
1063
  pemDecode: pemDecode,
971
1064
  pemEncode: pemEncode,
972
1065
  matches: matches,
1066
+ // @internal -- the RFC 9483 sec. 4.3 support-message values, read strictly from a genp.
1067
+ readCaCerts: readCaCerts,
1068
+ readRootCaKeyUpdate: readRootCaKeyUpdate,
1069
+ readCertReqTemplate: readCertReqTemplate,
1070
+ readCrls: readCrls,
1071
+ readAlgorithmIdentifier: readAlgorithmIdentifier,
1072
+ readName: readName,
973
1073
  };
package/lib/trust.js CHANGED
@@ -629,6 +629,9 @@ function _csvDate(cell, label) {
629
629
  if (!m) throw E("trust/bad-csv", label + " must be a Y-M-D date, got " + JSON.stringify(s));
630
630
  var pad = function (x) { return x.length === 1 ? "0" + x : x; };
631
631
  var payload = Buffer.from(m[1] + pad(m[2]) + pad(m[3]) + "235959Z", "latin1");
632
+ // _strictTime tags every rejection (a rolled-over month/day among them) with the code passed here,
633
+ // so the re-throw carries it unchanged; the wrap normalizes any other failure to the same typed code
634
+ // so a bad row is always trust/bad-csv, never an untyped exception escaping parseCcadbCsv.
632
635
  try { return _strictTime(payload, "trust/bad-csv", label); }
633
636
  catch (e) {
634
637
  if (e && e.code === "trust/bad-csv") throw e;
@@ -774,11 +777,16 @@ function parseCcadbCsv(text) {
774
777
  * await pki.path.validate([pki.schema.x509.parse(der)],
775
778
  * { time: new Date("2026-06-01T00:00:00Z"), trustAnchor: anchor, checkPurpose: "serverAuth" });
776
779
  */
780
+ // `purpose` is the only option, and it gates the authorization itself, so a near-miss spelling must not
781
+ // pass silently: a caller who wrote the plural `purposes` (or carried a sibling's word) would otherwise get
782
+ // the anchor with NO purpose restriction rather than an error naming the option they meant.
783
+ var _ANCHOR_OPTS = { purpose: 1 };
777
784
  function anchor(entry, opts) {
778
785
  if (!entry || typeof entry !== "object") {
779
786
  throw E("trust/bad-input", "anchor expects a trust-store entry ({ name, publicKey, algorithm, ... })");
780
787
  }
781
- opts = opts || {};
788
+ opts = guard.identifier.optionsObject(opts, E, "trust/bad-input", "pki.trust.anchor options");
789
+ guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, E, "trust/bad-input", "unknown pki.trust.anchor option ");
782
790
  // Where this entry came from a store, everything the anchor asserts is read from the record the
783
791
  // store minted, not from the entry -- the entry is a plain object the caller holds and can write
784
792
  // to. `purposes` is the authorization itself, so an entry whose serverAuth bit was flipped after
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
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:119ff1fc-7d17-4f0b-8f84-06c75f1e6ac1",
5
+ "serialNumber": "urn:uuid:3e85b1a0-56f6-459d-a525-da732c14e1d6",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-21T13:36:35.267Z",
8
+ "timestamp": "2026-08-23T02:01:07.534Z",
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.5.25",
22
+ "bom-ref": "@blamejs/pki@0.5.27",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.25",
25
+ "version": "0.5.27",
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.5.25",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.5.27",
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.5.25",
57
+ "ref": "@blamejs/pki@0.5.27",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]