@blamejs/pki 0.3.9 → 0.3.11

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.
@@ -0,0 +1,434 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.pkcs12
6
+ * @nav Signing
7
+ * @title PKCS#12
8
+ * @intro The PKCS#12 (.p12 / .pfx) producing side (RFC 7292, RFC 9579). `pki.pkcs12.build` assembles a
9
+ * password-integrity PFX -- key, certificate, CRL, and secret bags (shrouded keys and cert safes encrypted
10
+ * under RFC 8018 PBES2) wrapped in an AuthenticatedSafe, protected by either a classic Appendix B HMAC or
11
+ * an RFC 9579 PBMAC1 (PBKDF2 + HMAC) MAC, over AES-128/192/256-CBC and SHA-256/384/512. Every password is
12
+ * encoded the PKCS#12 way (BMPString + NULL, Appendix B.1), so a file it emits opens in OpenSSL and NSS.
13
+ * `pki.pkcs12.verifyMac` recomputes a store's MAC over the exact AuthenticatedSafe byte range and
14
+ * constant-time-compares it. Parsing lives at `pki.schema.pkcs12.parse`.
15
+ * @spec RFC 7292, RFC 9579, RFC 8018
16
+ * @card Build a password-integrity PKCS#12 store (RFC 7292 / RFC 9579) and verify its MAC.
17
+ */
18
+ //
19
+ // The PBES2 bag encryption and the PBMAC1 MAC compose the shared lib/pbes2.js home (the same PBKDF2 + AES-CBC
20
+ // + HMAC primitives pki.cms and pki.key use), fed PKCS#12 App. B.1-formatted password bytes -- NOT the CMS
21
+ // UTF-8 encoding, the single most common PKCS#12 interop failure. The classic Appendix B.2 KDF (ID=3, the MAC
22
+ // integrity purpose) is bespoke PKCS#12 crypto with no in-tree equivalent, built here as one primitive and
23
+ // cross-checked against OpenSSL. The MAC is computed over the CONTENT octets of the id-data authSafe OCTET
24
+ // STRING (excluding its TLV header) -- exactly the parser's `macedBytes`, the canonical off-by-the-header
25
+ // trap designed out. Public-key integrity (an id-signedData authSafe) and bag/store DECRYPTION are deferred:
26
+ // build produces password-integrity stores, and a `pki.pkcs12.open` reader is the re-open condition.
27
+
28
+ var nodeCrypto = require("crypto");
29
+ var asn1 = require("./asn1-der");
30
+ var oid = require("./oid");
31
+ var pbes2 = require("./pbes2");
32
+ var pkcs8 = require("./schema-pkcs8");
33
+ var x509 = require("./schema-x509");
34
+ var schemaCrl = require("./schema-crl");
35
+ var schemaPkcs12 = require("./schema-pkcs12");
36
+ var pkix = require("./schema-pkix");
37
+ var guard = require("./guard-all");
38
+ var frameworkError = require("./framework-error");
39
+ var C = require("./constants");
40
+
41
+ var b = asn1.build;
42
+ var Pkcs12Error = frameworkError.Pkcs12Error;
43
+ var PemError = frameworkError.PemError;
44
+ function O(n) { return oid.byName(n); }
45
+ function _err(code, msg, cause) { return new Pkcs12Error(code, msg, cause); }
46
+
47
+ // PKCS#12 integrity/privacy defaults (named, not call-site literals -- rule 12).
48
+ var DEFAULT_MAC_ITER = 2048;
49
+ var DEFAULT_PBMAC1_ITER = 2048;
50
+ var MAC_SALT_BYTES = 8;
51
+ var MAX_PBMAC1_KEYLEN = 1024; // an HMAC key beyond a hash block is pointless -- bound the derived length
52
+ // The classic Appendix B KDF is a SYNCHRONOUS JS hash loop (unlike native/async PBKDF2), so its iteration
53
+ // count is capped far lower than the PBKDF2 limit -- a store just under the PBKDF2 cap would block the event
54
+ // loop for seconds. 1e6 is ~500x the OpenSSL default (2048) yet bounds the loop to a fraction of a second.
55
+ var CLASSIC_MAC_MAX_ITERATIONS = 1000000;
56
+
57
+ // The classic App. B.2 KDF (u = hash output bytes, v = compression block bytes) per RFC 7292 App. B.2.
58
+ var P12_KDF_UV = {
59
+ sha1: { u: 20, v: 64 }, sha224: { u: 28, v: 64 }, sha256: { u: 32, v: 64 },
60
+ sha384: { u: 48, v: 128 }, sha512: { u: 64, v: 128 },
61
+ };
62
+ // PBMAC1 hash -> { prf AlgorithmIdentifier name, WebCrypto hash, HMAC key length }. RFC 9579 sec. 5/7 forbids
63
+ // a <= 160-bit digest, so SHA-1 is absent.
64
+ var PBMAC1_PRF = {
65
+ sha256: { prfName: "hmacWithSHA256", wc: "SHA-256", keyLen: 32 },
66
+ sha384: { prfName: "hmacWithSHA384", wc: "SHA-384", keyLen: 48 },
67
+ sha512: { prfName: "hmacWithSHA512", wc: "SHA-512", keyLen: 64 },
68
+ };
69
+ // PBMAC1 prf AlgorithmIdentifier name -> WebCrypto hash (verify direction).
70
+ var PRF_WC = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
71
+ // operator cipher name -> AES-CBC registry name the PBES2 home speaks.
72
+ var CIPHER_NAME = { "aes-128-cbc": "aes128-CBC", "aes-192-cbc": "aes192-CBC", "aes-256-cbc": "aes256-CBC" };
73
+ // classic-MAC hash node name -> the registered digest OID name (identical spelling here).
74
+ var DIGEST_NAME = { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "sha512" };
75
+
76
+ // ---- password + KDF primitives ---------------------------------------------
77
+
78
+ // RFC 7292 App. B.1: a PKCS#12 password is the BMPString (UTF-16BE) encoding of the string plus a trailing
79
+ // 2-byte NULL terminator. "Beavis" -> 14 bytes. A non-BMP scalar (surrogate) is rejected. A Buffer/Uint8Array
80
+ // is taken verbatim as already-formatted bytes (an escape hatch for a caller that pre-encodes).
81
+ function _p12Password(pw) {
82
+ if (pw == null) pw = "";
83
+ if (Buffer.isBuffer(pw)) return pw;
84
+ if (pw instanceof Uint8Array) return Buffer.from(pw);
85
+ if (typeof pw !== "string") throw _err("pkcs12/bad-input", "a password must be a string, Buffer, or Uint8Array");
86
+ var out = Buffer.alloc(pw.length * 2 + 2); // + the 2-byte NULL terminator
87
+ for (var i = 0; i < pw.length; i++) {
88
+ var u = pw.charCodeAt(i);
89
+ if (u >= 0xD800 && u <= 0xDFFF) throw _err("pkcs12/bad-input", "the password contains a non-BMP character (a surrogate code point cannot be BMPString-encoded)");
90
+ out[i * 2] = (u >> 8) & 0xFF;
91
+ out[i * 2 + 1] = u & 0xFF;
92
+ }
93
+ return out; // the final two bytes are already 0x00 0x00
94
+ }
95
+
96
+ // The password bytes for the RFC 8018 PBES2 bag ciphers and the RFC 9579 PBMAC1 MAC. Although RFC 9579 sec. 6
97
+ // specifies the BMPString encoding here too, OpenSSL and NSS -- and thus the interoperable ecosystem -- feed
98
+ // PBKDF2 the raw UTF-8 password for these modern schemes (confirmed byte-for-byte against `openssl pkcs12`),
99
+ // reserving the BMPString+NULL form for the bespoke Appendix B KDF only. A file we emit must open in OpenSSL,
100
+ // so the modern schemes use UTF-8 here; only the classic Appendix B MAC uses `_p12Password`.
101
+ function _pbePassword(pw) {
102
+ if (pw == null) pw = "";
103
+ if (Buffer.isBuffer(pw)) return pw;
104
+ if (pw instanceof Uint8Array) return Buffer.from(pw);
105
+ if (typeof pw !== "string") throw _err("pkcs12/bad-input", "a password must be a string, Buffer, or Uint8Array");
106
+ return Buffer.from(pw, "utf8");
107
+ }
108
+
109
+ // Concatenate copies of `src` to the smallest positive multiple of `blockSize` (>= src length), truncating
110
+ // the final copy. An empty source yields an empty buffer (RFC 7292 App. B.2 steps 2/3).
111
+ function _blockFill(src, blockSize) {
112
+ if (src.length === 0) return Buffer.alloc(0);
113
+ var total = blockSize * Math.ceil(src.length / blockSize);
114
+ var out = Buffer.alloc(total);
115
+ for (var i = 0; i < total; i++) out[i] = src[i % src.length];
116
+ return out;
117
+ }
118
+
119
+ // RFC 7292 Appendix B.2 KDF: derive `nBytes` of key material for purpose `id` (3 = MAC integrity) from the
120
+ // App. B.1 password bytes + salt over `iterations` rounds of `hashName`. NOT PBKDF2. The only consumer is the
121
+ // classic MAC, which always requests exactly the hash output length u, so the App. B.2 block count
122
+ // c = ceil(nBytes/u) is 1 -- one diversified, salted, iterated hash (D || S || P, hashed r times). The
123
+ // multi-block feedback (the c > 1 case, App. B.2 step 6C) has no consumer here; a future >u-byte derivation
124
+ // (e.g. a classic-PBE bag path) reintroduces it with its own known-answer test rather than shipping it
125
+ // untested. `nBytes` over u is rejected. The single-block path is proven bidirectionally against OpenSSL.
126
+ function _p12Kdf(hashName, id, pwBytes, salt, iterations, nBytes) {
127
+ var uv = P12_KDF_UV[hashName];
128
+ if (nBytes > uv.u) throw _err("pkcs12/unsupported-algorithm", "the App. B.2 KDF here derives at most the hash output length");
129
+ var v = uv.v;
130
+ var A = Buffer.concat([Buffer.alloc(v, id), _blockFill(salt, v), _blockFill(pwBytes, v)]); // D || S || P
131
+ for (var r = 0; r < iterations; r++) A = nodeCrypto.createHash(hashName).update(A).digest(); // H^r(D||I)
132
+ return A.subarray(0, nBytes);
133
+ }
134
+
135
+ // ---- input coercion --------------------------------------------------------
136
+
137
+ function _coerceDer(input, what) {
138
+ return pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: Pkcs12Error, prefix: "pkcs12" });
139
+ }
140
+ function _bytes(input, label) {
141
+ return guard.bytes.view(input, Pkcs12Error, "pkcs12/bad-input", label);
142
+ }
143
+ function _assertMacIter(n, cap) {
144
+ if (typeof n !== "number" || !isFinite(n) || n < 1 || Math.floor(n) !== n) throw _err("pkcs12/bad-input", "MAC iterations must be a positive integer");
145
+ if (n === 1) throw _err("pkcs12/bad-input", "MAC iterations must be greater than 1 (a DEFAULT-1 MacData iterations cannot be DER-encoded, X.690 sec. 11.5)");
146
+ if (n > cap) throw _err("pkcs12/bad-input", "MAC iterations exceeds the cap " + cap);
147
+ return n;
148
+ }
149
+
150
+ // ---- bag builders ----------------------------------------------------------
151
+
152
+ // PKCS12Attribute SET OF: friendlyName (a single BMPString) + localKeyId (a single OCTET STRING). Each
153
+ // attribute's attrValues is a single-value SET OF (RFC 2985 SINGLE VALUE).
154
+ function _bagAttributes(bag) {
155
+ var attrs = [];
156
+ if (bag.friendlyName != null) attrs.push(b.sequence([b.oid(O("friendlyName")), b.set([b.bmpString(String(bag.friendlyName))])]));
157
+ if (bag.localKeyId != null) {
158
+ if (bag.localKeyId === "ski") throw _err("pkcs12/bad-input", "localKeyId 'ski' auto-derivation is not yet supported -- supply an explicit Buffer");
159
+ attrs.push(b.sequence([b.oid(O("localKeyId")), b.set([b.octetString(_bytes(bag.localKeyId, "localKeyId"))])]));
160
+ }
161
+ return attrs.length ? b.set(attrs) : null;
162
+ }
163
+
164
+ // A secretBag secretTypeId is a caller-chosen content type: a registered OID name, or an arbitrary
165
+ // dotted-decimal OID string preserved verbatim (a name -> its OID; an already-dotted OID passes through).
166
+ function _secretTypeOid(id) {
167
+ try { return b.oid(O(id) || id); } // O() (oid.byName) throws OidError on a non-string -- keep it inside the domain wrap
168
+ catch (e) { throw _err("pkcs12/bad-input", "secretTypeId must be a registered name or a dotted-decimal OID string", e); }
169
+ }
170
+
171
+ // A SafeBag ::= SEQUENCE { bagId, bagValue [0] EXPLICIT DEFINED BY bagId, bagAttributes SET OF OPTIONAL }.
172
+ function _safeBag(bagName, bagValueDer, bag) {
173
+ var children = [b.oid(O(bagName)), b.explicit(0, bagValueDer)];
174
+ var attrs = _bagAttributes(bag);
175
+ if (attrs) children.push(attrs);
176
+ return b.sequence(children);
177
+ }
178
+
179
+ function _buildBag(bag, opts, depth) {
180
+ if (!bag || typeof bag !== "object") throw _err("pkcs12/bad-input", "each bag must be an object with a type");
181
+ switch (bag.type) {
182
+ case "key": {
183
+ var keyDer = _coerceDer(bag.key, "keyBag key");
184
+ try { pkcs8.parse(keyDer); } catch (e) { throw _err("pkcs12/bad-input", "keyBag key is not a well-formed PKCS#8 PrivateKeyInfo", e); }
185
+ return _safeBag("keyBag", keyDer, bag);
186
+ }
187
+ case "shroudedKey": {
188
+ var kDer = _coerceDer(bag.key, "shroudedKey key");
189
+ try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
190
+ var enc = bag.encrypt || {};
191
+ var pw = _pbePassword(enc.password != null ? enc.password : opts.password);
192
+ var r = pbes2.pbes2Encrypt(pw, kDer, _pbeOpts(enc), _err, "pkcs12");
193
+ return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
194
+ }
195
+ case "cert": {
196
+ var certDer = _coerceDer(bag.cert, "certBag cert");
197
+ try { x509.parse(certDer); } catch (e3) { throw _err("pkcs12/bad-input", "certBag cert is not a well-formed X.509 certificate", e3); }
198
+ return _safeBag("certBag", b.sequence([b.oid(O("x509Certificate")), b.explicit(0, b.octetString(certDer))]), bag);
199
+ }
200
+ case "crl": {
201
+ var crlDer = _coerceDer(bag.crl, "crlBag crl");
202
+ try { schemaCrl.parse(crlDer); } catch (e4) { throw _err("pkcs12/bad-input", "crlBag crl is not a well-formed X.509 CRL", e4); }
203
+ return _safeBag("crlBag", b.sequence([b.oid(O("x509CRL")), b.explicit(0, b.octetString(crlDer))]), bag);
204
+ }
205
+ case "secret": {
206
+ if (bag.secretTypeId == null) throw _err("pkcs12/bad-input", "a secret bag needs a secretTypeId");
207
+ var secretValueDer = _reqDer(bag.secretValue, "secretValue");
208
+ return _safeBag("secretBag", b.sequence([_secretTypeOid(bag.secretTypeId), b.explicit(0, secretValueDer)]), bag);
209
+ }
210
+ case "safeContents": {
211
+ if (depth + 1 > C.LIMITS.PKCS12_MAX_BAG_DEPTH) throw _err("pkcs12/bad-input", "safeContents bag nesting exceeds the depth cap " + C.LIMITS.PKCS12_MAX_BAG_DEPTH);
212
+ return _safeBag("safeContentsBag", _buildSafeContents(bag.nested || [], opts, depth + 1), bag);
213
+ }
214
+ default:
215
+ throw _err("pkcs12/bad-input", "unknown bag type " + JSON.stringify(bag.type) + " (key / shroudedKey / cert / crl / secret / safeContents)");
216
+ }
217
+ }
218
+
219
+ // A pre-encoded DER value (one well-formed TLV, no trailing bytes) supplied verbatim (secretValue).
220
+ function _reqDer(input, label) {
221
+ if (input == null) throw _err("pkcs12/bad-input", label + " is required");
222
+ var der = _bytes(input, label);
223
+ var node;
224
+ try { node = asn1.decode(der); } catch (e) { throw _err("pkcs12/bad-input", label + " must be one well-formed DER value", e); }
225
+ if (node.bytes.length !== der.length) throw _err("pkcs12/bad-input", label + " must be exactly one DER value with no trailing bytes");
226
+ return der;
227
+ }
228
+
229
+ function _pbeOpts(enc) {
230
+ var cipher = CIPHER_NAME[enc.cipher || "aes-256-cbc"];
231
+ if (!cipher) throw _err("pkcs12/bad-input", "unsupported PBES2 cipher " + JSON.stringify(enc.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
232
+ return {
233
+ cipher: cipher,
234
+ salt: enc.salt != null ? _bytes(enc.salt, "encrypt salt") : undefined,
235
+ iterations: enc.iterations,
236
+ prf: enc.prf,
237
+ };
238
+ }
239
+
240
+ // SafeContents ::= SEQUENCE OF SafeBag (ordered, NOT a SET OF).
241
+ function _buildSafeContents(bags, opts, depth) {
242
+ if (!Array.isArray(bags)) throw _err("pkcs12/bad-input", "bags must be an array");
243
+ if (bags.length > C.LIMITS.PKCS12_MAX_ELEMENTS) throw _err("pkcs12/bad-input", "a SafeContents exceeds the element cap " + C.LIMITS.PKCS12_MAX_ELEMENTS);
244
+ return b.sequence(bags.map(function (bag) { return _buildBag(bag, opts, depth); }));
245
+ }
246
+
247
+ // One AuthenticatedSafe element: a plaintext id-data ContentInfo, or an id-encryptedData ContentInfo whose
248
+ // EncryptedData is the PBES2 encryption of the SafeContents (RFC 7292 sec. 5.1 step 2A/2B).
249
+ function _buildAuthSafeElement(sc, opts) {
250
+ if (!sc || typeof sc !== "object") throw _err("pkcs12/bad-input", "each safeContents entry must be an object");
251
+ var safeContentsDer = _buildSafeContents(sc.bags || [], opts, 0);
252
+ if (!sc.encrypt) {
253
+ return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
254
+ }
255
+ var pw = _pbePassword(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
256
+ var r = pbes2.pbes2Encrypt(pw, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12");
257
+ var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
258
+ var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
259
+ return b.sequence([b.oid(O("encryptedData")), b.explicit(0, encData)]);
260
+ }
261
+
262
+ // ---- MacData ---------------------------------------------------------------
263
+
264
+ // MacData over the AuthenticatedSafe DER (= the parser's macedBytes). Classic App. B HMAC (default, max
265
+ // interop) or RFC 9579 PBMAC1. Returns the MacData DER.
266
+ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
267
+ var password = macOpts.password != null ? macOpts.password : sharedPassword;
268
+ var algorithm = macOpts.algorithm || "hmac";
269
+ var hash = macOpts.hash || "sha256";
270
+ var salt = macOpts.salt != null ? _bytes(macOpts.salt, "mac salt") : nodeCrypto.randomBytes(MAC_SALT_BYTES);
271
+ if (salt.length === 0) throw _err("pkcs12/bad-input", "the MAC salt must be non-empty (RFC 9579 sec. 4c)");
272
+ if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("pkcs12/bad-input", "the MAC salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
273
+
274
+ if (algorithm === "hmac") {
275
+ var node = DIGEST_NAME[hash];
276
+ if (!node || !P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + JSON.stringify(hash) + " (sha1 / sha256 / sha384 / sha512)");
277
+ var iter = _assertMacIter(macOpts.iterations == null ? DEFAULT_MAC_ITER : macOpts.iterations, CLASSIC_MAC_MAX_ITERATIONS);
278
+ var macKey = _p12Kdf(node, 3, _p12Password(password), salt, iter, P12_KDF_UV[node].u); // classic KDF -> BMPString
279
+ var digest = nodeCrypto.createHmac(node, macKey).update(authSafeDer).digest();
280
+ var digestInfo = b.sequence([b.sequence([b.oid(O(node)), b.nullValue()]), b.octetString(digest)]);
281
+ return b.sequence([digestInfo, b.octetString(salt), b.integer(BigInt(iter))]);
282
+ }
283
+ if (algorithm === "pbmac1") {
284
+ var prf = PBMAC1_PRF[hash];
285
+ if (!prf) throw _err("pkcs12/unsupported-algorithm", "PBMAC1 requires a SHA-256/384/512 digest (RFC 9579 sec. 5/7 forbids a <= 160-bit digest, e.g. SHA-1)");
286
+ var iter2 = _assertMacIter(macOpts.iterations == null ? DEFAULT_PBMAC1_ITER : macOpts.iterations, C.LIMITS.PBKDF2_MAX_ITERATIONS);
287
+ var keyLen = macOpts.keyLength != null ? macOpts.keyLength : prf.keyLen;
288
+ if (typeof keyLen !== "number" || !Number.isInteger(keyLen) || keyLen < 20 || keyLen > MAX_PBMAC1_KEYLEN) throw _err("pkcs12/bad-input", "PBMAC1 keyLength must be an integer in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
289
+ var mac = await pbes2.pbmac1(_pbePassword(password), salt, iter2, keyLen, prf.wc, prf.wc, authSafeDer); // PBKDF2 -> UTF-8; prf == messageAuthScheme on build
290
+ var desc = { salt: salt, iterationCount: iter2, keyLength: keyLen, prfName: prf.prfName, macName: prf.prfName };
291
+ var digestInfo2 = b.sequence([pbes2.pbmac1AlgId(desc), b.octetString(mac)]);
292
+ // MacData.macSalt + iterations are ignored on a PBMAC1 verify but MUST be present + non-1 (RFC 9579 4c/4d).
293
+ return b.sequence([digestInfo2, b.octetString(salt), b.integer(BigInt(iter2))]);
294
+ }
295
+ throw _err("pkcs12/bad-input", "opts.mac.algorithm must be 'hmac' or 'pbmac1', got " + JSON.stringify(algorithm));
296
+ }
297
+
298
+ // ---- spec normalization ----------------------------------------------------
299
+
300
+ // Accept the OpenSSL-style convenience form ({ key, cert, ca, friendlyName, localKeyId }) or the full
301
+ // inverse-of-parse form ({ safeContents: SafeContentsSpec[] }). The convenience form -> one privacy safe of
302
+ // PBES2 cert bags + one shrouded-key safe.
303
+ function _normalizeSpec(spec, opts) {
304
+ if (spec && Array.isArray(spec.safeContents)) return spec.safeContents;
305
+ if (spec && (spec.key != null || spec.cert != null)) {
306
+ var certBags = [];
307
+ if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
308
+ if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
309
+ (spec.ca || []).forEach(function (ca) { certBags.push({ type: "cert", cert: ca }); });
310
+ var sc = [];
311
+ if (certBags.length) sc.push({ encrypt: { password: opts.password }, bags: certBags });
312
+ if (spec.key != null) sc.push({ bags: [{ type: "shroudedKey", key: spec.key, encrypt: { password: opts.password }, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId }] });
313
+ return sc;
314
+ }
315
+ throw _err("pkcs12/bad-input", "spec must be { safeContents: [...] } or { key, cert, ca? }");
316
+ }
317
+
318
+ // ---- public API ------------------------------------------------------------
319
+
320
+ /**
321
+ * @primitive pki.pkcs12.build
322
+ * @signature pki.pkcs12.build(spec, opts?) -> Promise<Buffer|string>
323
+ * @since 0.3.11
324
+ * @status experimental
325
+ * @spec RFC 7292, RFC 9579, RFC 8018
326
+ * @related pki.schema.pkcs12.parse, pki.pkcs12.verifyMac
327
+ *
328
+ * Build a password-integrity PKCS#12 (.p12 / .pfx) store. `spec` is either the OpenSSL-style convenience
329
+ * form `{ key, cert, ca?, friendlyName?, localKeyId? }` (one PBES2-encrypted cert safe plus one shrouded-key
330
+ * safe) or the full form `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted
331
+ * `SafeContents` of key / shroudedKey / cert / crl / secret / nested safeContents bags. Keys and certs are
332
+ * validated before wrapping; every password is BMPString+NULL encoded (RFC 7292 App. B.1). The store is
333
+ * MACed with a classic Appendix B HMAC (default) or an RFC 9579 PBMAC1, and re-parsed before return.
334
+ *
335
+ * @opts
336
+ * - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
337
+ * - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, keyLength? }`.
338
+ * - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
339
+ * @example
340
+ * var p12 = await pki.pkcs12.build({ safeContents: [{ bags: [
341
+ * { type: 'cert', cert: signerCertDer },
342
+ * { type: 'shroudedKey', key: signerKeyPkcs8, encrypt: { password: 'changeit' } } ] }] },
343
+ * { password: 'changeit', mac: { algorithm: 'hmac', hash: 'sha256' } });
344
+ */
345
+ async function build(spec, opts) {
346
+ opts = opts || {};
347
+ if (opts.integrity && opts.integrity.mode === "public-key") {
348
+ throw _err("pkcs12/unsupported-algorithm", "public-key integrity (an id-signedData authSafe) is not yet supported -- use password integrity");
349
+ }
350
+ var safeContentsSpecs = _normalizeSpec(spec, opts);
351
+ if (!Array.isArray(safeContentsSpecs) || !safeContentsSpecs.length) throw _err("pkcs12/bad-input", "the store has no safe contents");
352
+ var elements = [];
353
+ for (var i = 0; i < safeContentsSpecs.length; i++) elements.push(_buildAuthSafeElement(safeContentsSpecs[i], opts));
354
+ var authSafeDer = b.sequence(elements); // AuthenticatedSafe ::= SEQUENCE OF ContentInfo
355
+
356
+ var pfxChildren = [b.integer(3n), b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(authSafeDer))])];
357
+ if (opts.mac != null && opts.mac !== false && typeof opts.mac !== "object") throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
358
+ if (opts.mac !== false) pfxChildren.push(await _buildMacData(opts.mac || {}, opts.password, authSafeDer));
359
+ var pfx = b.sequence(pfxChildren);
360
+
361
+ // Self-check: the emitted store round-trips through the strict parser (structure + MAC coherence).
362
+ try { schemaPkcs12.parse(pfx); } catch (e) { throw _err("pkcs12/bad-input", "the produced PKCS#12 store did not re-parse (build bug)", e); }
363
+ return opts.pem ? schemaPkcs12.pemEncode(pfx, "PKCS12") : pfx;
364
+ }
365
+
366
+ /**
367
+ * @primitive pki.pkcs12.verifyMac
368
+ * @signature pki.pkcs12.verifyMac(pfx, password, opts?) -> Promise<boolean>
369
+ * @since 0.3.11
370
+ * @status experimental
371
+ * @spec RFC 7292 sec. 5.1, RFC 9579
372
+ * @defends pkcs12-mac-forgery (CWE-347)
373
+ * @related pki.pkcs12.build, pki.schema.pkcs12.parse
374
+ *
375
+ * Verify a password-integrity PKCS#12 store's MAC. `pfx` is a `pki.schema.pkcs12.parse` result, a DER
376
+ * `Buffer`, or a PEM string. The password is BMPString+NULL encoded (RFC 7292 App. B.1), the MAC is
377
+ * recomputed over the store's exact AuthenticatedSafe byte range (`macedBytes`) using the store's own MAC
378
+ * parameters -- the classic Appendix B (ID=3) HMAC or the RFC 9579 PBMAC1 -- and constant-time-compared to
379
+ * the stored MAC value. Returns `true` / `false` for the password match; throws `Pkcs12Error` on a MAC-less
380
+ * or public-key-integrity store, or an unsupported MAC algorithm (never a falsy verdict standing in for an error).
381
+ *
382
+ * @example
383
+ * var p12 = await pki.pkcs12.build({ key: signerKeyPkcs8, cert: signerCertDer }, { password: 'changeit' });
384
+ * var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
385
+ */
386
+ async function verifyMac(pfx, password, opts) {
387
+ opts = opts || {};
388
+ var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
389
+ if (m.integrityMode !== "password" || !m.mac) throw _err("pkcs12/bad-input", "the store carries no password MAC (integrityMode " + m.integrityMode + ")");
390
+ var expected = m.mac.macValue;
391
+ var computed;
392
+ if (m.mac.kind === "hmac") {
393
+ var node = (m.mac.hashName || "").toLowerCase();
394
+ if (!P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + m.mac.hashName);
395
+ // The iteration count and salt are attacker-controlled work factors: bound them BEFORE the KDF runs.
396
+ _capWork(m.mac.iterations, m.mac.macSalt, opts, undefined, CLASSIC_MAC_MAX_ITERATIONS);
397
+ var macKey = _p12Kdf(node, 3, _p12Password(password), m.mac.macSalt, m.mac.iterations, P12_KDF_UV[node].u); // classic KDF -> BMPString
398
+ computed = nodeCrypto.createHmac(node, macKey).update(m.macedBytes).digest();
399
+ } else {
400
+ var kdf = m.mac.pbmac1.kdf;
401
+ var prfWc = PRF_WC[kdf.prfName];
402
+ // RFC 9579: HMAC under the messageAuthScheme, keyed by PBKDF2 under the (independent) prf.
403
+ var macWc = PRF_WC[m.mac.pbmac1.schemeName];
404
+ if (!prfWc) throw _err("pkcs12/unsupported-algorithm", "unsupported PBMAC1 prf " + kdf.prfName);
405
+ if (!macWc) throw _err("pkcs12/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " + m.mac.pbmac1.schemeName);
406
+ // RFC 9579 sec. 5/7: a <= 160-bit digest (SHA-1) MUST NOT be used for PBMAC1 -- refuse it on verify, so a
407
+ // downgraded store cannot pass under a weak MAC even though the algorithm identifiers parse.
408
+ if (prfWc === "SHA-1" || macWc === "SHA-1") throw _err("pkcs12/unsupported-algorithm", "PBMAC1 with a <= 160-bit digest (SHA-1) is refused (RFC 9579 sec. 5/7)");
409
+ _capWork(kdf.iterationCount, kdf.salt, opts, kdf.keyLength, C.LIMITS.PBKDF2_MAX_ITERATIONS);
410
+ computed = await pbes2.pbmac1(_pbePassword(password), kdf.salt, kdf.iterationCount, kdf.keyLength, prfWc, macWc, m.macedBytes); // PBKDF2 -> UTF-8
411
+ }
412
+ return computed.length === expected.length && guard.crypto.constantTimeEqual(computed, expected);
413
+ }
414
+
415
+ // Bound the attacker-controlled MAC work factors before a hostile store can force an expensive derivation:
416
+ // the iteration count (<= PBKDF2_MAX_ITERATIONS, downward-overridable via opts.maxIterations), the salt, and
417
+ // the PBMAC1 keyLength. A store that exceeds any cap is a typed reject, never a multi-second CPU burn.
418
+ function _capWork(iterations, salt, opts, keyLength, hardCap) {
419
+ var cap = hardCap;
420
+ if (opts.maxIterations != null) {
421
+ if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
422
+ cap = Math.min(opts.maxIterations, cap);
423
+ }
424
+ if (iterations > cap) throw _err("pkcs12/iteration-limit", "the MAC iteration count " + iterations + " exceeds the cap " + cap);
425
+ if (salt && salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("pkcs12/bad-input", "the MAC salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
426
+ // RFC 9579 sec. 9: the PBMAC1 keyLength floor (>= 20) is enforced on verify too, so a downgraded store with
427
+ // a short derived key is refused rather than accepted under a weak MAC.
428
+ if (keyLength != null && (keyLength < 20 || keyLength > MAX_PBMAC1_KEYLEN)) throw _err("pkcs12/bad-input", "the PBMAC1 keyLength must be in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
429
+ }
430
+
431
+ module.exports = {
432
+ build: build,
433
+ verifyMac: verifyMac,
434
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
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",
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:c2eb240a-1337-4a5a-862e-43dad1701426",
5
+ "serialNumber": "urn:uuid:253a6908-56c1-4a56-bdf4-33cb2035a161",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-23T16:52:13.594Z",
8
+ "timestamp": "2026-07-23T22:26:59.213Z",
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.9",
22
+ "bom-ref": "@blamejs/pki@0.3.11",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.9",
25
+ "version": "0.3.11",
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.9",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.11",
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.9",
57
+ "ref": "@blamejs/pki@0.3.11",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]