@blamejs/pki 0.2.22 → 0.2.23
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 +11 -1
- package/README.md +1 -1
- package/lib/cms-decrypt.js +539 -0
- package/lib/cms-encrypt.js +498 -0
- package/lib/cms-verify.js +63 -1
- package/lib/constants.js +6 -0
- package/lib/oid.js +18 -1
- package/lib/schema-cms.js +9 -0
- package/lib/webcrypto.js +63 -8
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/webcrypto.js
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
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, encoding,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* FIPS 203 ML-KEM key generation, encoding, certificate/PKCS#8 import,
|
|
26
|
+
* and encapsulation/decapsulation (encapsulateBits / decapsulateBits over
|
|
27
|
+
* Node's crypto.encapsulate/decapsulate) are all available.
|
|
28
28
|
* Because it is OpenSSL-backed, every key and signature it emits is
|
|
29
29
|
* interoperable with OpenSSL, NSS, and other PKI implementations.
|
|
30
30
|
*
|
|
@@ -83,6 +83,7 @@ function _normalizeAlg(algorithm, who) {
|
|
|
83
83
|
// backwards compatibility with legacy certificates and signatures.
|
|
84
84
|
var HASH_NODE = {
|
|
85
85
|
"SHA-1": "sha1",
|
|
86
|
+
"SHA-224": "sha224",
|
|
86
87
|
"SHA-256": "sha256",
|
|
87
88
|
"SHA-384": "sha384",
|
|
88
89
|
"SHA-512": "sha512",
|
|
@@ -130,9 +131,8 @@ var ML_KEM_NODE = { "ML-KEM-512": "ml-kem-512", "ML-KEM-768": "ml-kem-768", "ML-
|
|
|
130
131
|
|
|
131
132
|
// FIPS 205 SLH-DSA -- stateless hash-based signatures. All twelve
|
|
132
133
|
// parameter sets Node exposes; signing is one-shot (null algorithm), the
|
|
133
|
-
// same shape as ML-DSA / EdDSA. ML-KEM
|
|
134
|
-
// import
|
|
135
|
-
// to the CMS KEM-decrypt surface, not blocked by the engine.
|
|
134
|
+
// same shape as ML-DSA / EdDSA. ML-KEM covers key-generation / encoding /
|
|
135
|
+
// import plus encapsulateBits / decapsulateBits over Node's KEM primitive.
|
|
136
136
|
var SLH_DSA_NODE = {};
|
|
137
137
|
["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
|
|
138
138
|
"shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
|
|
@@ -147,13 +147,13 @@ var SIGN_VERIFY_NAMES = {};
|
|
|
147
147
|
.concat(Object.keys(ML_DSA_NODE), Object.keys(SLH_DSA_NODE))
|
|
148
148
|
.forEach(function (n) { SIGN_VERIFY_NAMES[n] = true; });
|
|
149
149
|
var ENCRYPT_DECRYPT_NAMES = { "RSA-OAEP": true, "AES-GCM": true, "AES-CBC": true, "AES-CTR": true };
|
|
150
|
-
var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true };
|
|
150
|
+
var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true, "X963KDF": true };
|
|
151
151
|
// The secret-key / KDF algorithms whose key material is raw octets (imported via "raw" or a
|
|
152
152
|
// JWK "oct"), NEVER an SPKI / PKCS#8 asymmetric-key structure. importKey("spki"|"pkcs8", ...)
|
|
153
153
|
// under one of these names is unsupported (W3C: NotSupportedError) -- and, without this gate, it
|
|
154
154
|
// would mint a mislabeled CryptoKey wrapping an asymmetric handle AND dodge the algorithm-keyed
|
|
155
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 };
|
|
156
|
+
var SECRET_KEY_NAMES = { "AES-GCM": true, "AES-CBC": true, "AES-CTR": true, "AES-KW": true, "HMAC": true, "HKDF": true, "PBKDF2": true, "X963KDF": true };
|
|
157
157
|
|
|
158
158
|
// ---- CryptoKey -------------------------------------------------------
|
|
159
159
|
|
|
@@ -533,9 +533,29 @@ function _deriveBitsRaw(alg, key, length) {
|
|
|
533
533
|
var out = nodeCrypto.pbkdf2Sync(_secretBytes(key), _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"));
|
|
534
534
|
return _toArrayBuffer(out);
|
|
535
535
|
}
|
|
536
|
+
if (name === "X963KDF") {
|
|
537
|
+
// ANSI-X9.63 / SEC1 sec. 3.6.1 single-step KDF: K = H(Z || INT32(counter) || SharedInfo)
|
|
538
|
+
// concatenated over counter = 1, 2, ... The RFC 5753 kari KEK derivation; the base key holds
|
|
539
|
+
// the ECDH shared secret Z, alg.info holds the DER ECC-CMS-SharedInfo.
|
|
540
|
+
_requireDeriveLength(length, "X963KDF");
|
|
541
|
+
return _toArrayBuffer(_x963Kdf(_hashNode(alg.hash, "X963KDF"), _secretBytes(key), _toBuf(alg.info || Buffer.alloc(0), "X963KDF SharedInfo"), length / 8));
|
|
542
|
+
}
|
|
536
543
|
throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + JSON.stringify(name));
|
|
537
544
|
}
|
|
538
545
|
|
|
546
|
+
// The X9.63 single-step KDF counter loop. Bounded by the 2^32 counter range; every input is a
|
|
547
|
+
// public/agreed value, so a plain (non-constant-time) hash concat is correct.
|
|
548
|
+
function _x963Kdf(hashNode, z, sharedInfo, lenBytes) {
|
|
549
|
+
var blocks = [], counter = 1, got = 0;
|
|
550
|
+
while (got < lenBytes) {
|
|
551
|
+
if (counter > 0xffffffff) throw new WebCryptoError("webcrypto/operation", "X963KDF: requested output exceeds the counter range");
|
|
552
|
+
var ctr = Buffer.alloc(4); ctr.writeUInt32BE(counter, 0);
|
|
553
|
+
var h = nodeCrypto.createHash(hashNode).update(z).update(ctr).update(sharedInfo).digest();
|
|
554
|
+
blocks.push(h); got += h.length; counter += 1;
|
|
555
|
+
}
|
|
556
|
+
return Buffer.concat(blocks).subarray(0, lenBytes);
|
|
557
|
+
}
|
|
558
|
+
|
|
539
559
|
SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
|
|
540
560
|
var alg = _normalizeAlg(algorithm, "deriveBits");
|
|
541
561
|
_requireUsage(key, "deriveBits");
|
|
@@ -579,6 +599,41 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
|
|
|
579
599
|
return this.importKey("raw", raw, dk, extractable, keyUsages);
|
|
580
600
|
};
|
|
581
601
|
|
|
602
|
+
// ML-KEM (FIPS 203) key encapsulation over Node's crypto.encapsulate/decapsulate.
|
|
603
|
+
// encapsulateBits takes the recipient's PUBLIC (encapsulation) key and returns a fresh
|
|
604
|
+
// { sharedKey, ciphertext }; decapsulateBits takes the PRIVATE (decapsulation) key + a
|
|
605
|
+
// ciphertext and recovers the shared key. ML-KEM's Fujisaki-Okamoto transform gives IMPLICIT
|
|
606
|
+
// rejection: a tampered but correctly-sized ciphertext decapsulates to a pseudo-random shared
|
|
607
|
+
// key rather than failing, so decapsulateBits only throws (typed) on a malformed / wrong-length
|
|
608
|
+
// ciphertext -- the "wrong key" case is indistinguishable by design, which the CMS uniform
|
|
609
|
+
// decrypt-failure verdict relies on. The usage split (encapsulateBits -> public, decapsulateBits
|
|
610
|
+
// -> private) already lives in the generateKey usage filters; the explicit type check hardens a
|
|
611
|
+
// key imported with custom usages.
|
|
612
|
+
SubtleCrypto.prototype.encapsulateBits = async function encapsulateBits(algorithm, encapsulationKey) {
|
|
613
|
+
var alg = _normalizeAlg(algorithm, "encapsulateBits");
|
|
614
|
+
_requireUsage(encapsulationKey, "encapsulateBits");
|
|
615
|
+
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "encapsulateBits: unsupported algorithm " + JSON.stringify(alg.name));
|
|
616
|
+
_requireAlgMatch(alg, encapsulationKey, "encapsulateBits");
|
|
617
|
+
if (encapsulationKey.type !== "public") throw new WebCryptoError("webcrypto/invalid-access", "encapsulateBits requires a public (encapsulation) key, got " + JSON.stringify(encapsulationKey.type));
|
|
618
|
+
var r;
|
|
619
|
+
try { r = nodeCrypto.encapsulate(encapsulationKey._handle); }
|
|
620
|
+
catch (e) { throw new WebCryptoError("webcrypto/operation", "encapsulateBits: ML-KEM encapsulation failed", e); }
|
|
621
|
+
return { sharedKey: _toArrayBuffer(Buffer.from(r.sharedKey)), ciphertext: _toArrayBuffer(Buffer.from(r.ciphertext)) };
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
SubtleCrypto.prototype.decapsulateBits = async function decapsulateBits(algorithm, decapsulationKey, ciphertext) {
|
|
625
|
+
var alg = _normalizeAlg(algorithm, "decapsulateBits");
|
|
626
|
+
_requireUsage(decapsulationKey, "decapsulateBits");
|
|
627
|
+
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "decapsulateBits: unsupported algorithm " + JSON.stringify(alg.name));
|
|
628
|
+
_requireAlgMatch(alg, decapsulationKey, "decapsulateBits");
|
|
629
|
+
if (decapsulationKey.type !== "private") throw new WebCryptoError("webcrypto/invalid-access", "decapsulateBits requires a private (decapsulation) key, got " + JSON.stringify(decapsulationKey.type));
|
|
630
|
+
var ct = _toBuf(ciphertext, "decapsulateBits ciphertext");
|
|
631
|
+
var ss;
|
|
632
|
+
try { ss = nodeCrypto.decapsulate(decapsulationKey._handle, ct); }
|
|
633
|
+
catch (e) { throw new WebCryptoError("webcrypto/operation", "decapsulateBits: ML-KEM decapsulation failed (malformed or wrong-length ciphertext)", e); }
|
|
634
|
+
return _toArrayBuffer(Buffer.from(ss));
|
|
635
|
+
};
|
|
636
|
+
|
|
582
637
|
SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey, wrapAlgorithm) {
|
|
583
638
|
var exported = await this.exportKey(format, key);
|
|
584
639
|
var bytes = (format === "jwk") ? Buffer.from(JSON.stringify(exported)) : Buffer.from(exported);
|
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:218fb396-9c81-482d-be9e-f08c7add5683",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T08:33:14.109Z",
|
|
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.23",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.23",
|
|
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.23",
|
|
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.23",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|