@blamejs/pki 0.9.4 → 0.9.5

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,29 @@ 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.9.5 — 2026-10-12
8
+
9
+ Refuse a non-boolean switch option instead of reading it as the state the caller did not ask for.
10
+
11
+ ### Changed
12
+
13
+ - For every one of these switches, `true` and `false` pass, `null` and an omitted option select the documented default, and any other value is refused with that module's own `domain/reason` code naming the option. For every value that was already valid the emitted bytes are unchanged. A call that passes a non-boolean to one of these options now throws instead of building a message the other way: pass the boolean, or omit the option to take its default.
14
+ - `opts.pem` is unchanged and still takes `true` or a PEM label string where a verb documents one, as do `signingTime` (a time or `false`) and `pki.pkcs12.build`'s `mac` (a descriptor or `false`). These carry a value as well as an off form, and each is held to the type its own value needs.
15
+
16
+ ### Fixed
17
+
18
+ - `pki.cms.sign` refuses a non-boolean `detached`. The option decided the message shape in one place and whether the content was carried in another, so `detached: "false"` took the attached branch and then omitted the content from it, emitting an attached SignedData with nothing in it.
19
+ - `pki.cms.authenticate` refuses a non-boolean `authenticatedAttributes`. With `false` the MAC covers the content octets directly (RFC 6211 sec. 2), so the two values are structurally different messages and a mistyped one produced the message the caller had declined.
20
+ - `pki.cms.digest` refuses a non-boolean `detached`; `pki.cms.sign` and `pki.cms.countersign` refuse a non-boolean `signedAttributes`; `pki.ocsp.sign` refuses a non-boolean `embedCert` or `extendedRevoke`; `pki.tsp.sign` refuses a non-boolean `ordering`; `pki.pkcs12.build` refuses a non-boolean `integrity.certificates`; `pki.path.crlChecker` and `pki.path.fetchingChecker` refuse a non-boolean `useDeltas`.
21
+ - `pki.est.serverkeygen` reads a `null` `requestedEncryption` as the absent option it documents rather than as a stated `false`. It tested whether the caller had stated an override by presence alone, so a `null` counted as stated and coerced to `false`, and a request against a CSR that does advertise key encryption was refused for contradicting a CSR the caller never disagreed with.
22
+
23
+ ### Security
24
+
25
+ - `pki.smime.sign` and `pki.smime.encrypt` refuse a non-boolean `protectHeaders`. Read by comparison to `true`, a value of any other type left RFC 9788 header protection off and the message went out unprotected with no error, so a caller that asked for the protection did not get it and was not told.
26
+ - The `pki.est` and `pki.scep` verbs refuse a non-boolean `allowCrossOriginRedirect`. Read by truthiness, `"false"` allowed the cross-origin redirect on an unsafe method that the check exists to refuse (RFC 7030 sec. 3.2.1), which is the opposite of what that value states.
27
+ - `pki.cms.sign` and `pki.cms.countersign` refuse a non-boolean `certificates`, the switch that withholds the signer chain. A value of any other type embedded the certificates the caller asked to leave out.
28
+ - `pki.x509.sign` and `pki.csr.sign` refuse a non-boolean criticality flag (`keyUsageCritical`, `extendedKeyUsageCritical`, `certificatePoliciesCritical`, `policyMappingsCritical`, `qcStatementsCritical`, `tlsFeatureCritical`). A value of any other type could emit an extension critical that the caller asked to be non-critical, and RFC 5280 sec. 6.1 requires a relying party that does not recognize a critical extension to reject the certificate.
29
+
7
30
  ## v0.9.4 — 2026-10-12
8
31
 
9
32
  Refuse to issue an elliptic curve certificate asserting a key usage its own algorithm forbids.
