@blamejs/pki 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +7 -6
  3. package/bin/pki.js +110 -34
  4. package/lib/acme.js +91 -46
  5. package/lib/asn1-der.js +6 -5
  6. package/lib/attrcert-sign.js +4 -3
  7. package/lib/cmp-build.js +16 -6
  8. package/lib/cmp-session.js +26 -3
  9. package/lib/cmp-verify.js +24 -2
  10. package/lib/cms-compress.js +3 -3
  11. package/lib/cms-digest.js +3 -3
  12. package/lib/cms-verify.js +18 -8
  13. package/lib/compromised-keys.js +114 -0
  14. package/lib/constants.js +1 -0
  15. package/lib/crl-sign.js +3 -3
  16. package/lib/crmf-sign.js +2 -0
  17. package/lib/csr-sign.js +5 -1
  18. package/lib/ct.js +16 -3
  19. package/lib/est.js +44 -29
  20. package/lib/framework-error.js +20 -7
  21. package/lib/guard-all.js +2 -0
  22. package/lib/guard-distrust.js +344 -0
  23. package/lib/guard-json.js +1 -1
  24. package/lib/guard-verdict.js +16 -3
  25. package/lib/http-transport.js +412 -41
  26. package/lib/jose.js +1 -1
  27. package/lib/lint.js +49 -7
  28. package/lib/ocsp-verify.js +9 -3
  29. package/lib/ocsp.js +8 -4
  30. package/lib/oid.js +4 -0
  31. package/lib/path-validate.js +131 -16
  32. package/lib/possession.js +6 -2
  33. package/lib/related-cert.js +2 -2
  34. package/lib/revocation-fetch.js +39 -9
  35. package/lib/scep.js +28 -13
  36. package/lib/schema-pkix.js +3 -3
  37. package/lib/sigstore.js +11 -6
  38. package/lib/smime.js +18 -13
  39. package/lib/tls-cert-compress.js +6 -5
  40. package/lib/trust.js +159 -20
  41. package/lib/trustanchor-build.js +16 -1
  42. package/lib/tsp-sign.js +7 -1
  43. package/lib/webauthn-mds.js +30 -8
  44. package/lib/webauthn.js +46 -14
  45. package/lib/x509-sign.js +16 -5
  46. package/package.json +1 -1
  47. package/sbom.cdx.json +6 -6
