@blamejs/pki 0.5.17 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,47 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.5.18 — 2026-08-20
8
+
9
+ Every producing verb refuses an authoring field it does not read, so a misspelling is a refusal at the call instead of a signed artifact that quietly omits what was asked for.
10
+
11
+ ### Changed
12
+
13
+ - The issuer argument of `pki.x509.sign` and `pki.crl.sign` accepts only the fields the form it selects reads: `{ key, cert }` when an issuing certificate is supplied, `{ key, name, publicKey }` otherwise. A caller who hands over a key-store object carrying anything else must narrow it to one of the two. The check is there because a misspelled `cert` is not otherwise refused on this argument -- the issuing certificate is optional, so a dropped one falls through to self-signing, and a cross-certificate issued that way names its own subject as its issuer. Supplying both forms is refused for the same reason: the certificate branch wins unconditionally and reads neither `name` nor `publicKey`, so a caller who named a different issuer got one issued under the certificate's own distinguished name.
14
+ - The TSA argument of `pki.tsp.sign` accepts only the certificate and key it reads. A misspelling of either was always refused for what it left missing, but that is half the class: a name belonging on the third argument, written on the TSA instead, is read by nothing there. `ordering` placed on the TSA emitted a token the same size as one that never asked for it, with the flag unset, while the caller believed they had set it. A caller passing a key-store object whole must narrow it to `{ cert, key }`.
15
+
16
+ ### Fixed
17
+
18
+ - `pki.x509.sign` refuses an unrecognized field on the certificate spec, the issuer and the options. `extension` in place of `extensions` signed a certificate carrying NONE of them: a caller asking for `basicConstraints: { cA: true }` and `keyUsage` got neither, and nothing in the returned certificate said so. An `issuer` nested inside the spec is refused by name as well -- the issuer is the second argument, and nesting it produced a self-signed certificate with no error, which reads as a broken signer rather than a mis-shaped call.
19
+ - `pki.crl.sign` refuses an unrecognized field on the CRL spec, the issuer and the options. The revoked list is `revoked`; the parser reports the same list as `revokedCertificates`, so a caller round-tripping a parsed CRL back into signing reached for that name and got a correctly signed, structurally valid CRL asserting that nothing is revoked. The refusal names the field the producer reads.
20
+ - `pki.pkcs12.build` refuses an unrecognized field on the store spec and the options, and no longer accepts an option belonging to `open` or `verifyMac`. A store that silently omits a certificate or a key looks well formed and opens cleanly; the omission surfaces wherever it is later imported.
21
+ - `pki.pkcs12.build` refuses an unrecognized field on a `safeContents` entry, on a bag, and on the PBE descriptor either of them carries. The safeContents entry is where the privacy directive lives, so a misspelling there was the costliest in the format: `encrypt` misspelled is neither present nor falsy, so it passed the guard that rejects a present-but-falsy directive, no privacy was selected, and that safe went out as plaintext `id-data` -- an unshrouded private-key bag in the clear, inside a PFX whose MAC still verified and which opened without complaint. A misspelled PBE parameter, on either the safe or the bag, silently reverted to the built-in default, so a caller who asked for a stronger KDF or a different cipher got neither. `opts.mac` is checked the same way, and every one of its fields has a default too: a misspelled `iterations` produced a store MAC'd at the shipped iteration count while the caller believed they had raised it, and an array passed as `opts.mac` carried no field any table lists, so it reached the builder as an empty descriptor and produced the default MAC.
22
+ - `pki.pkcs12.build` refuses a spec that mixes its two forms. `safeContents` and the `{ key, cert, ca }` shorthand are alternatives, and the builder returns `safeContents` the moment it is present without ever reading the shorthand fields -- so `{ safeContents: [...], key }` produced a store with the key silently absent.
23
+ - A `safeContents` entry is checked against the privacy branch it selects. `contentEncryptionAlgorithm` is the public-key branch's content cipher and is read by nothing on a password safe, whose cipher comes from `encrypt.cipher`, so an explicit `aes-128-cbc` request on a password safe was accepted and the default used anyway.
24
+ - A certificate recipient of `pki.cms.encrypt` is checked against the fields ITS CERTIFICATE'S KEY ALGORITHM reads. The arms differ: RSA reads `oaepHash` and never `ukm`; ECDH, X25519, X448 and ML-KEM read `ukm` and never `oaepHash`. One table across all three accepted `oaepHash` on an elliptic-curve recipient, where nothing reads it, and wrapped the content key under the arm's own defaults.
25
+ - A bag is checked against the fields ITS OWN TYPE reads, rather than against one table covering every type. `encrypt` is real on a `shroudedKey` bag and read by nothing on a plaintext `key` bag, so a union accepted the combination and emitted the private key as an unencrypted key bag while the caller had supplied an explicit encryption directive and the surrounding MAC gave integrity only.
26
+ - `pki.crl.sign` refuses an unrecognized field on a revoked-certificate entry. Every optional field on an entry is encoded only when present, so `reson` in place of `reason` listed the certificate as revoked carrying no reason code at all -- in a CRL that verifies, where a relying party cannot distinguish it from a revocation published without a reason.
27
+ - `pki.cms.encrypt` refuses an unrecognized field on a recipient descriptor, against a table per recipient type. The fields beyond the selector all have defaults, so `oaepHsh` encrypted the content key under the default SHA-256 OAEP, and a misspelled `iterations`, `salt` or `prf` on a password recipient derived the key at the default work factor. The single descriptor an `EncryptedData` takes is checked per arm as well: its `cek` branch derives nothing, so a work factor handed to it was discarded exactly as a misspelling would be.
28
+ - `pki.tsp.sign` refuses an unrecognized field on the `accuracy` descriptor. Its three fields are all optional, so `milis` emitted an empty Accuracy that reads back as zero seconds, millis and micros, and the token understated the precision the caller asked it to claim.
29
+ - `pki.cms.encrypt` and `pki.cms.authenticate` each refuse an unrecognized option, against separate tables. The two verbs have different option sets, so a single shared table would have accepted either verb's keys and defeated the check.
30
+ - `pki.tsp.sign` refuses an unrecognized field on the message imprint and the options. A one-letter slip -- `odering` for `ordering` -- emitted a token with the ordering flag unset, which is a claim about the timestamp the caller believed they had made.
31
+ - `pki.pkcs12.open` and `pki.pkcs12.verifyMac` each refuse an unrecognized option, against their own tables. `maxIterations` caps the work a hostile store can demand of the key derivation, so a misspelling restored the built-in ceiling and the tighter bound the caller set was never applied.
32
+ - `pki.tsp.request` refuses an unrecognized field on the message imprint as well as on the options -- the same table `pki.tsp.sign` holds its imprint to. The request's own options are the second argument, so `nonce` written on the imprint reached nothing and the request went out with no nonce.
33
+ - `pki.tsp.request` refuses an unrecognized option. Every field it encodes is optional in the request structure, so a misspelled one was dropped without complaint: `nonce` misspelled put a request on the wire carrying no nonce at all, and the client then matched the reply against a replay defense it had never actually asked for.
34
+ - `pki.tsp.response` refuses an unrecognized option. `status` carries the verdict and defaults to granted, so a misspelling did not lose a detail -- it inverted the answer, and a TSA declining a request emitted a granted response over a token it meant to withhold.
35
+ - The options of `pki.pkcs12.build` are checked against the spec form in force. `recipientCerts` selects public-key privacy only where the builder assembles the safes itself, so under the `safeContents` form it selected nothing: a plaintext key bag went into an `id-data` safe with the private key in the clear, while the caller had asked for recipient-enveloped privacy. The full form carries privacy per entry, as `safeContents[i].recipients`.
36
+ - `pki.cms.encrypt` and `pki.cms.authenticate` refuse a per-recipient default that no recipient of the message reads. `oaepHash` is read only by an RSA certificate recipient, `ukm` only by a key-agreement or KEM certificate recipient, and `keyIdentifier` only by a certificate recipient -- so a password-only message given an `oaepHash` described a wrapping it never performed. A recipient that supplies its own value reads that one and never the default, so a default every recipient overrides is refused too; one recipient that does read it is enough. An `EncryptedData` carries no recipients at all and so reads none of the three.
37
+ - `pki.cmp.build` refuses a signature parameter under MAC protection. `pss` and `digestAlgorithm` are read only by the `{ key, cert }` form; supplied alongside `mac`, they emitted a message MAC'd under the default PRF, byte for byte identical to the call that never named them.
38
+ - `pki.jose.sign` refuses `opts.jwk` when the protected header already embeds one. The embedded key wins, so the option is read in kid mode only, and a caller who named a different public JWK beside an embedded one had it discarded.
39
+ - `pki.jose.sign` serializes the protected header once and every check reads back those exact bytes, through the same bounded reader a received JWS goes through. The header is a caller-owned object, so a `toJSON` or an accessor could answer the public-only check and the serializer differently: a JWK whose own members carried no private half but which serialized `d` passed the check and shipped the private key in the header. Serializing runs that code, so the option check now runs before it -- a `toJSON` reaching into the options bag could otherwise delete a misspelled option before the door looked at it. A header that cannot be serialized is refused rather than reaching the signer.
40
+ - `pki.pkcs12.build` decides which of the two spec forms it has once, and the field check and the builder both act on that decision. Held separately the two tests disagreed on a `safeContents` that is present but not a list, so the refusal named a field that was correct instead of the one that was wrong.
41
+ - `pki.cms.encrypt` refuses `authAttrs` under a CBC content algorithm. Authenticated attributes are carried by an `AuthEnvelopedData`, which an AEAD algorithm produces; a CBC algorithm selects `EnvelopedData`, which has no field for them, so the attributes a caller asked to authenticate were dropped and the message went out without them.
42
+ - `pki.cms.authenticate` refuses `digestAlgorithm` and `authAttrs` when `authenticatedAttributes` is `false`. That branch MACs the content octets directly and builds no attribute set, so the digest algorithm that names its hash, and any attribute added to it, are read by nothing.
43
+ - `pem` on an `EncryptedData` descriptor is refused; it is an option, the third argument, as on every other verb here. A second spelling on the descriptor was read by the password arm alone, so a `{ cek, pem: true }` descriptor was accepted and returned DER.
44
+ - `pki.crl.sign` refuses `spec.issuer` when the issuer argument already names one. The distinguished name has three possible sources and `issuer.cert` and `issuer.name` both win outright, so a spec naming one issuer beside a certificate for another produced a CRL, which verifies, issued under the certificate's name. `spec.issuer` is still the name when the issuer argument supplies none.
45
+ - `opts.mac.keyLength` on a `pki.pkcs12.build` store is refused under the classic `hmac` algorithm, which is the default. The Appendix B derivation produces a key at the hash's own output length and reads no key length, so an explicit one was discarded and the store carried the same MAC as a call that never named it. `keyLength` remains a PBMAC1 parameter (RFC 9579).
46
+ - A `pki.pkcs12.build` public-key integrity signer is checked against the form it selects, `{ cert, key }` or `{ spki, keyIdentifier, key }`, plus the signature parameters. Every field beyond the identity has a default, so a misspelled `pss` or `digestAlgorithm` signed the store under PKCS#1 with SHA-256 while the caller had asked for something else, and a store records only what was used. `opts.integrity.signer` and `opts.integrity.signers` are the same setting spelled two ways and the list won outright, so supplying both signed the store under the list alone; exactly one is now required.
47
+
7
48
  ## v0.5.17 — 2026-08-20
