@blamejs/pki 0.2.20 → 0.2.22
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 +24 -0
- package/README.md +2 -1
- package/index.js +2 -0
- package/lib/cms-sign.js +7 -218
- package/lib/cms-verify.js +3 -2
- package/lib/lint.js +43 -2
- package/lib/ocsp-verify.js +226 -0
- package/lib/ocsp.js +378 -0
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +131 -197
- package/lib/schema-pkix.js +8 -0
- package/lib/sign-scheme.js +206 -0
- package/lib/webcrypto.js +112 -10
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/webcrypto.js
CHANGED
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
* still runs on -- RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP, ECDSA, ECDH,
|
|
23
23
|
* Ed25519 / Ed448, AES-GCM / CBC / KW, HMAC, HKDF, PBKDF2, and the SHA
|
|
24
24
|
* family (including legacy SHA-1 for old certificates and signatures).
|
|
25
|
-
* FIPS 203 ML-KEM key generation
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* FIPS 203 ML-KEM key generation, encoding, and certificate/PKCS#8
|
|
26
|
+
* import are available; KEM encapsulation/decapsulation is deferred to
|
|
27
|
+
* the CMS KEM-decrypt surface by decision (the Node primitive exists).
|
|
28
|
+
* Because it is OpenSSL-backed, every key and signature it emits is
|
|
29
|
+
* interoperable with OpenSSL, NSS, and other PKI implementations.
|
|
29
30
|
*
|
|
30
31
|
* @card
|
|
31
32
|
* A zero-dep, PQC-first W3C WebCrypto (`SubtleCrypto`) engine over
|
|
@@ -37,6 +38,8 @@ var nodeCrypto = require("node:crypto");
|
|
|
37
38
|
var frameworkError = require("./framework-error");
|
|
38
39
|
var guard = require("./guard-all");
|
|
39
40
|
var constants = require("./constants");
|
|
41
|
+
var asn1 = require("./asn1-der");
|
|
42
|
+
var oid = require("./oid");
|
|
40
43
|
|
|
41
44
|
// Single-owner error class -- co-located with its module (framework-error
|
|
42
45
|
// stays the cross-module home; this is webcrypto-private). withCause: a
|
|
@@ -127,8 +130,9 @@ var ML_KEM_NODE = { "ML-KEM-512": "ml-kem-512", "ML-KEM-768": "ml-kem-768", "ML-
|
|
|
127
130
|
|
|
128
131
|
// FIPS 205 SLH-DSA -- stateless hash-based signatures. All twelve
|
|
129
132
|
// parameter sets Node exposes; signing is one-shot (null algorithm), the
|
|
130
|
-
// same shape as ML-DSA / EdDSA. KEM
|
|
131
|
-
//
|
|
133
|
+
// same shape as ML-DSA / EdDSA. ML-KEM here is key-generation / encoding /
|
|
134
|
+
// import; encapsulation/decapsulation (which Node now exposes) is deferred
|
|
135
|
+
// to the CMS KEM-decrypt surface, not blocked by the engine.
|
|
132
136
|
var SLH_DSA_NODE = {};
|
|
133
137
|
["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
|
|
134
138
|
"shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
|
|
@@ -144,6 +148,12 @@ var SIGN_VERIFY_NAMES = {};
|
|
|
144
148
|
.forEach(function (n) { SIGN_VERIFY_NAMES[n] = true; });
|
|
145
149
|
var ENCRYPT_DECRYPT_NAMES = { "RSA-OAEP": true, "AES-GCM": true, "AES-CBC": true, "AES-CTR": true };
|
|
146
150
|
var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true };
|
|
151
|
+
// The secret-key / KDF algorithms whose key material is raw octets (imported via "raw" or a
|
|
152
|
+
// JWK "oct"), NEVER an SPKI / PKCS#8 asymmetric-key structure. importKey("spki"|"pkcs8", ...)
|
|
153
|
+
// under one of these names is unsupported (W3C: NotSupportedError) -- and, without this gate, it
|
|
154
|
+
// would mint a mislabeled CryptoKey wrapping an asymmetric handle AND dodge the algorithm-keyed
|
|
155
|
+
// pkcs8 pre-validation (e.g. the RFC 9935 ML-KEM CHOICE guard).
|
|
156
|
+
var SECRET_KEY_NAMES = { "AES-GCM": true, "AES-CBC": true, "AES-CTR": true, "AES-KW": true, "HMAC": true, "HKDF": true, "PBKDF2": true };
|
|
147
157
|
|
|
148
158
|
// ---- CryptoKey -------------------------------------------------------
|
|
149
159
|
|
|
@@ -654,14 +664,97 @@ function _assertAesImportLen(name, byteLen) {
|
|
|
654
664
|
}
|
|
655
665
|
}
|
|
656
666
|
|
|
667
|
+
// Run a node:crypto key-import call, mapping a raw engine error (a malformed DER, an
|
|
668
|
+
// inconsistent key) to a typed webcrypto/data DataError -- importKey over hostile bytes
|
|
669
|
+
// must never leak a bare OpenSSL/Node exception through the public API.
|
|
670
|
+
function _nodeKey(fn, who) {
|
|
671
|
+
try { return fn(); }
|
|
672
|
+
catch (e) {
|
|
673
|
+
// Coverage residual -- every current thunk calls node:crypto (which throws a node error),
|
|
674
|
+
// never a WebCryptoError, so this already-typed passthrough is defensive depth for a future
|
|
675
|
+
// thunk that pre-validates and throws typed; it prevents double-wrapping a typed error.
|
|
676
|
+
if (e instanceof WebCryptoError) throw e;
|
|
677
|
+
throw new WebCryptoError("webcrypto/data", who + ": the key material is malformed or internally inconsistent", e);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// RFC 9935 sec. 6 ML-KEM-*-PrivateKey CHOICE: the inner sizes, keyed by the OID -- the OID is
|
|
682
|
+
// the SOLE authority for the parameter set (never a length heuristic). ek = 384k+32, dk = the
|
|
683
|
+
// FIPS 203 decapsulation key length.
|
|
684
|
+
var ML_KEM_INNER = {};
|
|
685
|
+
ML_KEM_INNER[oid.byName("id-ml-kem-512")] = { ek: 800, dk: 1632 };
|
|
686
|
+
ML_KEM_INNER[oid.byName("id-ml-kem-768")] = { ek: 1184, dk: 2400 };
|
|
687
|
+
ML_KEM_INNER[oid.byName("id-ml-kem-1024")] = { ek: 1568, dk: 3168 };
|
|
688
|
+
|
|
689
|
+
function _isOctet(node, size) {
|
|
690
|
+
return node && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.OCTET_STRING &&
|
|
691
|
+
!node.constructed && node.content && node.content.length === size;
|
|
692
|
+
}
|
|
693
|
+
// Validate the RFC 9935 sec. 6 inner CHOICE structurally, dispatching on the DER tag (0x80 seed
|
|
694
|
+
// [0] / 0x04 expandedKey / 0x30 both) -- NEVER on the length of the enclosing OCTET STRING. A
|
|
695
|
+
// bare seed, a bare dk, an oqskeypair dk||ek, a constructed [0], or a wrong-size arm is rejected
|
|
696
|
+
// fail-closed (guards never guess a plausible layout). The seed/expandedKey CRYPTOGRAPHIC
|
|
697
|
+
// consistency (sec. 8 / FIPS 203 sec. 7.3) is enforced by the engine on import (a mismatch is a
|
|
698
|
+
// raw node error _nodeKey maps to webcrypto/data).
|
|
699
|
+
function _validateMlKemInner(innerBytes, sizes) {
|
|
700
|
+
var node;
|
|
701
|
+
try { node = asn1.decode(innerBytes); }
|
|
702
|
+
catch (e) { throw new WebCryptoError("webcrypto/data", "an ML-KEM private key must be the RFC 9935 sec. 6 seed/expandedKey/both CHOICE, not a bare key", e); }
|
|
703
|
+
if (node.tagClass === "context" && node.tagNumber === 0 && !node.constructed) {
|
|
704
|
+
if (!node.content || node.content.length !== 64) throw new WebCryptoError("webcrypto/data", "an ML-KEM seed must be exactly 64 octets (RFC 9935 sec. 6)");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (_isOctet(node, sizes.dk)) return;
|
|
708
|
+
if (node.tagClass === "universal" && node.tagNumber === asn1.TAGS.OCTET_STRING && !node.constructed) {
|
|
709
|
+
throw new WebCryptoError("webcrypto/data", "an ML-KEM expandedKey must be exactly " + sizes.dk + " octets for this parameter set (RFC 9935 sec. 6)");
|
|
710
|
+
}
|
|
711
|
+
if (node.tagClass === "universal" && node.tagNumber === asn1.TAGS.SEQUENCE) {
|
|
712
|
+
// Coverage residual -- a decoded universal SEQUENCE always carries a children array, so the
|
|
713
|
+
// `|| []` fallback is defensive (asn1.decode never yields a childless constructed node).
|
|
714
|
+
var kids = node.children || [];
|
|
715
|
+
if (kids.length !== 2 || !_isOctet(kids[0], 64) || !_isOctet(kids[1], sizes.dk)) {
|
|
716
|
+
throw new WebCryptoError("webcrypto/data", "an ML-KEM both-arm must be SEQUENCE { seed OCTET STRING(64), expandedKey OCTET STRING(" + sizes.dk + ") } (RFC 9935 sec. 6)");
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
throw new WebCryptoError("webcrypto/data", "an ML-KEM private key must be the seed, expandedKey, or both CHOICE (RFC 9935 sec. 6)");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// PKCS#8 inner-key pre-validation, keyed by the privateKeyAlgorithm OID (a registry row, not a
|
|
724
|
+
// switch). The parse surfaces the inner key as opaque octets by design; the fail-closed
|
|
725
|
+
// per-algorithm structural check lives here, at the one import boundary. Only ML-KEM needs it
|
|
726
|
+
// today: the engine ACCEPTS the OpenSSL-legacy bare-seed layout RFC 9935 sec. 6 forbids.
|
|
727
|
+
var PKCS8_INNER_VALIDATORS = {};
|
|
728
|
+
Object.keys(ML_KEM_INNER).forEach(function (o) { PKCS8_INNER_VALIDATORS[o] = function (inner) { _validateMlKemInner(inner, ML_KEM_INNER[o]); }; });
|
|
729
|
+
|
|
730
|
+
function _preValidatePkcs8(p8, name) {
|
|
731
|
+
if (!ML_KEM_NODE[name]) return; // only the ML-KEM CHOICE carries the bare-seed hazard
|
|
732
|
+
var algOid, innerBytes;
|
|
733
|
+
try {
|
|
734
|
+
var root = asn1.decode(p8);
|
|
735
|
+
algOid = asn1.read.oid(root.children[1].children[0]);
|
|
736
|
+
innerBytes = asn1.read.octetString(root.children[2]);
|
|
737
|
+
} catch (e) { throw new WebCryptoError("webcrypto/data", "importKey pkcs8: the ML-KEM PKCS#8 envelope is not well-formed", e); }
|
|
738
|
+
var validate = PKCS8_INNER_VALIDATORS[algOid];
|
|
739
|
+
if (!validate) throw new WebCryptoError("webcrypto/data", "importKey pkcs8: " + name + " does not match the key's algorithm OID");
|
|
740
|
+
validate(innerBytes);
|
|
741
|
+
}
|
|
742
|
+
|
|
657
743
|
SubtleCrypto.prototype.importKey = async function importKey(format, keyData, algorithm, extractable, keyUsages) {
|
|
658
744
|
var alg = _normalizeAlg(algorithm, "importKey");
|
|
659
745
|
var usages = keyUsages || [];
|
|
660
746
|
var name = alg.name;
|
|
661
747
|
|
|
748
|
+
// spki / pkcs8 carry an asymmetric key; a secret-key / KDF name has no such structure. Reject
|
|
749
|
+
// it up front (W3C NotSupportedError) so it can neither mint a mislabeled key nor bypass the
|
|
750
|
+
// per-algorithm pkcs8 pre-validation below.
|
|
751
|
+
if ((format === "spki" || format === "pkcs8") && SECRET_KEY_NAMES[name]) {
|
|
752
|
+
throw new WebCryptoError("webcrypto/not-supported", "importKey: " + name + " does not support the " + format + " key format");
|
|
753
|
+
}
|
|
754
|
+
|
|
662
755
|
if (format === "raw") {
|
|
663
756
|
// Symmetric raw material, or a raw public key for EC/OKP.
|
|
664
|
-
if (name
|
|
757
|
+
if (SECRET_KEY_NAMES[name]) {
|
|
665
758
|
var raw = _toBuf(keyData, "importKey raw");
|
|
666
759
|
_assertAesImportLen(name, raw.length);
|
|
667
760
|
var secret = nodeCrypto.createSecretKey(raw);
|
|
@@ -673,15 +766,24 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
|
|
|
673
766
|
}
|
|
674
767
|
|
|
675
768
|
if (format === "spki") {
|
|
676
|
-
var
|
|
769
|
+
var spkiBuf = _toBuf(keyData, "importKey spki");
|
|
770
|
+
var pub = _nodeKey(function () { return nodeCrypto.createPublicKey({ key: spkiBuf, format: "der", type: "spki" }); }, "importKey spki");
|
|
677
771
|
return new CryptoKey("public", true, _algFromImport(name, alg, pub), usages, pub);
|
|
678
772
|
}
|
|
679
773
|
if (format === "pkcs8") {
|
|
680
|
-
var
|
|
774
|
+
var p8Buf = _toBuf(keyData, "importKey pkcs8");
|
|
775
|
+
_preValidatePkcs8(p8Buf, name);
|
|
776
|
+
var priv = _nodeKey(function () { return nodeCrypto.createPrivateKey({ key: p8Buf, format: "der", type: "pkcs8" }); }, "importKey pkcs8");
|
|
681
777
|
return new CryptoKey("private", extractable, _algFromImport(name, alg, priv), usages, priv);
|
|
682
778
|
}
|
|
683
779
|
if (format === "jwk") {
|
|
684
780
|
var jwk = keyData;
|
|
781
|
+
// A JWK is a JSON object; null / undefined / an array / a primitive (incl. the JSON token
|
|
782
|
+
// `null` from an unwrapKey over non-authenticating ciphertext) fails closed as a typed
|
|
783
|
+
// DataError, never a raw property-access TypeError.
|
|
784
|
+
if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) {
|
|
785
|
+
throw new WebCryptoError("webcrypto/data", "importKey jwk: keyData must be a JsonWebKey object");
|
|
786
|
+
}
|
|
685
787
|
if (jwk.kty === "oct") {
|
|
686
788
|
var kbuf = _b64urlToBuf(jwk.k, "JWK oct key material");
|
|
687
789
|
_assertAesImportLen(name, kbuf.length);
|
|
@@ -690,7 +792,7 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
|
|
|
690
792
|
return new CryptoKey("secret", extractable, a2, usages, s2);
|
|
691
793
|
}
|
|
692
794
|
var isPrivate = Object.prototype.hasOwnProperty.call(jwk, "d");
|
|
693
|
-
var ko = isPrivate ? nodeCrypto.createPrivateKey({ key: jwk, format: "jwk" }) : nodeCrypto.createPublicKey({ key: jwk, format: "jwk" });
|
|
795
|
+
var ko = _nodeKey(function () { return isPrivate ? nodeCrypto.createPrivateKey({ key: jwk, format: "jwk" }) : nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }, "importKey jwk");
|
|
694
796
|
return new CryptoKey(isPrivate ? "private" : "public", isPrivate ? extractable : true, _algFromImport(name, alg, ko), usages, ko);
|
|
695
797
|
}
|
|
696
798
|
throw new WebCryptoError("webcrypto/not-supported", "importKey: unsupported format " + JSON.stringify(format));
|
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:54db6dce-3101-401d-be5e-baa7f068783f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T03:51:39.360Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.22",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.22",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.22",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.22",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|