@blamejs/pki 0.5.17 → 0.5.19
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 +60 -0
- package/lib/acme.js +6 -4
- package/lib/cmp-build.js +15 -1
- package/lib/cms-encrypt.js +155 -14
- package/lib/cms-sign.js +15 -1
- package/lib/crl-sign.js +61 -2
- package/lib/guard-intrinsic.js +27 -0
- package/lib/jose.js +82 -35
- package/lib/key.js +6 -0
- package/lib/pkcs12-build.js +201 -11
- package/lib/tsp-sign.js +64 -0
- package/lib/webcrypto.js +165 -90
- package/lib/x509-sign.js +37 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/jose.js
CHANGED
|
@@ -42,6 +42,24 @@ var webcrypto = wcEngine.webcrypto;
|
|
|
42
42
|
var frameworkError = require("./framework-error");
|
|
43
43
|
var guard = require("./guard-all");
|
|
44
44
|
var edwardsPoint = require("./edwards-point");
|
|
45
|
+
// What this module reads from the runtime while it is deciding something, taken at load. A JWS is
|
|
46
|
+
// signed and verified against a header the caller owns, and the reads below are the ones that decide
|
|
47
|
+
// what that header IS: whether a value is a list, whether a name is the caller's own, which
|
|
48
|
+
// conversion turns a claim into the string compared against a profile. A caller who receives control
|
|
49
|
+
// afterwards -- through an accessor on the header, or a `toJSON` while it is being serialized -- can
|
|
50
|
+
// replace any of them, and every check around them still runs and still passes. See guard-intrinsic.
|
|
51
|
+
var intrinsic = require("./guard-intrinsic");
|
|
52
|
+
var _isArray = intrinsic.isArray;
|
|
53
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
54
|
+
var _keys = intrinsic.keys;
|
|
55
|
+
var _isBuffer = intrinsic.isBuffer;
|
|
56
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
57
|
+
var _stringify = intrinsic.stringify;
|
|
58
|
+
var _ObjectProto = intrinsic.ObjectProto;
|
|
59
|
+
var _push = intrinsic.uncurry(Array.prototype.push);
|
|
60
|
+
var _join = intrinsic.join;
|
|
61
|
+
var _map = intrinsic.map;
|
|
62
|
+
var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
|
|
45
63
|
|
|
46
64
|
var JoseError = frameworkError.JoseError;
|
|
47
65
|
function E(code, message, cause) { return new JoseError(code, message, cause); }
|
|
@@ -65,11 +83,11 @@ var LIMITS = constants.LIMITS;
|
|
|
65
83
|
* pki.jose.base64url.encode(Buffer.from([1, 2, 3])); // -> "AQID"
|
|
66
84
|
*/
|
|
67
85
|
function b64uEncode(bytes) {
|
|
68
|
-
if (!
|
|
86
|
+
if (!_isBuffer(bytes)) throw E("jose/bad-input", "base64url.encode requires a Buffer");
|
|
69
87
|
// Re-view through the byte guard: a detached-backed Buffer reads as zero-length,
|
|
70
88
|
// so a bare .toString would silently encode "" -- fail closed instead.
|
|
71
89
|
bytes = guard.bytes.view(bytes, JoseError, "jose/bad-input", "base64url.encode input");
|
|
72
|
-
return bytes
|
|
90
|
+
return _bufToString(bytes, "base64url");
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
/**
|
|
@@ -182,19 +200,19 @@ var PROFILES = {
|
|
|
182
200
|
// { alg, algRow, header }; throws a typed jose/* fault on any violation.
|
|
183
201
|
function _checkHeader(header, profileName) {
|
|
184
202
|
var profile = PROFILES[profileName];
|
|
185
|
-
if (!profile) throw E("jose/bad-input", "unknown JWS profile " +
|
|
186
|
-
if (!header || typeof header !== "object" ||
|
|
203
|
+
if (!profile) throw E("jose/bad-input", "unknown JWS profile " + _stringify(profileName));
|
|
204
|
+
if (!header || typeof header !== "object" || _isArray(header)) throw E("jose/bad-header", "the protected header must be a JSON object");
|
|
187
205
|
// Every field is read as an OWN property. JSON.stringify (used by sign) omits
|
|
188
206
|
// inherited members, so an alg/nonce/url living on the prototype (a polluted or
|
|
189
207
|
// Object.create(...) header) would pass here yet serialize to a header MISSING
|
|
190
208
|
// them -- so require own-ness, matching what the serialized JWS will actually carry.
|
|
191
|
-
var own = function (k) { return
|
|
209
|
+
var own = function (k) { return _hasOwn(header, k); };
|
|
192
210
|
// b64 (RFC 7797) MUST NOT be present at all -- the unencoded-payload downgrade.
|
|
193
211
|
if (own("b64")) throw E("jose/bad-jws", "the RFC 7797 b64 header parameter is forbidden");
|
|
194
212
|
var alg = header.alg;
|
|
195
213
|
if (!own("alg") || typeof alg !== "string") throw E("jose/bad-header", "the protected header must carry a string alg");
|
|
196
214
|
var algRow = profile.algs[alg];
|
|
197
|
-
if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " +
|
|
215
|
+
if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " + _stringify(alg) + " for this JWS profile");
|
|
198
216
|
if (profile.requireUrl && (!own("url") || typeof header.url !== "string" || header.url.length === 0)) throw E("jose/bad-header", "the protected header must carry a non-empty string url");
|
|
199
217
|
// nonce discipline.
|
|
200
218
|
var hasNonce = own("nonce");
|
|
@@ -219,20 +237,20 @@ function _checkHeader(header, profileName) {
|
|
|
219
237
|
} else if (profile.keyId === "jwk") {
|
|
220
238
|
if (!hasJwk || hasKid) throw E("jose/bad-header", "this JWS must carry an embedded jwk, not kid");
|
|
221
239
|
}
|
|
222
|
-
if (hasJwk && (!header.jwk || typeof header.jwk !== "object" ||
|
|
240
|
+
if (hasJwk && (!header.jwk || typeof header.jwk !== "object" || _isArray(header.jwk))) throw E("jose/bad-header", "jwk must be a JSON object");
|
|
223
241
|
// crit: protected-only, non-empty, no standard names, no duplicates, all understood.
|
|
224
|
-
if (
|
|
242
|
+
if (_hasOwn(header, "crit")) {
|
|
225
243
|
var crit = header.crit;
|
|
226
|
-
if (!
|
|
244
|
+
if (!_isArray(crit) || crit.length === 0) throw E("jose/bad-crit", "crit must be a non-empty array");
|
|
227
245
|
var seen = {};
|
|
228
246
|
for (var c = 0; c < crit.length; c++) {
|
|
229
247
|
var name = crit[c];
|
|
230
248
|
if (typeof name !== "string") throw E("jose/bad-crit", "crit entries must be strings");
|
|
231
|
-
if (
|
|
249
|
+
if (_hasOwn(seen, name)) throw E("jose/bad-crit", "duplicate crit entry " + _stringify(name));
|
|
232
250
|
seen[name] = 1;
|
|
233
|
-
if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " +
|
|
251
|
+
if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " + _stringify(name));
|
|
234
252
|
// Anything else is an extension parameter this implementation does not process.
|
|
235
|
-
throw E("jose/bad-crit", "unprocessed critical header parameter " +
|
|
253
|
+
throw E("jose/bad-crit", "unprocessed critical header parameter " + _stringify(name));
|
|
236
254
|
}
|
|
237
255
|
}
|
|
238
256
|
return { alg: alg, algRow: algRow, header: header };
|
|
@@ -267,9 +285,9 @@ function _cryptoAlg(algRow, jwk, key) {
|
|
|
267
285
|
// The key type the JWK MUST have for this alg (fail closed on confusion).
|
|
268
286
|
function _assertKeyType(algRow, jwk) {
|
|
269
287
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
270
|
-
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " +
|
|
288
|
+
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + _stringify(jwk.kty) + " does not match the algorithm");
|
|
271
289
|
if (algRow.kty === "EC" && jwk.crv !== algRow.crv) throw E("jose/bad-alg", "the EC curve does not match the algorithm");
|
|
272
|
-
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " +
|
|
290
|
+
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + _stringify(jwk.crv));
|
|
273
291
|
// node/OpenSSL import a low-order (e.g. identity) OKP point without complaint and it verifies a
|
|
274
292
|
// forged signature; reject a non-full-order Edwards point through the shared gate, the same check
|
|
275
293
|
// every EdDSA verify path in the toolkit applies.
|
|
@@ -278,7 +296,7 @@ function _assertKeyType(algRow, jwk) {
|
|
|
278
296
|
}
|
|
279
297
|
// An AKP (ML-DSA) JWK carries its parameter set in `alg` (RFC 9964); it MUST match
|
|
280
298
|
// the algorithm's set, or an ML-DSA-44 key could embed an ML-DSA-65 public JWK.
|
|
281
|
-
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " +
|
|
299
|
+
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + _stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
// The exact signature byte length this alg + key must produce.
|
|
@@ -317,10 +335,10 @@ var PRIVATE_JWK_MEMBERS = ["d", "p", "q", "dp", "dq", "qi", "k", "priv"];
|
|
|
317
335
|
* pki.jose.assertPublicJwk({ kty: "EC", crv: "P-256", x: "...", y: "..." });
|
|
318
336
|
*/
|
|
319
337
|
function assertPublicJwk(jwk) {
|
|
320
|
-
if (!jwk || typeof jwk !== "object" ||
|
|
338
|
+
if (!jwk || typeof jwk !== "object" || _isArray(jwk)) throw E("jose/bad-key", "a JWK object is required");
|
|
321
339
|
for (var i = 0; i < PRIVATE_JWK_MEMBERS.length; i++) {
|
|
322
|
-
if (
|
|
323
|
-
throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " +
|
|
340
|
+
if (_hasOwn(jwk, PRIVATE_JWK_MEMBERS[i])) {
|
|
341
|
+
throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " + _stringify(PRIVATE_JWK_MEMBERS[i]));
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
344
|
return jwk;
|
|
@@ -374,9 +392,9 @@ var _VERIFY_KEYS = { key: 1, profile: 1 };
|
|
|
374
392
|
async function verify(jws, opts) {
|
|
375
393
|
opts = opts || {};
|
|
376
394
|
guard.identifier.assertKnownKeys(opts, _VERIFY_KEYS, E, "jose/bad-input", "unknown pki.jose.verify option ");
|
|
377
|
-
if (!jws || typeof jws !== "object" ||
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
395
|
+
if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
|
|
396
|
+
if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
|
|
397
|
+
if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
|
|
380
398
|
if (typeof jws.protected !== "string" || typeof jws.signature !== "string") throw E("jose/bad-jws", "protected and signature must be base64url strings");
|
|
381
399
|
if (typeof jws.payload !== "string") throw E("jose/bad-jws", "payload must be a base64url string (never detached)");
|
|
382
400
|
var header = parseJson(b64uDecode(jws.protected));
|
|
@@ -393,7 +411,7 @@ async function verify(jws, opts) {
|
|
|
393
411
|
// matters most -- a `key` that came back null from a lookup would verify against whatever the
|
|
394
412
|
// message carried, and the verdict would report the embedded key as though none was named.
|
|
395
413
|
// `undefined` alone means "not supplied", so spreading an options object stays safe.
|
|
396
|
-
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null ||
|
|
414
|
+
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || _isArray(opts.key))) {
|
|
397
415
|
throw E("jose/bad-key", "opts.key was supplied but is not a JWK object, so the key this message must be signed under cannot be established");
|
|
398
416
|
}
|
|
399
417
|
var jwk = header.jwk || opts.key;
|
|
@@ -419,7 +437,7 @@ async function verify(jws, opts) {
|
|
|
419
437
|
var want = _expectedSigBytes(checked.algRow, jwk);
|
|
420
438
|
if (want != null && sig.length !== want) throw E("jose/bad-signature", "the signature length " + sig.length + " is not the " + want + " bytes " + checked.alg + " requires");
|
|
421
439
|
// Signing input from the RECEIVED base64url strings verbatim (never a re-serialize).
|
|
422
|
-
var signingInput =
|
|
440
|
+
var signingInput = _bufferFrom(jws.protected + "." + jws.payload, "ascii");
|
|
423
441
|
var key;
|
|
424
442
|
try { key = await webcrypto.subtle.importKey("jwk", jwk, _importParams(checked.algRow, jwk), false, ["verify"]); }
|
|
425
443
|
catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
|
|
@@ -457,7 +475,7 @@ async function verify(jws, opts) {
|
|
|
457
475
|
* payload: Buffer // the raw payload octets ("" for POST-as-GET)
|
|
458
476
|
* key: CryptoKey // the private signing key
|
|
459
477
|
* profile: string // default "acme-outer"
|
|
460
|
-
* jwk: object // the public JWK,
|
|
478
|
+
* jwk: object // the public JWK, in kid mode (a header that embeds jwk supplies its own)
|
|
461
479
|
*
|
|
462
480
|
* @example
|
|
463
481
|
* var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
@@ -466,13 +484,40 @@ async function verify(jws, opts) {
|
|
|
466
484
|
* nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
|
|
467
485
|
* var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
|
|
468
486
|
*/
|
|
487
|
+
// The public JWK has two sources and the embedded one wins, so opts.jwk is read only in kid mode.
|
|
488
|
+
// Supplied alongside a header that embeds its own, it selected nothing: the signature parameters
|
|
489
|
+
// came from the header's key while the caller had named a different one.
|
|
469
490
|
var _SIGN_KEYS = { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 };
|
|
491
|
+
var _SIGN_KEYS_EMBEDDED_JWK = { protected: 1, payload: 1, key: 1, profile: 1 };
|
|
470
492
|
async function sign(opts) {
|
|
471
493
|
opts = opts || {};
|
|
494
|
+
// The options are checked BEFORE the header is touched. Serializing it runs the caller's own code
|
|
495
|
+
// -- a toJSON, an accessor -- and that code can delete an option from the very bag being checked,
|
|
496
|
+
// so a misspelling reached the door already removed and the message went out signed.
|
|
472
497
|
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
|
|
473
|
-
|
|
498
|
+
// The header is then serialized ONCE, and every read below comes from a snapshot of those exact
|
|
499
|
+
// bytes: the header validated, the header embedded in the JWS and the key confirmed against the
|
|
500
|
+
// produced signature are then all the same values. Read live, a caller getter answers each of
|
|
501
|
+
// those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
|
|
502
|
+
// the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
|
|
503
|
+
var headerJson;
|
|
504
|
+
try { headerJson = _stringify(opts.protected); }
|
|
505
|
+
catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
|
|
506
|
+
// Read back through the same bounded reader a received JWS goes through, so a header this side
|
|
507
|
+
// produces is held to the caps and the duplicate-member rule the other side enforces.
|
|
508
|
+
var headerBytes = headerJson === undefined ? null : _bufferFrom(headerJson, "utf8");
|
|
509
|
+
var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
|
|
510
|
+
// Which of the two key sources is in force is known only once the header has been read, so the
|
|
511
|
+
// narrower table runs here. It re-walks the bag, which also catches a name that code added.
|
|
512
|
+
var embedsJwk = !!(header && typeof header === "object" && header.jwk);
|
|
513
|
+
if (embedsJwk) {
|
|
514
|
+
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
|
|
515
|
+
return "unknown pki.jose.sign option " + _stringify(k) +
|
|
516
|
+
(k === "jwk" ? "; the protected header already embeds the public JWK" : "");
|
|
517
|
+
});
|
|
518
|
+
}
|
|
474
519
|
var payload = opts.payload;
|
|
475
|
-
if (!
|
|
520
|
+
if (!_isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
|
|
476
521
|
// Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
|
|
477
522
|
// and would be silently signed as an empty POST-as-GET body -- fail closed instead.
|
|
478
523
|
payload = guard.bytes.view(payload, JoseError, "jose/bad-input", "payload");
|
|
@@ -494,16 +539,16 @@ async function sign(opts) {
|
|
|
494
539
|
// also PUBLISHED to the server, so it must be public-only: an accidentally exported
|
|
495
540
|
// private JWK must never be serialized into the protected header.
|
|
496
541
|
if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
|
|
497
|
-
var protectedB64 = b64uEncode(
|
|
542
|
+
var protectedB64 = b64uEncode(headerBytes); // the exact bytes every check above read
|
|
498
543
|
var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
|
|
499
|
-
var signingInput =
|
|
544
|
+
var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
|
|
500
545
|
var jwk = header.jwk || opts.jwk || {};
|
|
501
546
|
// opts.key is documented as a CryptoKey, so one created outside this engine signs here too: it
|
|
502
547
|
// carries none of the material this engine signs with, and is re-imported through it rather than
|
|
503
548
|
// reaching subtle.sign as a key it cannot read. A key whose material this process cannot reach at
|
|
504
549
|
// all -- non-extractable, or held behind another implementation's interface -- is refused there.
|
|
505
550
|
var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
|
|
506
|
-
var sigBuf =
|
|
551
|
+
var sigBuf = _bufferFrom(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
|
|
507
552
|
// Pin the produced signature length the same way verify does. A key whose curve /
|
|
508
553
|
// hash / modulus does not match the header alg (a P-384 key under ES256, an
|
|
509
554
|
// HS512 key under HS256) still signs, but emits the wrong length -- caught here
|
|
@@ -563,15 +608,17 @@ var THUMBPRINT_MEMBERS = {
|
|
|
563
608
|
async function thumbprint(jwk) {
|
|
564
609
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
565
610
|
var members = THUMBPRINT_MEMBERS[jwk.kty];
|
|
566
|
-
if (!members) throw E("jose/bad-key", "unsupported JWK key type " +
|
|
611
|
+
if (!members) throw E("jose/bad-key", "unsupported JWK key type " + _stringify(jwk.kty));
|
|
567
612
|
var parts = [];
|
|
568
613
|
for (var i = 0; i < members.length; i++) {
|
|
569
614
|
var m = members[i];
|
|
570
|
-
if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " +
|
|
571
|
-
|
|
615
|
+
if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
|
|
616
|
+
// The canonical form IS the thumbprint preimage (RFC 7638 sec. 3.3), so the serializer that
|
|
617
|
+
// builds it is the captured one: a replacement changes the identity every consumer compares.
|
|
618
|
+
_push(parts, _stringify(m) + ":" + _stringify(jwk[m]));
|
|
572
619
|
}
|
|
573
|
-
var canonical = "{" + parts
|
|
574
|
-
var digest =
|
|
620
|
+
var canonical = "{" + _join(parts, ",") + "}";
|
|
621
|
+
var digest = _bufferFrom(await webcrypto.subtle.digest("SHA-256", _bufferFrom(canonical, "utf8")));
|
|
575
622
|
return b64uEncode(digest);
|
|
576
623
|
}
|
|
577
624
|
|
|
@@ -603,7 +650,7 @@ async function thumbprint(jwk) {
|
|
|
603
650
|
* pss.map(function (r) { return r.alg; }); // -> ["PS256", "PS384", "PS512"]
|
|
604
651
|
*/
|
|
605
652
|
function sigAlgs() {
|
|
606
|
-
return
|
|
653
|
+
return _map(_keys(SIG_ALGS), function (alg) {
|
|
607
654
|
var row = SIG_ALGS[alg];
|
|
608
655
|
var out = { alg: alg, kty: row.kty };
|
|
609
656
|
if (row.crv) out.crv = row.crv;
|
package/lib/key.js
CHANGED
|
@@ -305,6 +305,10 @@ async function import_(input, opts) {
|
|
|
305
305
|
}
|
|
306
306
|
var algorithm = opts.algorithm != null ? opts.algorithm : _inferAlgorithm(detected);
|
|
307
307
|
var isPublic = detected.format === "spki";
|
|
308
|
+
// Checked at this verb's own door rather than left for the engine to discover: a non-list reached
|
|
309
|
+
// the CryptoKey copy, where `Array.prototype.slice` on a number yields an empty list, so the key
|
|
310
|
+
// was minted permitted for NOTHING instead of the call naming the argument that was wrong.
|
|
311
|
+
if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
|
|
308
312
|
var usages = opts.usages || _importUsages(_algName(algorithm), isPublic);
|
|
309
313
|
var extractable = opts.extractable != null ? opts.extractable : false;
|
|
310
314
|
try {
|
|
@@ -344,6 +348,8 @@ async function generate(algorithm, opts) {
|
|
|
344
348
|
guard.identifier.assertKnownKeys(opts, _GENERATE_OPTS, _err, "key/bad-input",
|
|
345
349
|
"pki.key.generate has an unknown option (the WebCrypto spelling is `extractable`): ");
|
|
346
350
|
var extractable = opts.extractable != null ? opts.extractable : true;
|
|
351
|
+
// The same door as import: a non-list is named here rather than becoming an empty usage set.
|
|
352
|
+
if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
|
|
347
353
|
var usages = opts.usages || _generateUsages(_algName(algorithm));
|
|
348
354
|
var pair;
|
|
349
355
|
try { pair = await subtle.generateKey(algorithm, extractable, usages); }
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -43,6 +43,7 @@ var x509 = require("./schema-x509");
|
|
|
43
43
|
var schemaCrl = require("./schema-crl");
|
|
44
44
|
var key = require("./key");
|
|
45
45
|
var cms = require("./cms-verify"); // pki.cms.sign (build the id-signedData authSafe) + pki.cms.verify (the open integrity gate) + cms.encrypt (the privacy envelope)
|
|
46
|
+
var cmsSign = require("./cms-sign"); // the signer-descriptor field tables, for the integrity signers this verb authors
|
|
46
47
|
var cmsDecrypt = require("./cms-decrypt"); // the @internal decryptEnvelopedData seam -- opens a privacy safe off the pkcs12 parser's already-walked (BER-capable) EnvelopedData node
|
|
47
48
|
var schemaPkcs12 = require("./schema-pkcs12");
|
|
48
49
|
var pkix = require("./schema-pkix");
|
|
@@ -116,6 +117,91 @@ var _MISSING_PASSWORD = "a password must be a string, Buffer, or Uint8Array -- a
|
|
|
116
117
|
// Every field opts.integrity reads. Adding one here is the only way to make it accepted, so a
|
|
117
118
|
// capability cannot arrive with its option silently ignored at this boundary.
|
|
118
119
|
var _INTEGRITY_OPTS = { mode: 1, signer: 1, signers: 1, certificates: 1, sid: 1, signingTime: 1 };
|
|
120
|
+
// The two caller-owned objects pki.pkcs12.build reads. The spec keys are what _normalizeSpec
|
|
121
|
+
// consumes -- the explicit `safeContents` form and the `{ key, cert, ca }` shorthand -- so the table
|
|
122
|
+
// is derived from that function rather than from the doc block. opts.integrity already had its own
|
|
123
|
+
// door; the objects containing it did not, so a misspelled `certificates` at the top level was
|
|
124
|
+
// dropped and the store built without them.
|
|
125
|
+
// The spec comes in two mutually exclusive FORMS, so it is checked against the one it selects.
|
|
126
|
+
// `_normalizeSpec` returns `spec.safeContents` the moment it is an array and never looks at the
|
|
127
|
+
// shorthand fields, so a union accepted `{ safeContents: [...], key }` and built a store with the
|
|
128
|
+
// key silently absent -- the same omission the door exists to refuse, reached by mixing the forms
|
|
129
|
+
// rather than by misspelling one.
|
|
130
|
+
var _BUILD_SPEC_FULL_KEYS = { safeContents: 1 };
|
|
131
|
+
var _BUILD_SPEC_SHORTHAND_KEYS = { key: 1, cert: 1, ca: 1, friendlyName: 1, localKeyId: 1 };
|
|
132
|
+
// `recipientCerts` selects the privacy recipients and is read by _normalizeSpec, which _build hands
|
|
133
|
+
// opts to -- it belongs here even though _build never names it. The three verbs this module exports
|
|
134
|
+
// have disjoint option sets (verifyMac reads maxIterations / recipientKey / allowUnauthenticated,
|
|
135
|
+
// open reads signerCerts / keys / importAlgorithm), so build gets its own table rather than a
|
|
136
|
+
// shared one that would admit theirs.
|
|
137
|
+
// Per spec FORM, because `recipientCerts` is read only by the shorthand branch of _normalizeSpec.
|
|
138
|
+
// Under the full form it is read by nothing: a caller who put a plaintext key bag in safeContents
|
|
139
|
+
// and supplied recipientCerts expecting public-key privacy got an id-data safe holding the private
|
|
140
|
+
// key in cleartext. The full form builds its own safes, so privacy there is `recipients` on the
|
|
141
|
+
// entry.
|
|
142
|
+
var _BUILD_OPTS_FULL = { integrity: 1, mac: 1, password: 1, pem: 1 };
|
|
143
|
+
var _BUILD_OPTS_SHORTHAND = { integrity: 1, mac: 1, password: 1, pem: 1, recipientCerts: 1 };
|
|
144
|
+
// The fields a safeContents entry carries. This entry is where the PRIVACY directive lives, which
|
|
145
|
+
// makes an unrecognized name here the costliest misspelling in the format: a misspelled `encrypt`
|
|
146
|
+
// is neither present nor falsy, so it passes the present-but-falsy guard in _buildAuthSafeElement,
|
|
147
|
+
// no privacy is selected, and the safe goes out as plaintext id-data -- an unshrouded keyBag in the
|
|
148
|
+
// clear, inside a PFX whose MAC still verifies and which opens without complaint.
|
|
149
|
+
// Per privacy branch, because the branches read different fields:
|
|
150
|
+
// `contentEncryptionAlgorithm` is the public-key branch's content cipher and is read by NOTHING on
|
|
151
|
+
// a password safe, whose cipher comes from `encrypt.cipher`. A union accepted an explicit cipher
|
|
152
|
+
// request on a password safe and encrypted under the default anyway.
|
|
153
|
+
var _SAFE_RECIPIENTS_KEYS = { bags: 1, recipients: 1, contentEncryptionAlgorithm: 1 };
|
|
154
|
+
var _SAFE_PASSWORD_KEYS = { bags: 1, encrypt: 1 };
|
|
155
|
+
var _SAFE_PLAINTEXT_KEYS = { bags: 1 };
|
|
156
|
+
// The fields a bag carries, PER TYPE rather than as one union. A union would admit a field that
|
|
157
|
+
// this bag's own arm never reads, which is worse here than a dropped attribute: `encrypt` on a
|
|
158
|
+
// `key` bag is read by nothing, so the private key is emitted as a plaintext keyBag while the
|
|
159
|
+
// caller supplied an explicit encryption directive and the outer MAC gives integrity only. Naming
|
|
160
|
+
// the fields per type makes that combination a refusal instead of a silent downgrade. Written out
|
|
161
|
+
// per type rather than merged at load, so each row reads as the whole contract for that bag.
|
|
162
|
+
var _BAG_KEYS_BY_TYPE = {
|
|
163
|
+
key: { type: 1, friendlyName: 1, localKeyId: 1, key: 1 },
|
|
164
|
+
shroudedKey: { type: 1, friendlyName: 1, localKeyId: 1, key: 1, encrypt: 1 },
|
|
165
|
+
cert: { type: 1, friendlyName: 1, localKeyId: 1, cert: 1 },
|
|
166
|
+
crl: { type: 1, friendlyName: 1, localKeyId: 1, crl: 1 },
|
|
167
|
+
secret: { type: 1, friendlyName: 1, localKeyId: 1, secretTypeId: 1, secretValue: 1 },
|
|
168
|
+
safeContents: { type: 1, friendlyName: 1, localKeyId: 1, nested: 1 },
|
|
169
|
+
};
|
|
170
|
+
// The PBE descriptor both privacy levels share (_pbeOpts is the one place it is read, so one door
|
|
171
|
+
// covers the safe-level and bag-level spellings). A misspelled parameter reverts to the built-in
|
|
172
|
+
// default, so a caller who asked for a stronger KDF or a different cipher silently gets neither.
|
|
173
|
+
var _ENCRYPT_KEYS = { password: 1, cipher: 1, salt: 1, iterations: 1, prf: 1 };
|
|
174
|
+
// The MAC descriptor. Every field has a default, so a misspelled one reverts to the shipped value:
|
|
175
|
+
// a caller who asked for a higher iteration count, a different hash or a longer key gets the
|
|
176
|
+
// default instead, and the store carries no record of which was used.
|
|
177
|
+
// Per MAC algorithm: the classic Appendix B key derivation produces a key at the hash's own output
|
|
178
|
+
// length and has no keyLength to set, so admitting one there took an explicit security parameter and
|
|
179
|
+
// emitted the same MAC as a call that never named it. Only PBMAC1 (RFC 9579) derives to a length.
|
|
180
|
+
var _MAC_KEYS = { algorithm: 1, hash: 1, salt: 1, iterations: 1, keyLength: 1, password: 1 };
|
|
181
|
+
var _MAC_KEYS_HMAC = { algorithm: 1, hash: 1, salt: 1, iterations: 1, password: 1 };
|
|
182
|
+
var _MAC_KEYS_PBMAC1 = { algorithm: 1, hash: 1, salt: 1, iterations: 1, keyLength: 1, password: 1 };
|
|
183
|
+
// The reading verbs take their own option sets, disjoint from build's. `maxIterations` is the one
|
|
184
|
+
// that matters most: it caps the work a hostile store can demand, so a misspelling restores the
|
|
185
|
+
// built-in ceiling and the caller's tighter bound is never applied.
|
|
186
|
+
// The table is what the CALL reads, including through its delegations -- and those cross a module
|
|
187
|
+
// boundary here. recipientKey / recipientCert are read by _openEncryptedSafe; `recipientIndex` is
|
|
188
|
+
// read by cms-decrypt's decryptEnvelopedData, which _openEncryptedSafe hands this whole options
|
|
189
|
+
// object to, so a table built from this file's own `opts.` reads alone would refuse a documented
|
|
190
|
+
// option and break selecting a recipient by index in a multi-recipient store.
|
|
191
|
+
//
|
|
192
|
+
// One table here, not one per branch, and the distinction is who selects the branch. A builder's
|
|
193
|
+
// options are checked against the form the CALLER chose, because a field the chosen form never
|
|
194
|
+
// reads is a request that silently did not happen. Reading a store, the branch is chosen by the
|
|
195
|
+
// BYTES: which integrity mode it carries, and whether a safe is password- or recipient-encrypted,
|
|
196
|
+
// are not known until it is parsed. These options are the policy a caller declares in advance for
|
|
197
|
+
// whatever the store turns out to be, so one that goes unread means the store did not need it --
|
|
198
|
+
// signerCerts on a MAC'd store, recipientKey on one with no enveloped safe. Refusing those would
|
|
199
|
+
// make a correct, store-agnostic call fail on the shape of its input.
|
|
200
|
+
var _OPEN_OPTS = {
|
|
201
|
+
allowUnauthenticated: 1, importAlgorithm: 1, keys: 1, maxIterations: 1, signerCerts: 1,
|
|
202
|
+
recipientKey: 1, recipientCert: 1, recipientIndex: 1,
|
|
203
|
+
};
|
|
204
|
+
var _VERIFY_MAC_OPTS = { maxIterations: 1 };
|
|
119
205
|
|
|
120
206
|
// The same encoding, reporting ownership, mirroring pbes2.passwordBytesOwned. A caller-supplied
|
|
121
207
|
// Buffer is returned as-is and is borrowed: clearing it would destroy the caller's own credential,
|
|
@@ -280,6 +366,14 @@ function _safeBag(bagName, bagValueDer, bag) {
|
|
|
280
366
|
|
|
281
367
|
function _buildBag(bag, opts, depth) {
|
|
282
368
|
if (!bag || typeof bag !== "object") throw _err("pkcs12/bad-input", "each bag must be an object with a type");
|
|
369
|
+
// Checked against THIS type's table. An unrecognized type falls through to the switch below,
|
|
370
|
+
// which is the one place that names the permitted set.
|
|
371
|
+
if (guard.intrinsic.hasOwn(_BAG_KEYS_BY_TYPE, bag.type)) {
|
|
372
|
+
guard.identifier.assertKnownKeys(bag, _BAG_KEYS_BY_TYPE[bag.type], _err, "pkcs12/bad-input", function (k) {
|
|
373
|
+
return "unknown field " + JSON.stringify(k) + " on a " + JSON.stringify(bag.type) +
|
|
374
|
+
" bag; that field is not read for this bag type";
|
|
375
|
+
});
|
|
376
|
+
}
|
|
283
377
|
switch (bag.type) {
|
|
284
378
|
case "key": {
|
|
285
379
|
var keyDer = _coerceDer(bag.key, "keyBag key");
|
|
@@ -290,9 +384,12 @@ function _buildBag(bag, opts, depth) {
|
|
|
290
384
|
var kDer = _coerceDer(bag.key, "shroudedKey key");
|
|
291
385
|
try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
|
|
292
386
|
var enc = bag.encrypt || {};
|
|
387
|
+
// The descriptor is checked BEFORE its password is read, so no field of an object carrying an
|
|
388
|
+
// unrecognized name reaches the KDF -- the same order the safe-level path uses below.
|
|
389
|
+
var bagPbe = _pbeOpts(enc);
|
|
293
390
|
var pw = _pbePasswordOwned(enc.password != null ? enc.password : opts.password);
|
|
294
391
|
var r;
|
|
295
|
-
try { r = pbes2.pbes2Encrypt(pw.bytes, kDer,
|
|
392
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, bagPbe, _err, "pkcs12"); }
|
|
296
393
|
finally { _wipePw(pw); }
|
|
297
394
|
return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
|
|
298
395
|
}
|
|
@@ -349,6 +446,10 @@ function _reqDer(input, label) {
|
|
|
349
446
|
}
|
|
350
447
|
|
|
351
448
|
function _pbeOpts(enc) {
|
|
449
|
+
guard.identifier.assertKnownKeys(enc, _ENCRYPT_KEYS, _err, "pkcs12/bad-input", function (k) {
|
|
450
|
+
return "unknown encrypt field " + JSON.stringify(k) +
|
|
451
|
+
"; the PBE descriptor is { password?, cipher?, salt?, iterations?, prf? }";
|
|
452
|
+
});
|
|
352
453
|
var cipher = CIPHER_NAME[enc.cipher || "aes-256-cbc"];
|
|
353
454
|
if (!cipher) throw _err("pkcs12/bad-input", "unsupported PBES2 cipher " + JSON.stringify(enc.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
|
|
354
455
|
return {
|
|
@@ -379,6 +480,18 @@ async function _buildAuthSafeElement(sc, opts) {
|
|
|
379
480
|
var hasRecipients = sc.recipients !== undefined;
|
|
380
481
|
var hasEncrypt = sc.encrypt !== undefined;
|
|
381
482
|
if (hasEncrypt && hasRecipients) throw _err("pkcs12/bad-input", "a safeContents cannot combine encrypt (password) and recipients (public-key) -- one ContentInfo is one privacy type (RFC 7292 sec. 4.1)");
|
|
483
|
+
// Checked against the branch the directive above selects, and BEFORE that branch reads anything.
|
|
484
|
+
// The branches read different fields: `contentEncryptionAlgorithm` is the public-key branch's
|
|
485
|
+
// cipher and is read by nothing on a password safe, whose cipher comes from `encrypt.cipher` --
|
|
486
|
+
// so a union accepted an explicit cipher request on a password safe and encrypted under the
|
|
487
|
+
// default anyway.
|
|
488
|
+
guard.identifier.assertKnownKeys(sc, hasRecipients ? _SAFE_RECIPIENTS_KEYS : (hasEncrypt ? _SAFE_PASSWORD_KEYS : _SAFE_PLAINTEXT_KEYS),
|
|
489
|
+
_err, "pkcs12/bad-input", function (k) {
|
|
490
|
+
return "unknown safeContents field " + JSON.stringify(k) + " for a " +
|
|
491
|
+
(hasRecipients ? "public-key safe; it takes { bags, recipients, contentEncryptionAlgorithm? }"
|
|
492
|
+
: hasEncrypt ? "password safe; it takes { bags, encrypt } and its cipher comes from encrypt.cipher"
|
|
493
|
+
: "plaintext safe; it takes { bags }, and a privacy directive must be encrypt or recipients");
|
|
494
|
+
});
|
|
382
495
|
var safeContentsDer = _buildSafeContents(sc.bags || [], opts, 0);
|
|
383
496
|
if (hasRecipients) {
|
|
384
497
|
if (!Array.isArray(sc.recipients) || !sc.recipients.length) throw _err("pkcs12/bad-input", "safeContents.recipients must be a non-empty array of recipient descriptors (RFC 5652 sec. 6.1)");
|
|
@@ -401,9 +514,12 @@ async function _buildAuthSafeElement(sc, opts) {
|
|
|
401
514
|
return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
|
|
402
515
|
}
|
|
403
516
|
if (!sc.encrypt || typeof sc.encrypt !== "object") throw _err("pkcs12/bad-input", "safeContents.encrypt must be an object { password? } (RFC 7292 sec. 5.1) -- omit it entirely for a plaintext safe");
|
|
517
|
+
// Checked before the password is read, so no field of a descriptor carrying an unrecognized name
|
|
518
|
+
// reaches the KDF.
|
|
519
|
+
var safePbe = _pbeOpts(sc.encrypt);
|
|
404
520
|
var pw = _pbePasswordOwned(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
|
|
405
521
|
var r;
|
|
406
|
-
try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer,
|
|
522
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, safePbe, _err, "pkcs12"); }
|
|
407
523
|
finally { _wipePw(pw); }
|
|
408
524
|
var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
|
|
409
525
|
var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
|
|
@@ -464,11 +580,26 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
|
|
|
464
580
|
// Accept the OpenSSL-style convenience form ({ key, cert, ca, friendlyName, localKeyId }) or the full
|
|
465
581
|
// inverse-of-parse form ({ safeContents: SafeContentsSpec[] }). The convenience form -> one privacy safe of
|
|
466
582
|
// PBES2 cert bags + one shrouded-key safe.
|
|
467
|
-
|
|
468
|
-
|
|
583
|
+
// Which of the two forms a spec is gets decided ONCE, and that one decision is what both the door
|
|
584
|
+
// and this normalizer act on. Decided separately they can disagree: two spellings of "is this the
|
|
585
|
+
// full form" drift apart on their own, and a caller getter returning an array to one and something
|
|
586
|
+
// else to the other makes them disagree on demand -- the full form checked while the shorthand form
|
|
587
|
+
// is the one assembled.
|
|
588
|
+
function _specForm(spec) {
|
|
589
|
+
var isObject = !!spec && typeof spec === "object" && !Buffer.isBuffer(spec);
|
|
590
|
+
var safeContents = isObject ? spec.safeContents : null;
|
|
591
|
+
var full = guard.intrinsic.isArray(safeContents);
|
|
592
|
+
return { isObject: isObject, full: full, safeContents: full ? safeContents : null };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function _normalizeSpec(spec, opts, form) {
|
|
596
|
+
if (form.full) return form.safeContents;
|
|
469
597
|
if (spec && (spec.key != null || spec.cert != null)) {
|
|
470
|
-
|
|
471
|
-
|
|
598
|
+
// One read of the caller's array, so the emptiness check and the recipients built from it cannot
|
|
599
|
+
// be handed different values.
|
|
600
|
+
var recipientCerts = opts.recipientCerts;
|
|
601
|
+
var enveloped = recipientCerts != null;
|
|
602
|
+
if (enveloped && (!guard.intrinsic.isArray(recipientCerts) || !recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
|
|
472
603
|
var certBags = [];
|
|
473
604
|
if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
|
|
474
605
|
if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
|
|
@@ -480,7 +611,7 @@ function _normalizeSpec(spec, opts) {
|
|
|
480
611
|
// caller password, if any, remains the MAC (integrity) password; privacy and integrity are independent.
|
|
481
612
|
var bags = certBags.slice();
|
|
482
613
|
if (spec.key != null) bags.push({ type: "key", key: spec.key, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
|
|
483
|
-
sc.push({ recipients:
|
|
614
|
+
sc.push({ recipients: recipientCerts.map(function (c) { return { cert: c }; }), bags: bags });
|
|
484
615
|
return sc;
|
|
485
616
|
}
|
|
486
617
|
if (certBags.length) sc.push({ encrypt: { password: opts.password }, bags: certBags });
|
|
@@ -512,7 +643,7 @@ function _normalizeSpec(spec, opts) {
|
|
|
512
643
|
*
|
|
513
644
|
* @opts
|
|
514
645
|
* - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
|
|
515
|
-
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations
|
|
646
|
+
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations? }`. `keyLength` is a `pbmac1` field: the classic Appendix B derivation produces a key at the hash's own output length, and naming one under `hmac` is refused.
|
|
516
647
|
* - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected.
|
|
517
648
|
* - `recipientCerts` -- (public-key privacy convenience) `[certDer|PEM, ...]`; the convenience-form cert + key are placed in one recipient-enveloped safe (a plaintext keyBag). For the full form, set per-`safeContents` `recipients: [{ cert }, ...]` -- CERTIFICATE recipients only (RSA-OAEP / ECDH / X25519 / X448 / ML-KEM, dispatched off the cert key); a password or KEK recipient is not public-key privacy and is rejected -- with an optional `contentEncryptionAlgorithm` (`aes-128|192|256-cbc`, default 256; GCM/AEAD rejected). `encrypt` (password) and `recipients` (public-key) on the same safe is rejected; privacy is independent of integrity.
|
|
518
649
|
* - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
|
|
@@ -541,6 +672,26 @@ function build(spec, opts) {
|
|
|
541
672
|
|
|
542
673
|
async function _build(spec, opts) {
|
|
543
674
|
opts = opts || {};
|
|
675
|
+
// Before the spec is normalized, so a misspelled bag field is a refusal rather than a store built
|
|
676
|
+
// without it. A PKCS#12 file that silently omits the certificate or the key a caller asked for
|
|
677
|
+
// looks well-formed and opens cleanly; the omission surfaces wherever it is later imported.
|
|
678
|
+
var form = _specForm(spec);
|
|
679
|
+
if (form.isObject) {
|
|
680
|
+
guard.identifier.assertKnownKeys(spec, form.full ? _BUILD_SPEC_FULL_KEYS : _BUILD_SPEC_SHORTHAND_KEYS,
|
|
681
|
+
_err, "pkcs12/bad-input", function (k) {
|
|
682
|
+
return "unknown PKCS#12 spec field " + JSON.stringify(k) + " for the " +
|
|
683
|
+
(form.full ? "safeContents form; the shorthand fields belong to { key, cert, ca } and are not read here"
|
|
684
|
+
: "shorthand form; pass safeContents instead to build the safes yourself");
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
// The options answer to the same form: recipientCerts is read only by the shorthand branch.
|
|
688
|
+
guard.identifier.assertKnownKeys(opts, form.full ? _BUILD_OPTS_FULL : _BUILD_OPTS_SHORTHAND, _err, "pkcs12/bad-input",
|
|
689
|
+
function (k) {
|
|
690
|
+
return "pki.pkcs12.build has an unknown option " + JSON.stringify(k) +
|
|
691
|
+
(form.full && k === "recipientCerts"
|
|
692
|
+
? " for the safeContents form; the full form carries privacy per entry, as safeContents[i].recipients"
|
|
693
|
+
: "");
|
|
694
|
+
});
|
|
544
695
|
// The integrity mode is checked against the permitted set rather than compared to one literal.
|
|
545
696
|
// Compared, any other spelling reads as "not public-key" and silently selects password integrity,
|
|
546
697
|
// dropping the signer with it: a caller who wrote "publicKey" gets a MAC where they asked for a
|
|
@@ -556,7 +707,7 @@ async function _build(spec, opts) {
|
|
|
556
707
|
// RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
|
|
557
708
|
// config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
|
|
558
709
|
if (pubKey && opts.mac != null && opts.mac !== false) throw _err("pkcs12/bad-integrity-mode", "public-key integrity has no MacData -- do not combine opts.mac with opts.integrity.mode 'public-key' (RFC 7292 sec. 4)");
|
|
559
|
-
var safeContentsSpecs = _normalizeSpec(spec, opts);
|
|
710
|
+
var safeContentsSpecs = _normalizeSpec(spec, opts, form);
|
|
560
711
|
if (!Array.isArray(safeContentsSpecs) || !safeContentsSpecs.length) throw _err("pkcs12/bad-input", "the store has no safe contents");
|
|
561
712
|
var elements = [];
|
|
562
713
|
for (var i = 0; i < safeContentsSpecs.length; i++) elements.push(await _buildAuthSafeElement(safeContentsSpecs[i], opts));
|
|
@@ -569,13 +720,48 @@ async function _build(spec, opts) {
|
|
|
569
720
|
// password) provides the integrity, and there is NO MacData. Compose pki.cms.sign -- so every cms.sign
|
|
570
721
|
// signer algorithm (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite) ships for free.
|
|
571
722
|
var ig = opts.integrity;
|
|
723
|
+
// `signer` is the one-signer spelling of `signers`, and the list wins outright, so supplying
|
|
724
|
+
// both signed the store under the list alone and dropped the single signer without a word.
|
|
725
|
+
if (ig.signers != null && ig.signer != null) throw _err("pkcs12/bad-input", "supply exactly one of opts.integrity.signer or opts.integrity.signers, not both");
|
|
572
726
|
var signers = ig.signers != null ? ig.signers : (ig.signer != null ? [ig.signer] : null);
|
|
573
727
|
if (!Array.isArray(signers) || !signers.length) throw _err("pkcs12/bad-input", "public-key integrity requires opts.integrity.signer or opts.integrity.signers (a cms.sign signer descriptor)");
|
|
728
|
+
// The signer descriptors are authoring input for this store, so they answer to the same rule as
|
|
729
|
+
// every other field the caller writes here, against the form each one selects. Every field
|
|
730
|
+
// beyond the identity has a default: without the door a misspelled `pss` or `digestAlgorithm`
|
|
731
|
+
// signed the store under PKCS#1 with SHA-256 while the caller had asked for something else, and
|
|
732
|
+
// the store records only what was used, never what was requested.
|
|
733
|
+
for (var sx = 0; sx < signers.length; sx++) {
|
|
734
|
+
var sd = signers[sx];
|
|
735
|
+
if (sd == null || typeof sd !== "object" || Buffer.isBuffer(sd)) throw _err("pkcs12/bad-input", "each opts.integrity signer must be a descriptor object");
|
|
736
|
+
var keyOnly = sd.cert == null && sd.spki != null;
|
|
737
|
+
guard.identifier.assertKnownKeys(sd, keyOnly ? cmsSign.KNOWN_SIGNER_KEY_ONLY_KEYS : cmsSign.KNOWN_SIGNER_CERT_KEYS,
|
|
738
|
+
_err, "pkcs12/bad-input", function (k) {
|
|
739
|
+
return "unknown opts.integrity signer field " + JSON.stringify(k) + " for the " + (keyOnly
|
|
740
|
+
? "key-only form; it takes { key, spki, keyIdentifier } plus the signature parameters"
|
|
741
|
+
: "certificate form; it takes { key, cert } plus the signature parameters");
|
|
742
|
+
});
|
|
743
|
+
}
|
|
574
744
|
var authSafeCi = await cms.sign(authSafeDer, signers, { eContentType: "data", detached: false, certificates: ig.certificates !== false, sid: ig.sid, signingTime: ig.signingTime });
|
|
575
745
|
pfx = b.sequence([b.integer(3n), b.raw(authSafeCi)]); // PFX ::= SEQUENCE { version v3, authSafe (id-signedData) } -- NO MacData
|
|
576
746
|
} else {
|
|
577
747
|
var pfxChildren = [b.integer(3n), b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(authSafeDer))])];
|
|
578
|
-
|
|
748
|
+
// An array passes `typeof x === "object"`, and its own names are indices no table lists, so an
|
|
749
|
+
// array reached _buildMacData as an empty descriptor and produced the DEFAULT MAC. Excluded by
|
|
750
|
+
// the same test opts.integrity already applies, read through the load-time capture so a
|
|
751
|
+
// replaced Array.isArray cannot reopen it.
|
|
752
|
+
if (opts.mac != null && opts.mac !== false && (typeof opts.mac !== "object" || guard.intrinsic.isArray(opts.mac))) throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
|
|
753
|
+
if (opts.mac != null && opts.mac !== false) {
|
|
754
|
+
// The algorithm selects the derivation, so the table that runs is the one belonging to it.
|
|
755
|
+
// An unrecognized spelling keeps the union; _buildMacData refuses it by name a moment later.
|
|
756
|
+
var macAlg = opts.mac.algorithm == null ? "hmac" : opts.mac.algorithm;
|
|
757
|
+
var macKeys = macAlg === "hmac" ? _MAC_KEYS_HMAC : macAlg === "pbmac1" ? _MAC_KEYS_PBMAC1 : _MAC_KEYS;
|
|
758
|
+
guard.identifier.assertKnownKeys(opts.mac, macKeys, _err, "pkcs12/bad-input", function (k) {
|
|
759
|
+
return "opts.mac has an unknown option " + JSON.stringify(k) +
|
|
760
|
+
(macAlg === "hmac" && k === "keyLength"
|
|
761
|
+
? " for the classic HMAC algorithm; its key is derived at the hash's own output length, and only pbmac1 takes a keyLength"
|
|
762
|
+
: "");
|
|
763
|
+
});
|
|
764
|
+
}
|
|
579
765
|
if (opts.mac !== false) pfxChildren.push(await _buildMacData(opts.mac || {}, opts.password, authSafeDer));
|
|
580
766
|
pfx = b.sequence(pfxChildren);
|
|
581
767
|
}
|
|
@@ -612,6 +798,9 @@ async function _build(spec, opts) {
|
|
|
612
798
|
* var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
|
|
613
799
|
*/
|
|
614
800
|
async function verifyMac(pfx, password, opts) {
|
|
801
|
+
if (opts != null && typeof opts === "object" && !Buffer.isBuffer(opts)) {
|
|
802
|
+
guard.identifier.assertKnownKeys(opts, _VERIFY_MAC_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.verifyMac has an unknown option ");
|
|
803
|
+
}
|
|
615
804
|
return _verifyMacOfStore(_storeFromBytes(pfx), password, opts);
|
|
616
805
|
}
|
|
617
806
|
|
|
@@ -715,7 +904,8 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
|
|
|
715
904
|
*/
|
|
716
905
|
async function open(pfx, password, opts) {
|
|
717
906
|
opts = opts || {};
|
|
718
|
-
|
|
907
|
+
guard.identifier.assertKnownKeys(opts, _OPEN_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.open has an unknown option ");
|
|
908
|
+
if (opts.maxIterations != null &&(typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
|
|
719
909
|
throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
|
|
720
910
|
}
|
|
721
911
|
var m = _storeFromBytes(pfx);
|