8
49
 
9
50
  A failed integrity check destroys the plaintext it recovered, AuthEnvelopedData validates the attributes it is asked to authenticate, and every refusal names the rule it is actually applying.
package/lib/acme.js CHANGED
@@ -694,7 +694,9 @@ var _promised = guard.async.deferred;
694
694
  function _signOuter(o, payloadObj) {
695
695
  if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
696
696
  if (!o.key) throw E("acme/bad-input", "a signing key (opts.key) is required");
697
- return jose.sign({ protected: _outerHeader(o), payload: _payloadBuf(payloadObj), key: o.key, jwk: o.jwk, profile: "acme-outer" });
697
+ // The header carries the account jwk in jwk mode and the kid in kid mode, so the public JWK is
698
+ // already there whenever there is one to name; jose.sign reads its own opts.jwk in kid mode only.
699
+ return jose.sign({ protected: _outerHeader(o), payload: _payloadBuf(payloadObj), key: o.key, profile: "acme-outer" });
698
700
  }
699
701
 
700
702
  /**
@@ -778,7 +780,7 @@ function newAccount(o) {
778
780
  if (!_isObject(o.externalAccountBinding)) throw E("acme/bad-input", "externalAccountBinding must be an EAB inner JWS object");
779
781
  payload.externalAccountBinding = o.externalAccountBinding;
780
782
  }
781
- return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, jwk: o.jwk }, payload: _payloadBuf(payload), key: o.key, jwk: o.jwk, profile: "acme-outer" });
783
+ return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, jwk: o.jwk }, payload: _payloadBuf(payload), key: o.key, profile: "acme-outer" });
782
784
  });
783
785
  }
784
786
 
@@ -1123,7 +1125,7 @@ function revokeCert(o) {
1123
1125
  }
1124
1126
  var header = { alg: o.alg, nonce: o.nonce, url: o.url };
1125
1127
  if (hasKid) header.kid = o.kid; else header.jwk = o.jwk;
1126
- return jose.sign({ protected: header, payload: _payloadBuf(payload), key: o.key, jwk: o.jwk, profile: "acme-outer" });
1128
+ return jose.sign({ protected: header, payload: _payloadBuf(payload), key: o.key, profile: "acme-outer" });
1127
1129
  });
1128
1130
  }
1129
1131
 
@@ -1164,7 +1166,7 @@ async function keyChange(o) {
1164
1166
  var inner = await jose.sign({
1165
1167
  protected: { alg: o.newAlg, url: o.url, jwk: o.newJwk },
1166
1168
  payload: _payloadBuf({ account: o.account, oldKey: o.oldKey }),
1167
- key: o.newKey, jwk: o.newJwk, profile: "keychange-inner",
1169
+ key: o.newKey, profile: "keychange-inner",
1168
1170
  });
1169
1171
  return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload: _payloadBuf(inner), key: o.key, profile: "acme-outer" });
1170
1172
  }
package/lib/cmp-build.js CHANGED
@@ -75,7 +75,13 @@ var BODY_TAG = {
75
75
  };
76
76
  var CRMF_BODY = { ir: 1, cr: 1, kur: 1 }; // arms whose content is a CertReqMessages via pki.crmf.build
77
77
  var CERT_REP_ARM = { ip: 1, cp: 1, kup: 1, ccp: 1 }; // arms carrying a CertRepMessage
78
+ // Protection has two mutually exclusive forms, and the signature parameters are read only by the
79
+ // signature one: under PBMAC1 a requested pss or digestAlgorithm selects nothing and the message
80
+ // goes out MAC'd under the default PRF, byte-identical to the call that never named them. The union
81
+ // stays in force where the form is ambiguous, so _resolveProtection keeps its both/neither verdicts.
78
82
  var KNOWN_OPTS_KEYS = { key: 1, cert: 1, mac: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
83
+ var KNOWN_OPTS_SIG_KEYS = { key: 1, cert: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
84
+ var KNOWN_OPTS_MAC_KEYS = { mac: 1, extraCerts: 1, pem: 1 };
79
85
  var KNOWN_MESSAGE_KEYS = { header: 1, body: 1 };
80
86
  var KNOWN_MAC_KEYS = { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength: 1, algorithm: 1 };
81
87
 
@@ -565,7 +571,15 @@ function _build(message, opts) {
565
571
  guard.identifier.assertKnownKeys(message, KNOWN_MESSAGE_KEYS, _err, "cmp/bad-input", function (k) {
566
572
  return "unknown message field " + JSON.stringify(k) + " (a message carries only header + body)";
567
573
  });
568
- guard.identifier.assertKnownKeys(opts, KNOWN_OPTS_KEYS, _err, "cmp/bad-input", "unknown opts field ");
574
+ var sigForm = opts.key != null || opts.cert != null, macForm = opts.mac != null;
575
+ guard.identifier.assertKnownKeys(opts,
576
+ sigForm === macForm ? KNOWN_OPTS_KEYS : sigForm ? KNOWN_OPTS_SIG_KEYS : KNOWN_OPTS_MAC_KEYS,
577
+ _err, "cmp/bad-input", function (k) {
578
+ return "unknown opts field " + JSON.stringify(k) +
579
+ (macForm && (k === "pss" || k === "digestAlgorithm")
580
+ ? " for MAC protection; the signature parameters are read only by the { key, cert } form"
581
+ : "");
582
+ });
569
583
  if (message.header == null) throw _err("cmp/bad-input", "message.header is required");
570
584
  if (message.body == null) throw _err("cmp/bad-input", "message.body is required");
571
585
 
@@ -364,22 +364,91 @@ var PRF_HASH = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA38
364
364
  function _prfHash(prf) { if (!PRF_HASH[prf]) throw _err("cms/bad-input", "unsupported pwri prf " + JSON.stringify(prf)); return PRF_HASH[prf]; }
365
365
 
366
366
  // ---- recipient dispatch ----------------------------------------------------
367
- async function _buildRecipient(cek, desc, opts) {
367
+ // The fields a recipient descriptor carries, PER ARM. Each arm reads a disjoint set, and every
368
+ // field beyond the selector is optional with a default, so an unrecognized name is missed by
369
+ // nothing and the recipient is built with the shipped parameters instead: `oaepHsh` for `oaepHash`
370
+ // encrypts the content key under the default SHA-256 OAEP, and a misspelled `iterations`, `salt`
371
+ // or `prf` on a password recipient derives the key at the default work factor. A union table would
372
+ // admit another arm's fields, which the chosen arm never reads.
373
+ var _PWRI_KEYS = { password: 1, iterations: 1, prf: 1, salt: 1 };
374
+ var _KEKRI_KEYS = { kek: 1, kekId: 1 };
375
+ // A certificate recipient dispatches on the certificate's KEY ALGORITHM, and the arms read
376
+ // different fields: ktri (RSA) reads oaepHash and never ukm; kari (ECDH / X25519 / X448) and kemri
377
+ // (ML-KEM) read ukm and never oaepHash. One table across all three accepted `oaepHash` on an EC
378
+ // certificate, where nothing reads it, so the content key was wrapped under the arm's own defaults
379
+ // while the caller had named a hash. Checked after the key algorithm resolves, since that is what
380
+ // selects the arm.
381
+ var _KTRI_RECIPIENT_KEYS = { cert: 1, oaepHash: 1, keyIdentifier: 1 };
382
+ var _AGREE_RECIPIENT_KEYS = { cert: 1, keyIdentifier: 1, ukm: 1 };
383
+
384
+ // The three top-level options that are per-recipient DEFAULTS rather than message-wide settings.
385
+ // Which of them any given message actually consumes depends on the recipient arms it selects, so
386
+ // they are checked against the arms after those resolve, not against a fixed list.
387
+ var _RECIPIENT_DEFAULTS = ["oaepHash", "keyIdentifier", "ukm"];
388
+
389
+ function _assertRecipientKeys(desc, known, arm) {
390
+ guard.identifier.assertKnownKeys(desc, known, _err, "cms/bad-input", function (k) {
391
+ return "unknown field " + JSON.stringify(k) + " on a " + arm +
392
+ " recipient; that field is not read for this recipient type";
393
+ });
394
+ }
395
+
396
+ // A default no selected recipient reads is discarded exactly as a misspelling would be: `oaepHash`
397
+ // on a message whose only recipient is a password recipient names a wrapping the message does not
398
+ // perform. Run after every recipient's arm has resolved and BEFORE anything is emitted.
399
+ function _assertDefaultsConsumed(opts, consumed) {
400
+ for (var i = 0; i < _RECIPIENT_DEFAULTS.length; i++) {
401
+ var name = _RECIPIENT_DEFAULTS[i];
402
+ if (opts[name] != null && !consumed[name]) {
403
+ throw _err("cms/bad-input", "opts." + name + " is a per-recipient default and no recipient of this message reads it" +
404
+ (name === "oaepHash" ? " (only an RSA certificate recipient does)"
405
+ : name === "ukm" ? " (only a key-agreement or KEM certificate recipient does)"
406
+ : " (only a certificate recipient does)"));
407
+ }
408
+ }
409
+ }
410
+
411
+ async function _buildRecipient(cek, desc, opts, consumed) {
368
412
  if (desc == null || typeof desc !== "object") throw _err("cms/bad-input", "each recipient must be a descriptor object");
369
- if (desc.password != null) return _buildPwri(cek, desc);
370
- if (desc.kek != null) return _buildKekri(cek, desc);
413
+ // Checked per arm, on the same selector the dispatch below uses, so the table that runs is the
414
+ // one belonging to the arm that will build. `consumed` records which top-level defaults the
415
+ // selected arms read, so a default no recipient consumes can be refused once they all resolve.
416
+ if (desc.password != null) { _assertRecipientKeys(desc, _PWRI_KEYS, "password"); return _buildPwri(cek, desc); }
417
+ if (desc.kek != null) { _assertRecipientKeys(desc, _KEKRI_KEYS, "KEK"); return _buildKekri(cek, desc); }
371
418
  if (desc.cert != null) {
372
419
  var cert = x509.parse(_normCertDer(desc.cert, "a recipient certificate"));
373
420
  var keyOid = cert.subjectPublicKeyInfo.algorithm.oid;
374
- if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) return _buildKtri(cek, cert, mergeOpts(opts, desc));
375
- if (keyOid === O("ecPublicKey") || MONT_KA[keyOid]) return _buildKari(cek, cert, mergeOpts(opts, desc));
376
- if (KEM_WC[keyOid]) return _buildKemri(cek, cert, mergeOpts(opts, desc));
421
+ if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
422
+ _assertRecipientKeys(desc, _KTRI_RECIPIENT_KEYS, "RSA certificate");
423
+ return _buildKtri(cek, cert, mergeOpts(opts, desc, _KTRI_RECIPIENT_KEYS, consumed));
424
+ }
425
+ if (keyOid === O("ecPublicKey") || MONT_KA[keyOid]) {
426
+ _assertRecipientKeys(desc, _AGREE_RECIPIENT_KEYS, "key-agreement certificate");
427
+ return _buildKari(cek, cert, mergeOpts(opts, desc, _AGREE_RECIPIENT_KEYS, consumed));
428
+ }
429
+ if (KEM_WC[keyOid]) {
430
+ _assertRecipientKeys(desc, _AGREE_RECIPIENT_KEYS, "KEM certificate");
431
+ return _buildKemri(cek, cert, mergeOpts(opts, desc, _AGREE_RECIPIENT_KEYS, consumed));
432
+ }
377
433
  throw _err("cms/unsupported-algorithm", "unsupported recipient certificate key algorithm " + keyOid);
378
434
  }
379
435
  throw _err("cms/bad-input", "a recipient needs { cert }, { password }, or { kek, kekId }");
380
436
  }
381
- function mergeOpts(opts, desc) {
382
- return { oaepHash: desc.oaepHash || opts.oaepHash, keyIdentifier: desc.keyIdentifier || opts.keyIdentifier, ukm: desc.ukm != null ? desc.ukm : opts.ukm };
437
+ // Each default is taken from the descriptor when it supplies one and from the top-level options
438
+ // otherwise. That one choice is also the record of which top-level defaults the recipient CONSUMED:
439
+ // an arm that does not read the name never consumes it, and neither does one whose descriptor
440
+ // overrode it, so recording it from the arm's table alone would accept a value nothing read.
441
+ function _pickDefault(desc, opts, name, known, consumed, nullish) {
442
+ var supplied = desc[name], fromDesc = nullish ? supplied != null : !!supplied;
443
+ if (!fromDesc && known[name] && consumed && opts[name] != null) consumed[name] = 1;
444
+ return fromDesc ? supplied : opts[name];
445
+ }
446
+ function mergeOpts(opts, desc, known, consumed) {
447
+ return {
448
+ oaepHash: _pickDefault(desc, opts, "oaepHash", known, consumed, false),
449
+ keyIdentifier: _pickDefault(desc, opts, "keyIdentifier", known, consumed, false),
450
+ ukm: _pickDefault(desc, opts, "ukm", known, consumed, true),
451
+ };
383
452
  }
384
453
  // RecipientInfo CHOICE: ktri untagged; kari [1], kekri [2], pwri [3], ori [4], all IMPLICIT,
385
454
  // so the arm's SEQUENCE tag is replaced by the context tag.
@@ -397,12 +466,46 @@ function _envelopedVersion(recips, hasUnprotected) {
397
466
  return forcesTwo ? 2 : 0;
398
467
  }
399
468
 
469
+ // The two exported verbs have DISJOINT option sets, so they get separate tables: one shared table
470
+ // would admit every other verb's keys, which is the hole rather than the door. Each is the union of
471
+ // what the verb reads THROUGH ITS DELEGATIONS -- encrypt never names oaepHash, keyIdentifier or ukm
472
+ // itself, but hands opts to the per-recipient builders that do, and to _emit for pem.
473
+ // Authenticated attributes live in an AuthEnvelopedData, which only an AEAD content algorithm
474
+ // produces: a CBC algorithm selects EnvelopedData, which has no field for them and is not handed
475
+ // opts at all. Requested there, they named a protection the message does not carry.
476
+ var _ENCRYPT_OPTS_AEAD = {
477
+ contentEncryptionAlgorithm: 1, contentType: 1, authAttrs: 1, oaepHash: 1, keyIdentifier: 1,
478
+ ukm: 1, pem: 1,
479
+ };
480
+ var _ENCRYPT_OPTS_CBC = {
481
+ contentEncryptionAlgorithm: 1, contentType: 1, oaepHash: 1, keyIdentifier: 1, ukm: 1, pem: 1,
482
+ };
483
+ // authenticatedAttributes: false MACs the content octets directly, and the attribute set is what
484
+ // carries the digest: neither the digest algorithm nor any extra attribute is read on that branch.
485
+ var _AUTHENTICATE_OPTS_ATTRS = {
486
+ macAlgorithm: 1, digestAlgorithm: 1, authenticatedAttributes: 1, authAttrs: 1, contentType: 1,
487
+ keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1,
488
+ };
489
+ var _AUTHENTICATE_OPTS_BARE = {
490
+ macAlgorithm: 1, authenticatedAttributes: 1, contentType: 1,
491
+ keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1,
492
+ };
493
+
400
494
  async function encrypt(content, recipients, opts) {
401
495
  opts = opts || {};
402
- var contentBytes = guard.bytes.view(content, CmsError, "cms/bad-input", "content");
496
+ // The content algorithm selects the structure, so it is resolved before the door and the table
497
+ // that runs is the one belonging to the structure this call will emit.
403
498
  var algName = opts.contentEncryptionAlgorithm || "aes-256-gcm";
404
499
  var ca = CONTENT_ALGS[algName];
405
500
  if (!ca) throw _err("cms/bad-input", "unsupported contentEncryptionAlgorithm " + JSON.stringify(algName));
501
+ guard.identifier.assertKnownKeys(opts, ca.aead ? _ENCRYPT_OPTS_AEAD : _ENCRYPT_OPTS_CBC, _err, "cms/bad-input",
502
+ function (k) {
503
+ return "pki.cms.encrypt has an unknown option " + JSON.stringify(k) +
504
+ (!ca.aead && k === "authAttrs"
505
+ ? " for a CBC content algorithm; authenticated attributes are carried only by an AuthEnvelopedData, which an AEAD algorithm produces"
506
+ : "");
507
+ });
508
+ var contentBytes = guard.bytes.view(content, CmsError, "cms/bad-input", "content");
406
509
  var contentType = opts.contentType || "data";
407
510
  var cek = nodeCrypto.randomBytes(ca.keyBits / 8);
408
511
 
@@ -415,7 +518,9 @@ async function encrypt(content, recipients, opts) {
415
518
 
416
519
  if (!recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 6.1)");
417
520
  var recips = [];
418
- for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(cek, recipients[i], opts));
521
+ var consumed = {};
522
+ for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(cek, recipients[i], opts, consumed));
523
+ _assertDefaultsConsumed(opts, consumed);
419
524
  var riNodes = recips.map(_taggedRecipient);
420
525
 
421
526
  if (ca.aead) return _emit(_authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, recips), "authEnvelopedData", opts);
@@ -500,8 +605,31 @@ function _authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, r
500
605
  return b.sequence(kids);
501
606
  }
502
607
 
608
+ // The single descriptor an EncryptedData takes, PER ARM. `cek` and `password` select the arm and
609
+ // the arms read disjoint sets, so a union would admit the other arm's fields: `{ cek, iterations }`
610
+ // supplies a work factor to a branch that derives nothing, and the caller's parameter is discarded
611
+ // exactly as a misspelling would be. `pem` is read after both arms, so it belongs to each.
612
+ // pem selects the output encoding of the whole call and is an OPTION, the third argument, on every
613
+ // other verb here. A second spelling on the descriptor was read by the password arm alone, so the
614
+ // cek arm accepted it and returned DER; there is now one place to ask for PEM.
615
+ var _ENCRYPTED_DATA_CEK_KEYS = { cek: 1 };
616
+ var _ENCRYPTED_DATA_PW_KEYS = { password: 1, iterations: 1, salt: 1, prf: 1 };
617
+
503
618
  function _encryptedData(contentBytes, desc, ca, contentType, opts, cek) {
504
619
  if (ca.aead) throw _err("cms/bad-input", "EncryptedData supports only CBC content encryption");
620
+ // An EncryptedData carries no RecipientInfos at all, so nothing here reads a per-recipient
621
+ // default. This branch returns before the recipient loop, so it answers for them itself.
622
+ _assertDefaultsConsumed(opts, {});
623
+ // Checked per arm, on the same selector the dispatch below uses, so the table that runs belongs
624
+ // to the branch that will encrypt.
625
+ if (desc != null && typeof desc === "object") {
626
+ var edKeys = desc.cek != null ? _ENCRYPTED_DATA_CEK_KEYS : _ENCRYPTED_DATA_PW_KEYS;
627
+ var edArm = desc.cek != null ? "cek" : "password";
628
+ guard.identifier.assertKnownKeys(desc, edKeys, _err, "cms/bad-input", function (k) {
629
+ return "unknown field " + JSON.stringify(k) + " on a " + edArm +
630
+ " EncryptedData descriptor; that field is not read for this arm";
631
+ });
632
+ }
505
633
  var iv = nodeCrypto.randomBytes(16);
506
634
  var contentAlgNode, encKey;
507
635
  if (desc && desc.cek != null) {
@@ -537,7 +665,7 @@ function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
537
665
  var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
538
666
  var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
539
667
  var inner = b.sequence([b.integer(0n), eci]);
540
- return _emit(inner, "encryptedData", { pem: opts && opts.pem != null ? opts.pem : desc.pem });
668
+ return _emit(inner, "encryptedData", opts);
541
669
  } finally {
542
670
  guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key");
543
671
  }
@@ -581,13 +709,22 @@ var SUPPORTED_DIGEST = { sha256: 1, sha384: 1, sha512: 1 };
581
709
 
582
710
  async function authenticate(content, recipients, opts) {
583
711
  opts = opts || {};
712
+ // The attribute branch is selected once, and the door and the MAC below both act on that one
713
+ // decision, so the table that runs belongs to the branch that will compute the MAC.
714
+ var withAttrs = opts.authenticatedAttributes !== false;
715
+ guard.identifier.assertKnownKeys(opts, withAttrs ? _AUTHENTICATE_OPTS_ATTRS : _AUTHENTICATE_OPTS_BARE,
716
+ _err, "cms/bad-input", function (k) {
717
+ return "pki.cms.authenticate has an unknown option " + JSON.stringify(k) +
718
+ (!withAttrs && (k === "digestAlgorithm" || k === "authAttrs")
719
+ ? " with authenticatedAttributes: false; the MAC then covers the content octets directly and no attribute set is built"
720
+ : "");
721
+ });
584
722
  var contentBytes = guard.bytes.view(content, CmsError, "cms/bad-input", "content");
585
723
  var macName = opts.macAlgorithm || "hmac-sha256";
586
724
  var mac = MAC_ALGS[macName];
587
725
  if (!mac) throw _err("cms/bad-input", "unsupported macAlgorithm " + JSON.stringify(macName) + " (hmac-sha256/384/512)");
588
726
  if (!Array.isArray(recipients) || !recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 9.1)");
589
727
  var contentType = opts.contentType || "data";
590
- var withAttrs = opts.authenticatedAttributes !== false;
591
728
  // RFC 5652 sec. 9.1: authAttrs MUST be present when the eContentType is not id-data.
592
729
  if (contentType !== "data" && !withAttrs) throw _err("cms/bad-input", "AuthenticatedData with a non-data contentType requires authenticated attributes (RFC 5652 sec. 9.1)");
593
730
 
@@ -596,12 +733,16 @@ async function authenticate(content, recipients, opts) {
596
733
  var macKey = nodeCrypto.randomBytes(MAC_KEY_OCTETS);
597
734
  try {
598
735
  var recips = [];
599
- for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts));
736
+ var consumedAuth = {};
737
+ for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts, consumedAuth));
738
+ _assertDefaultsConsumed(opts, consumedAuth);
600
739
  var riNodes = recips.map(_taggedRecipient);
601
740
 
602
- var digestName = opts.digestAlgorithm || mac.node;
603
741
  var digestAlgTagged = null, authAttrsDer = null, preimage;
604
742
  if (withAttrs) {
743
+ // Inside the branch that uses it: the attribute set is what carries the digest, so on the
744
+ // other branch there is no digest to name and opts.digestAlgorithm is refused at the door.
745
+ var digestName = opts.digestAlgorithm || mac.node;
605
746
  if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
606
747
  // sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
607
748
  // SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
package/lib/cms-sign.js CHANGED
@@ -43,6 +43,14 @@ var KNOWN_SIGN_OPTS = {
43
43
  signedAttributes: 1, signingTime: 1, additionalSignedAttributes: 1, unsignedAttributes: 1,
44
44
  sid: 1, eContentType: 1, detached: 1, certificates: 1, pem: 1,
45
45
  };
46
+ // The SIGNER descriptor has two forms, and every field it reads is named here so a caller that
47
+ // authors one can be held to the form it selects: a certificate signer, and the key-only signer
48
+ // RFC 5272 sec. 3.2 requires for a Full PKI Request, whose identity comes from `spki` +
49
+ // `keyIdentifier` instead. The signature parameters are read by both. Exported for the verbs that
50
+ // take signer descriptors as AUTHORING input and gate them at their own door -- pki.cms.sign takes
51
+ // them straight from its caller, where a bag carrying extra handles is an ordinary shape.
52
+ var KNOWN_SIGNER_CERT_KEYS = { cert: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 };
53
+ var KNOWN_SIGNER_KEY_ONLY_KEYS = { spki: 1, keyIdentifier: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 };
46
54
  // Countersign has its own set: it takes signerIndex and countersignatureOf to SELECT which
47
55
  // signature it attaches to, and has no content of its own, so eContentType, detached and
48
56
  // unsignedAttributes are not among the options it reads.
@@ -658,4 +666,10 @@ function _countersign(cmsInput, signers, opts) {
658
666
  // belt-and-suspenders against a future caller reordering the two.
659
667
  // * the `crls [1]` preservation branches in `countersign`. pki.cms.sign never emits a crls field,
660
668
  // so a store this producer countersigns never carries one to preserve.
661
- module.exports = { sign: sign, countersign: countersign };
669
+ module.exports = {
670
+ sign: sign, countersign: countersign,
671
+ // @internal -- the signer-descriptor field tables, for a verb that AUTHORS signer descriptors
672
+ // from its own caller's options and gates them at its own door.
673
+ KNOWN_SIGNER_CERT_KEYS: KNOWN_SIGNER_CERT_KEYS,
674
+ KNOWN_SIGNER_KEY_ONLY_KEYS: KNOWN_SIGNER_KEY_ONLY_KEYS,
675
+ };
package/lib/crl-sign.js CHANGED
@@ -72,8 +72,38 @@ var REASON_BY_NAME = {};
72
72
  Object.keys(CRL_REASON).forEach(function (v) { REASON_BY_NAME[CRL_REASON[v]] = Number(v); });
73
73
 
74
74
  var KNOWN_CRL_EXT_KEYS = { authorityKeyIdentifier: 1, issuingDistributionPoint: 1, deltaCRLIndicator: 1, freshestCRL: 1, authorityInfoAccess: 1 };
75
+ // The three caller-owned objects pki.crl.sign reads. `crlNumber` and `extensions` are read by
76
+ // _buildCrlExtensions, which _sign hands the whole spec to, so they belong here even though _sign
77
+ // never names them -- the table is derived from what the call ACTUALLY reads, including through
78
+ // its delegations, not from the doc block.
79
+ var KNOWN_SPEC_KEYS = {
80
+ issuer: 1, thisUpdate: 1, nextUpdate: 1, revoked: 1, crlNumber: 1, extensions: 1,
81
+ };
82
+ // spec.issuer is the third place an issuer distinguished name can come from, and it is read only
83
+ // when neither issuer.cert nor issuer.name supplies one: both of those win outright. A spec naming
84
+ // one issuer beside a certificate for another was signed under the certificate's.
85
+ var KNOWN_SPEC_KEYS_ISSUER_NAMED = {
86
+ thisUpdate: 1, nextUpdate: 1, revoked: 1, crlNumber: 1, extensions: 1,
87
+ };
88
+ // Two FORMS, exactly as x509-sign has: when `issuer.cert` is present the builder takes the DN and
89
+ // the public key from it and never reads `name` or `publicKey`, so a union signed under the
90
+ // certificate's identity while discarding the one the caller wrote out.
91
+ var KNOWN_ISSUER_CERT_KEYS = { key: 1, cert: 1 };
92
+ var KNOWN_ISSUER_EXPLICIT_KEYS = { key: 1, name: 1, publicKey: 1 };
93
+ var KNOWN_SIGN_OPTS = { digestAlgorithm: 1, pem: 1, pss: 1 };
94
+
75
95
  var KNOWN_IDP_KEYS = { fullName: 1, onlyContainsUserCerts: 1, onlyContainsCACerts: 1, onlyContainsAttributeCerts: 1, indirectCRL: 1 };
76
96
 
97
+ // The fields ONE revoked-certificate entry carries. Every optional one is encoded only when
98
+ // present, so a misspelling is missed by nothing and the entry is signed without it: `reson` for
99
+ // `reason` lists the certificate as revoked with no reasonCode at all, and a relying party reading
100
+ // the CRL cannot tell a caller who meant keyCompromise from one who gave no reason. The entry is
101
+ // where this bites hardest, because the CRL as a whole still verifies. `certificateIssuer` is
102
+ // listed because the builder DOES read it -- to refuse it with its own message.
103
+ var KNOWN_REVOKED_KEYS = {
104
+ serialNumber: 1, revocationDate: 1, reason: 1, invalidityDate: 1, certificateIssuer: 1, extensions: 1,
105
+ };
106
+
77
107
  // RFC 5280 sec. 5.2 -- the profile-fixed criticality of each recognized CRL extension. cRLNumber (5.2.3),
78
108
  // authorityKeyIdentifier (5.2.1), freshestCRL (5.2.6), and authorityInfoAccess (5.2.7) MUST be non-critical;
79
109
  // issuingDistributionPoint (5.2.5) and deltaCRLIndicator (5.2.4) MUST be critical. Enforced on the object
@@ -312,6 +342,12 @@ function _buildRevoked(entryList, isDelta) {
312
342
  var anyExt = false, seenSerials = {};
313
343
  var entries = entryList.map(function (e, idx) {
314
344
  if (!e || typeof e !== "object" || Buffer.isBuffer(e)) throw _err("crl/bad-input", "each revoked entry must be an object");
345
+ // Before any field of the entry is encoded, so an unrecognized name is a refusal rather than an
346
+ // entry signed without what it asked for.
347
+ guard.identifier.assertKnownKeys(e, KNOWN_REVOKED_KEYS, _err, "crl/bad-input", function (k) {
348
+ return "unknown field " + JSON.stringify(k) + " in revoked entry [" + idx +
349
+ "]; an entry is { serialNumber, revocationDate, reason?, invalidityDate?, extensions? }";
350
+ });
315
351
  if (e.serialNumber == null) throw _err("crl/bad-input", "revoked entry [" + idx + "] requires a serialNumber");
316
352
  var serialTlv = _serialInteger(e.serialNumber);
317
353
  // RFC 5280 sec. 5.1.2.6 -- a serial number identifies a certificate uniquely, so a CRL MUST NOT list the
@@ -399,11 +435,34 @@ function _sign(spec, issuer, opts) {
399
435
  opts = opts || {};
400
436
  if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crl/bad-input", "the CRL spec must be an object");
401
437
  issuer = issuer || {};
438
+ // Before any field is read. A dropped key here is worse than elsewhere in the toolkit: a CRL
439
+ // asserts what is revoked, so the failure is a correctly signed, structurally valid CRL saying
440
+ // NOTHING is revoked. `revokedCertificates` earns its own hint because it is the name the parser
441
+ // uses for the same list -- a caller round-tripping a parsed CRL back into sign reaches for it
442
+ // first, and got a signed empty list.
443
+ // The issuer form is settled once, before either door, so the spec table matches the source the
444
+ // distinguished name will actually come from.
445
+ var byCert = issuer.cert != null, byName = issuer.name != null;
446
+ guard.identifier.assertKnownKeys(spec, byCert || byName ? KNOWN_SPEC_KEYS_ISSUER_NAMED : KNOWN_SPEC_KEYS,
447
+ _err, "crl/bad-input", function (k) {
448
+ return "unknown CRL spec field " + JSON.stringify(k) +
449
+ (k === "revokedCertificates" ? "; the producer reads `revoked` (the parser reports the same list as `revokedCertificates`)"
450
+ : k === "issuer" ? "; the issuer argument already names one, through " + (byCert ? "issuer.cert" : "issuer.name") +
451
+ ", and that is the name the CRL is signed under"
452
+ : "");
453
+ });
454
+ guard.identifier.assertKnownKeys(issuer, byCert ? KNOWN_ISSUER_CERT_KEYS : KNOWN_ISSUER_EXPLICIT_KEYS,
455
+ _err, "crl/bad-input", function (k) {
456
+ return "unknown issuer field " + JSON.stringify(k) + " for the " + (byCert
457
+ ? "certificate form; issuer.cert supplies the name and the public key, so they are not read here"
458
+ : "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
459
+ });
460
+ guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "crl/bad-input", "pki.crl.sign has an unknown option ");
402
461
  if (issuer.key == null) throw _err("crl/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
403
462
 
404
463
  // Resolve the issuer name + signing-key SPKI. `{ cert }` supplies both; else `{ name|spec.issuer, publicKey }`.
405
464
  var issuerDer, issuerSpki, issuerCert = null;
406
- if (issuer.cert != null) {
465
+ if (byCert) {
407
466
  issuerCert = _parseIssuerCert(issuer.cert);
408
467
  _assertIssuerCanSignCrl(issuerCert);
409
468
  issuerDer = pkiBuild.tbsNameField(issuerCert, "subject");
@@ -411,7 +470,7 @@ function _sign(spec, issuer, opts) {
411
470
  } else {
412
471
  issuerSpki = _reqDer(issuer.publicKey, "issuer.publicKey (the issuer SPKI DER)");
413
472
  _assertValidSpki(issuerSpki, "issuer.publicKey");
414
- var dnSource = issuer.name != null ? issuer.name : spec.issuer;
473
+ var dnSource = byName ? issuer.name : spec.issuer;
415
474
  if (dnSource == null) throw _err("crl/bad-issuer", "an issuer distinguished name is required (issuer.name or spec.issuer) when no issuer.cert is given");
416
475
  issuerDer = _encodeName(dnSource);
417
476
  }
package/lib/jose.js CHANGED
@@ -457,7 +457,7 @@ async function verify(jws, opts) {
457
457
  * payload: Buffer // the raw payload octets ("" for POST-as-GET)
458
458
  * key: CryptoKey // the private signing key
459
459
  * profile: string // default "acme-outer"
460
- * jwk: object // the public JWK, when the header embeds jwk
460
+ * jwk: object // the public JWK, in kid mode (a header that embeds jwk supplies its own)
461
461
  *
462
462
  * @example
463
463
  * var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
@@ -466,11 +466,38 @@ async function verify(jws, opts) {
466
466
  * nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
467
467
  * var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
468
468
  */
