@blamejs/pki 0.5.16 → 0.5.18

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/lib/acme.js +7 -4
  3. package/lib/asn1-der.js +1 -0
  4. package/lib/attrcert-sign.js +1 -0
  5. package/lib/cbor-det.js +1 -0
  6. package/lib/cmc-verify.js +1 -0
  7. package/lib/cmp-build.js +16 -1
  8. package/lib/cms-decrypt.js +58 -29
  9. package/lib/cms-encrypt.js +200 -38
  10. package/lib/cms-sign.js +18 -4
  11. package/lib/cms-verify.js +5 -1
  12. package/lib/constants.js +1 -0
  13. package/lib/crl-sign.js +62 -2
  14. package/lib/crmf-sign.js +1 -0
  15. package/lib/csr-sign.js +1 -0
  16. package/lib/ct.js +1 -0
  17. package/lib/est.js +1 -0
  18. package/lib/framework-error.js +1 -0
  19. package/lib/guard-secret.js +45 -1
  20. package/lib/hpke.js +6 -2
  21. package/lib/http-transport.js +1 -0
  22. package/lib/inspect.js +1 -0
  23. package/lib/jose.js +31 -3
  24. package/lib/key.js +2 -1
  25. package/lib/lint.js +1 -0
  26. package/lib/merkle.js +1 -0
  27. package/lib/ocsp.js +1 -0
  28. package/lib/oid.js +1 -0
  29. package/lib/path-validate.js +1 -0
  30. package/lib/pbes2.js +16 -8
  31. package/lib/pkcs12-build.js +203 -12
  32. package/lib/schema-all.js +1 -0
  33. package/lib/schema-c509.js +1 -0
  34. package/lib/schema-csrattrs.js +1 -0
  35. package/lib/schema-engine.js +1 -0
  36. package/lib/schema-pkcs8.js +1 -0
  37. package/lib/schema-smime.js +1 -0
  38. package/lib/schema-tsp.js +1 -0
  39. package/lib/schema-x509.js +1 -0
  40. package/lib/shbs.js +1 -0
  41. package/lib/sigstore.js +1 -0
  42. package/lib/smime.js +1 -0
  43. package/lib/tls-cert-compress.js +1 -0
  44. package/lib/trust.js +1 -0
  45. package/lib/tsp-sign.js +65 -0
  46. package/lib/webauthn.js +1 -0
  47. package/lib/webcrypto.js +11 -8
  48. package/lib/x509-sign.js +38 -0
  49. package/package.json +1 -1
  50. package/sbom.cdx.json +6 -6
@@ -5,6 +5,7 @@
5
5
  * @module pki.pkcs12
6
6
  * @nav Signing
7
7
  * @title PKCS#12
8
+ * @fullname PKCS#12 keystores (.p12 and .pfx): build and open
8
9
  * @intro The PKCS#12 (.p12 / .pfx) producing side (RFC 7292, RFC 9579). `pki.pkcs12.build` assembles a
9
10
  * password-integrity PFX -- key, certificate, CRL, and secret bags (shrouded keys and cert safes encrypted
10
11
  * under RFC 8018 PBES2) wrapped in an AuthenticatedSafe, protected by either a classic Appendix B HMAC or
@@ -42,6 +43,7 @@ var x509 = require("./schema-x509");
42
43
  var schemaCrl = require("./schema-crl");
43
44
  var key = require("./key");
44
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
45
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
46
48
  var schemaPkcs12 = require("./schema-pkcs12");
47
49
  var pkix = require("./schema-pkix");
@@ -115,6 +117,91 @@ var _MISSING_PASSWORD = "a password must be a string, Buffer, or Uint8Array -- a
115
117
  // Every field opts.integrity reads. Adding one here is the only way to make it accepted, so a
116
118
  // capability cannot arrive with its option silently ignored at this boundary.
117
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 };
118
205
 
119
206
  // The same encoding, reporting ownership, mirroring pbes2.passwordBytesOwned. A caller-supplied
120
207
  // Buffer is returned as-is and is borrowed: clearing it would destroy the caller's own credential,
