@blamejs/pki 0.5.24 → 0.5.26

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.
@@ -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/pki-build.js CHANGED
@@ -18,9 +18,16 @@ var intrinsic = require("./guard-intrinsic");
18
18
  // Each of these doors refuses a caller argument that is not a list. Read live, a replacement
19
19
  // answering true lets a non-list through the check written to stop it.
20
20
  var _isArray = intrinsic.isArray;
21
+ // Taken at load for the reason every capture here is: the question "do these two encodings name the
22
+ // same key" decides whether a proof of possession is accepted, and a caller who replaces the copy or
23
+ // the comparison afterwards decides it instead.
24
+ var _bufferFrom = intrinsic.bufferFrom;
25
+ var _bufferEquals = intrinsic.bufferEquals;
21
26
  var oid = require("./oid");
22
27
  var ipUtils = require("./ip-utils");
23
28
  var nodeCrypto = require("crypto");
29
+ // The key-material comparison, taken at load like every other operation a decision here rests on.
30
+ var _keyEquals = intrinsic.uncurry(nodeCrypto.KeyObject.prototype.equals);
24
31
 
25
32
  var b = asn1.build;
26
33
 
@@ -402,6 +409,27 @@ function makeBuilder(ctx) {
402
409
  if (!ok) throw E("bad-input", "the signing key does not correspond to the public key -- the signature would not verify");
403
410
  }
404
411
 
412
+ // Do two SubjectPublicKeyInfo encodings name the same key? Bytes first, because that is the answer
413
+ // almost always and it costs nothing. When they differ the question is still open, since one key has
414
+ // more than one legal SPKI: an EC point may be carried compressed or uncompressed, so a P-256 key
415
+ // written the compressed way is byte-unequal to the same key written the other. Comparing the DER
416
+ // alone would refuse a proof of possession over a key the requester genuinely holds, while the
417
+ // sibling arms of the same verb accept that template. The fallback asks the key layer, which
418
+ // compares key material and still answers false for a different key, a different curve, or a
419
+ // different algorithm. A key neither side can import is equal to nothing, so the refusal stands.
420
+ function samePublicKey(spkiA, spkiB) {
421
+ var a = _bufferFrom(spkiA), bb = _bufferFrom(spkiB);
422
+ if (_bufferEquals(a, bb)) return true;
423
+ var ka, kb;
424
+ // allow:swallow-unverified an SPKI the key layer cannot import matches nothing, which is the
425
+ // refusal the caller wants; the caller raises its own typed error on the false.
426
+ try {
427
+ ka = nodeCrypto.createPublicKey({ key: a, format: "der", type: "spki" });
428
+ kb = nodeCrypto.createPublicKey({ key: bb, format: "der", type: "spki" });
429
+ } catch (_e) { return false; }
430
+ return _keyEquals(ka, kb) === true;
431
+ }
432
+
405
433
  return {
406
434
  E: E, code: code, KU_BIT: KU_BIT,
407
435
  encodeName: encodeName, isEmptyName: isEmptyName, encodeGeneralName: encodeGeneralName,
@@ -413,6 +441,7 @@ function makeBuilder(ctx) {
413
441
  spkiKeyId: spkiKeyId, skiKeyId: skiKeyId,
414
442
  reqDer: reqDer, assertValidSpki: assertValidSpki, assertValidExtension: assertValidExtension,
415
443
  certLikeFromSpki: certLikeFromSpki, assertSignatureVerifies: assertSignatureVerifies,
444
+ samePublicKey: samePublicKey,
416
445
  };
417
446
  }
418
447
 
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.24",
3
+ "version": "0.5.26",
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:1d864685-81db-45d8-92b0-ef5b74906999",
5
+ "serialNumber": "urn:uuid:af65ad7a-ef35-4bc4-8aef-24a43cb8ece0",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-21T11:03:39.559Z",
8
+ "timestamp": "2026-08-23T00:05:35.940Z",
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.24",
22
+ "bom-ref": "@blamejs/pki@0.5.26",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.24",
25
+ "version": "0.5.26",
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.24",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.5.26",
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.24",
57
+ "ref": "@blamejs/pki@0.5.26",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]