469
+ // The public JWK has two sources and the embedded one wins, so opts.jwk is read only in kid mode.
470
+ // Supplied alongside a header that embeds its own, it selected nothing: the signature parameters
471
+ // came from the header's key while the caller had named a different one.
469
472
  var _SIGN_KEYS = { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 };
473
+ var _SIGN_KEYS_EMBEDDED_JWK = { protected: 1, payload: 1, key: 1, profile: 1 };
470
474
  async function sign(opts) {
471
475
  opts = opts || {};
476
+ // The options are checked BEFORE the header is touched. Serializing it runs the caller's own code
477
+ // -- a toJSON, an accessor -- and that code can delete an option from the very bag being checked,
478
+ // so a misspelling reached the door already removed and the message went out signed.
472
479
  guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
473
- var header = opts.protected;
480
+ // The header is then serialized ONCE, and every read below comes from a snapshot of those exact
481
+ // bytes: the header validated, the header embedded in the JWS and the key confirmed against the
482
+ // produced signature are then all the same values. Read live, a caller getter answers each of
483
+ // those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
484
+ // the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
485
+ var headerJson;
486
+ try { headerJson = JSON.stringify(opts.protected); }
487
+ catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
488
+ // Read back through the same bounded reader a received JWS goes through, so a header this side
489
+ // produces is held to the caps and the duplicate-member rule the other side enforces.
490
+ var headerBytes = headerJson === undefined ? null : Buffer.from(headerJson, "utf8");
491
+ var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
492
+ // Which of the two key sources is in force is known only once the header has been read, so the
493
+ // narrower table runs here. It re-walks the bag, which also catches a name that code added.
494
+ var embedsJwk = !!(header && typeof header === "object" && header.jwk);
495
+ if (embedsJwk) {
496
+ guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
497
+ return "unknown pki.jose.sign option " + JSON.stringify(k) +
498
+ (k === "jwk" ? "; the protected header already embeds the public JWK" : "");
499
+ });
500
+ }
474
501
  var payload = opts.payload;
