@blamejs/pki 0.7.40 → 0.7.41
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 +9 -0
- package/lib/crmf-sign.js +34 -1
- package/lib/est.js +15 -1
- package/lib/key.js +59 -17
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.7.41 — 2026-09-12
|
|
8
|
+
|
|
9
|
+
A delivered or archived private key is proven the private half of its certificate with the key itself.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- pki.est.serverkeygen proves a cleartext server-generated private key is the private half of the returned certificate it binds it to (a signature, an agreement, or an encapsulation the key completes), and refuses with est/key-cert-mismatch a key whose stated public half matches the certificate while its private material cannot use it. The selection of the certificate by public-key match and the ambiguity refusal are unchanged, and a finite-field Diffie-Hellman key, whose private structure carries the exponent alone, is still bound by the value it derives, at any width the runtime reads.
|
|
14
|
+
- pki.crmf.build proves the key enclosed in an encryptedKey proof of possession is the private half of certTemplate.publicKey with the key itself, and refuses with crmf/bad-popo an RSA, EC, EdDSA, ML-DSA, ML-KEM or composite key that only states the template's public components; a key the toolkit cannot read stays crmf/bad-input. A finite-field Diffie-Hellman key, whose private structure carries the exponent alone, keeps the derived comparison at any width, so the archival path still accepts a key wider than any group the toolkit agrees over.
|
|
15
|
+
|
|
7
16
|
## v0.7.40 — 2026-09-12
|
|
8
17
|
|
|
9
18
|
A PKCS#12 store links a certificate only to its own key.
|
package/lib/crmf-sign.js
CHANGED
|
@@ -836,7 +836,38 @@ function _buildEncryptedKeyPop(pop, outerTag, template) {
|
|
|
836
836
|
}
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
+
async function _assertEnclosedKeyCorresponds(privateKeyDer, templateSpki) {
|
|
840
|
+
var pair;
|
|
841
|
+
try {
|
|
842
|
+
pair = await key.correspondsTo(privateKeyDer, templateSpki);
|
|
843
|
+
} catch (e) {
|
|
844
|
+
if (e instanceof CrmfError) throw e;
|
|
845
|
+
/** @internal A key the key module cannot read at all is bad input; one it reads and cannot
|
|
846
|
+
* exercise together with the template's key proves possession of nothing, which is the proof
|
|
847
|
+
* failing, not the input. */
|
|
848
|
+
if (e && e.isPkiError === true && e.code === "key/bad-input") {
|
|
849
|
+
throw _err("crmf/bad-input", "pop.privateKey could not be read as a private key to check against " +
|
|
850
|
+
"certTemplate.publicKey: " + (e.message || e), e);
|
|
851
|
+
}
|
|
852
|
+
throw _err("crmf/bad-popo", "the private key enclosed in the encryptedKey proof of possession could not be " +
|
|
853
|
+
"exercised with certTemplate.publicKey, so it cannot be shown to be that key's private half (RFC 4211 sec. 4.2)", e);
|
|
854
|
+
}
|
|
855
|
+
if (pair !== true) {
|
|
856
|
+
throw _err("crmf/bad-popo", "the private key enclosed in the encryptedKey proof of possession is not the " +
|
|
857
|
+
"private half of certTemplate.publicKey, so it proves possession of a key this request does not ask to " +
|
|
858
|
+
"have certified (RFC 4211 sec. 4.2)");
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
839
862
|
function _assertEnclosedKeyMatches(privateKeyDer, templateSpki) {
|
|
863
|
+
/** @internal Which proof the enclosed key takes depends on what its structure can carry. A
|
|
864
|
+
* finite-field PrivateKeyInfo carries the exponent alone, so the public value derived from it IS
|
|
865
|
+
* the proof, and this arm keeps that derivation at any width, with the cheap tests below. Every
|
|
866
|
+
* other family's private structure carries public components of its own (RFC 5958 sec. 2,
|
|
867
|
+
* RFC 5915, PKCS #1), which a caller could set to the template's beside private material that
|
|
868
|
+
* cannot use it, so those are proven by USING the key against the template. */
|
|
869
|
+
var enclosedAlg = key.algorithmOidAt(privateKeyDer, 1);
|
|
870
|
+
if (enclosedAlg !== null && !key.isDhOid(enclosedAlg)) return _assertEnclosedKeyCorresponds(privateKeyDer, templateSpki);
|
|
840
871
|
return Promise.resolve()
|
|
841
872
|
.then(function () { return key.publicFromPrivate(privateKeyDer); })
|
|
842
873
|
.then(function (derivedSpki) {
|
|
@@ -991,7 +1022,9 @@ function _buildProofOfPossession(pop, certReqDer, template, signingKey, opts) {
|
|
|
991
1022
|
* intended, so it takes `pop.archive: true`; `pop.privateKey` is the PKCS#8 DER, `pop.recipients` the CMS
|
|
992
1023
|
* recipients to envelope it to, and `pop.identifier` (a string or GeneralName) is required, because
|
|
993
1024
|
* sec. 4.2.1 makes it mandatory whenever the purpose is proving possession: without it a decrypting
|
|
994
|
-
* agent cannot tell whose key it holds.
|
|
1025
|
+
* agent cannot tell whose key it holds. The enclosed key is proven the private half of
|
|
1026
|
+
* `certTemplate.publicKey` with the key itself (`crmf/bad-popo` when it is not), not by the public
|
|
1027
|
+
* components its structure states. `pop.contentEncryptionAlgorithm` defaults to `aes-256-cbc`; an
|
|
995
1028
|
* AEAD algorithm is refused, since it would produce an `AuthEnvelopedData` this field cannot carry.
|
|
996
1029
|
* - `agreeMAC`, which agrees a secret with the authority and MACs the request under a key derived from it,
|
|
997
1030
|
* so nothing is signed (RFC 4211 sec. 4.3, RFC 2875 sec. 3). `pop.key` is the requester's finite-field
|
package/lib/est.js
CHANGED
|
@@ -1742,6 +1742,18 @@ async function _serverkeygenResult(res, opts, derived) {
|
|
|
1742
1742
|
var bound = findIssuedCert(out.certificates, spki);
|
|
1743
1743
|
if (!bound) throw E("est/key-cert-mismatch", "the cleartext server-generated private key matches no returned certificate's public key (RFC 7030 sec. 4.4.2)");
|
|
1744
1744
|
if (findIssuedCert(out.certificates.filter(function (c) { return c !== bound; }), spki)) throw E("est/ambiguous-issued-cert", "more than one returned certificate carries the server-generated key; the issued certificate is ambiguous (RFC 7030 sec. 4.4.2)");
|
|
1745
|
+
/** @internal The public half the key states selected the certificate; the pair is then proven
|
|
1746
|
+
* with the PRIVATE half, since a private structure states public components of its own that a
|
|
1747
|
+
* server could set to the certificate's beside private material that cannot use it. A
|
|
1748
|
+
* finite-field Diffie-Hellman PrivateKeyInfo carries the exponent alone, so the value derived
|
|
1749
|
+
* from it above IS the proof, at any width the runtime reads. */
|
|
1750
|
+
var keyAlg = key.algorithmOidAt(out.privateKeyDer, 1);
|
|
1751
|
+
if (keyAlg === null || !key.isDhOid(keyAlg)) {
|
|
1752
|
+
var pair;
|
|
1753
|
+
try { pair = await key.correspondsTo(out.privateKeyDer, spki); }
|
|
1754
|
+
catch (e2) { throw E("est/key-cert-mismatch", "the cleartext server-generated private key could not be exercised with the returned certificate's public key, so the two cannot be shown to be a pair (RFC 7030 sec. 4.4.2)", e2); }
|
|
1755
|
+
if (pair !== true) throw E("est/key-cert-mismatch", "the cleartext server-generated private key is not the private half of the returned certificate's public key (RFC 7030 sec. 4.4.2)");
|
|
1756
|
+
}
|
|
1745
1757
|
delete out.privateKeyDer;
|
|
1746
1758
|
}
|
|
1747
1759
|
return out;
|
|
@@ -1765,7 +1777,9 @@ async function _serverkeygenResult(res, opts, derived) {
|
|
|
1765
1777
|
* the key so the issued certificate's public key is the generated one, not the throwaway CSR key. A cleartext
|
|
1766
1778
|
* key is bound to its certificate before it resolves: the delivered private key's public half MUST match
|
|
1767
1779
|
* exactly one returned certificate (`est/key-cert-mismatch` on none, `est/ambiguous-issued-cert` on more than
|
|
1768
|
-
* one),
|
|
1780
|
+
* one), and the key is then proven the private half of that certificate's public key with the key itself
|
|
1781
|
+
* (`est/key-cert-mismatch` when it is not; a finite-field DH key, whose structure carries the exponent alone,
|
|
1782
|
+
* is proven by the value it derives), so a mis-associated or unusable key is refused, never handed back.
|
|
1769
1783
|
* The encryption requirement + expected recipient are derived from the CSR's own DecryptKeyIdentifier /
|
|
1770
1784
|
* AsymmetricDecryptKeyIdentifier attribute; an `opts` value that contradicts the CSR is `est/bad-input`
|
|
1771
1785
|
* (a cleartext-key downgrade cannot slip past). The delivered key's channel is asserted confidentiality-
|
package/lib/key.js
CHANGED
|
@@ -680,7 +680,22 @@ function _dhAsPkcs3(der, at, what) {
|
|
|
680
680
|
if (!algNode.children || algNode.children.length !== 2) {
|
|
681
681
|
throw _err("key/bad-input", what + " X9.42 AlgorithmIdentifier does not carry DomainParameters");
|
|
682
682
|
}
|
|
683
|
-
|
|
683
|
+
/** @internal A field the codec refuses to read as the type the structure states is a malformed
|
|
684
|
+
* key, reported as one rather than as the codec's own complaint. */
|
|
685
|
+
var dom, y = null;
|
|
686
|
+
try {
|
|
687
|
+
dom = x942Domain(algNode.children[1], KeyError, "key/bad-input", what);
|
|
688
|
+
if (at === 0) {
|
|
689
|
+
var pub = asn1.read.bitString(node.children[1]);
|
|
690
|
+
if (!guard.crypto.isOctetAligned(pub)) {
|
|
691
|
+
throw _err("key/bad-input", what + " Diffie-Hellman public key BIT STRING must be octet-aligned (0 unused bits)");
|
|
692
|
+
}
|
|
693
|
+
y = asn1.read.integer(asn1.decode(pub.bytes));
|
|
694
|
+
}
|
|
695
|
+
} catch (e) {
|
|
696
|
+
if (e instanceof KeyError) throw e;
|
|
697
|
+
throw _err("key/bad-input", what + " X9.42 Diffie-Hellman key could not be read", e);
|
|
698
|
+
}
|
|
684
699
|
assertDhOperandsWithin([dom.p, dom.g, dom.q, dom.j], KeyError, "key/bad-input", what);
|
|
685
700
|
assertDhDomainCoherent(dom.p, dom.g, dom.q, dom.j, KeyError, "key/bad-input", what);
|
|
686
701
|
assertDhGeneratorOrder(dom.p, dom.g, dom.q, KeyError, "key/bad-input", what);
|
|
@@ -689,11 +704,6 @@ function _dhAsPkcs3(der, at, what) {
|
|
|
689
704
|
/** @internal The one field the X9.42 form states that the PKCS #3 form cannot be held to is the
|
|
690
705
|
* subgroup, so a public value is held to it here, where q is still known. */
|
|
691
706
|
if (at === 0) {
|
|
692
|
-
var pub = asn1.read.bitString(node.children[1]);
|
|
693
|
-
if (!guard.crypto.isOctetAligned(pub)) {
|
|
694
|
-
throw _err("key/bad-input", what + " Diffie-Hellman public key BIT STRING must be octet-aligned (0 unused bits)");
|
|
695
|
-
}
|
|
696
|
-
var y = asn1.read.integer(asn1.decode(pub.bytes));
|
|
697
707
|
if (y <= 1n || y >= dom.p - 1n) {
|
|
698
708
|
throw _err("key/bad-input", what + " Diffie-Hellman public value is outside the group (RFC 2875 sec. 3)");
|
|
699
709
|
}
|
|
@@ -711,19 +721,45 @@ function _dhAsPkcs3(der, at, what) {
|
|
|
711
721
|
* algorithm; every secret the probe makes is wiped on every way out. A public key of another
|
|
712
722
|
* algorithm, composite or not, is another family and not a pair. */
|
|
713
723
|
async function _compositeCorrespondsTo(privDer, spkiDer, privateOid) {
|
|
724
|
+
/** @internal The private half is read first, as on the classical route: a composite key the KEM
|
|
725
|
+
* cannot read is bad input whatever the public half names, decided before the family shortcut
|
|
726
|
+
* answers that the two are not a pair. The derivation's result is not what is compared; the
|
|
727
|
+
* read is what it answers for. */
|
|
728
|
+
try { await compositeKem.publicFromPrivate(privDer); }
|
|
729
|
+
catch (e0) {
|
|
730
|
+
if (_isKemStructureFault(e0)) throw _err("key/bad-input", "the private key could not be read", e0);
|
|
731
|
+
throw _err("key/unsupported-algorithm", "the key pair could not be exercised to decide whether its two halves correspond", e0);
|
|
732
|
+
}
|
|
714
733
|
if (_algorithmOidAt(spkiDer, 0) !== privateOid) return false;
|
|
715
734
|
var sealed = null, opened = null;
|
|
716
735
|
try {
|
|
717
|
-
|
|
718
|
-
|
|
736
|
+
/** @internal A structure the KEM cannot read is bad input, the verdict an unreadable classical
|
|
737
|
+
* key gets from the runtime import above; a decapsulation that runs and fails is the pair not
|
|
738
|
+
* deciding. The KEM tells the two apart by its own codes, and so does this. */
|
|
739
|
+
try { sealed = await compositeKem.encapsulate(spkiDer); }
|
|
740
|
+
catch (e) {
|
|
741
|
+
if (_isKemStructureFault(e)) throw _err("key/bad-input", "the public key could not be read", e);
|
|
742
|
+
throw _err("key/unsupported-algorithm", "the key pair could not be exercised to decide whether its two halves correspond", e);
|
|
743
|
+
}
|
|
744
|
+
try { opened = await compositeKem.decapsulate(privDer, sealed.ciphertext); }
|
|
745
|
+
catch (e2) {
|
|
746
|
+
if (_isKemStructureFault(e2)) throw _err("key/bad-input", "the private key could not be read", e2);
|
|
747
|
+
throw _err("key/unsupported-algorithm", "the key pair could not be exercised to decide whether its two halves correspond", e2);
|
|
748
|
+
}
|
|
719
749
|
return guard.crypto.constantTimeEqual(opened, sealed.sharedSecret);
|
|
720
|
-
} catch (e) {
|
|
721
|
-
throw _err("key/unsupported-algorithm", "the key pair could not be exercised to decide whether its two halves correspond", e);
|
|
722
750
|
} finally {
|
|
723
751
|
guard.secret.zeroizeAll([sealed && sealed.sharedSecret, opened], KeyError, "key/bad-input", "the correspondence probe secrets");
|
|
724
752
|
}
|
|
725
753
|
}
|
|
726
754
|
|
|
755
|
+
var _KEM_STRUCTURE_FAULTS = _assign(_create(null), { "kem/bad-key": 1, "kem/bad-input": 1, "kem/bad-algorithm": 1, "kem/bad-ciphertext": 1 });
|
|
756
|
+
|
|
757
|
+
/** @internal Whether a KEM error says the key or ciphertext could not be READ, as opposed to a
|
|
758
|
+
* decapsulation that ran and failed. */
|
|
759
|
+
function _isKemStructureFault(e) {
|
|
760
|
+
return !!(e && e.isPkiError === true && _KEM_STRUCTURE_FAULTS[guard.text.keyOf(e.code)] === 1);
|
|
761
|
+
}
|
|
762
|
+
|
|
727
763
|
async function publicFromPrivate(privateKey, opts) {
|
|
728
764
|
opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.publicFromPrivate options");
|
|
729
765
|
guard.identifier.assertKnownKeys(opts, _PUBLIC_FROM_PRIVATE_OPTS, _err, "key/bad-input",
|
|
@@ -860,13 +896,15 @@ function _pssSalt(a, b) {
|
|
|
860
896
|
|
|
861
897
|
async function correspondsTo(privateKey, spkiDer) {
|
|
862
898
|
var privDer = await _toPrivateKeyDer(privateKey);
|
|
899
|
+
var spki = _bufferFrom(spkiDer);
|
|
863
900
|
var compositeOid = _compositeKemOidOf(privDer);
|
|
864
|
-
if (compositeOid !== null) return _compositeCorrespondsTo(privDer,
|
|
865
|
-
/** @internal
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
|
|
901
|
+
if (compositeOid !== null) return _compositeCorrespondsTo(privDer, spki, compositeOid);
|
|
902
|
+
/** @internal The private half is read FIRST, whatever the public half names: a private key that
|
|
903
|
+
* cannot be read is bad input, and a caller tells that apart from a pair that is not one. Only
|
|
904
|
+
* then is the family question asked, from the two identifiers, before the public structure is
|
|
905
|
+
* validated: a composite or a finite-field half against a half of another family is not a pair
|
|
906
|
+
* whatever it states, so the runtime is never asked to read a structure it cannot, and a DH
|
|
907
|
+
* structure's validation, which proves its modulus prime, is never run to say so. */
|
|
870
908
|
var priv, pub;
|
|
871
909
|
/** @internal The PKCS #3 rewrite of an X9.42 private key is this function's own copy of the
|
|
872
910
|
* exponent, wiped once the runtime holds the key. */
|
|
@@ -876,7 +914,10 @@ async function correspondsTo(privateKey, spkiDer) {
|
|
|
876
914
|
try { priv = nodeCrypto.createPrivateKey({ key: privAsPkcs3, format: "der", type: "pkcs8" }); }
|
|
877
915
|
catch (e) { throw _err("key/bad-input", "the private key could not be read", e); }
|
|
878
916
|
} finally { if (privAsPkcs3 !== privDer) guard.secret.zeroize(privAsPkcs3, KeyError, "key/bad-input", "the rewritten private key"); }
|
|
879
|
-
var
|
|
917
|
+
var publicOid = _algorithmOidAt(spki, 0);
|
|
918
|
+
if (publicOid !== null && compositeKem.isCompositeKem(publicOid)) return false;
|
|
919
|
+
if (_isDhOid(_algorithmOidAt(privDer, 1)) !== _isDhOid(publicOid)) return false;
|
|
920
|
+
var pubAsPkcs3 = _dhAsPkcs3(spki, 0, "the public key's");
|
|
880
921
|
_assertDhWithin(pubAsPkcs3, 0, "the public key's");
|
|
881
922
|
try { pub = nodeCrypto.createPublicKey({ key: pubAsPkcs3, format: "der", type: "spki" }); }
|
|
882
923
|
catch (e) { throw _err("key/bad-input", "the public key could not be read", e); }
|
|
@@ -1000,6 +1041,7 @@ module.exports = {
|
|
|
1000
1041
|
x942Domain: x942Domain, // @internal
|
|
1001
1042
|
pkcs3Params: pkcs3Params, // @internal
|
|
1002
1043
|
isDhOid: _isDhOid, // @internal
|
|
1044
|
+
algorithmOidAt: _algorithmOidAt, // @internal
|
|
1003
1045
|
assertDhOperandsWithin: assertDhOperandsWithin, // @internal
|
|
1004
1046
|
assertDhDomainCoherent: assertDhDomainCoherent, // @internal
|
|
1005
1047
|
assertDhGeneratorOrder: assertDhGeneratorOrder, // @internal
|
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:2f774cf6-ab61-4b1c-8dab-ee0085cfb2a5",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-12T20:24:38.981Z",
|
|
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.7.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.7.41",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.41",
|
|
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.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.7.41",
|
|
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.7.
|
|
57
|
+
"ref": "@blamejs/pki@0.7.41",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|