@@ -279,6 +366,14 @@ function _safeBag(bagName, bagValueDer, bag) {
279
366
 
280
367
  function _buildBag(bag, opts, depth) {
281
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
+ }
282
377
  switch (bag.type) {
283
378
  case "key": {
284
379
  var keyDer = _coerceDer(bag.key, "keyBag key");
@@ -289,9 +384,12 @@ function _buildBag(bag, opts, depth) {
289
384
  var kDer = _coerceDer(bag.key, "shroudedKey key");
290
385
  try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
291
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);
292
390
  var pw = _pbePasswordOwned(enc.password != null ? enc.password : opts.password);
293
391
  var r;
294
- try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, _pbeOpts(enc), _err, "pkcs12"); }
392
+ try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, bagPbe, _err, "pkcs12"); }
295
393
  finally { _wipePw(pw); }
296
394
  return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
297
395
  }
@@ -348,6 +446,10 @@ function _reqDer(input, label) {
348
446
  }
349
447
 
350
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
+ });
351
453
  var cipher = CIPHER_NAME[enc.cipher || "aes-256-cbc"];
352
454
  if (!cipher) throw _err("pkcs12/bad-input", "unsupported PBES2 cipher " + JSON.stringify(enc.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
353
455
  return {
@@ -378,6 +480,18 @@ async function _buildAuthSafeElement(sc, opts) {
378
480
  var hasRecipients = sc.recipients !== undefined;
379
481
  var hasEncrypt = sc.encrypt !== undefined;
380
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
+ });
381
495
  var safeContentsDer = _buildSafeContents(sc.bags || [], opts, 0);
382
496
  if (hasRecipients) {
383
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)");
@@ -400,9 +514,12 @@ async function _buildAuthSafeElement(sc, opts) {
400
514
  return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
401
515
  }
402
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);
403
520
  var pw = _pbePasswordOwned(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
404
521
  var r;
405
- try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12"); }
522
+ try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, safePbe, _err, "pkcs12"); }
406
523
  finally { _wipePw(pw); }
407
524
  var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
408
525
  var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
@@ -463,11 +580,26 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
463
580
  // Accept the OpenSSL-style convenience form ({ key, cert, ca, friendlyName, localKeyId }) or the full
464
581
  // inverse-of-parse form ({ safeContents: SafeContentsSpec[] }). The convenience form -> one privacy safe of
465
582
  // PBES2 cert bags + one shrouded-key safe.