475
502
  if (!Buffer.isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
476
503
  // Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
@@ -494,7 +521,7 @@ async function sign(opts) {
494
521
  // also PUBLISHED to the server, so it must be public-only: an accidentally exported
495
522
  // private JWK must never be serialized into the protected header.
496
523
  if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
497
- var protectedB64 = b64uEncode(Buffer.from(JSON.stringify(header), "utf8"));
524
+ var protectedB64 = b64uEncode(headerBytes); // the exact bytes every check above read
498
525
  var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
499
526
  var signingInput = Buffer.from(protectedB64 + "." + payloadB64, "ascii");
500
527
  var jwk = header.jwk || opts.jwk || {};
@@ -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, _pbeOpts(enc), _err, "pkcs12"); }
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, _pbeOpts(sc.encrypt), _err, "pkcs12"); }
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
- function _normalizeSpec(spec, opts) {
468
- 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;
469
597
  if (spec && (spec.key != null || spec.cert != null)) {
470
- var enveloped = opts.recipientCerts != null;
471
- 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");
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: opts.recipientCerts.map(function (c) { return { cert: c }; }), bags: bags });
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?, 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.
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
- 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
+ }
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
- 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)) {
719
909
  throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
720
910
  }
721
911
  var m = _storeFromBytes(pfx);
