@blamejs/pki 0.3.8 → 0.3.10
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 +23 -0
- package/README.md +2 -0
- package/index.js +10 -0
- package/lib/cms-decrypt.js +16 -60
- package/lib/cms-encrypt.js +13 -50
- package/lib/crl-sign.js +574 -0
- package/lib/crl-verify.js +27 -0
- package/lib/framework-error.js +10 -0
- package/lib/key.js +420 -0
- package/lib/path-validate.js +6 -5
- package/lib/pbes2.js +136 -0
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/key.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.key
|
|
6
|
+
* @nav Signing
|
|
7
|
+
* @title Keys
|
|
8
|
+
* @intro The key-material lifecycle: export / import a private key as PKCS#8 (`OneAsymmetricKey`,
|
|
9
|
+
* RFC 5958) or a public key as SPKI (RFC 5280 sec. 4.1.2.7), encrypt / decrypt a private key under
|
|
10
|
+
* RFC 8018 PBES2 (`EncryptedPrivateKeyInfo`, PBKDF2 + AES-CBC-Pad), and `generate` /
|
|
11
|
+
* `publicFromPrivate` over every algorithm the WebCrypto engine drives -- RSA, EC, Ed25519/Ed448,
|
|
12
|
+
* X25519/X448, and the FIPS post-quantum ML-DSA / ML-KEM. Unencrypted export / import DELEGATES to the
|
|
13
|
+
* WebCrypto `exportKey` / `importKey` PKCS#8 / SPKI encoders (which already emit each algorithm's
|
|
14
|
+
* `AlgorithmIdentifier.parameters` correctly -- RSA NULL, EC namedCurve, Ed/X ABSENT), so the wrapper
|
|
15
|
+
* never re-encodes an AlgorithmIdentifier. PBES2 encrypt / decrypt composes the one shared `lib/pbes2.js`
|
|
16
|
+
* home (the same PBKDF2 + AES-CBC primitives `pki.cms` uses). Parsing lives at `pki.schema.pkcs8.parse`.
|
|
17
|
+
* @spec RFC 5958, RFC 8018, RFC 5280
|
|
18
|
+
* @card Export / import PKCS#8 and SPKI keys and encrypt a private key under RFC 8018 PBES2.
|
|
19
|
+
*/
|
|
20
|
+
//
|
|
21
|
+
// PBES2 is composed from lib/pbes2.js (the ONE PBKDF2 + AES-CBC home shared with pki.cms), bound to the key
|
|
22
|
+
// namespace through the `_err` error FACTORY + the "key" domain prefix so every reject keeps a key/* code.
|
|
23
|
+
// A MAC-less PBES2-CBC decrypt is not a padding oracle (RFC 8018 sec. 8): every post-derivation failure --
|
|
24
|
+
// a bad PKCS#7 pad OR a valid pad that does not re-parse as a PrivateKeyInfo -- collapses into ONE uniform
|
|
25
|
+
// key/decrypt-failed; the structural pre-derivation faults (a non-PBKDF2 KDF, a non-AES-CBC scheme, an
|
|
26
|
+
// over-cap salt / iteration count, a wrong-length IV, a malformed parameter SEQUENCE) stay distinct and are
|
|
27
|
+
// thrown BEFORE any key derivation, so they leak nothing password-dependent. Unencrypted export / import
|
|
28
|
+
// and publicFromPrivate DELEGATE to the WebCrypto / node key engines rather than re-serialize an
|
|
29
|
+
// AlgorithmIdentifier -- a re-encode is exactly where a params-absent-vs-NULL divergence sneaks back in.
|
|
30
|
+
|
|
31
|
+
var nodeCrypto = require("crypto");
|
|
32
|
+
var asn1 = require("./asn1-der");
|
|
33
|
+
var oid = require("./oid");
|
|
34
|
+
var pbes2 = require("./pbes2");
|
|
35
|
+
var pkcs8 = require("./schema-pkcs8");
|
|
36
|
+
var pkix = require("./schema-pkix");
|
|
37
|
+
var webcrypto = require("./webcrypto");
|
|
38
|
+
var frameworkError = require("./framework-error");
|
|
39
|
+
var guard = require("./guard-all");
|
|
40
|
+
|
|
41
|
+
var b = asn1.build;
|
|
42
|
+
var subtle = webcrypto.webcrypto.subtle;
|
|
43
|
+
var CryptoKey = webcrypto.CryptoKey;
|
|
44
|
+
var KeyError = frameworkError.KeyError;
|
|
45
|
+
var PemError = frameworkError.PemError;
|
|
46
|
+
function O(n) { return oid.byName(n); }
|
|
47
|
+
// The guard / pbes2 error convention: a (code, msg, cause) FACTORY, never the KeyError class -- pbes2 and
|
|
48
|
+
// the guards invoke it as E(code, msg) with no `new` (a class there crashes on the error path).
|
|
49
|
+
function _err(code, msg, cause) { return new KeyError(code, msg, cause); }
|
|
50
|
+
|
|
51
|
+
// The operator-facing node cipher name <-> the AES-CBC registry name the PBES2 home speaks.
|
|
52
|
+
var CIPHER_NAME = { "aes-128-cbc": "aes128-CBC", "aes-192-cbc": "aes192-CBC", "aes-256-cbc": "aes256-CBC" };
|
|
53
|
+
|
|
54
|
+
// import inference: the algorithm OIDs that name exactly ONE WebCrypto algorithm and need no parameters --
|
|
55
|
+
// the Edwards/Montgomery curves and the FIPS post-quantum ML-DSA, ML-KEM, and SLH-DSA (all signing-only or
|
|
56
|
+
// agreement-only, so unambiguous). RSA (sign vs OAEP) and EC (ECDSA vs ECDH) are deliberately ABSENT -- they
|
|
57
|
+
// are ambiguous, so import fails closed and asks for opts.algorithm rather than guess a plausible use.
|
|
58
|
+
var INFER_ALG = {};
|
|
59
|
+
["Ed25519", "Ed448", "X25519", "X448"].forEach(function (n) { INFER_ALG[O(n)] = { name: n }; });
|
|
60
|
+
[["id-ml-dsa-44", "ML-DSA-44"], ["id-ml-dsa-65", "ML-DSA-65"], ["id-ml-dsa-87", "ML-DSA-87"],
|
|
61
|
+
["id-ml-kem-512", "ML-KEM-512"], ["id-ml-kem-768", "ML-KEM-768"], ["id-ml-kem-1024", "ML-KEM-1024"]
|
|
62
|
+
].forEach(function (r) { INFER_ALG[O(r[0])] = { name: r[1] }; });
|
|
63
|
+
// SLH-DSA (FIPS 205) -- all twelve parameter sets; the WebCrypto name is the OID name minus "id-", uppercased.
|
|
64
|
+
["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
|
|
65
|
+
"shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
|
|
66
|
+
].forEach(function (s) { INFER_ALG[O("id-slh-dsa-" + s)] = { name: ("SLH-DSA-" + s).toUpperCase() }; });
|
|
67
|
+
|
|
68
|
+
function _isCryptoKey(x) { return x instanceof CryptoKey; }
|
|
69
|
+
function _algName(a) { return typeof a === "string" ? a : (a && a.name); }
|
|
70
|
+
|
|
71
|
+
// A private-key input (CryptoKey | DER Buffer | 'PRIVATE KEY' PEM) -> the raw PKCS#8 PrivateKeyInfo DER.
|
|
72
|
+
async function _toPrivateKeyDer(input) {
|
|
73
|
+
if (_isCryptoKey(input)) {
|
|
74
|
+
if (input.type !== "private") throw _err("key/bad-input", "a private CryptoKey is required (got a " + input.type + " key)");
|
|
75
|
+
return Buffer.from(await subtle.exportKey("pkcs8", input));
|
|
76
|
+
}
|
|
77
|
+
return pkix.coerceToDer(input, { pemLabel: "PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @primitive pki.key.encrypt
|
|
82
|
+
* @signature pki.key.encrypt(privateKey, password, opts?) -> Promise<Buffer|string>
|
|
83
|
+
* @since 0.3.10
|
|
84
|
+
* @status experimental
|
|
85
|
+
* @spec RFC 5958 sec. 3, RFC 8018
|
|
86
|
+
* @related pki.key.decrypt, pki.schema.pkcs8.parseEncrypted, pki.cms.encrypt
|
|
87
|
+
*
|
|
88
|
+
* Encrypt a PKCS#8 private key into an RFC 5958 `EncryptedPrivateKeyInfo` under RFC 8018 PBES2 (PBKDF2 +
|
|
89
|
+
* AES-CBC-Pad). `privateKey` is a DER `Buffer`, a `PRIVATE KEY` PEM string, or an extractable private
|
|
90
|
+
* `CryptoKey`; `password` is a string (UTF-8-encoded, byte-identical to OpenSSL), `Buffer`, or `Uint8Array`.
|
|
91
|
+
* The plaintext is the DER `PrivateKeyInfo`, validated as a well-formed PKCS#8 structure before encryption
|
|
92
|
+
* (never encrypt opaque bytes), and the produced `EncryptedPrivateKeyInfo` is re-parsed before return.
|
|
93
|
+
*
|
|
94
|
+
* The PBKDF2 `prf` equal to the default (`hmacWithSHA1`) is omitted from the parameters (X.690 sec. 11.5),
|
|
95
|
+
* `keyLength` is omitted (the AES cipher OID fixes the key size), and the salt / iteration count are bounded
|
|
96
|
+
* -- so the output is byte-exact with OpenSSL's `pkcs8 -topk8 -v2`. A bad input throws a typed `KeyError`.
|
|
97
|
+
*
|
|
98
|
+
* @opts
|
|
99
|
+
* - `cipher` (string) -- `aes-256-cbc` (default), `aes-192-cbc`, or `aes-128-cbc`.
|
|
100
|
+
* - `prf` (string) -- `hmacWithSHA256` (default), `hmacWithSHA384`, `hmacWithSHA512`, or `hmacWithSHA1`.
|
|
101
|
+
* - `iterations` (number) -- PBKDF2 iteration count, default 600000 (bounded by the decryptor's cap).
|
|
102
|
+
* - `salt` (Buffer) -- an explicit PBKDF2 salt (default 16 random octets).
|
|
103
|
+
* - `pem` (boolean) -- return an `ENCRYPTED PRIVATE KEY` PEM string instead of DER.
|
|
104
|
+
* @example
|
|
105
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
106
|
+
* var der = await pki.key.export(pair.privateKey);
|
|
107
|
+
* var enc = await pki.key.encrypt(der, "s3cr3t", { pem: true });
|
|
108
|
+
* var back = await pki.key.decrypt(enc, "s3cr3t");
|
|
109
|
+
*/
|
|
110
|
+
async function encrypt(privateKey, password, opts) {
|
|
111
|
+
opts = opts || {};
|
|
112
|
+
var der = await _toPrivateKeyDer(privateKey);
|
|
113
|
+
// RFC 5958 sec. 3: the plaintext is the DER PrivateKeyInfo -- validate it IS one (the strict parser
|
|
114
|
+
// rejects BER / non-canonical / trailing bytes) before encrypting, so we never wrap opaque input.
|
|
115
|
+
try { pkcs8.parse(der); } catch (e) { throw _err("key/bad-input", "the private key is not a well-formed PKCS#8 PrivateKeyInfo", e); }
|
|
116
|
+
var cipherName = CIPHER_NAME[opts.cipher || "aes-256-cbc"];
|
|
117
|
+
if (!cipherName) throw _err("key/bad-input", "unsupported cipher " + JSON.stringify(opts.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
|
|
118
|
+
var keyBits = pbes2.CONTENT_KEYBITS[O(cipherName)];
|
|
119
|
+
var prf = opts.prf || "hmacWithSHA256";
|
|
120
|
+
var prfNode = pbes2.prfNodeByName(prf, _err, "key");
|
|
121
|
+
var iterations = pbes2.assertIterations(opts.iterations == null ? 600000 : opts.iterations, _err, "key");
|
|
122
|
+
var salt = opts.salt != null ? pbes2.assertSalt(guard.bytes.view(opts.salt, KeyError, "key/bad-input", "salt"), _err, "key") : nodeCrypto.randomBytes(16);
|
|
123
|
+
var iv = nodeCrypto.randomBytes(16);
|
|
124
|
+
var dk = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(password, _err, "key"), salt, iterations, keyBits / 8, prfNode);
|
|
125
|
+
var ciphertext = pbes2.cbcEncrypt(dk, iv, der, keyBits);
|
|
126
|
+
var epki = b.sequence([pbes2.pbes2AlgId(salt, iterations, prf, cipherName, iv), b.octetString(ciphertext)]);
|
|
127
|
+
pkcs8.parseEncrypted(epki); // self-check: the produced EncryptedPrivateKeyInfo re-parses
|
|
128
|
+
return opts.pem ? pkcs8.pemEncode(epki, "ENCRYPTED PRIVATE KEY") : epki;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @primitive pki.key.decrypt
|
|
133
|
+
* @signature pki.key.decrypt(encrypted, password, opts?) -> Promise<Buffer|string>
|
|
134
|
+
* @since 0.3.10
|
|
135
|
+
* @status experimental
|
|
136
|
+
* @spec RFC 5958 sec. 3, RFC 8018 sec. 6.2, RFC 8018 sec. 8
|
|
137
|
+
* @defends pbes2-padding-oracle (CWE-208), pbkdf2-work-dos (CWE-400)
|
|
138
|
+
* @related pki.key.encrypt, pki.schema.pkcs8.parse
|
|
139
|
+
*
|
|
140
|
+
* Decrypt an RFC 5958 `EncryptedPrivateKeyInfo` (DER `Buffer` or `ENCRYPTED PRIVATE KEY` PEM) under RFC 8018
|
|
141
|
+
* PBES2, returning the inner `PrivateKeyInfo` (re-validated via `pki.schema.pkcs8.parse`). Only PBES2 with a
|
|
142
|
+
* PBKDF2 key-derivation function and an AES-CBC encryption scheme is accepted; PBES1, PBMAC1, scrypt, and any
|
|
143
|
+
* other `encryptionAlgorithm` fail closed with `key/unsupported-algorithm`.
|
|
144
|
+
*
|
|
145
|
+
* The salt and iteration count are attacker-controlled work: both caps are enforced BEFORE any derivation
|
|
146
|
+
* (`opts.maxIterations` may lower the cap, never raise it), and a wrong-length IV or malformed parameter set
|
|
147
|
+
* is a typed `key/bad-algorithm-parameters`. A MAC-less PBES2-CBC decrypt is not a padding oracle -- a wrong
|
|
148
|
+
* password and a valid-pad-but-not-a-PrivateKeyInfo both surface the single uniform `key/decrypt-failed`.
|
|
149
|
+
*
|
|
150
|
+
* @opts
|
|
151
|
+
* - `maxIterations` (number) -- lower the PBKDF2 iteration cap for this decrypt (downward-only).
|
|
152
|
+
* - `pem` (boolean) -- return a `PRIVATE KEY` PEM string instead of DER.
|
|
153
|
+
* @example
|
|
154
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
155
|
+
* var enc = await pki.key.encrypt(await pki.key.export(pair.privateKey), "s3cr3t", { pem: true });
|
|
156
|
+
* var der = await pki.key.decrypt(enc, "s3cr3t");
|
|
157
|
+
*/
|
|
158
|
+
async function decrypt(encrypted, password, opts) {
|
|
159
|
+
opts = opts || {};
|
|
160
|
+
// opts.maxIterations is a CALLER authoring bound (a downward-only cap): validate it config-time so an
|
|
161
|
+
// invalid value is a key/bad-input, never a NaN that silently disables the DoS cap and never mis-coded as
|
|
162
|
+
// a fault in the attacker-controlled input parameters.
|
|
163
|
+
if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
|
|
164
|
+
throw _err("key/bad-input", "maxIterations must be a positive integer");
|
|
165
|
+
}
|
|
166
|
+
var input = pkix.coerceToDer(encrypted, { pemLabel: "ENCRYPTED PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
|
|
167
|
+
var epki;
|
|
168
|
+
try { epki = pkcs8.parseEncrypted(input); }
|
|
169
|
+
catch (e) { throw _err("key/bad-input", "the input is not a well-formed EncryptedPrivateKeyInfo", e); }
|
|
170
|
+
var encAlg = epki.encryptionAlgorithm;
|
|
171
|
+
if (encAlg.oid !== O("pbes2")) throw _err("key/unsupported-algorithm", "unsupported key encryption algorithm " + (encAlg.name || encAlg.oid) + " (only RFC 8018 PBES2 is supported)");
|
|
172
|
+
var plaintext = _decryptPbes2(encAlg, epki.encryptedData, password, opts);
|
|
173
|
+
return opts.pem ? pkcs8.pemEncode(plaintext, "PRIVATE KEY") : plaintext;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function _decryptPbes2(encAlg, ciphertext, password, opts) {
|
|
177
|
+
var pb, keyBits, iv;
|
|
178
|
+
// Structural parse (typed, pre-derivation). These faults are not password-dependent, so a distinct code
|
|
179
|
+
// per fault leaks no oracle; a raw asn1 fault is normalized to key/bad-algorithm-parameters.
|
|
180
|
+
try {
|
|
181
|
+
var params = pbes2.seqChildren(encAlg.parameters, 2, "PBES2 parameters", _err, "key");
|
|
182
|
+
var kdf = pbes2.requireChildren(params[0], 2, "PBES2 keyDerivationFunc", _err, "key");
|
|
183
|
+
var encScheme = pbes2.requireChildren(params[1], 2, "PBES2 encryptionScheme", _err, "key");
|
|
184
|
+
if (asn1.read.oid(kdf[0]) !== O("pbkdf2")) throw _err("key/unsupported-algorithm", "the PBES2 keyDerivationFunc must be PBKDF2 (RFC 8018 sec. 6.2)");
|
|
185
|
+
pb = pbes2.parsePbkdf2Params(kdf[1].bytes, opts, _err, "key", true); // strictPrf: reject a non-canonical explicit default prf
|
|
186
|
+
var encOid = asn1.read.oid(encScheme[0]);
|
|
187
|
+
keyBits = pbes2.CONTENT_KEYBITS[encOid];
|
|
188
|
+
if (!keyBits || !/CBC/.test(oid.name(encOid) || "")) throw _err("key/unsupported-algorithm", "unsupported PBES2 encryptionScheme " + (oid.name(encOid) || encOid) + " (AES-CBC only)");
|
|
189
|
+
iv = asn1.read.octetString(encScheme[1]);
|
|
190
|
+
if (iv.length !== 16) throw _err("key/bad-algorithm-parameters", "the AES-CBC IV must be 16 octets");
|
|
191
|
+
} catch (e) {
|
|
192
|
+
// The shared PBES2 home rejects a malformed / truncated / over-cap parameter with the generic
|
|
193
|
+
// key/bad-input; in the PBES2 parameter-structure context that IS a bad-algorithm-parameters. The
|
|
194
|
+
// semantic verdicts it raises (unsupported-algorithm, iteration-limit) keep their precise codes.
|
|
195
|
+
if (e instanceof KeyError) {
|
|
196
|
+
if (e.code === "key/bad-input") throw _err("key/bad-algorithm-parameters", e.message, e.cause);
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
199
|
+
throw _err("key/bad-algorithm-parameters", "malformed PBES2 parameters", e);
|
|
200
|
+
}
|
|
201
|
+
var dk = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(password, _err, "key"), pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
|
|
202
|
+
// Post-derivation failures collapse to ONE uniform verdict (RFC 8018 sec. 8): a bad PKCS#7 pad and a
|
|
203
|
+
// valid pad whose plaintext is not a PrivateKeyInfo are indistinguishable -- the re-parse is the integrity
|
|
204
|
+
// check for MAC-less PBES2-CBC, and it MUST run (never skipped).
|
|
205
|
+
var plaintext;
|
|
206
|
+
try { plaintext = pbes2.cbcDecrypt(dk, iv, ciphertext, keyBits); }
|
|
207
|
+
catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
|
|
208
|
+
try { pkcs8.parse(plaintext); }
|
|
209
|
+
catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
|
|
210
|
+
return plaintext;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @primitive pki.key.export
|
|
215
|
+
* @signature pki.key.export(key, opts?) -> Promise<Buffer|string>
|
|
216
|
+
* @since 0.3.10
|
|
217
|
+
* @status experimental
|
|
218
|
+
* @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
|
|
219
|
+
* @related pki.key.import, pki.key.publicFromPrivate, pki.schema.pkcs8.parse
|
|
220
|
+
*
|
|
221
|
+
* Export an extractable `CryptoKey` to DER (or PEM): a private key as PKCS#8 `OneAsymmetricKey`, a public
|
|
222
|
+
* key as SubjectPublicKeyInfo. The encoding is delegated to the WebCrypto `exportKey` PKCS#8 / SPKI encoder,
|
|
223
|
+
* so the algorithm-specific `AlgorithmIdentifier.parameters` are byte-correct -- RSA carries an explicit
|
|
224
|
+
* NULL, EC a namedCurve OID, and Ed25519 / Ed448 / X25519 / X448 omit parameters (RFC 8410 sec. 3); the
|
|
225
|
+
* wrapper never re-encodes the AlgorithmIdentifier.
|
|
226
|
+
*
|
|
227
|
+
* @opts
|
|
228
|
+
* - `format` (string) -- `der` (default) or `pem`.
|
|
229
|
+
* - `label` (string) -- the PEM label (defaults `PRIVATE KEY` / `PUBLIC KEY` by key type).
|
|
230
|
+
* @example
|
|
231
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
232
|
+
* var spkiPem = await pki.key.export(pair.publicKey, { format: "pem" });
|
|
233
|
+
*/
|
|
234
|
+
async function export_(key, opts) {
|
|
235
|
+
opts = opts || {};
|
|
236
|
+
if (!_isCryptoKey(key)) throw _err("key/bad-input", "export expects a WebCrypto CryptoKey");
|
|
237
|
+
var format, defaultLabel;
|
|
238
|
+
if (key.type === "private") { format = "pkcs8"; defaultLabel = "PRIVATE KEY"; }
|
|
239
|
+
else if (key.type === "public") { format = "spki"; defaultLabel = "PUBLIC KEY"; }
|
|
240
|
+
else throw _err("key/bad-input", "export supports asymmetric (private / public) CryptoKeys only");
|
|
241
|
+
var der = Buffer.from(await subtle.exportKey(format, key));
|
|
242
|
+
var fmt = opts.format || "der";
|
|
243
|
+
if (fmt === "der") return der;
|
|
244
|
+
if (fmt === "pem") return pkix.pemEncode(der, opts.label || defaultLabel, PemError);
|
|
245
|
+
throw _err("key/bad-input", "unsupported format " + JSON.stringify(opts.format) + " (der / pem)");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @primitive pki.key.import
|
|
250
|
+
* @signature pki.key.import(input, opts?) -> Promise<CryptoKey>
|
|
251
|
+
* @since 0.3.10
|
|
252
|
+
* @status experimental
|
|
253
|
+
* @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8018
|
|
254
|
+
* @related pki.key.export, pki.key.decrypt
|
|
255
|
+
*
|
|
256
|
+
* Import a DER / PEM PKCS#8 private key, SPKI public key, or (with `opts.password`) an `ENCRYPTED PRIVATE
|
|
257
|
+
* KEY` -- auto-detecting the structure -- into a `CryptoKey`. The WebCrypto algorithm is inferred from the
|
|
258
|
+
* key's OID for the algorithms that name exactly one (Ed25519 / Ed448 / X25519 / X448 / ML-DSA / ML-KEM /
|
|
259
|
+
* SLH-DSA); RSA and EC are ambiguous between signing and key agreement, so `opts.algorithm` must be supplied
|
|
260
|
+
* for them (import fails closed rather than guess a use). Default key usages follow the algorithm and key type.
|
|
261
|
+
*
|
|
262
|
+
* @opts
|
|
263
|
+
* - `algorithm` (string | object) -- the WebCrypto algorithm (required for RSA / EC; overrides inference).
|
|
264
|
+
* - `usages` (string[]) -- key usages (default derived from the algorithm and public/private type).
|
|
265
|
+
* - `extractable` (boolean) -- default false.
|
|
266
|
+
* - `password` (string | Buffer) -- decrypt an `ENCRYPTED PRIVATE KEY` first.
|
|
267
|
+
* @example
|
|
268
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
269
|
+
* var spki = await pki.key.export(pair.publicKey);
|
|
270
|
+
* var pub = await pki.key.import(spki); // Ed25519 -- algorithm inferred
|
|
271
|
+
*/
|
|
272
|
+
async function import_(input, opts) {
|
|
273
|
+
opts = opts || {};
|
|
274
|
+
var detected = _detectKeyInput(input);
|
|
275
|
+
if (detected.format === "encrypted") {
|
|
276
|
+
if (opts.password == null) throw _err("key/bad-input", "an ENCRYPTED PRIVATE KEY requires opts.password to import");
|
|
277
|
+
detected = { format: "pkcs8", der: await decrypt(detected.der, opts.password) };
|
|
278
|
+
}
|
|
279
|
+
var algorithm = opts.algorithm != null ? opts.algorithm : _inferAlgorithm(detected);
|
|
280
|
+
var isPublic = detected.format === "spki";
|
|
281
|
+
var usages = opts.usages || _importUsages(_algName(algorithm), isPublic);
|
|
282
|
+
var extractable = opts.extractable != null ? opts.extractable : false;
|
|
283
|
+
try {
|
|
284
|
+
return await subtle.importKey(detected.format, detected.der, algorithm, extractable, usages);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
if (e && e.isPkiError) throw e;
|
|
287
|
+
throw _err("key/bad-input", "importKey failed", e);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @primitive pki.key.generate
|
|
293
|
+
* @signature pki.key.generate(algorithm, opts?) -> Promise<{ privateKey, publicKey }>
|
|
294
|
+
* @since 0.3.10
|
|
295
|
+
* @status experimental
|
|
296
|
+
* @spec W3C WebCrypto, FIPS 203, FIPS 204
|
|
297
|
+
* @related pki.key.export, pki.key.publicFromPrivate
|
|
298
|
+
*
|
|
299
|
+
* Generate an asymmetric key pair over the WebCrypto engine: RSA, ECDSA / ECDH, Ed25519 / Ed448, X25519 /
|
|
300
|
+
* X448, and the FIPS post-quantum ML-DSA / ML-KEM. `algorithm` is a WebCrypto algorithm string or object;
|
|
301
|
+
* usages default to the algorithm's natural set (sign/verify, deriveBits/deriveKey, or encapsulate/
|
|
302
|
+
* decapsulate) and keys are extractable by default. Returns the `{ privateKey, publicKey }` `CryptoKey` pair.
|
|
303
|
+
*
|
|
304
|
+
* @opts
|
|
305
|
+
* - `extractable` (boolean) -- default true.
|
|
306
|
+
* - `usages` (string[]) -- override the default key usages.
|
|
307
|
+
* @example
|
|
308
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
309
|
+
* var pkcs8 = await pki.key.export(pair.privateKey);
|
|
310
|
+
*/
|
|
311
|
+
async function generate(algorithm, opts) {
|
|
312
|
+
opts = opts || {};
|
|
313
|
+
var extractable = opts.extractable != null ? opts.extractable : true;
|
|
314
|
+
var usages = opts.usages || _generateUsages(_algName(algorithm));
|
|
315
|
+
var pair;
|
|
316
|
+
try { pair = await subtle.generateKey(algorithm, extractable, usages); }
|
|
317
|
+
catch (e) { if (e && e.isPkiError) throw e; throw _err("key/bad-input", "generateKey failed", e); }
|
|
318
|
+
if (!pair || !pair.privateKey || !pair.publicKey) throw _err("key/bad-input", "the algorithm does not generate an asymmetric key pair");
|
|
319
|
+
return { privateKey: pair.privateKey, publicKey: pair.publicKey };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @primitive pki.key.publicFromPrivate
|
|
324
|
+
* @signature pki.key.publicFromPrivate(privateKey, opts?) -> Promise<Buffer|string>
|
|
325
|
+
* @since 0.3.10
|
|
326
|
+
* @status experimental
|
|
327
|
+
* @spec RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
|
|
328
|
+
* @related pki.key.export, pki.key.import
|
|
329
|
+
*
|
|
330
|
+
* Derive the SubjectPublicKeyInfo (SPKI) public key from a PKCS#8 private key (DER `Buffer`, `PRIVATE KEY`
|
|
331
|
+
* PEM, or extractable private `CryptoKey`). The derivation is delegated to the node key engine, which infers
|
|
332
|
+
* the algorithm from the key structure, so no `AlgorithmIdentifier` is re-encoded -- Ed25519 stays
|
|
333
|
+
* parameters-absent, RSA keeps its NULL, EC keeps its namedCurve.
|
|
334
|
+
*
|
|
335
|
+
* @opts
|
|
336
|
+
* - `pem` (boolean) -- return a `PUBLIC KEY` PEM string instead of DER.
|
|
337
|
+
* @example
|
|
338
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
339
|
+
* var spki = await pki.key.publicFromPrivate(await pki.key.export(pair.privateKey));
|
|
340
|
+
*/
|
|
341
|
+
async function publicFromPrivate(privateKey, opts) {
|
|
342
|
+
opts = opts || {};
|
|
343
|
+
var der = await _toPrivateKeyDer(privateKey);
|
|
344
|
+
var spki;
|
|
345
|
+
try {
|
|
346
|
+
var priv = nodeCrypto.createPrivateKey({ key: der, format: "der", type: "pkcs8" });
|
|
347
|
+
spki = nodeCrypto.createPublicKey(priv).export({ format: "der", type: "spki" });
|
|
348
|
+
} catch (e) { throw _err("key/bad-input", "could not derive the public key from the private key", e); }
|
|
349
|
+
return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---- import helpers --------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
// Auto-detect a key input by structure (works for a raw DER Buffer or a label-agnostic PEM): a PrivateKeyInfo
|
|
355
|
+
// leads with an INTEGER version; an SPKI is SEQ{ algId, BIT STRING }; an EncryptedPrivateKeyInfo is
|
|
356
|
+
// SEQ{ algId, OCTET STRING }. Returns { format, der }.
|
|
357
|
+
function _detectKeyInput(input) {
|
|
358
|
+
var der = pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: KeyError, prefix: "key" });
|
|
359
|
+
var root;
|
|
360
|
+
try { root = asn1.decode(der); } catch (e) { throw _err("key/bad-input", "the input is not DER or a PEM key", e); }
|
|
361
|
+
var kids = root.children || [];
|
|
362
|
+
if (kids.length >= 1 && kids[0].tagClass === "universal" && kids[0].tagNumber === asn1.TAGS.INTEGER) return { format: "pkcs8", der: der };
|
|
363
|
+
if (kids.length === 2 && kids[1].tagClass === "universal" && kids[1].tagNumber === asn1.TAGS.BIT_STRING) return { format: "spki", der: der };
|
|
364
|
+
if (kids.length === 2 && kids[1].tagClass === "universal" && kids[1].tagNumber === asn1.TAGS.OCTET_STRING) return { format: "encrypted", der: der };
|
|
365
|
+
throw _err("key/bad-input", "the input is not a recognized PKCS#8, SPKI, or EncryptedPrivateKeyInfo");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// The AlgorithmIdentifier OID from a detected key: privateKeyAlgorithm (PKCS#8) or algorithm (SPKI).
|
|
369
|
+
function _readAlgOid(detected) {
|
|
370
|
+
try {
|
|
371
|
+
if (detected.format === "pkcs8") return pkcs8.parse(detected.der).privateKeyAlgorithm.oid;
|
|
372
|
+
// SPKI: SEQUENCE { algorithm AlgorithmIdentifier SEQUENCE { OID, ... }, subjectPublicKey BIT STRING }.
|
|
373
|
+
// Validate the algorithm SEQUENCE shape before dereferencing its OID -- a malformed SPKI must be a typed
|
|
374
|
+
// reject, never a raw children[] fault.
|
|
375
|
+
var alg = asn1.decode(detected.der).children[0];
|
|
376
|
+
if (!alg || alg.tagClass !== "universal" || alg.tagNumber !== asn1.TAGS.SEQUENCE || !alg.children || !alg.children.length) {
|
|
377
|
+
throw _err("key/bad-input", "malformed SubjectPublicKeyInfo algorithm identifier");
|
|
378
|
+
}
|
|
379
|
+
return asn1.read.oid(alg.children[0]);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
if (e instanceof KeyError) throw e;
|
|
382
|
+
throw _err("key/bad-input", "the key algorithm could not be read for inference", e);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function _inferAlgorithm(detected) {
|
|
387
|
+
var algOid = _readAlgOid(detected);
|
|
388
|
+
var a = INFER_ALG[algOid];
|
|
389
|
+
if (a) return a;
|
|
390
|
+
throw _err("key/unsupported-algorithm", "cannot infer the WebCrypto algorithm for " + (oid.name(algOid) || algOid) +
|
|
391
|
+
" (RSA and EC are ambiguous between signing and key agreement -- pass opts.algorithm)");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Default key usages by algorithm CLASS. The name is normalized to upper case first: WebCrypto matches
|
|
395
|
+
// algorithm names case-insensitively, so a caller passing { name: "x25519" } / "ml-kem-768" must land on the
|
|
396
|
+
// same usage class as the canonical spelling rather than fall through to the signing default.
|
|
397
|
+
function _importUsages(name, isPublic) {
|
|
398
|
+
var n = String(name == null ? "" : name).toUpperCase();
|
|
399
|
+
if (n === "X25519" || n === "X448" || n === "ECDH") return isPublic ? [] : ["deriveBits", "deriveKey"];
|
|
400
|
+
if (/^ML-KEM/.test(n)) return isPublic ? ["encapsulateBits"] : ["decapsulateBits"];
|
|
401
|
+
if (n === "RSA-OAEP") return isPublic ? ["encrypt"] : ["decrypt"];
|
|
402
|
+
return isPublic ? ["verify"] : ["sign"]; // signing default (Ed / ECDSA / RSA-* sign / ML-DSA / SLH-DSA)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function _generateUsages(name) {
|
|
406
|
+
var n = String(name == null ? "" : name).toUpperCase();
|
|
407
|
+
if (n === "X25519" || n === "X448" || n === "ECDH") return ["deriveBits", "deriveKey"];
|
|
408
|
+
if (/^ML-KEM/.test(n)) return ["encapsulateBits", "decapsulateBits"];
|
|
409
|
+
if (n === "RSA-OAEP") return ["encrypt", "decrypt"];
|
|
410
|
+
return ["sign", "verify"];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = {
|
|
414
|
+
encrypt: encrypt,
|
|
415
|
+
decrypt: decrypt,
|
|
416
|
+
export: export_,
|
|
417
|
+
import: import_,
|
|
418
|
+
generate: generate,
|
|
419
|
+
publicFromPrivate: publicFromPrivate,
|
|
420
|
+
};
|
package/lib/path-validate.js
CHANGED
|
@@ -39,6 +39,7 @@ var x509 = require("./schema-x509");
|
|
|
39
39
|
var crl = require("./schema-crl");
|
|
40
40
|
var ocsp = require("./schema-ocsp");
|
|
41
41
|
var ocspVerify = require("./ocsp-verify");
|
|
42
|
+
var crlVerify = require("./crl-verify");
|
|
42
43
|
var guard = require("./guard-all");
|
|
43
44
|
var constants = require("./constants");
|
|
44
45
|
var validator = require("./validator-all");
|
|
@@ -1762,7 +1763,7 @@ function crlChecker(crls) {
|
|
|
1762
1763
|
// replayed old CRL must not read "good". Treat it as unusable.
|
|
1763
1764
|
if (!theCrl.nextUpdate || theCrl.nextUpdate < time) continue; // stale / no bound
|
|
1764
1765
|
|
|
1765
|
-
var sigOk = await verifyCrlSignature(theCrl, issuer);
|
|
1766
|
+
var sigOk = await crlVerify.verifyCrlSignature(theCrl, issuer.workingPublicKey);
|
|
1766
1767
|
if (!sigOk) continue; // unverifiable -> not authoritative
|
|
1767
1768
|
|
|
1768
1769
|
// The CRL is now authoritative + current + verified. An authoritative
|
|
@@ -1853,10 +1854,10 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
|
|
|
1853
1854
|
}).then(function (ok) { return ok === true; }, function () { return false; });
|
|
1854
1855
|
}
|
|
1855
1856
|
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1857
|
+
// Inject this validator's signature engine into the shared internal crl-verify seam, so pki.crl.verify and
|
|
1858
|
+
// pki.path.crlChecker both route through this ONE engine (no second, weaker CRL verifier) without exposing
|
|
1859
|
+
// the seam on the public pki.path surface.
|
|
1860
|
+
crlVerify.setEngine(_verifyWithSpki);
|
|
1860
1861
|
|
|
1861
1862
|
// ---- the OCSP revocation checker (RFC 6960) -------------------------------
|
|
1862
1863
|
|
package/lib/pbes2.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- the RFC 8018 PBES2 / PBKDF2 primitives, the ONE home shared by pki.cms EncryptedData / PWRI
|
|
6
|
+
// password encryption and pki.key EncryptedPrivateKeyInfo (RFC 5958). Error-factory-parameterized like the
|
|
7
|
+
// guard family: every reject goes through the CALLER's `E(code, msg, cause)` full-code factory plus a domain
|
|
8
|
+
// `prefix`, so pki.cms keeps cms/* codes and pki.key keeps key/* codes off ONE implementation. Extracted
|
|
9
|
+
// behavior-preservingly from cms-encrypt.js + cms-decrypt.js -- the shipped CMS PBES2 tests are the guard.
|
|
10
|
+
// A shape here that CANNOT route through a guard stays with its caller; the reusable PBES2 shape lives here
|
|
11
|
+
// so a fourth copy (the drift a reviewer flags) cannot re-accrete.
|
|
12
|
+
|
|
13
|
+
var nodeCrypto = require("crypto");
|
|
14
|
+
var asn1 = require("./asn1-der");
|
|
15
|
+
var oid = require("./oid");
|
|
16
|
+
var guard = require("./guard-all");
|
|
17
|
+
var C = require("./constants");
|
|
18
|
+
|
|
19
|
+
var b = asn1.build;
|
|
20
|
+
function O(n) { return oid.byName(n); }
|
|
21
|
+
|
|
22
|
+
// PBKDF2 prf tables: the OID name -> node digest (encrypt names the prf) and the OID -> node digest (decrypt
|
|
23
|
+
// reads the OID). One source, both directions.
|
|
24
|
+
var PRF_NODE_BY_NAME = { hmacWithSHA1: "sha1", hmacWithSHA256: "sha256", hmacWithSHA384: "sha384", hmacWithSHA512: "sha512" };
|
|
25
|
+
var PRF_NODE_BY_OID = {}; Object.keys(PRF_NODE_BY_NAME).forEach(function (n) { PRF_NODE_BY_OID[O(n)] = PRF_NODE_BY_NAME[n]; });
|
|
26
|
+
|
|
27
|
+
// content-encryption OID -> AES key bits (CBC + GCM). The PBES2 encryptionScheme + CMS content cipher table.
|
|
28
|
+
var CONTENT_KEYBITS = {};
|
|
29
|
+
[["aes128-CBC", 128], ["aes192-CBC", 192], ["aes256-CBC", 256], ["aes128-GCM", 128], ["aes192-GCM", 192], ["aes256-GCM", 256]].forEach(function (r) { CONTENT_KEYBITS[O(r[0])] = r[1]; });
|
|
30
|
+
|
|
31
|
+
// A password is an octet string (RFC 8018 sec. 2): a string is UTF-8-encoded deterministically (correct for
|
|
32
|
+
// non-ASCII, and byte-identical to OpenSSL), a Buffer/Uint8Array used verbatim.
|
|
33
|
+
function passwordBytes(p, E, prefix) {
|
|
34
|
+
if (Buffer.isBuffer(p)) return p;
|
|
35
|
+
if (p instanceof Uint8Array) return Buffer.from(p);
|
|
36
|
+
if (typeof p === "string") return Buffer.from(p, "utf8");
|
|
37
|
+
throw E(prefix + "/bad-input", "a password must be a string, Buffer, or Uint8Array");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// iterationCount for a PRODUCED PBES2/PWRI structure: a positive integer at or below the cap the decryptor
|
|
41
|
+
// enforces, so a structure we emit is always one we can read back (config-time throw on a bad value).
|
|
42
|
+
function assertIterations(n, E, prefix) {
|
|
43
|
+
if (typeof n !== "number" || !isFinite(n) || n < 1 || Math.floor(n) !== n) throw E(prefix + "/bad-input", "iterations must be a positive integer");
|
|
44
|
+
if (n > C.LIMITS.PBKDF2_MAX_ITERATIONS) throw E(prefix + "/bad-input", "iterations exceeds the " + C.LIMITS.PBKDF2_MAX_ITERATIONS + " cap");
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Bound a PRODUCED salt to the same cap the decryptor enforces.
|
|
49
|
+
function assertSalt(salt, E, prefix) {
|
|
50
|
+
guard.limits.byteCap(salt, C.LIMITS.PBKDF2_MAX_SALT, E, prefix + "/bad-input", "salt");
|
|
51
|
+
return salt;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function prfNodeByName(prf, E, prefix) { if (!PRF_NODE_BY_NAME[prf]) throw E(prefix + "/bad-input", "unsupported prf " + JSON.stringify(prf)); return PRF_NODE_BY_NAME[prf]; }
|
|
55
|
+
function prfNodeByOid(oidStr, E, prefix) { if (!PRF_NODE_BY_OID[oidStr]) throw E(prefix + "/unsupported-algorithm", "unsupported PBKDF2 prf " + oidStr); return PRF_NODE_BY_OID[oidStr]; }
|
|
56
|
+
|
|
57
|
+
// PBKDF2-params { salt OCTET STRING, iterationCount INTEGER, prf DEFAULT hmacWithSHA1 }. prf equal to the
|
|
58
|
+
// DEFAULT (hmacWithSHA1) MUST be omitted on encode (X.690 sec. 11.5 / RFC 8018 App. A.2); every other prf is
|
|
59
|
+
// an hmacWithSHA_n AlgorithmIdentifier carrying NULL parameters.
|
|
60
|
+
function pbkdf2ParamsSeq(salt, iterations, prf) {
|
|
61
|
+
var kids = [b.octetString(salt), b.integer(BigInt(iterations))];
|
|
62
|
+
if (prf !== "hmacWithSHA1") kids.push(b.sequence([b.oid(O(prf)), b.nullValue()]));
|
|
63
|
+
return b.sequence(kids);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The PBES2 AlgorithmIdentifier: SEQUENCE { id-PBES2, SEQUENCE { keyDerivationFunc PBKDF2, encryptionScheme } }.
|
|
67
|
+
// The ONE home for the CMS EncryptedData contentEncryptionAlgorithm AND the RFC 5958 EncryptedPrivateKeyInfo
|
|
68
|
+
// encryptionAlgorithm -- both wrap the same PBES2 structure around a PBKDF2 KDF plus an AES-CBC scheme whose
|
|
69
|
+
// IV rides in the encryptionScheme parameter. `cipherName` is a registry NAME (an AES-CBC OID name).
|
|
70
|
+
function pbes2AlgId(salt, iterations, prf, cipherName, iv) {
|
|
71
|
+
var kdf = b.sequence([b.oid(O("pbkdf2")), pbkdf2ParamsSeq(salt, iterations, prf)]);
|
|
72
|
+
var encScheme = b.sequence([b.oid(O(cipherName)), b.octetString(iv)]);
|
|
73
|
+
return b.sequence([b.oid(O("pbes2")), b.sequence([kdf, encScheme])]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// SEQUENCE structural guards -- a malformed (primitive / too-short) attacker-controlled parameter SEQUENCE is
|
|
77
|
+
// a typed E(prefix + "/bad-input"), never a raw `.children` dereference fault (a PBES2 structure is public,
|
|
78
|
+
// not a decrypt oracle).
|
|
79
|
+
function requireChildren(node, minLen, what, E, prefix) {
|
|
80
|
+
if (!node || !node.children || node.children.length < minLen) throw E(prefix + "/bad-input", "malformed " + what);
|
|
81
|
+
return node.children;
|
|
82
|
+
}
|
|
83
|
+
function seqChildren(paramsDer, minLen, what, E, prefix) {
|
|
84
|
+
if (paramsDer == null) throw E(prefix + "/bad-input", "missing " + what);
|
|
85
|
+
return requireChildren(asn1.decode(paramsDer), minLen, what, E, prefix);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Strict PBKDF2-params decode: salt (capped BEFORE derivation), iterationCount (positiveInt31 + cap, downward-
|
|
89
|
+
// overridable via a validated-positive-int opts.maxIterations so Math.min(NaN, cap) cannot silently disable
|
|
90
|
+
// the DoS cap), prf. Returns { salt, iterations, prfNode }.
|
|
91
|
+
function parsePbkdf2Params(paramsDer, opts, E, prefix, strictPrf) {
|
|
92
|
+
var node = Buffer.isBuffer(paramsDer) ? asn1.decode(paramsDer) : paramsDer;
|
|
93
|
+
var kids = requireChildren(node, 2, "PBKDF2 parameters", E, prefix);
|
|
94
|
+
var salt = asn1.read.octetString(kids[0]);
|
|
95
|
+
if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw E(prefix + "/bad-input", "PBKDF2 salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
|
|
96
|
+
var iterations = guard.range.positiveInt31(asn1.read.integer(kids[1]), E, prefix + "/bad-input", "PBKDF2 iterationCount");
|
|
97
|
+
var cap = C.LIMITS.PBKDF2_MAX_ITERATIONS;
|
|
98
|
+
if (opts && opts.maxIterations != null) {
|
|
99
|
+
if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw E(prefix + "/bad-input", "maxIterations must be a positive integer");
|
|
100
|
+
cap = Math.min(opts.maxIterations, cap);
|
|
101
|
+
}
|
|
102
|
+
if (iterations > cap) throw E(prefix + "/iteration-limit", "PBKDF2 iterationCount " + iterations + " exceeds the cap " + cap);
|
|
103
|
+
var prfNode = "sha1";
|
|
104
|
+
for (var i = 2; i < node.children.length; i++) {
|
|
105
|
+
var ch = node.children[i];
|
|
106
|
+
if (ch.tagClass === "universal" && ch.tagNumber === asn1.TAGS.SEQUENCE) {
|
|
107
|
+
var prfOid = asn1.read.oid(ch.children[0]);
|
|
108
|
+
// X.690 sec. 11.5 / RFC 8018 App. A.2: a prf equal to the DEFAULT (hmacWithSHA1 with NULL parameters)
|
|
109
|
+
// MUST be omitted -- an explicit encoding of it is non-canonical. A strictPrf caller rejects it.
|
|
110
|
+
if (strictPrf && prfOid === O("hmacWithSHA1") && ch.children.length > 1 && ch.children[1].tagClass === "universal" && ch.children[1].tagNumber === asn1.TAGS.NULL) {
|
|
111
|
+
throw E(prefix + "/bad-input", "a PBKDF2 prf equal to the default hmacWithSHA1 must be omitted (X.690 sec. 11.5)");
|
|
112
|
+
}
|
|
113
|
+
prfNode = prfNodeByOid(prfOid, E, prefix);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { salt: salt, iterations: iterations, prfNode: prfNode };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// AES-CBC content encryption / decryption (PKCS#7 pad). decrypt's final() throws on bad pad -- the caller
|
|
120
|
+
// collapses that into its uniform decrypt-failed verdict.
|
|
121
|
+
function cbcEncrypt(key, iv, plaintext, keyBits) {
|
|
122
|
+
var c = nodeCrypto.createCipheriv("aes-" + keyBits + "-cbc", key, iv);
|
|
123
|
+
return Buffer.concat([c.update(plaintext), c.final()]);
|
|
124
|
+
}
|
|
125
|
+
function cbcDecrypt(key, iv, ct, keyBits) {
|
|
126
|
+
var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-cbc", key, iv);
|
|
127
|
+
return Buffer.concat([d.update(ct), d.final()]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
passwordBytes: passwordBytes, assertIterations: assertIterations, assertSalt: assertSalt,
|
|
132
|
+
prfNodeByName: prfNodeByName, prfNodeByOid: prfNodeByOid,
|
|
133
|
+
pbkdf2ParamsSeq: pbkdf2ParamsSeq, pbes2AlgId: pbes2AlgId, parsePbkdf2Params: parsePbkdf2Params,
|
|
134
|
+
requireChildren: requireChildren, seqChildren: seqChildren,
|
|
135
|
+
cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, CONTENT_KEYBITS: CONTENT_KEYBITS,
|
|
136
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
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",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"check:vendor-currency": "node scripts/check-vendor-currency.js"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"c8": "
|
|
75
|
+
"c8": "12.0.0",
|
|
76
76
|
"esbuild": "0.28.1",
|
|
77
77
|
"eslint": "10.7.0"
|
|
78
78
|
}
|
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:0bc607cc-142c-4a49-a525-90a6f0216e38",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-23T18:57:16.468Z",
|
|
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.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.10",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.10",
|
|
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.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.10",
|
|
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.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.10",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|