package/lib/cms-digest.js CHANGED
@@ -78,7 +78,8 @@ function _digestName(opts) {
78
78
  * @opts
79
79
  * - `digestAlgorithm` (string) -- `sha256` (default), `sha384` or `sha512`.
80
80
  * - `contentType` (string) -- the encapsulated content type by OID name, default `data`.
81
- * - `detached` (boolean) -- omit the content, leaving the digest to travel alone.
81
+ * - `detached` (boolean) -- omit the content, leaving the digest to travel alone. A value other
82
+ * than `true`, `false`, `null` or an absent option is refused `cms/bad-input`.
82
83
  * - `pem` (boolean) -- return a PEM `CMS` block instead of DER.
83
84
  * @example
84
85
  * var der = await pki.cms.digest(Buffer.from("hello"));
@@ -94,6 +95,10 @@ async function _digest(content, opts) {
94
95
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.digest options");
95
96
  guard.identifier.assertKnownKeys(opts, _DIGEST_OPTS, _err, "cms/bad-input",
96
97
  "pki.cms.digest has an unknown option ");
98
+ /** @internal Whether the content rides in the message is decided below by comparing this option
99
+ * to `true`, so a value of any other type attached a content the caller asked to leave out. */
100
+ var detached = guard.identifier.booleanOption(opts.detached, _err, "cms/bad-input",
101
+ "pki.cms.digest opts.detached");
97
102
  var raw = guard.bytes.source(content, CmsError, "cms/bad-input", "content");
98
103
  var alg = _digestName(opts);
99
104
  var ctName = opts.contentType === undefined ? "data" : opts.contentType;
@@ -101,7 +106,7 @@ async function _digest(content, opts) {
101
106
  if (!ctOid) throw _err("cms/bad-input", "opts.contentType is not a known OID name: " + guard.text.showValue(ctName));
102
107
 
103
108
  var value = guard.crypto.digest(NODE_DIGEST[alg], raw);
104
- var eciChildren = opts.detached === true
109
+ var eciChildren = detached
105
110
  ? [b.oid(ctOid)]
106
111
  : [b.oid(ctOid), b.explicit(0, b.octetString(raw))];
107
112
  var dd = b.sequence([
@@ -649,7 +649,12 @@ function authenticate(content, recipients, opts) {
649
649
 
650
650
  async function _authenticateImpl(content, recipients, opts) {
651
651
  opts = opts || intrinsic.create(null);
652
- var withAttrs = opts.authenticatedAttributes !== false;
652
+ /** @internal Typed before it is used, because this switch picks the option set the door below
653
+ * checks against as well as the message that gets built: with `authenticatedAttributes: false`
654
+ * the MAC covers the content octets directly, so a value of any other type selected a
655
+ * structurally different message than the caller asked for and said nothing. */
656
+ var withAttrs = guard.identifier.booleanOptionDefaultOn(opts.authenticatedAttributes, _err,
657
+ "cms/bad-input", "pki.cms.authenticate opts.authenticatedAttributes");
653
658
  guard.identifier.assertKnownKeys(opts, withAttrs ? _AUTHENTICATE_OPTS_ATTRS : _AUTHENTICATE_OPTS_BARE,
654
659
  _err, "cms/bad-input", function (k) {
655
660
  return "pki.cms.authenticate has an unknown option " + JSON.stringify(k) +
package/lib/cms-sign.js CHANGED
@@ -186,6 +186,19 @@ function _algorithmProtectionPair(attrOid, digestAlgId, armAlgId, armTag) {
186
186
  * The emitter below reads the same option again, which is sound because the options door refuses
187
187
  * one supplied through an accessor, own or inherited, before any value is read: two reads of a
188
188
  * plain field are one answer. */
189
+ /** @internal The three switches these verbs document, typed where the options arrive. Each is read
190
+ * further down by comparing it to `false`, which answers for a value of any other type as the
191
+ * state the caller did not ask for: measured, `certificates: "false"` embedded the signer
192
+ * certificate the caller asked to withhold, and `detached: "false"` is read as "not detached" by
193
+ * the shape test and as "detached" by the content test, emitting an attached SignedData with no
194
+ * content in it. Typed here, those comparisons read a value already known to be a boolean.
195
+ * `detached` is not an option every one of these verbs accepts; an absent one types as absent. */
196
+ function _assertSignSwitches(opts, verb) {
197
+ guard.identifier.booleanOptionDefaultOn(opts.certificates, _err, "cms/bad-input", verb + " opts.certificates");
198
+ guard.identifier.booleanOptionDefaultOn(opts.signedAttributes, _err, "cms/bad-input", verb + " opts.signedAttributes");
199
+ guard.identifier.booleanOption(opts.detached, _err, "cms/bad-input", verb + " opts.detached");
200
+ }
201
+
189
202
  function _assertProtectionPlaceable(opts) {
190
203
  var wanted = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
191
204
  if (opts.signedAttributes === false && wanted) {
@@ -373,6 +386,7 @@ function _signDispatch(content, signers, opts) {
373
386
  async function _signStream(stream, signers, opts) {
374
387
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
375
388
  guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
389
+ _assertSignSwitches(opts, "pki.cms.sign");
376
390
  _assertProtectionPlaceable(opts);
377
391
  if (opts.detached !== true) {
378
392
  throw _err("cms/bad-input", "a streaming (async-iterable) content requires opts.detached: true; an attached " +
@@ -413,6 +427,7 @@ async function _signStream(stream, signers, opts) {
413
427
  function _sign(content, signers, opts) {
414
428
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
415
429
  guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
430
+ _assertSignSwitches(opts, "pki.cms.sign");
416
431
  _assertProtectionPlaceable(opts);
417
432
  var contentBuf = _toBuf(content, "content");
418
433
  var list = Array.isArray(signers) ? _b.reqDenseArray(signers, "the signer list") : [signers];
@@ -680,6 +695,7 @@ function countersign(cmsInput, signers, opts) {
680
695
  function _countersign(cmsInput, signers, opts) {
681
696
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.countersign options");
682
697
  guard.identifier.assertKnownKeys(opts, KNOWN_COUNTERSIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
698
+ _assertSignSwitches(opts, "pki.cms.countersign");
683
699
  _assertProtectionPlaceable(opts);
684
700
  var list = Array.isArray(signers) ? signers : [signers];
685
701
  if (!list.length) throw _err("cms/bad-input", "pki.cms.countersign requires at least one countersigner");
package/lib/cms-verify.js CHANGED
@@ -1275,16 +1275,20 @@ function _addCert(out, der) {
1275
1275
  * the same content as the buffered form.
1276
1276
  *
1277
1277
  * @opts detached Omit the encapsulated content (a detached signature; the verifier
1278
- * supplies the content). Default false.
1278
+ * supplies the content). Default false. A value other than `true`,
1279
+ * `false`, `null` or an absent option is refused `cms/bad-input`.
1279
1280
  * @opts eContentType The encapsulated content type (an OID name). Default `data`.
1280
1281
  * @opts signedAttributes Include signed attributes (content-type, message-digest, signing-time);
1281
- * false signs the content directly. Default true.
1282
+ * false signs the content directly. Default true. A value other than
1283
+ * `true`, `false`, `null` or an absent option is refused `cms/bad-input`.
1282
1284
  * @opts additionalSignedAttributes Extra signed attributes to add to every SignerInfo, each
1283
1285
  * `{ type, values }` (`type` an OID name or dotted string, `values` an
1284
1286
  * array of DER value `Buffer`s). Covered by the signature.
1285
1287
  * @opts signingTime A `Date` for the signing-time attribute, or false to omit it.
1286
1288
  * @opts sid `"issuerAndSerial"` (default) or `"ski"` (subjectKeyIdentifier).
1287
- * @opts certificates Embed the signer certificates in the output. Default true.
1289
+ * @opts certificates Embed the signer certificates in the output. Default true. A value other
1290
+ * than `true`, `false`, `null` or an absent option is refused
1291
+ * `cms/bad-input`.
1288
1292
  * @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
1289
1293
  * @opts unsignedAttributes Unsigned attributes for every SignerInfo, each `{ type, values }`
1290
1294
  * (`type` an OID name or dotted string, `values` an array of DER value
@@ -1341,12 +1345,15 @@ var sign = cmsSign.sign;
1341
1345
  * instead of the primary signature (a nested countersignature).
1342
1346
  * @opts signedAttributes Include signed attributes (message-digest + signing-time; never a
1343
1347
  * content-type). Default true; false signs the target signature octets
1344
- * directly.
1348
+ * directly. A value other than `true`, `false`, `null` or an absent
1349
+ * option is refused `cms/bad-input`.
1345
1350
  * @opts signingTime A `Date` for the countersignature's signing-time, or false to omit it.
1346
1351
  * @opts additionalSignedAttributes Extra signed attributes on the countersignature (a content-type
1347
1352
  * is rejected, RFC 5652 sec. 11.4).
1348
1353
  * @opts sid `"issuerAndSerial"` (default) or `"ski"`.
1349
- * @opts certificates Embed the countersigner certificate(s) in the SignedData. Default true.
1354
+ * @opts certificates Embed the countersigner certificate(s) in the SignedData. Default
1355
+ * true. A value other than `true`, `false`, `null` or an absent option
1356
+ * is refused `cms/bad-input`.
1350
1357
  * @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` attribute on the
1351
1358
  * countersignature, copying ITS own digest and signature algorithm
1352
1359
  * identifiers rather than the countersigned signature's. Off by
@@ -1440,7 +1447,8 @@ var encrypt = cmsEncrypt.encrypt;
1440
1447
  * @opts digestAlgorithm The authenticated-attributes message-digest hash (defaults to the MAC hash).
1441
1448
  * @opts authenticatedAttributes Include the content-type + message-digest authenticated attributes
1442
1449
  * (the default, and required for a non-`data` content type); false MACs the
1443
- * content directly.
1450
+ * content directly. A value other than `true`, `false`, `null` or an absent
1451
+ * option is refused `cms/bad-input`.
1444
1452
  * @opts authAttrs Extra authenticated attributes appended to the content-type + message-digest pair.
1445
1453
  * @opts contentType The encapsulated content type (an OID name). Default `data`.
1446
1454
  * @opts keyIdentifier `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
package/lib/csr-sign.js CHANGED
@@ -172,7 +172,11 @@ function _relatedCertRequest(spec) {
172
172
  * subject-owned set `pki.x509.sign` takes, or an array of pre-encoded Extension DER) and
173
173
  * `challengePassword`. An extension the issuing CA assigns (authorityKeyIdentifier, the
174
174
  * certificate-transparency pair, the CA version and previous-certificate hash, ocspNoCheck) is not a
175
- * request and is refused by name. `key` (or
175
+ * request and is refused by name. The per-extension criticality flags `pki.x509.sign` takes
176
+ * (`keyUsageCritical`, default true, and `extendedKeyUsageCritical`,
177
+ * `certificatePoliciesCritical`, `policyMappingsCritical`, `qcStatementsCritical`,
178
+ * `tlsFeatureCritical`, each default false) apply here too; each is a boolean, and a value other
179
+ * than `true`, `false`, `null` or an absent flag is refused `csr/bad-input`. `key` (or
176
180
  * `{ key }`) is the subject's own PKCS#8 private key, WebCrypto CryptoKey, or signer
177
181
  * `{ algorithm, publicKey, sign }`, so the request is self-signed
178
182
  * to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the
package/lib/est.js CHANGED
@@ -173,6 +173,17 @@ function _knownOpts(opts, known, verb) {
173
173
  Object.keys(known).sort().join(", ");
174
174
  });
175
175
  _assertTransport(settled);
176
+ /** @internal The two switches these verbs document, typed here because this is the one door every
177
+ * one of them passes through and it runs before any request goes out. Both are read further down
178
+ * by truthiness: `allowCrossOriginRedirect: "false"` is truthy, so it ALLOWED the cross-origin
179
+ * redirect on an unsafe method that the check exists to refuse (sec. 3.2.1), and
180
+ * `requestedEncryption: "false"` is read as "encryption was requested", which skips the
181
+ * sec. 4.4.2 check that an encrypted key part was advertised. A verb that does not accept one of
182
+ * them never sees a value for it. */
183
+ guard.identifier.booleanOption(settled.allowCrossOriginRedirect, E, "est/bad-input",
184
+ "pki.est." + verb + " opts.allowCrossOriginRedirect");
185
+ guard.identifier.booleanOption(settled.requestedEncryption, E, "est/bad-input",
186
+ "pki.est." + verb + " opts.requestedEncryption");
176
187
  return settled;
177
188
  }
178
189
 
@@ -1643,6 +1654,8 @@ function _enroll(op, baseUrl, csrInput, opts, proxySnap) {
1643
1654
  * There is no `"auto"`: the scheme is chosen here, not by whatever a server offers. `username` / `password`
1644
1655
  * at the top level are the older form and mean Basic. Answered only after the transport authenticated the server.
1645
1656
  * - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on an unsafe method.
1657
+ * A value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
1658
+ * before the request goes out.
1646
1659
  * @example
1647
1660
  * // a live CA uses the default pki.transport.https; here an injected transport returns a canned bag
1648
1661
  * var r = await pki.est.cacerts("https://ca.example",
@@ -1695,7 +1708,9 @@ function cacerts(baseUrl, opts) {
1695
1708
  * - `transport` / `tls` / `label` / `timeout` / `maxResponseBytes` / `maxRedirects` / `proxy` / `now`, as in pki.est.cacerts.
1696
1709
  * - `strict`: reject an enroll response that carries more than the single issued certificate.
1697
1710
  * - `username` / `password` / `auth` -- HTTP credentials: top-level `username` / `password` mean Basic; `auth` (`{ scheme: "basic" | "digest", username, password, ... }`, as in pki.est.cacerts) selects Basic or Digest. Answered only after server authorization (empty username allowed).
1698
- * - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST.
1711
+ * - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST. A
1712
+ * value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
1713
+ * before the request goes out.
1699
1714
  * @example
1700
1715
  * var req = await pki.csr.sign({ subject: "device.example", subjectPublicKey: signerSpki }, { key: signerKeyPkcs8 });
1701
1716
  * // a 202 means the CA queued the request -- the verb surfaces the delay, never sleeps
@@ -1919,6 +1934,8 @@ async function _serverkeygenResult(res, opts, derived) {
1919
1934
  * @opts
1920
1935
  * - `requestedEncryption` / `expectedRecipientKeyId` / `expectedRecipientIssuerSerial` -- OPTIONAL
1921
1936
  * overrides of the CSR-derived recipient coherence; a value that contradicts the CSR is `est/bad-input`.
1937
+ * `requestedEncryption` is a boolean: a value other than `true`, `false`, `null` or an absent
1938
+ * option is refused `est/bad-input`.
1922
1939
  * - every option of pki.est.simpleenroll (transport, tls, label, budgets, credentials incl. `auth`).
1923
1940
  * @example
1924
1941
  * var r = await pki.est.serverkeygen("https://ca.example", csrDer,
@@ -1933,7 +1950,11 @@ function serverkeygen(baseUrl, csrInput, opts) {
1933
1950
  var checkedTransport = opts.transport;
1934
1951
  var csrDer = _csrDer(csrInput);
1935
1952
  var derived = _serverkeygenEncryptionFromCsr(csrDer);
1936
- if (opts.requestedEncryption !== undefined && !!opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + !!opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
1953
+ /** @internal An override is a value the caller stated, and `null` states nothing: it is the
1954
+ * absent form the switch door accepts, so the CSR's own answer stands. Read as present it
1955
+ * coerced to `false` and contradicted a CSR the caller never disagreed with. The value is a
1956
+ * boolean by the time it reaches here, so it is compared rather than coerced. */
1957
+ if (opts.requestedEncryption != null && opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
1937
1958
  if (opts.expectedRecipientKeyId !== undefined) {
1938
1959
  if (!Buffer.isBuffer(opts.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId must be a Buffer");
1939
1960
  if (derived.expectedRecipientKeyId && !opts.expectedRecipientKeyId.equals(derived.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId contradicts the key identifier the CSR advertised (RFC 7030 sec. 4.4.1)");
@@ -743,7 +743,32 @@ function assertCallableOption(opts, key, E, code, label, shape) {
743
743
  // @enforced-by behavioral -- guard: each drives the shipped verb with a non-boolean and asserts the
744
744
  // @enforced-by behavioral -- typed refusal, and with false and absence to pin the two off forms.
745
745
  function booleanOption(value, E, code, label) {
746
- if (value === undefined || value === null) return false;
746
+ return _switchValue(value, false, E, code, label);
747
+ }
748
+
749
+ /** @internal The same read for a switch whose absent form is ON: an option the caller turns OFF,
750
+ * where the documented default is the state an omitted option selects. The two differ only in what
751
+ * absence means, so the type rule is written once and each boundary names the default it documents.
752
+ * A boundary comparing such an option to `false` answers for every other value as the default
753
+ * state, which is how `certificates: "false"` embedded the certificate the caller asked to
754
+ * withhold; once the value has been through here, that comparison reads a boolean.
755
+ *
756
+ * @enforced-by behavioral -- the per-verb vectors in coercion-safe-options.test.js drive each
757
+ * @enforced-by behavioral -- shipped verb with a non-boolean and assert the typed refusal names the
758
+ * @enforced-by behavioral -- option, and with true, false and absence to pin the three accepted
759
+ * @enforced-by behavioral -- forms. The comparison this replaces has its own codebase-wide
760
+ * @enforced-by behavioral -- detector, optout-switch-read-by-coercion.
761
+ */
762
+ function booleanOptionDefaultOn(value, E, code, label) {
763
+ return _switchValue(value, true, E, code, label);
764
+ }
765
+
766
+ /** @internal A switch is the boolean it documents, an absent one is the stated default, and every
767
+ * other value is refused. The value is rendered from its own type rather than coerced, so a caller
768
+ * value with a throwing `toString` reports the typed refusal. `null` is read as absence, the same
769
+ * reading the rest of this module gives it. */
770
+ function _switchValue(value, whenAbsent, E, code, label) {
771
+ if (value === undefined || value === null) return whenAbsent;
747
772
  if (value === true || value === false) return value;
748
773
  throw E(code, label + " must be true or false, got " + _showSwitch(value));
749
774
  }
@@ -776,6 +801,7 @@ function declaresOwn(obj, name) {
776
801
  module.exports = intrinsic.freeze({
777
802
  assertCallable: assertCallable,
778
803
  booleanOption: booleanOption,
804
+ booleanOptionDefaultOn: booleanOptionDefaultOn,
779
805
  declaresOwn: declaresOwn,
780
806
  assertCallableOption: assertCallableOption,
781
807
  assertCanonicalOid: assertCanonicalOid,
package/lib/ocsp.js CHANGED
@@ -316,6 +316,8 @@ function _normCertDer(cert, what) {
316
316
  * extendedRevoke emit the id-pkix-ocsp-extended-revoke extension (RFC 6960 sec. 4.4.8); implied
317
317
  * by a non-issued response, where `false` is refused.
318
318
  * embedCert `false` to omit certs [0] (a direct-CA response the client already trusts).
319
+ * For both switches, a value other than `true`, `false`, `null` or an absent
320
+ * option is refused `ocsp/bad-input`.
319
321
  * pem emit a PEM `OCSP RESPONSE` string instead of DER.
320
322
  * @example
321
323
  * var ca = await pki.key.generate("Ed25519");
@@ -350,6 +352,12 @@ function _sign(responseData, responder, opts) {
350
352
  opts = guard.identifier.optionsObject(opts, _err, "ocsp/bad-input", "pki.ocsp.sign options");
351
353
  guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "ocsp/bad-input",
352
354
  "pki.ocsp.sign has an unknown option (the nonce to echo back is `nonce`): ");
355
+ /** @internal Both switches are read below against one boolean each, which answered for a value
356
+ * of any other type as the opposite of what the caller asked: `embedCert: "false"` carried the
357
+ * responder certificate a direct-CA response leaves out, and `extendedRevoke: "false"` placed
358
+ * the extension while escaping the sec. 2.2 refusal that reads the same option. */
359
+ guard.identifier.booleanOptionDefaultOn(opts.embedCert, _err, "ocsp/bad-input", "pki.ocsp.sign opts.embedCert");
360
+ guard.identifier.booleanOption(opts.extendedRevoke, _err, "ocsp/bad-input", "pki.ocsp.sign opts.extendedRevoke");
353
361
  responseData = responseData || _ocspIntrinsic.create(null);
354
362
  if (typeof responseData !== "object") throw _err("ocsp/bad-input", "the responseData must be an object { responderID, responses, producedAt? }");
355
363
  guard.identifier.assertKnownKeys(responseData, _RESPONSE_DATA_KEYS, _err, "ocsp/bad-input", "unknown responseData field (accepted: responderID, responses, producedAt): ");
@@ -2147,7 +2147,9 @@ function selectDelta(candidates) {
2147
2147
  * revocationDate.
2148
2148
  *
2149
2149
  * @opts
2150
- * useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)).
2150
+ * useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)). Default `true`;
2151
+ * a value other than `true`, `false`, `null` or an absent option is refused
2152
+ * `path/bad-input`.
2151
2153
  * Default true. When false a delta is never merged; it is
2152
2154
  * still consulted for revocation.
2153
2155
  *
@@ -2171,7 +2173,10 @@ function crlChecker(crls, opts) {
2171
2173
  return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
2172
2174
  "crlChecker: crls[" + ci + "]");
2173
2175
  });
2174
- var useDeltas = !(opts && opts.useDeltas === false);
2176
+ /** @internal The sec. 6.3.1(b) merge, read as the boolean it documents: the comparison this
2177
+ * replaces answered for a value of any other type by merging deltas the caller turned off. */
2178
+ var useDeltas = guard.identifier.booleanOptionDefaultOn(opts && opts.useDeltas, E,
2179
+ "path/bad-input", "crlChecker: opts.useDeltas");
2175
2180
  return {
2176
2181
  check: async function (cert, issuer, ctx) {
2177
2182
  var time = ctx.time;
@@ -532,7 +532,7 @@ function _normalizeSpec(spec, opts, form) {
532
532
  * @opts
533
533
  * - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
534
534
  * - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, password? }`. `password` overrides the shared integrity password for the MAC. `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.
535
- * - `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.
535
+ * - `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. `certificates` embeds the signer certificates in the authSafe, default true; a value other than `true`, `false`, `null` or an absent option is refused `pkcs12/bad-input`.
536
536
  * - `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.
537
537
  * - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
538
538
  * @example
@@ -572,6 +572,11 @@ async function _build(spec, opts) {
572
572
  if (opts.integrity != null) {
573
573
  if (typeof opts.integrity !== "object" || Array.isArray(opts.integrity)) throw _err("pkcs12/bad-input", "opts.integrity must be an object { mode, signer|signers, ... }");
574
574
  guard.identifier.assertKnownKeys(opts.integrity, _INTEGRITY_OPTS, _err, "pkcs12/bad-input", "opts.integrity has an unknown option ");
575
+ /** @internal Forwarded to `pki.cms.sign` below by comparing it to `false`, which answered for a
576
+ * value of any other type by embedding the signer certificates the caller asked to leave out
577
+ * of the authSafe. */
578
+ guard.identifier.booleanOptionDefaultOn(opts.integrity.certificates, _err, "pkcs12/bad-input",
579
+ "opts.integrity.certificates");
575
580
  if (opts.integrity.mode !== "public-key") {
576
581
  throw _err("pkcs12/bad-integrity-mode", "opts.integrity.mode must be \"public-key\" (the only mode it selects); omit opts.integrity for password integrity, got " + guard.text.showValue(opts.integrity.mode));
577
582
  }
package/lib/pki-build.js CHANGED
@@ -648,6 +648,36 @@ function makeBuilder(ctx) {
648
648
  if (bc.pathLen != null) pathLen(bc.pathLen);
649
649
  guard.identifier.assertKnownKeys(bc, BC_KEYS, E, "bad-input", "unknown basicConstraints field ");
650
650
  }
651
+
652
+ /** @internal The criticality a caller states for an extension, read as the boolean each flag
653
+ * documents and resolved to the default an omitted flag selects. RFC 5280 sec. 4.2 fixes
654
+ * keyUsage critical and leaves the other five non-critical, which is why the defaults differ.
655
+ * `basicConstraints` carries its own flag inside the value and is typed by `validateBcSpec`.
656
+ *
657
+ * The flags are RETURNED, so the emitters below read criticality from this record rather than
658
+ * from the caller's spec a second time. A flag read off the spec at the point of emission is a
659
+ * read of a value that was never typed: measured, `extendedKeyUsageCritical: "false"` emitted
660
+ * that extension critical, and an extension a relying party does not recognize as critical is
661
+ * one RFC 5280 sec. 6.1 requires it to reject. */
662
+ var CRITICALITY_FLAGS = [
663
+ ["keyUsageCritical", true],
664
+ ["extendedKeyUsageCritical", false],
665
+ ["certificatePoliciesCritical", false],
666
+ ["policyMappingsCritical", false],
667
+ ["qcStatementsCritical", false],
668
+ ["tlsFeatureCritical", false],
669
+ ];
670
+ function criticality(extSpec, label) {
671
+ var out = intrinsic.create(null);
672
+ var where = label || "extensions";
673
+ for (var i = 0; i < CRITICALITY_FLAGS.length; i++) {
674
+ var k = CRITICALITY_FLAGS[i][0];
675
+ out[k] = CRITICALITY_FLAGS[i][1]
676
+ ? guard.identifier.booleanOptionDefaultOn(extSpec[k], E, "bad-input", where + "." + k)
677
+ : guard.identifier.booleanOption(extSpec[k], E, "bad-input", where + "." + k);
678
+ }
679
+ return out;
680
+ }
651
681
  function extBasicConstraints(spec) {
652
682
  var children = [];
653
683
  if (spec.cA === true) _push(children,b.boolean(true));
@@ -1368,16 +1398,17 @@ function makeBuilder(ctx) {
1368
1398
  guard.identifier.assertKnownKeys(extSpec, REQ_EXT_KEYS, E, "bad-input", function (k) {
1369
1399
  return "unknown requested extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension";
1370
1400
  });
1401
+ var crit = criticality(extSpec, "requested extensions");
1371
1402
  var out = [];
1372
1403
  if (extSpec.subjectKeyIdentifier != null) {
1373
1404
  if (extSpec.subjectKeyIdentifier === true && spki == null) throw E("bad-input", "subjectKeyIdentifier auto-derive (true) requires the public key -- supply a Buffer key id, or include the public key");
1374
1405
  _push(out,ext(O("subjectKeyIdentifier"), false, extSki(skiKeyId(extSpec.subjectKeyIdentifier, spki))));
1375
1406
  }
1376
- if (extSpec.keyUsage != null) _push(out,ext(O("keyUsage"), extSpec.keyUsageCritical !== false, extKeyUsage(extSpec.keyUsage)));
1377
- if (extSpec.extendedKeyUsage != null) _push(out,ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, extExtKeyUsage(extSpec.extendedKeyUsage)));
1407
+ if (extSpec.keyUsage != null) _push(out,ext(O("keyUsage"), crit.keyUsageCritical, extKeyUsage(extSpec.keyUsage)));
1408
+ if (extSpec.extendedKeyUsage != null) _push(out,ext(O("extKeyUsage"), crit.extendedKeyUsageCritical, extExtKeyUsage(extSpec.extendedKeyUsage)));
1378
1409
  if (extSpec.basicConstraints != null) { validateBcSpec(extSpec.basicConstraints); _push(out,ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, extBasicConstraints(extSpec.basicConstraints))); }
1379
1410
  if (extSpec.subjectAltName != null) _push(out,ext(O("subjectAltName"), false, extSan(extSpec.subjectAltName)));
1380
- if (extSpec.certificatePolicies != null) _push(out,ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, extCertPolicies(extSpec.certificatePolicies)));
1411
+ if (extSpec.certificatePolicies != null) _push(out,ext(O("certificatePolicies"), crit.certificatePoliciesCritical, extCertPolicies(extSpec.certificatePolicies)));
1381
1412
  /** @internal RFC 5280 sec. 4.2.1.10 fixes this extension critical, in a request as in the certificate. */
1382
1413
  if (extSpec.nameConstraints != null) guard.list.append(out, ext(O("nameConstraints"), true, extNameConstraints(extSpec.nameConstraints)));
1383
1414
  if (extSpec.authorityInfoAccess != null) guard.list.append(out, ext(O("authorityInfoAccess"), false, extAuthorityInfoAccess(extSpec.authorityInfoAccess)));
@@ -1385,14 +1416,14 @@ function makeBuilder(ctx) {
1385
1416
  if (extSpec.freshestCRL != null) guard.list.append(out, ext(O("freshestCRL"), false, extCrlDistributionPoints(extSpec.freshestCRL, "freshestCRL")));
1386
1417
  if (extSpec.policyConstraints != null) guard.list.append(out, ext(O("policyConstraints"), true, extPolicyConstraints(extSpec.policyConstraints)));
1387
1418
  if (extSpec.inhibitAnyPolicy != null) guard.list.append(out, ext(O("inhibitAnyPolicy"), true, extInhibitAnyPolicy(extSpec.inhibitAnyPolicy)));
1388
- if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), !!extSpec.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
1419
+ if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), crit.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
1389
1420
  if (extSpec.issuerAltName != null) guard.list.append(out, ext(O("issuerAltName"), false, encodeGeneralNames(reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
1390
- if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), !!extSpec.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
1421
+ if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), crit.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
1391
1422
  /** @internal RFC 7633 sec. 4.2.1 lets a request state the features the server supports, and
1392
1423
  * sec. 4.3.2 asks a server to generate it, so a feature declaration is the requester's wish and
1393
1424
  * belongs in this table. The criticality knob is here too, because a request carries the
1394
1425
  * Extension shape whole and the CA decides what it issues. */
1395
- if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
1426
+ if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), crit.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
1396
1427
  if (extSpec.msCertificateTemplate != null) guard.list.append(out, ext(O("msCertificateTemplate"), false, extMsCertificateTemplate(extSpec.msCertificateTemplate)));
1397
1428
  if (extSpec.msEnrollCertType != null) guard.list.append(out, ext(O("msEnrollCertType"), false, extMsEnrollCertType(extSpec.msEnrollCertType)));
1398
1429
  if (extSpec.msApplicationPolicies != null) guard.list.append(out, ext(O("msApplicationPolicies"), false, extCertPolicies(extSpec.msApplicationPolicies)));
@@ -1457,6 +1488,7 @@ function makeBuilder(ctx) {
1457
1488
  encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
1458
1489
  requestedExtensions: requestedExtensions,
1459
1490
  extKeyUsage: extKeyUsage, extExtKeyUsage: extExtKeyUsage, validateBcSpec: validateBcSpec,
1491
+ criticality: criticality,
1460
1492
  extBasicConstraints: extBasicConstraints, pathLen: pathLen, extSki: extSki, extAki: extAki, AKI_KEYS: AKI_KEYS,
1461
1493
  extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
1462
1494
  extSubjectInfoAccess: extSubjectInfoAccess, extSubjectDirectoryAttributes: extSubjectDirectoryAttributes,
@@ -670,7 +670,9 @@ function _deltaUrlsFor(parsedCrl, state) {
670
670
  * of the same shape. A transport that does not declare that it blocks
671
671
  * private addresses is refused a hostname.
672
672
  * @opts allowPlaintextHttp Whether an `http` URL is fetched. Default `true`.
673
- * @opts useDeltas Whether a freshestCRL delta is fetched and merged. Default `true`.
673
+ * @opts useDeltas Whether a freshestCRL delta is fetched and merged. Default `true`. A
674
+ * value other than `true`, `false`, `null` or an absent option is refused
675
+ * `path/bad-input`.
674
676
  * @opts nonce Whether an OCSP request carries a 32-octet nonce. `true` or `false`,
675
677
  * and anything else is `path/bad-input` rather than the default, since a
676
678
  * control read as off is one an operator asked for and did not get.
@@ -709,7 +711,11 @@ function fetchingChecker(opts) {
709
711
  * door is the value the rule is applied on. */
710
712
  net: intrinsic.freeze({ allowHttp: allowPlaintextHttp, guardsAddresses: _declaresAddressGuard(transport) }),
711
713
  allowPlaintextHttp: allowPlaintextHttp,
712
- useDeltas: !(opts.useDeltas === false),
714
+ /** @internal Read the way `nonce` on the next line has been read since it was written: the
715
+ * comparison this replaces answered for a value of any other type by fetching the deltas the
716
+ * caller declined. */
717
+ useDeltas: guard.identifier.booleanOptionDefaultOn(opts.useDeltas, E, "path/bad-input",
718
+ "fetchingChecker: opts.useDeltas"),
713
719
  nonce: _booleanOption(opts.nonce, "fetchingChecker: opts.nonce"),
714
720
  tls: opts.tls,
715
721
  maxFetches: guard.limits.cap(opts.maxFetches, "fetchingChecker: opts.maxFetches", LIMITS.REVOCATION_MAX_FETCHES,
package/lib/scep.js CHANGED
@@ -28,6 +28,12 @@
28
28
  * the message layer, transport-agnostic; a live SCEP CA is driven over
29
29
  * pki.transport separately.
30
30
  *
31
+ * Every verb that reaches a CA accepts the same transport bounds, listed on
32
+ * each of them. One of those bounds is a switch: `allowCrossOriginRedirect`
33
+ * opts in to following a cross-origin redirect on an unsafe method. It is a
34
+ * boolean, and a value other than `true`, `false`, `null` or an absent option
35
+ * is refused `scep/bad-input` before the first request goes out.
36
+ *
31
37
  * @card
32
38
  * Build and read SCEP (RFC 8894) messages: a PKCSReq, RenewalReq, CertPoll,
33
39
  * GetCert, or GetCRL request, or a CA-side CertRep response, wrapped in the
@@ -731,7 +737,11 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
731
737
  var redirects = 0, curMethod = method, curBody = body, initialOrigin = _urlOrigin(url);
732
738
  var proxySnap = _snapshotProxy(opts.proxy);
733
739
  var tlsBase = _tlsForRequest(opts);
734
- var allowCrossOrigin = !!opts.allowCrossOriginRedirect;
740
+ /** @internal Read as the boolean it documents, on the one path every network verb here takes and
741
+ * before the first request. Taken by truthiness, `allowCrossOriginRedirect: "false"` ALLOWED the
742
+ * cross-origin redirect on an unsafe method that the check below exists to refuse. */
743
+ var allowCrossOrigin = guard.identifier.booleanOption(opts.allowCrossOriginRedirect, _err,
744
+ "scep/bad-input", "opts.allowCrossOriginRedirect");
735
745
  function _hopTls(current) {
736
746
  var t = tlsBase;
737
747
  if (_urlOrigin(current) !== initialOrigin) {
package/lib/smime.js CHANGED
@@ -76,12 +76,23 @@ var DECOMPRESS_OPTS = Object.assign(Object.create(null), { maxOutputBytes: 1 });
76
76
  /** @internal Checks the option names and RETURNS the options taken once. A verb reads one option to
77
77
  * decide whether a rule applies and reads it again for the value the rule uses, and an option
78
78
  * reached through an accessor answers each read separately. Callers assign the return value. */
79
+ /** @internal Header protection is turned on below by comparing this option to `true`, so a value
80
+ * of any other type sent the message with the RFC 9788 protection the caller asked for left off.
81
+ * Typed at the one door every verb here passes through, which is before the entity is built. */
82
+ function _assertSmimeSwitches(opts, verb) {
83
+ guard.identifier.booleanOption(opts.protectHeaders, _err, "smime/bad-input",
84
+ "pki.smime." + verb + " opts.protectHeaders");
85
+ }
86
+
79
87
  function _knownOpts(opts, known, verb) {
80
88
  guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
81
89
  return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
82
90
  Object.keys(known).sort().join(", ");
83
91
  });
84
- return guard.identifier.snapshotOptions(opts, known);
92
+ /** @internal Typed on the snapshot, which is the object the verb goes on to read. */
93
+ var settled = guard.identifier.snapshotOptions(opts, known);
94
+ _assertSmimeSwitches(settled, verb);
95
+ return settled;
85
96
  }
86
97
 
87
98
  function _isPkcs7(type, kind) {
@@ -654,7 +665,7 @@ function _base64Body(der) {
654
665
  * @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
655
666
  * @opts contentType the wrapped entity's Content-Type (default `text/plain; charset=utf-8`).
656
667
  * @opts signingTime a `Date` for the CMS signing-time attribute, or false to omit it.
657
- * @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers.
668
+ * @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
658
669
  * @opts headers the Non-Structural fields to protect + display: an object `{ Name: value }` or an array `[{ name, value }]` (Subject / From / To / Date / ...); used with `protectHeaders`.
659
670
  * @opts hcp the Header Confidentiality Policy applied to the OUTER display copies: `"hcp_baseline"` (default) or `"hcp_no_confidentiality"`. A signed message's payload is not encrypted, so this governs presentation, not secrecy; used with `protectHeaders`.
660
671
  * @opts sid forwarded to cms.sign: the SignerIdentifier form, `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
@@ -875,7 +886,7 @@ function _cmsEncryptOpts(opts) {
875
886
  *
876
887
  * @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
877
888
  * @opts contentType the wrapped entity's MIME Content-Type (default `text/plain; charset=utf-8`).
878
- * @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside.
889
+ * @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
879
890
  * @opts headers the Non-Structural fields to protect (object `{ Name: value }` or array `[{ name, value }]`); the real values, hidden by the HCP.
880
891
  * @opts hcp the Header Confidentiality Policy: `"hcp_baseline"` (the default, obscuring Subject and removing Comments/Keywords) or `"hcp_no_confidentiality"` (leave all outer values visible). Per RFC 9788 sec. 3.2.1 / sec. 11.4, `hcp_baseline` deliberately does not strip `Bcc` (removing it can break deliverability to a Bcc'd recipient); to keep a blind recipient out of the plaintext outer headers, omit `Bcc` from `opts.headers`.
881
892
  * @opts contentEncryptionAlgorithm forwarded to cms.encrypt: `"aes-256-gcm"` (default) / `"aes-128-gcm"` / `"aes-256-cbc"` / `"aes-128-cbc"`.
package/lib/tsp-sign.js CHANGED
@@ -123,7 +123,9 @@ function _signingCertV2(certDer, hashName) {
123
123
  * @opts nonce The request nonce to echo (a number or BigInt), for replay protection.
124
124
  * @opts accuracy The genTime +/- accuracy, as `{ seconds?, millis?, micros? }`, naming at
125
125
  * least one field; omit it to make no accuracy claim.
126
- * @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean).
126
+ * @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean). A value
127
+ * other than `true`, `false`, `null` or an absent option is refused
128
+ * `tsp/bad-input`.
127
129
  * @opts certHashAlgorithm The ESSCertIDv2 hash algorithm name. Default `sha256`.
128
130
  * @opts digestAlgorithm / pss Passed through to `pki.cms.sign` (the signer digest algorithm, RSA-PSS padding).
129
131
  * @opts additionalSignedAttributes Extra signed attributes to carry on the token, as `[{ type, values }]` (forwarded to `pki.cms.sign`).
@@ -163,6 +165,11 @@ function _sign(messageImprint, tsa, opts) {
163
165
  }
164
166
  guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "tsp/bad-input", "pki.tsp.sign has an unknown option ");
165
167
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
168
+ /** @internal The ordering claim is emitted below only for the boolean `true`, so a value of any
169
+ * other type left the TSTInfo saying the opposite of what the caller asked, which is what RFC
170
+ * 3161 sec. 2.4.2 has a relying party read to compare two tokens from this TSA. `certReq` on the
171
+ * request verb has been typed since it was written; this is the same read. */
172
+ guard.identifier.booleanOption(opts.ordering, _err, "tsp/bad-input", "pki.tsp.sign opts.ordering");
166
173
  if (tsa && typeof tsa === "object" && !Buffer.isBuffer(tsa)) {
167
174
  guard.identifier.assertKnownKeys(tsa, _TSA_KEYS, _err, "tsp/bad-input", function (k) {
168
175
  return "unknown TSA field " + JSON.stringify(k) +
package/lib/x509-sign.js CHANGED
@@ -228,6 +228,10 @@ function _buildExtensions(extSpec, ctx) {
228
228
  return "unknown extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form for a custom extension";
229
229
  });
230
230
 
231
+ /** @internal The criticality flags, typed at this door and read from the record below: the
232
+ * emitters took them off the caller's spec, where a value that was never a boolean selected a
233
+ * criticality nobody asked for. */
234
+ var crit = _b.criticality(extSpec);
231
235
  var bc = extSpec.basicConstraints;
232
236
  if (bc != null) _validateBcSpec(bc);
233
237
  var caTrue = !!(bc && bc.cA === true);
@@ -257,14 +261,14 @@ function _buildExtensions(extSpec, ctx) {
257
261
  var akiF = _akiFields(akiVal, ctx);
258
262
  _push(out,_ext(O("authorityKeyIdentifier"), false, _extAki(akiF[0], akiF[1], akiF[2])));
259
263
  }
260
- if (ku != null) _push(out,_ext(O("keyUsage"), extSpec.keyUsageCritical !== false, _extKeyUsage(ku)));
261
- if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, _extExtKeyUsage(extSpec.extendedKeyUsage)));
264
+ if (ku != null) _push(out,_ext(O("keyUsage"), crit.keyUsageCritical, _extKeyUsage(ku)));
265
+ if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"), crit.extendedKeyUsageCritical, _extExtKeyUsage(extSpec.extendedKeyUsage)));
262
266
  if (bc != null) {
263
267
  if (bc.cA === true && bc.critical === false) throw _err("x509/bad-input", "a CA certificate's basicConstraints MUST be critical (RFC 5280 sec. 4.2.1.9)");
264
268
  _push(out,_ext(O("basicConstraints"), bc.critical !== false, _extBasicConstraints(bc)));
265
269
  }
266
270
  if (extSpec.subjectAltName != null) _push(out,_ext(O("subjectAltName"), ctx.subjectEmpty, _extSan(extSpec.subjectAltName)));
267
- if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, _extCertPolicies(extSpec.certificatePolicies)));
271
+ if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"), crit.certificatePoliciesCritical, _extCertPolicies(extSpec.certificatePolicies)));
268
272
  /** @internal Sections 4.2.2.1 and 4.2.1.15 fix these non-critical; sec. 4.2.1.13 states a SHOULD
269
273
  * and the toolkit issues the conforming form. */
270
274
  if (extSpec.authorityInfoAccess != null) guard.list.append(out, _ext(O("authorityInfoAccess"), false, _extAia(extSpec.authorityInfoAccess)));
@@ -279,15 +283,15 @@ function _buildExtensions(extSpec, ctx) {
279
283
  * does not process the extension, so a critical one is a certificate this toolkit's own validator
280
284
  * must reject and pki.lint.certificate grades `unknown-critical-extension`. The default is the
281
285
  * usable form and the knob reaches the SHOULD-conforming one, as certificatePolicies does. */
282
- if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"), !!extSpec.policyMappingsCritical, _extPolicyMappings(extSpec.policyMappings)));
286
+ if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"), crit.policyMappingsCritical, _extPolicyMappings(extSpec.policyMappings)));
283
287
  if (extSpec.issuerAltName != null) guard.list.append(out, _ext(O("issuerAltName"), false, _encodeGeneralNames(_reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
284
288
  /** @internal pki.path.validate does not process qcStatements, so a critical one is rejected as an
285
289
  * unrecognized critical extension; the default is the form that validates and the knob reaches
286
290
  * the other, as policyMappings and certificatePolicies do. */
287
- if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"), !!extSpec.qcStatementsCritical, _extQcStatements(extSpec.qcStatements)));
291
+ if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"), crit.qcStatementsCritical, _extQcStatements(extSpec.qcStatements)));
288
292
  /** @internal RFC 7633 sec. 4: SHOULD NOT be critical, and the clause names the case where critical
289
293
  * is the desired behavior, so the knob exists and the default is the compatible form. */
290
- if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, _b.extTlsFeature(extSpec.tlsFeature)));
294
+ if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"), crit.tlsFeatureCritical, _b.extTlsFeature(extSpec.tlsFeature)));
291
295
  if (extSpec.nameConstraints != null) {
292
296
  if (!caTrue) throw _err("x509/bad-input", "nameConstraints appears only in a CA certificate, so it requires basicConstraints cA=TRUE (RFC 5280 sec. 4.2.1.10)");
293
297
  guard.list.append(out, _ext(O("nameConstraints"), true, _extNameConstraints(extSpec.nameConstraints)));
@@ -565,6 +569,14 @@ function _hasCriticalSan(extSpec) {
565
569
  * (positive, <= 20 octets), the validity UTCTime/GeneralizedTime cutover, the DER DEFAULT omissions
566
570
  * (v1 tag, `critical=FALSE`, `cA=FALSE`), and the CA cross-field rules (keyCertSign and
567
571
  * pathLenConstraint require cA=TRUE) are all enforced; a violation throws a typed `CertificateError`.
572
+ *
573
+ * Criticality is stated per extension: `keyUsageCritical` (default true), and
574
+ * `extendedKeyUsageCritical`, `certificatePoliciesCritical`, `policyMappingsCritical`,
575
+ * `qcStatementsCritical` and `tlsFeatureCritical` (each default false). `basicConstraints` carries
576
+ * its own `critical` inside the value. Each is a boolean: a value other than `true`, `false`,
577
+ * `null` or an absent flag is refused `x509/bad-input`. The extensions RFC 5280 fixes one way
578
+ * (`nameConstraints` critical, `authorityInfoAccess` and `cRLDistributionPoints` non-critical) take
579
+ * no flag.
568
580
  * Where the spec carries raw DER (a `Name` Buffer, a pre-encoded `Extension`, an issuer `publicKey`
569
581
  * SPKI), a structural fault throws `CertificateError`, while a malformed leaf inside those bytes
570
582
  * throws `Asn1Error`, the same two-error contract the parsers present.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
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",
@@ -87,6 +87,6 @@
87
87
  "devDependencies": {
88
88
  "c8": "12.0.0",
89
89
  "esbuild": "0.28.2",
90
- "eslint": "10.11.0"
90
+ "eslint": "10.12.0"
91
91
  }
92
92
  }
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:9f1767af-18a9-4a06-85fd-6273c947d9b9",
5
+ "serialNumber": "urn:uuid:e1f0a0ad-8621-4fc9-a62a-62df66fe4d21",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-12T04:46:50.282Z",
8
+ "timestamp": "2026-10-12T15:58:58.900Z",
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.9.4",
22
+ "bom-ref": "@blamejs/pki@0.9.5",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.9.4",
25
+ "version": "0.9.5",
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.9.4",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.9.5",
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.9.4",
57
+ "ref": "@blamejs/pki@0.9.5",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]