package/lib/tsp-sign.js CHANGED
@@ -142,9 +142,38 @@ function sign(messageImprint, tsa, opts) {
142
142
  ], _sign);
143
143
  }
144
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
+
145
162
  function _sign(messageImprint, tsa, opts) {
146
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 ");
147
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
+ }
148
177
  var mi = messageImprint || {};
149
178
  if (!mi.hashAlgorithm || !NODE_DIGEST[mi.hashAlgorithm]) throw _err("tsp/unsupported-algorithm", "messageImprint.hashAlgorithm must be a supported hash name");
150
179
  if (!Buffer.isBuffer(mi.hashedMessage) && !(mi.hashedMessage instanceof Uint8Array)) throw _err("tsp/bad-input", "messageImprint.hashedMessage must be a Buffer");
@@ -187,7 +216,17 @@ function _sign(messageImprint, tsa, opts) {
187
216
  // Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] INTEGER (1..999) OPTIONAL,
188
217
  // micros [1] INTEGER (1..999) OPTIONAL } (RFC 3161 sec. 2.4.2). seconds is a non-negative
189
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
+
190
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
+ });
191
230
  var f = [];
192
231
  if (a.seconds != null) {
193
232
  var s = Number(a.seconds);
@@ -264,10 +303,29 @@ function _assertImprint(mi) {
264
303
  * var sha256Digest = Buffer.from(await pki.webcrypto.subtle.digest("SHA-256", Buffer.from("hello")));
265
304
  * var req = pki.tsp.request({ hashAlgorithm: "sha256", hashedMessage: sha256Digest }, { nonce: 0x0102030405060708n, certReq: true });
266
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
+
267
312
  function request(messageImprint, opts) {
268
313
  opts = opts || {};
269
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 ");
270
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
+ }
271
329
  _assertImprint(mi);
272
330
  var imprint = b.sequence([_hashAlgId(mi.hashAlgorithm), b.octetString(Buffer.from(mi.hashedMessage))]);
273
331
  // TimeStampReq: version(1), messageImprint, then OPTIONAL reqPolicy / nonce / certReq /
@@ -349,9 +407,15 @@ function parseRequest(input) { return schemaTsp.parseRequest(input); }
349
407
  * var resp = pki.tsp.response(token, {}); // granted
350
408
  * var rej = pki.tsp.response(null, { status: 2, failInfo: ["badAlg"] }); // rejection
351
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
+
352
415
  function response(token, opts) {
353
416
  opts = opts || {};
354
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 ");
355
419
  var status = opts.status == null ? 0 : Number(opts.status);
356
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)");
357
421
  var granted = status === 0 || status === 1;
package/lib/x509-sign.js CHANGED
@@ -53,6 +53,27 @@ var OID_SKI = O("subjectKeyIdentifier");
53
53
 
54
54
  // The recognized keys of the `extensions` spec object; an unknown key is a typo and throws at
55
55
  // config-time (a custom extension is passed as pre-encoded DER via the array form).
56
+ // The three caller-owned objects pki.x509.sign reads, each with its own door. A key nothing here
57
+ // names is a typo or a misplaced argument, and the failure it causes is invisible: the certificate
58
+ // builds, signs, and simply does not carry what was asked for. `spec.extension` (one letter short
59
+ // of `extensions`) emitted a certificate with NO extensions at all -- no basicConstraints on an
60
+ // intended CA, no keyUsage on an intended constrained leaf. `spec.issuer` is the other shape: the
61
+ // issuer is argument TWO, and nesting it in the spec left the certificate SELF-SIGNED with nothing
62
+ // to read as a failure.
63
+ //
64
+ // Every table is derived from what the function actually reads, not from the doc block, because a
65
+ // table naming a key nothing reads reopens the hole it was added to close.
66
+ var KNOWN_SPEC_KEYS = {
67
+ subject: 1, subjectPublicKey: 1, notBefore: 1, notAfter: 1, serialNumber: 1, extensions: 1,
68
+ };
69
+ // The issuer names itself in one of two FORMS, and the cert form wins unconditionally: when
70
+ // `issuer.cert` is present the builder takes the DN and the public key from it and never reads
71
+ // `name` or `publicKey`. A union accepted `{ key, cert, name: "Different CA", publicKey }` and
72
+ // signed under the certificate's identity while discarding the identity the caller wrote out.
73
+ var KNOWN_ISSUER_CERT_KEYS = { key: 1, cert: 1 };
74
+ var KNOWN_ISSUER_EXPLICIT_KEYS = { key: 1, name: 1, publicKey: 1 };
75
+ var KNOWN_SIGN_OPTS = { digestAlgorithm: 1, pem: 1, pss: 1 };
76
+
56
77
  var KNOWN_EXT_KEYS = {
57
78
  subjectKeyIdentifier: 1, authorityKeyIdentifier: 1, keyUsage: 1, keyUsageCritical: 1,
58
79
  extendedKeyUsage: 1, extendedKeyUsageCritical: 1, basicConstraints: 1, subjectAltName: 1,
@@ -282,6 +303,22 @@ function _sign(spec, issuer, opts) {
282
303
  opts = opts || {};
283
304
  if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("x509/bad-input", "the certificate spec must be an object");
284
305
  issuer = issuer || {};
306
+ // Before a single field is read, so a misspelling is a refusal rather than an omission carried
307
+ // into a signed certificate. The issuer message names its own argument because the mistake this
308
+ // catches most often is putting the issuer inside the spec, where it self-signed in silence.
309
+ guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, _err, "x509/bad-input", function (k) {
310
+ return "unknown certificate spec field " + JSON.stringify(k) +
311
+ (k === "issuer" ? "; the issuer is the SECOND argument to pki.x509.sign, not a spec field" : "") +
312
+ (k === "extension" ? "; the extensions spec field is `extensions`" : "");
313
+ });
314
+ var byCert = issuer.cert != null;
315
+ guard.identifier.assertKnownKeys(issuer, byCert ? KNOWN_ISSUER_CERT_KEYS : KNOWN_ISSUER_EXPLICIT_KEYS,
316
+ _err, "x509/bad-input", function (k) {
317
+ return "unknown issuer field " + JSON.stringify(k) + " for the " + (byCert
318
+ ? "certificate form; issuer.cert supplies the name and the public key, so they are not read here"
319
+ : "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
320
+ });
321
+ guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "x509/bad-input", "pki.x509.sign has an unknown option ");
285
322
  if (issuer.key == null) throw _err("x509/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
286
323
 
287
324
  var spki = _reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the certified key)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:b8b3d2cf-001e-4a5b-8644-a627ed3ce3c5",
5
+ "serialNumber": "urn:uuid:123ddb5e-05ea-4e65-9d7d-7a667c46e3a3",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-20T14:50:41.972Z",
8
+ "timestamp": "2026-08-20T20:31:47.390Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.5.17",
22
+ "bom-ref": "@blamejs/pki@0.5.18",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.17",
25
+ "version": "0.5.18",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.5.17",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.5.18",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.5.17",
57
+ "ref": "@blamejs/pki@0.5.18",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]