package/lib/webauthn.js CHANGED
@@ -1,6 +1,14 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
+ // codebase-patterns:allow-file caller-option-presence-tested-against-undefined
5
+ /** @internal The `expected*` and `require*` options are SECURITY EXPECTATIONS, so presence
6
+ * is asked against `undefined` alone and a `null` is not read as an omission. A caller whose
7
+ * `expectedChallenge` arrives null, from an expired session or a missing field, must be
8
+ * refused rather than have the ceremony verified with the challenge comparison skipped:
9
+ * read as absence the check does not run, the verdict reports `checked.challenge` false, and
10
+ * the call still succeeds. `expectedTopOrigin: null` is a stated value on top of that, naming
11
+ * an unframed ceremony (WebAuthn sec. 5.8.1). */
4
12
  /**
5
13
  * @module pki.webauthn
6
14
  * @nav Validation
@@ -413,7 +421,7 @@ function _aaguidIsSigned(fmt) { return _AAGUID_SIGNED_BY_FMT[fmt] !== false; }
413
421
  var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), {
414
422
  time: 1, metadata: 1, tpmPolicy: 1, safetyNetRoots: 1, verifySafetyNetJws: 1, requireCtsProfileMatch: 1,
415
423
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
416
- rootCertificates: 1,
424
+ rootCertificates: 1, distrustedKeys: 1,
417
425
  clientDataJSON: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
418
426
  });
419
427
 
@@ -484,12 +492,12 @@ function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
484
492
  throw _err("webauthn/anchor-not-applicable",
485
493
  "opts.rootCertificates was supplied, but this attestation carries no trust path to anchor (format '" + res.fmt + "')");
486
494
  }
487
- var at = vopts.time !== undefined ? vopts.time : new intrinsic.Date();
495
+ var at = vopts.time != null ? vopts.time : new intrinsic.Date();
488
496
  var anchoredAll = intrinsic.reduce(paths, function (p, info) {
489
497
  return intrinsic.chain(p, function () {
490
- var pathAt = vopts.time !== undefined ? vopts.time : (info.at || at);
498
+ var pathAt = vopts.time != null ? vopts.time : (info.at || at);
491
499
  return mds.chainToAnchor(_reverse(intrinsic.copyList(info.tp)), roots, pathAt,
492
- "attestation trust path (against the roots supplied as opts.rootCertificates)");
500
+ "attestation trust path (against the roots supplied as opts.rootCertificates)", undefined, vopts.distrustedKeys);
493
501
  });
494
502
  }, _promiseResolve(_Promise, undefined));
495
503
  return intrinsic.chain(anchoredAll, function () {
@@ -559,6 +567,7 @@ function _applyBindings(authData, coseKey, opts) {
559
567
  "signature made by this credential would use SHA-1, so it is refused unless opts.allowedAlgorithms " +
560
568
  "names -65535 explicitly");
561
569
  }
570
+ // allow:caller-option-presence-tested-against-undefined -- absence means every supported algorithm is accepted, so a null must be refused rather than drop the allowlist
562
571
  if (opts.allowedAlgorithms !== undefined) {
563
572
  /** @internal The entries are required to be the list's OWN. A hole answers from the prototype at
564
573
  * that index, and the universal test skips it, so a sparse list with a number installed on
@@ -720,7 +729,7 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
720
729
  if (!intrinsic.isArray(roots) || roots.length === 0) {
721
730
  throw _err("webauthn/safetynet-no-root", "verifying an android-safetynet attestation requires opts.safetyNetRoots -- the Google root(s) to anchor the x5c chain to; this library bundles none (WebAuthn 8.5)");
722
731
  }
723
- if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
732
+ if (opts.time != null) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
724
733
 
725
734
  _requireAttShape(att.attStmt, ["ver", "response"], ["ver", "response"]);
726
735
  _attRead(att.attStmt, "ver", cbor.read.textString, "a text string");
@@ -783,10 +792,10 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
783
792
  var jwsTrusted = intrinsic.chain(_verifySig(-257, sigBytes, leaf.subjectPublicKeyInfo.bytes,
784
793
  intrinsic.bufferFrom(segs[0] + "." + segs[1], "ascii"), _err), function (ok) {
785
794
  if (!ok) throw _err("webauthn/verify-failed", "the android-safetynet JWS signature does not verify under the x5c leaf key");
786
- chainAt = opts.time !== undefined ? opts.time
795
+ chainAt = opts.time != null ? opts.time
787
796
  : (typeof payload.timestampMs === "number" && intrinsic.isFinite(payload.timestampMs) && payload.timestampMs > 0
788
797
  ? new intrinsic.Date(payload.timestampMs) : undefined);
789
- return _safetyNetChainTrusted(chain, roots, chainAt);
798
+ return _safetyNetChainTrusted(chain, roots, chainAt, opts.distrustedKeys);
790
799
  });
791
800
  return intrinsic.chain(jwsTrusted, function () {
792
801
  var res = guard.verdict.of(_result("android-safetynet", "Basic", chain, att), { safetyNet: signals });
@@ -879,7 +888,7 @@ function _safetyNetHostnameOk(leaf) {
879
888
  });
880
889
  }
881
890
 
882
- function _safetyNetChainTrusted(chain, roots, time) {
891
+ function _safetyNetChainTrusted(chain, roots, time, distrusted) {
883
892
  var anchors;
884
893
  try {
885
894
  anchors = intrinsic.mapList(roots, function (root, i) {
@@ -892,7 +901,7 @@ function _safetyNetChainTrusted(chain, roots, time) {
892
901
  });
893
902
  } catch (e) { return _promiseReject(_Promise, e); }
894
903
  return mds.chainToAnchor(chain, anchors, time === undefined ? new intrinsic.Date() : time,
895
- "android-safetynet x5c certificate chain", "webauthn/safetynet-cert-untrusted");
904
+ "android-safetynet x5c certificate chain", "webauthn/safetynet-cert-untrusted", distrusted);
896
905
  }
897
906
 
898
907
  function _result(fmt, attestationType, chain, att) {
@@ -1015,6 +1024,9 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1015
1024
  * allowedAlgorithms -- the COSE algorithms this relying party accepts
1016
1025
  * metadata -- a verifyMetadataBlob result; the model's own roots govern
1017
1026
  * rootCertificates -- trust anchors you pin, for the models no catalogue lists
1027
+ * distrustedKeys -- SHA-256 digests of SubjectPublicKeyInfo DER (64 hex characters
1028
+ * or 32 bytes); a listed key on a trust path refuses that path on
1029
+ * the metadata, rootCertificates and safetyNetRoots routes
1018
1030
  * time -- the instant certificate validity is judged at
1019
1031
  * tpmPolicy -- required TPM key properties; refuses a non-TPM attestation
1020
1032
  * safetyNetRoots / verifySafetyNetJws / requireCtsProfileMatch -- the
@@ -1045,11 +1057,16 @@ function verify(attestationObject, clientDataHash, opts) {
1045
1057
  /** @internal A caller who passes no options gets a record with NO prototype. Defaulted to `{}` it
1046
1058
  * inherits, so a property installed on `Object.prototype` reads as an option this call never set. */