466
- function _normalizeSpec(spec, opts) {
467
- if (spec && Array.isArray(spec.safeContents)) return spec.safeContents;
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;
468
597
  if (spec && (spec.key != null || spec.cert != null)) {
469
- var enveloped = opts.recipientCerts != null;
470
- if (enveloped && (!Array.isArray(opts.recipientCerts) || !opts.recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
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");
471
603
  var certBags = [];
472
604
  if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
473
605
  if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
@@ -479,7 +611,7 @@ function _normalizeSpec(spec, opts) {
479
611
  // caller password, if any, remains the MAC (integrity) password; privacy and integrity are independent.
480
612
  var bags = certBags.slice();
481
613
  if (spec.key != null) bags.push({ type: "key", key: spec.key, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
482
- sc.push({ recipients: opts.recipientCerts.map(function (c) { return { cert: c }; }), bags: bags });
614
+ sc.push({ recipients: recipientCerts.map(function (c) { return { cert: c }; }), bags: bags });
483
615
  return sc;
484
616
  }
485
617
  if (certBags.length) sc.push({ encrypt: { password: opts.password }, bags: certBags });
@@ -511,7 +643,7 @@ function _normalizeSpec(spec, opts) {
511
643
  *
512
644
  * @opts
513
645
  * - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
514
- * - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, keyLength? }`.
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.
515
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.
516
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.
517
649
  * - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
@@ -540,6 +672,26 @@ function build(spec, opts) {
540
672
 
541
673
  async function _build(spec, opts) {
542
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
+ });
543
695
  // The integrity mode is checked against the permitted set rather than compared to one literal.
544
696
  // Compared, any other spelling reads as "not public-key" and silently selects password integrity,
545
697
  // dropping the signer with it: a caller who wrote "publicKey" gets a MAC where they asked for a
@@ -555,7 +707,7 @@ async function _build(spec, opts) {
555
707
  // RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
556
708
  // config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
557
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)");
558
- var safeContentsSpecs = _normalizeSpec(spec, opts);
710
+ var safeContentsSpecs = _normalizeSpec(spec, opts, form);
559
711
  if (!Array.isArray(safeContentsSpecs) || !safeContentsSpecs.length) throw _err("pkcs12/bad-input", "the store has no safe contents");
560
712
  var elements = [];
561
713
  for (var i = 0; i < safeContentsSpecs.length; i++) elements.push(await _buildAuthSafeElement(safeContentsSpecs[i], opts));
@@ -568,13 +720,48 @@ async function _build(spec, opts) {
568
720
  // password) provides the integrity, and there is NO MacData. Compose pki.cms.sign -- so every cms.sign
569
721
  // signer algorithm (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite) ships for free.
570
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");
571
726
  var signers = ig.signers != null ? ig.signers : (ig.signer != null ? [ig.signer] : null);
572
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
+ }
573
744
  var authSafeCi = await cms.sign(authSafeDer, signers, { eContentType: "data", detached: false, certificates: ig.certificates !== false, sid: ig.sid, signingTime: ig.signingTime });
574
745
  pfx = b.sequence([b.integer(3n), b.raw(authSafeCi)]); // PFX ::= SEQUENCE { version v3, authSafe (id-signedData) } -- NO MacData
575
746
  } else {
576
747
  var pfxChildren = [b.integer(3n), b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(authSafeDer))])];
577
- 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");
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
+ }
578
765
  if (opts.mac !== false) pfxChildren.push(await _buildMacData(opts.mac || {}, opts.password, authSafeDer));
579
766
  pfx = b.sequence(pfxChildren);
580
767
  }
@@ -611,6 +798,9 @@ async function _build(spec, opts) {
611
798
  * var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
612
799
  */
613
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
+ }
614
804
  return _verifyMacOfStore(_storeFromBytes(pfx), password, opts);
615
805
  }
616
806
 
@@ -714,7 +904,8 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
714
904
  */
715
905
  async function open(pfx, password, opts) {
716
906
  opts = opts || {};
717
- if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
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)) {
718
909
  throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
719
910
  }
720
911
  var m = _storeFromBytes(pfx);
@@ -848,7 +1039,7 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
848
1039
  try {
849
1040
  if (scheme.rc2) return rc2.cbcDecrypt(keyM, scheme.rc2, iv, ct, _err, "pkcs12/decrypt-failed");
850
1041
  var d = nodeCrypto.createDecipheriv(scheme.cipher, keyM, iv);
851
- return Buffer.concat([d.update(ct), d.final()]);
1042
+ return guard.secret.cipherFinish(d, ct, Pkcs12Error, "pkcs12/bad-input", "the recovered safe contents");
852
1043
  } catch (_e) { throw _err("pkcs12/decrypt-failed", "decryption failed"); }
853
1044
  finally { guard.secret.zeroize(keyM, Pkcs12Error, "pkcs12/bad-input", "the password-derived decryption key"); }
854
1045
  }
package/lib/schema-all.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema
6
6
  * @nav Schema
7
7
  * @title Schema
8
+ * @fullname Format detection: parse DER without knowing the format first
8
9
  * @order 10
9
10
  * @featured true
10
11
  * @slug schema
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema.c509
6
6
  * @nav Schema
7
7
  * @title C509
8
+ * @fullname C509 compact certificates (CBOR-encoded X.509)
8
9
  * @intro C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert). A compact CBOR
9
10
  * re-encoding of an X.509 v3 certificate: a deterministic-CBOR array of exactly 11 elements
10
11
  * (10 TBS fields + the issuer signature). Two modes: c509CertificateType 2 is a natively-signed
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema.csrattrs
6
6
  * @nav Schema
7
7
  * @title CSR Attributes
8
+ * @fullname CSR attributes: what an EST server asks an enrollee to include
8
9
  * @order 185
9
10
  * @slug csrattrs
10
11
  *
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema.engine
6
6
  * @nav Schema
7
7
  * @title Schema engine
8
+ * @fullname ASN.1 schema engine: one definition drives encode and decode
8
9
  * @order 60
9
10
  *
10
11
  * @intro
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema.pkcs8
6
6
  * @nav Schema
7
7
  * @title PKCS#8
8
+ * @fullname PKCS#8 private keys, encrypted and unencrypted
8
9
  * @order 140
9
10
  * @slug pkcs8
10
11
  *
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema.smime
6
6
  * @nav Schema
7
7
  * @title S/MIME (ESS)
8
+ * @fullname S/MIME signed and encrypted mail, with ESS security labels
8
9
  * @order 180
9
10
  * @slug smime
10
11
  *
package/lib/schema-tsp.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema.tsp
6
6
  * @nav Schema
7
7
  * @title TSP
8
+ * @fullname RFC 3161 timestamps (TSP): request, sign and verify
8
9
  * @order 170
9
10
  * @slug tsp
10
11
  *
@@ -5,6 +5,7 @@
5
5
  * @module pki.schema.x509
6
6
  * @nav Schema
7
7
  * @title X.509
8
+ * @fullname X.509 certificates: parse, sign and inspect
8
9
  * @order 100
9
10
  * @featured true
10
11
  * @slug x509
package/lib/shbs.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.shbs
6
6
  * @nav Signatures
7
7
  * @title Stateful hash-based
8
+ * @fullname Stateful hash-based signatures: LMS and XMSS
8
9
  * @intro Stateful hash-based signature VERIFICATION -- HSS/LMS (RFC 8554),
9
10
  * carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST
10
11
  * SP 800-208. Verify only, by deliberate design: stateful hash-based signing
package/lib/sigstore.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.sigstore
6
6
  * @nav Supply chain
7
7
  * @title Sigstore
8
+ * @fullname Sigstore bundles: keyless signing and provenance
8
9
  * @intro Zero-dependency verifier for a Sigstore bundle, the exact artifact
9
10
  * `npm publish --provenance` produces and the npm registry serves at its
10
11
  * attestations API. A bundle is a keyless (Fulcio) signature over a DSSE-wrapped
package/lib/smime.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.smime
6
6
  * @nav Messaging
7
7
  * @title S/MIME
8
+ * @fullname S/MIME signed and encrypted mail
8
9
  * @order 175
9
10
  * @slug smime
10
11
  *
@@ -5,6 +5,7 @@
5
5
  * @module pki.tls
6
6
  * @nav Transparency
7
7
  * @title TLS
8
+ * @fullname TLS certificate compression (RFC 8879)
8
9
  * @order 215
9
10
  * @slug tls
10
11
  *
package/lib/trust.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.trust
6
6
  * @nav Path validation
7
7
  * @title Trust-store ingestion
8
+ * @fullname Trust stores: load and pin root certificate anchors
8
9
  * @order 230
9
10
  * @slug trust
10
11
  *
package/lib/tsp-sign.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.tsp
6
6
  * @nav Signing
7
7
  * @title Timestamps
8
+ * @fullname RFC 3161 trusted timestamps: request, sign and verify
8
9
  * @intro Create an RFC 3161 timestamp token. A TimeStampToken IS a CMS SignedData whose
9
10
  * encapsulated content is a `TSTInfo` (the timestamped message imprint + trusted time), so
10
11
  * `sign(messageImprint, tsa, opts)` builds the `TSTInfo`, attaches the RFC 3161 sec. 2.4.2
@@ -141,9 +142,38 @@ function sign(messageImprint, tsa, opts) {
141
142
  ], _sign);
142
143
  }
143
144
 
145
+ // The three caller-owned objects pki.tsp.sign reads. A dropped option here is a token that quietly
146
+ // lacks what was asked for -- `odering` for `ordering` emitted a token with the ordering flag unset,
147
+ // which is a claim about the timestamp's orderability that the caller believes they made.
148
+ // The TSA is gated too, and the reason it needs to be is not the one it looks like. Both fields it
149
+ // carries are REQUIRED, so a MISSPELLING of either is already refused for what it leaves missing --
150
+ // but that is only half the class. A name belonging on the OPTIONS, written on the TSA instead, is
151
+ // read by nothing here: `ordering` placed on the TSA emits a token the same size as one that never
152
+ // asked for it, with the flag unset, while the caller believes they set it. Requiring the argument
153
+ // to carry only what it reads is what closes that, so all three caller-owned arguments answer the
154
+ // same question -- is this a name I read? -- rather than two of them answering it.
155
+ var _TSA_KEYS = { cert: 1, key: 1 };
156
+ var _IMPRINT_KEYS = { hashAlgorithm: 1, hashedMessage: 1 };
157
+ var _SIGN_OPTS = {
158
+ policy: 1, serialNumber: 1, genTime: 1, nonce: 1, accuracy: 1, ordering: 1, sid: 1,
159
+ digestAlgorithm: 1, certHashAlgorithm: 1, additionalSignedAttributes: 1, pss: 1, pem: 1,
160
+ };
161
+
144
162
  function _sign(messageImprint, tsa, opts) {
145
163
  opts = opts || {};
164
+ if (messageImprint && typeof messageImprint === "object" && !Buffer.isBuffer(messageImprint)) {
165
+ guard.identifier.assertKnownKeys(messageImprint, _IMPRINT_KEYS, _err, "tsp/bad-input", function (k) {
166
+ return "unknown messageImprint field " + JSON.stringify(k) + " (the imprint is { hashAlgorithm, hashedMessage })";
167
+ });
168
+ }
169
+ guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "tsp/bad-input", "pki.tsp.sign has an unknown option ");
146
170
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
171
+ if (tsa && typeof tsa === "object" && !Buffer.isBuffer(tsa)) {
172
+ guard.identifier.assertKnownKeys(tsa, _TSA_KEYS, _err, "tsp/bad-input", function (k) {
173
+ return "unknown TSA field " + JSON.stringify(k) +
174
+ " (the TSA is { cert, key }; a signing option belongs in the third argument)";
175
+ });
176
+ }
147
177
  var mi = messageImprint || {};
148
178
  if (!mi.hashAlgorithm || !NODE_DIGEST[mi.hashAlgorithm]) throw _err("tsp/unsupported-algorithm", "messageImprint.hashAlgorithm must be a supported hash name");
149
179
  if (!Buffer.isBuffer(mi.hashedMessage) && !(mi.hashedMessage instanceof Uint8Array)) throw _err("tsp/bad-input", "messageImprint.hashedMessage must be a Buffer");
@@ -186,7 +216,17 @@ function _sign(messageImprint, tsa, opts) {
186
216
  // Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] INTEGER (1..999) OPTIONAL,
187
217
  // micros [1] INTEGER (1..999) OPTIONAL } (RFC 3161 sec. 2.4.2). seconds is a non-negative
188
218
  // integer; millis and micros MUST each be in 1..999 -- enforced before encoding.
219
+ // Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] OPTIONAL, micros [1] OPTIONAL }
220
+ // (RFC 3161 sec. 2.4.2). All three are OPTIONAL and encoded only when present, so a misspelling is
221
+ // missed by nothing: `milis` emitted an EMPTY Accuracy that parses back as zero seconds, millis and
222
+ // micros, and the token then understates its own precision -- a claim about the timestamp that the
223
+ // caller believed they had made.
224
+ var _ACCURACY_KEYS = { seconds: 1, millis: 1, micros: 1 };
225
+
189
226
  function _accuracy(a) {
227
+ guard.identifier.assertKnownKeys(a, _ACCURACY_KEYS, _err, "tsp/bad-input", function (k) {
228
+ return "unknown accuracy field " + JSON.stringify(k) + "; Accuracy is { seconds?, millis?, micros? }";
229
+ });
190
230
  var f = [];
191
231
  if (a.seconds != null) {
192
232
  var s = Number(a.seconds);
@@ -263,10 +303,29 @@ function _assertImprint(mi) {
263
303
  * var sha256Digest = Buffer.from(await pki.webcrypto.subtle.digest("SHA-256", Buffer.from("hello")));
264
304
  * var req = pki.tsp.request({ hashAlgorithm: "sha256", hashedMessage: sha256Digest }, { nonce: 0x0102030405060708n, certReq: true });
265
305
  */
306
+ // The options pki.tsp.request encodes into the TimeStampReq. Every one is OPTIONAL in the
307
+ // structure, so a misspelled name is missed by nothing and the request goes out claiming less than
308
+ // was asked for. `nonce` is the one that costs most: misspelled, the request carries no nonce at
309
+ // all, and the caller matches a reply against a replay defense that was never asked for.
310
+ var _REQUEST_OPTS = { reqPolicy: 1, nonce: 1, certReq: 1, extensions: 1, pem: 1 };
311
+
266
312
  function request(messageImprint, opts) {
267
313
  opts = opts || {};
268
314
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.request options must be an object");
315
+ guard.identifier.assertKnownKeys(opts, _REQUEST_OPTS, _err, "tsp/bad-input", "pki.tsp.request has an unknown option ");
269
316
  var mi = messageImprint || {};
317
+ // The same table pki.tsp.sign holds its imprint to. The request's own options live in the second
318
+ // argument, so `nonce` written on the imprint reaches nothing and the request goes out without
319
+ // the replay defense the caller believes they asked for.
320
+ // The same table pki.tsp.sign holds its imprint to. The request's own options live in the second
321
+ // argument, so `nonce` written on the imprint reaches nothing and the request goes out without
322
+ // the replay defense the caller believes they asked for.
323
+ if (mi && typeof mi === "object" && !Buffer.isBuffer(mi)) {
324
+ guard.identifier.assertKnownKeys(mi, _IMPRINT_KEYS, _err, "tsp/bad-input", function (k) {
325
+ return "unknown messageImprint field " + JSON.stringify(k) +
326
+ " (the imprint is { hashAlgorithm, hashedMessage }; a request option belongs in the second argument)";
327
+ });
328
+ }
270
329
  _assertImprint(mi);
271
330
  var imprint = b.sequence([_hashAlgId(mi.hashAlgorithm), b.octetString(Buffer.from(mi.hashedMessage))]);
272
331
  // TimeStampReq: version(1), messageImprint, then OPTIONAL reqPolicy / nonce / certReq /
@@ -348,9 +407,15 @@ function parseRequest(input) { return schemaTsp.parseRequest(input); }
348
407
  * var resp = pki.tsp.response(token, {}); // granted
349
408
  * var rej = pki.tsp.response(null, { status: 2, failInfo: ["badAlg"] }); // rejection
350
409
  */
410
+ // The options pki.tsp.response encodes into the TimeStampResp. `status` carries the verdict and
411
+ // defaults to granted(0), so a misspelled name does not lose a detail -- it INVERTS the answer: a
412
+ // TSA refusing a request emits a granted response instead, over a token it did mean to withhold.
413
+ var _RESPONSE_OPTS = { status: 1, statusString: 1, failInfo: 1, pem: 1 };
414
+
351
415
  function response(token, opts) {
352
416
  opts = opts || {};
353
417
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.response options must be an object");
418
+ guard.identifier.assertKnownKeys(opts, _RESPONSE_OPTS, _err, "tsp/bad-input", "pki.tsp.response has an unknown option ");
354
419
  var status = opts.status == null ? 0 : Number(opts.status);
355
420
  if (!Number.isInteger(status) || status < 0 || status > 5) throw _err("tsp/bad-input", "PKIStatus must be an integer in 0..5 (RFC 3161 sec. 2.4.2)");
356
421
  var granted = status === 0 || status === 1;
package/lib/webauthn.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module pki.webauthn
6
6
  * @nav Validation
7
7
  * @title WebAuthn
8
+ * @fullname WebAuthn: verify registration attestation and assertions
8
9
  * @intro Trust evaluation of a W3C WebAuthn (Level 3) / passkey attestation: parse
9
10
  * the attestation object + authenticatorData, decode the COSE credential public
10
11
  * key, and verify each defined attestation-statement format (packed, tpm,