@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.
- package/CHANGELOG.md +54 -0
- package/README.md +2 -2
- package/lib/cmp-build.js +220 -6
- package/lib/cmp-session.js +753 -11
- package/lib/crmf-sign.js +304 -1
- package/lib/guard-identifier.js +36 -0
- package/lib/guard-name.js +90 -1
- package/lib/oid.js +55 -2
- package/lib/path-validate.js +15 -27
- package/lib/pki-build.js +29 -0
- package/lib/schema-cmp.js +100 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -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
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
// a
|
|
1704
|
-
//
|
|
1705
|
-
//
|
|
1706
|
-
//
|
|
1707
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:af65ad7a-ef35-4bc4-8aef-24a43cb8ece0",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.26",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.26",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|