1047
1059
  opts = opts || intrinsic.create(null);
1060
+ var distrusted;
1048
1061
  try {
1049
1062
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1050
1063
  guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1051
1064
  opts = guard.identifier.ownOptions(opts);
1052
- if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
1065
+ if (opts.time != null) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
1066
+ /** @internal Typed here, before any chain is walked, because the walk turns a validation
1067
+ * rejection into an untrusted verdict, and a malformed list must be reported as the caller's
1068
+ * input fault on every route. */
1069
+ distrusted = guard.distrust.keys(opts.distrustedKeys, _err, "webauthn/bad-input", "opts.distrustedKeys");
1053
1070
  /** @internal The walk comes from the capture. Each pass of it is a REFUSAL of a mistyped option,
1054
1071
  * and a mistyped one that survives is read for truthiness later where the rule wants exactly
1055
1072
  * `true`, so a walk that visits nothing admits `"true"` as the opt-in. */
@@ -1061,7 +1078,7 @@ function verify(attestationObject, clientDataHash, opts) {
1061
1078
  var attBytes, cdh, clientData = null;
1062
1079
  try {
1063
1080
  attBytes = _bytesArg(attestationObject, "attestationObject");
1064
- var haveJson = opts.clientDataJSON !== undefined, haveHash = clientDataHash !== undefined;
1081
+ var haveJson = opts.clientDataJSON != null, haveHash = clientDataHash !== undefined;
1065
1082
  if (haveJson === haveHash) {
1066
1083
  throw _err("webauthn/bad-input", "verify takes exactly one of clientDataJSON or the clientDataHash argument");
1067
1084
  }
@@ -1075,6 +1092,9 @@ function verify(attestationObject, clientDataHash, opts) {
1075
1092
  });
1076
1093
  cdh = _sha("sha256", cdjBytes);
1077
1094
  } else {
1095
+ /** @internal `expectedTopOrigin: null` states that the ceremony must be unframed, so
1096
+ * it is an expectation the caller supplied and counts as supplied here too. */
1097
+ // allow:caller-option-presence-tested-against-undefined
1078
1098
  if (opts.expectedChallenge !== undefined || opts.expectedOrigin !== undefined ||
1079
1099
  opts.expectedTopOrigin !== undefined) {
1080
1100
  throw _err("webauthn/bad-input",
@@ -1093,6 +1113,7 @@ function verify(attestationObject, clientDataHash, opts) {
1093
1113
  }
1094
1114
  var verifier = VERIFIERS[att.fmt];
1095
1115
  if (!verifier) return _promiseReject(_Promise, _err("webauthn/unsupported-format", "attestation statement format '" + att.fmt + "' is not supported"));
1116
+ // allow:caller-option-presence-tested-against-undefined -- absence means no TPM policy is applied, so a null must be refused rather than drop it
1096
1117
  if (opts.tpmPolicy !== undefined && !_formatCarries(att, "tpm")) {
1097
1118
  return _promiseReject(_Promise, _err("webauthn/tpm-policy", "opts.tpmPolicy requires a TPM attestation, but this attestation is format '" + att.fmt + "', which carries no TPM public area"));
1098
1119
  }
@@ -1105,6 +1126,7 @@ function verify(attestationObject, clientDataHash, opts) {
1105
1126
  vopts = guard.identifier.ownOptions(opts);
1106
1127
  vopts.rootCertificates = _snapshotRoots(opts.rootCertificates);
1107
1128
  vopts.safetyNetRoots = _snapshotRoots(opts.safetyNetRoots);
1129
+ vopts.distrustedKeys = distrusted;
1108
1130
  } catch (e) { return _promiseReject(_Promise, e); }
1109
1131
  var ranVerifier = intrinsic.chain(_promiseResolve(_Promise, undefined),
1110
1132
  function () { return verifier(att, clientDataHash, vopts); });
@@ -1112,7 +1134,9 @@ function verify(attestationObject, clientDataHash, opts) {
1112
1134
  return guard.verdict.of(res, { clientData: clientData });
1113
1135
  });
1114
1136
  var anchoredRes = intrinsic.chain(withClientData, function (res) {
1137
+ // allow:caller-option-presence-tested-against-undefined -- a trust input: absence falls back to signature-only verification, so a null must be refused rather than skip anchoring
1115
1138
  if (vopts.metadata === undefined) {
1139
+ // allow:caller-option-presence-tested-against-undefined -- a trust input: absence falls back to signature-only verification, so a null must be refused rather than skip anchoring
1116
1140
  if (vopts.rootCertificates !== undefined) return _applyCallerRoots(res, vopts.rootCertificates, vopts);
1117
1141
  var anchored = guard.verdict.of(res, { anchoredTo: _anchoredRoutes(res, null) });
1118
1142
  var sn = _safetyNetAnchored(anchored);
@@ -1127,6 +1151,7 @@ function verify(attestationObject, clientDataHash, opts) {
1127
1151
  function () { return _applyMetadata(res, att, vopts); });
1128
1152
  return intrinsic.chain(applyMd,
1129
1153
  function (out) { return guard.verdict.of(out, { anchoredTo: _anchoredRoutes(out, "metadata") }); }, function (e) {
1154
+ // allow:caller-option-presence-tested-against-undefined -- a trust input: absence falls back to signature-only verification, so a null must be refused rather than skip anchoring
1130
1155
  if (!e || e.code !== "webauthn/metadata-not-found" || vopts.rootCertificates === undefined) throw e;
1131
1156
  return intrinsic.chain(_promiseResolve(_Promise, _applyCallerRoots(res, vopts.rootCertificates, vopts, e.missedPaths)),
1132
1157
  function (withRoots) {
@@ -1186,7 +1211,7 @@ function _applyMetadata(res, att, opts) {
1186
1211
  guard.list.append(applied, { entry: entry, anchors: anchors, identifier: identifier });
1187
1212
  return { anchors: anchors, identifier: identifier };
1188
1213
  }
1189
- var at = opts.time !== undefined ? opts.time : new intrinsic.Date();
1214
+ var at = opts.time != null ? opts.time : new intrinsic.Date();
1190
1215
  mds.assertFresh(md, at, "the metadata supplied as opts.metadata");
1191
1216
  var governed = [];
1192
1217
  var missed = null;
@@ -1207,9 +1232,9 @@ function _applyMetadata(res, att, opts) {
1207
1232
  var governedAnchored = intrinsic.reduce(governed, function (p, item) {
1208
1233
  return intrinsic.chain(p, function () {
1209
1234
  var g = item.g, info = item.info;
1210
- var pathAt = opts.time !== undefined ? opts.time : (info.at || at);
1235
+ var pathAt = opts.time != null ? opts.time : (info.at || at);
1211
1236
  return mds.chainToAnchor(_reverse(intrinsic.copyList(info.tp)), g.anchors, pathAt,
1212
- "attestation trust path for " + g.identifier + " (against the roots its metadata entry registers)");
1237
+ "attestation trust path for " + g.identifier + " (against the roots its metadata entry registers)", undefined, opts.distrustedKeys);
1213
1238
  });
1214
1239
  }, _promiseResolve(_Promise, undefined));
1215
1240
  return intrinsic.chain(governedAnchored,
@@ -1272,6 +1297,9 @@ void constants;
1272
1297
  * @opts
1273
1298
  * - `rootCertificates` -- REQUIRED. The trust anchors the BLOB's own signing chain
1274
1299
  * must reach. A certificate, PEM, or DER bytes.
1300
+ * - `distrustedKeys` -- SHA-256 digests of SubjectPublicKeyInfo DER (64 hex characters
1301
+ * or 32 bytes). A listed key anywhere on the BLOB's signing chain, the root
1302
+ * included, refuses the BLOB as `webauthn/metadata-untrusted`.
1275
1303
  * - `time` -- the instant freshness is judged at. Defaults to now.
1276
1304
  * - `previousNo` -- the sequence number of the BLOB you already hold. A BLOB whose
1277
1305
  * `no` is not greater is refused as a rollback.
@@ -1507,6 +1535,10 @@ function parseClientData(bytes, opts) {
1507
1535
  }
1508
1536
  checked.origin = true;
1509
1537
  }
1538
+ /** @internal `null` is a VALUE here, not absence: it states that the ceremony must be
1539
+ * unframed, which the branch below checks. So presence is asked the one way that keeps
1540
+ * it, and an option left out is the only form that skips the check. */
1541
+ // allow:caller-option-presence-tested-against-undefined -- see above
1510
1542
  if (opts.expectedTopOrigin !== undefined) {
1511
1543
  var wantTop = opts.expectedTopOrigin;
1512
1544
  var framed = doc.crossOrigin === true;
package/lib/x509-sign.js CHANGED
@@ -384,11 +384,13 @@ function _resolveAltSigning(issuer, extSpec, issuerCert, selfSigned) {
384
384
  KNOWN_ALT_ISSUER_KEYS, _err, "x509/bad-input", "unknown alternative issuer field: ");
385
385
  var subjectAltSpki = _reqDer(subjectAltRaw, "extensions.subjectAltPublicKeyInfo (the subject's alternative SPKI DER)");
386
386
  _assertValidSpki(subjectAltSpki, "extensions.subjectAltPublicKeyInfo");
387
+ guard.distrust.refuseCompromised(subjectAltSpki, _err, "x509/compromised-key", "extensions.subjectAltPublicKeyInfo");
387
388
  if (issuer.altPublicKey == null) {
388
389
  throw _err("x509/bad-input", "an issuer altKey needs issuer.altPublicKey, the SPKI of its public half, to resolve the alternative signature algorithm");
389
390
  }
390
391
  var issuerAltSpki = _reqDer(issuer.altPublicKey, "issuer.altPublicKey (the issuer's alternative SPKI DER)");
391
392
  _assertValidSpki(issuerAltSpki, "issuer.altPublicKey");
393
+ guard.distrust.refuseCompromised(issuerAltSpki, _err, "x509/compromised-key", "issuer.altPublicKey");
392
394
  /** @internal Bound to the issuer, the way the native signature already is: its key is read OUT of
393
395
  * issuer.cert, while this one arrives beside the certificate, so nothing otherwise stops a CA signing
394
396
  * with a second alternative pair. Clause 9.8.4 has a relying party check an alternative signature with
@@ -555,7 +557,9 @@ function _hasCriticalSan(extSpec) {
555
557
  * fail-closed into its form (`"host.example"`, `"a@b.example"`, `"10.0.0.1"`, `"https://host.example/"`).
556
558
  * The signature algorithm is resolved from the signing key: RSA (PKCS#1 v1.5
557
559
  * or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm, so every algorithm the
558
- * toolkit signs with is available here without a per-algorithm branch.
560
+ * toolkit signs with is available here without a per-algorithm branch. A subject key, issuer key or
561
+ * alternative key whose private key is public (see `pki.trust.knownCompromisedKeys`) is refused with
562
+ * `x509/compromised-key`.
559
563
  *
560
564
  * The version is derived from the field set (v3 when extensions are present, else v1). Two
561
565
  * extensions are emitted without being named, because RFC 5280 places them on the issuing CA: a
@@ -741,6 +745,7 @@ function _sign(spec, issuer, opts) {
741
745
 
742
746
  var spki = _reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the certified key)");
743
747
  _assertValidSpki(spki, "spec.subjectPublicKey");
748
+ guard.distrust.refuseCompromised(spki, _err, "x509/compromised-key", "spec.subjectPublicKey");
744
749
  var subjectDer = _encodeName(spec.subject == null ? [] : spec.subject);
745
750
  var subjectEmpty = _isEmptyName(subjectDer);
746
751
 
@@ -754,10 +759,12 @@ function _sign(spec, issuer, opts) {
754
759
  issuerPathLen = _assertIssuerIsCa(issuerCert);
755
760
  issuerDer = pkiBuild.tbsNameField(issuerCert, "subject");
756
761
  issuerSpki = issuerCert.subjectPublicKeyInfo.bytes;
762
+ guard.distrust.refuseCompromised(issuerSpki, _err, "x509/compromised-key", "the key of issuer.cert");
757
763
  } else {
758
764
  issuerDer = _encodeName(issuer.name == null ? [] : issuer.name);
759
765
  issuerSpki = _reqDer(issuer.publicKey, "issuer.publicKey (the issuer SPKI DER)");
760
766
  _assertValidSpki(issuerSpki, "issuer.publicKey");
767
+ guard.distrust.refuseCompromised(issuerSpki, _err, "x509/compromised-key", "issuer.publicKey");
761
768
  }
762
769
  if (_isEmptyName(issuerDer)) throw _err("x509/bad-issuer", "issuer must be a non-empty distinguished name");
763
770
 
@@ -971,14 +978,18 @@ function _extensionRow(name) {
971
978
  if (oid.isDottedDecimal(name)) return intrinsic.hasOwn(EXTENSION_BY_OID, name) ? EXTENSION_BY_OID[name] : null;
972
979
  throw _err("x509/bad-input", "unknown extension " + guard.text.showValue(name) + "; the names spec.extensions takes are accepted, and any other extension is named by its dotted OID with its extnValue pre-encoded");
973
980
  }
981
+ /** @internal `stated` is the caller's option, carried here from the door that typed it,
982
+ * so absence has the same two forms there and here: a `null` states no criticality and
983
+ * takes the row's default, the way an omitted option does. Read as stated, it was
984
+ * returned AS the criticality and reached the emitter as a non-boolean. */
974
985
  function _extensionCritical(row, opts) {
975
986
  var rule = row.rule, stated = opts.critical;
976
987
  if (rule.inValue) {
977
- if (stated !== undefined) throw _err("x509/bad-input", row.name + " carries its criticality in the value ({ cA, pathLen, critical }), so opts.critical does not apply");
988
+ if (stated != null) throw _err("x509/bad-input", row.name + " carries its criticality in the value ({ cA, pathLen, critical }), so opts.critical does not apply");
978
989
  return null;
979
990
  }
980
- if (rule.knob) return stated === undefined ? rule.dflt : stated;
981
- if (stated !== undefined && stated !== rule.fixed) {
991
+ if (rule.knob) return stated == null ? rule.dflt : stated;
992
+ if (stated != null && stated !== rule.fixed) {
982
993
  throw _err("x509/bad-input", row.name + " is emitted " + (rule.fixed ? "critical" : "non-critical") + " (" + rule.cite + "), so opts.critical cannot say otherwise");
983
994
  }
984
995
  return rule.fixed;
@@ -1037,7 +1048,7 @@ function extension(name, value, opts) {
1037
1048
  function _extension(name, value, opts) {
1038
1049
  opts = guard.identifier.optionsObject(opts, _err, "x509/bad-input", "pki.x509.extension options");
1039
1050
  guard.identifier.assertKnownKeys(opts, KNOWN_EXTENSION_OPTS, _err, "x509/bad-input", "pki.x509.extension has an unknown option ");
1040
- if (opts.critical !== undefined && typeof opts.critical !== "boolean") throw _err("x509/bad-input", "opts.critical must be a boolean");
1051
+ if (opts.critical != null && typeof opts.critical !== "boolean") throw _err("x509/bad-input", "opts.critical must be a boolean");
1041
1052
  if (value === undefined || value === null) throw _err("x509/bad-input", "the extension value is required");
1042
1053
  var row = _extensionRow(name);
1043
1054
  if (row === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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:e1f0a0ad-8621-4fc9-a62a-62df66fe4d21",
5
+ "serialNumber": "urn:uuid:1f2b6ec8-3dcd-4f4f-abc6-92aeb5ba8795",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-12T15:58:58.900Z",
8
+ "timestamp": "2026-10-21T20:18:37.023Z",
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.5",
22
+ "bom-ref": "@blamejs/pki@0.9.7",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.9.5",
25
+ "version": "0.9.7",
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.5",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.9.7",
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.5",
57
+ "ref": "@blamejs/pki@0.9.7",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]