@blamejs/pki 0.5.6 → 0.5.8

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 (104) hide show
  1. package/CHANGELOG.md +419 -378
  2. package/MIGRATING.md +65 -0
  3. package/README.md +12 -12
  4. package/lib/acme.js +31 -31
  5. package/lib/asn1-der.js +10 -10
  6. package/lib/attrcert-sign.js +24 -20
  7. package/lib/byte-reader.js +6 -6
  8. package/lib/byte-writer.js +5 -5
  9. package/lib/cbor-det.js +27 -24
  10. package/lib/cmc-build.js +104 -104
  11. package/lib/cmc-verify.js +108 -32
  12. package/lib/cmp-build.js +31 -26
  13. package/lib/cmp-session.js +74 -72
  14. package/lib/cmp-verify.js +72 -58
  15. package/lib/cms-compress.js +8 -9
  16. package/lib/cms-decrypt.js +92 -76
  17. package/lib/cms-encrypt.js +33 -34
  18. package/lib/cms-sign.js +100 -54
  19. package/lib/cms-verify.js +141 -82
  20. package/lib/composite-sig.js +13 -13
  21. package/lib/constants.js +4 -4
  22. package/lib/crl-sign.js +31 -25
  23. package/lib/crl-verify.js +7 -6
  24. package/lib/crmf-sign.js +19 -15
  25. package/lib/csr-sign.js +13 -9
  26. package/lib/ct.js +37 -37
  27. package/lib/edwards-point.js +7 -7
  28. package/lib/est.js +103 -59
  29. package/lib/framework-error.js +5 -5
  30. package/lib/guard-all.js +3 -3
  31. package/lib/guard-async.js +4 -4
  32. package/lib/guard-bytes.js +378 -15
  33. package/lib/guard-compress.js +17 -17
  34. package/lib/guard-crypto.js +1 -1
  35. package/lib/guard-encoding.js +15 -15
  36. package/lib/guard-header.js +3 -3
  37. package/lib/guard-identifier.js +16 -16
  38. package/lib/guard-json.js +15 -15
  39. package/lib/guard-limits.js +7 -7
  40. package/lib/guard-name.js +81 -16
  41. package/lib/guard-parsed.js +144 -75
  42. package/lib/guard-range.js +19 -19
  43. package/lib/guard-secret.js +11 -10
  44. package/lib/guard-text.js +6 -6
  45. package/lib/guard-time.js +10 -10
  46. package/lib/hpke.js +18 -17
  47. package/lib/http-digest.js +35 -35
  48. package/lib/http-retry-after.js +13 -13
  49. package/lib/http-transport.js +20 -19
  50. package/lib/inspect.js +53 -53
  51. package/lib/ip-utils.js +2 -2
  52. package/lib/jose.js +13 -13
  53. package/lib/key.js +16 -16
  54. package/lib/lint.js +51 -51
  55. package/lib/merkle.js +51 -36
  56. package/lib/mime.js +18 -18
  57. package/lib/ocsp-verify.js +10 -10
  58. package/lib/ocsp.js +32 -20
  59. package/lib/oid.js +29 -29
  60. package/lib/path-validate.js +114 -113
  61. package/lib/pbes2.js +16 -16
  62. package/lib/pkcs12-build.js +71 -56
  63. package/lib/pki-build.js +23 -22
  64. package/lib/rc2.js +1 -1
  65. package/lib/rfc3339.js +5 -5
  66. package/lib/schema-all.js +31 -31
  67. package/lib/schema-attrcert.js +12 -12
  68. package/lib/schema-c509.js +144 -142
  69. package/lib/schema-cmc.js +58 -58
  70. package/lib/schema-cmp.js +43 -43
  71. package/lib/schema-cms.js +169 -36
  72. package/lib/schema-crl.js +7 -7
  73. package/lib/schema-crmf.js +28 -28
  74. package/lib/schema-csr.js +12 -12
  75. package/lib/schema-csrattrs.js +16 -16
  76. package/lib/schema-engine.js +18 -18
  77. package/lib/schema-ocsp.js +15 -15
  78. package/lib/schema-pkcs12.js +20 -20
  79. package/lib/schema-pkcs8.js +2 -2
  80. package/lib/schema-pkix.js +131 -126
  81. package/lib/schema-smime.js +19 -19
  82. package/lib/schema-tsp.js +12 -12
  83. package/lib/schema-x509.js +3 -3
  84. package/lib/shbs.js +18 -18
  85. package/lib/sign-scheme.js +19 -16
  86. package/lib/sigstore.js +10 -11
  87. package/lib/sleep.js +1 -1
  88. package/lib/smime.js +308 -96
  89. package/lib/tls-cert-compress.js +18 -18
  90. package/lib/trust.js +27 -27
  91. package/lib/tsp-sign.js +22 -18
  92. package/lib/validator-all.js +1 -1
  93. package/lib/validator-attcert.js +1 -1
  94. package/lib/validator-cose.js +43 -44
  95. package/lib/validator-keydesc.js +3 -3
  96. package/lib/validator-sig.js +13 -13
  97. package/lib/validator-tls.js +11 -11
  98. package/lib/validator-tpm.js +21 -20
  99. package/lib/webauthn-mds.js +67 -67
  100. package/lib/webauthn.js +34 -34
  101. package/lib/webcrypto.js +15 -15
  102. package/lib/x509-sign.js +24 -15
  103. package/package.json +3 -2
  104. package/sbom.cdx.json +6 -6
package/lib/guard-time.js CHANGED
@@ -10,19 +10,19 @@
10
10
  //
11
11
  // Defends the NaN-Date fail-open class (CWE-20 improper input validation feeding
12
12
  // a silent security-gate bypass). An Invalid Date is still `instanceof Date`, and
13
- // EVERY relational comparison against its NaN getTime() (NaN < x, NaN >= x) is
14
- // false -- so a validity / not-before / not-after / currency window built on an
15
- // unvalidated Date silently ACCEPTS when it should reject. The class recurred at
13
+ // every relational comparison against its NaN getTime() (NaN < x, NaN >= x) is
14
+ // false, so a validity / not-before / not-after / currency window built on an
15
+ // unvalidated Date silently accepts when it should reject. The class recurred at
16
16
  // three independent boundaries (TSP genTime, OCSP producedAt/thisUpdate, CT
17
17
  // temporal-interval) before it was centralized here; the sibling
18
18
  // nan-date-comparison-unguarded codebase-patterns detector flags any lib function
19
- // that re-inlines a `.getTime()` comparison WITHOUT first rejecting a NaN, so a
20
- // new boundary is routed to this guard rather than re-growing the bug.
19
+ // that re-inlines a `.getTime()` comparison without first rejecting a NaN, so a
20
+ // new boundary is routed to this guard instead of re-growing the bug.
21
21
  //
22
- // Tier split: `value instanceof Date` with a NaN time is malformed CALLER input
23
- // (a config-time / entry-point Date -- an operator-supplied distrustAfter, a
24
- // caller `opts.time`), so assertValid throws the caller's typed error rather than
25
- // returning a silent default. within() is the fail-closed window primitive: it
22
+ // Tier split: `value instanceof Date` with a NaN time is malformed caller input
23
+ // (a config-time or entry-point Date, such as an operator-supplied distrustAfter
24
+ // or a caller `opts.time`), so assertValid throws the caller's typed error instead
25
+ // of returning a silent default. within() is the fail-closed window primitive: it
26
26
  // THROWS on a malformed operand but RETURNS a boolean for in/out-of-window, so the
27
27
  // OCSP / CRL currency callers that treat out-of-window as a `continue` skip keep
28
28
  // their control flow while a NaN operand can never slip through as a false.
@@ -30,7 +30,7 @@
30
30
  // assertValid(value, E, code, label) -> the same Date, once proven valid.
31
31
  // value : a Date (or an alleged one) from a caller boundary.
32
32
  // E : the (code, message[, cause]) typed-error FACTORY in scope at the call
33
- // site (ns.E / the module-local _err) -- NEVER a defineClass class, which
33
+ // site (ns.E / the module-local _err), never a defineClass class, which
34
34
  // would crash `class cannot be invoked without new` on the error path.
35
35
  // code : the frozen domain/reason code this boundary rejects malformed time under.
36
36
  // label : field phrase for the message.
package/lib/hpke.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * Appendix A known-answer vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered
15
15
  * but Appendix A ships no test vector for them, so they are omitted (a request
16
16
  * fails closed) until an authoritative KAT is available. Pure composition over
17
- * node:crypto -- no ASN.1, no schema engine. Post-quantum KEMs (ML-KEM, X-Wing)
17
+ * node:crypto: no ASN.1, no schema engine. Post-quantum KEMs (ML-KEM, X-Wing)
18
18
  * are a data-row extension the registry is shaped to admit once their
19
19
  * specifications stabilize.
20
20
  * The RFC 9180 sec. 7 registry code points live in pki.hpke.suites
@@ -44,14 +44,15 @@ function xor(a, b) { var o = Buffer.alloc(a.length); for (var i = 0; i < a.lengt
44
44
  var HPKE_V1 = Buffer.from("HPKE-v1", "ascii");
45
45
  function L(s) { return Buffer.from(s, "ascii"); }
46
46
 
47
- // ---- registries (RFC 9180 sec. 7, Tables 2/3/5 -- data, not switch) ----------
47
+ // ---- registries: the RFC 9180 sec. 7 data tables (Tables 2/3/5) --------------
48
48
 
49
49
  // KDF: id -> { hash (node name), Nh }.
50
50
  // HKDF-SHA384 (0x0002) and DHKEM(P-384) (0x0011) are registered by RFC 9180 sec.
51
51
  // 7 but Appendix A / the cited [TestVectors] file ship no known-answer vector for
52
- // them, so they are omitted here until an authoritative KAT exists -- a request
53
- // for either fails closed as hpke/unknown-suite rather than running crypto no
54
- // test vector proves. They admit as a one-row addition the moment a KAT lands.
52
+ // them, so they are omitted here until an authoritative KAT exists. A request for
53
+ // either fails closed as hpke/unknown-suite, because no test vector proves the
54
+ // crypto it would otherwise run. They admit as a one-row addition the moment a
55
+ // KAT lands.
55
56
  var KDFS = {
56
57
  0x0001: { hash: "sha256", Nh: 32 },
57
58
  0x0003: { hash: "sha512", Nh: 64 },
@@ -112,7 +113,7 @@ function _expand(hash, Nh, prk, info, len) {
112
113
  return exact;
113
114
  }
114
115
  function _labeledExtract(kdf, suiteId, salt, label, ikm) {
115
- // The concatenation copies the input keying material -- a DH secret or a PSK -- into a fresh
116
+ // The concatenation copies the input keying material (a DH secret or a PSK) into a fresh
116
117
  // buffer, so that copy is cleared once extract has consumed it. The caller still owns .
117
118
  var labeled = concat([HPKE_V1, suiteId, L(label), ikm]);
118
119
  try { return _extract(kdf.hash, kdf.Nh, salt, labeled); }
@@ -174,7 +175,7 @@ function _importPrivate(kem, rawSk, rawPk) {
174
175
  // The single KEM Diffie-Hellman choke point. node:crypto throws a raw
175
176
  // ERR_OSSL_FAILED_DURING_DERIVATION when the peer point is low-order / invalid
176
177
  // (an all-zero X25519 shared secret, a point not on the curve). RFC 9180 sec.
177
- // 4.1: Decap raises an error on DH failure -- surface it as a typed hpke/bad-key
178
+ // 4.1: Decap raises an error on DH failure. Surface it as a typed hpke/bad-key
178
179
  // so no raw OpenSSL error escapes any encap / decap path.
179
180
  function _dh(privateKey, publicKey) {
180
181
  try {
@@ -211,9 +212,9 @@ function _ephemeral(kem, eph) {
211
212
  // The raw DH output is the KEM's shared secret before extraction: it is this module's allocation
212
213
  // and is cleared once _extractAndExpand has consumed it, on every DHKEM arm. In the authenticated
213
214
  // arms builds a THIRD copy holding both DH results, so the concatenation is cleared as
214
- // well -- clearing only the pieces would leave the joined buffer readable.
215
+ // well, since clearing only the pieces would leave the joined buffer readable.
215
216
  function _wipeDh(dh) { guard.secret.zeroize(dh, HpkeError, "hpke/bad-input", "the KEM shared secret"); }
216
- // EVERY agreement happens inside the protected region. In the authenticated modes the second
217
+ // Every agreement happens inside the protected region. In the authenticated modes the second
217
218
  // agreement can throw on an attacker-supplied low-order sender key, and a first secret computed
218
219
  // outside the region would be left behind on exactly that failure path.
219
220
  function _encap(kem, pkR, pkRm, eph) {
@@ -361,7 +362,7 @@ function _recipPublic(suite, pk) {
361
362
  if (Buffer.isBuffer(pk)) return { key: _importPublic(suite.kem, pk), pkm: pk };
362
363
  if (pk && pk.pkm) return { key: _importPublic(suite.kem, pk.pkm), pkm: pk.pkm };
363
364
  // A node KeyObject: derive its wire form. A null/undefined/invalid value must
364
- // fail closed here rather than let node's export throw a raw error upward.
365
+ // fail closed here, so no raw error from node's export escapes upward.
365
366
  try {
366
367
  return { key: pk, pkm: _exportPublic(suite.kem, pk) };
367
368
  } catch (e) {
@@ -373,8 +374,8 @@ function _recipPrivate(suite, sk) {
373
374
  if (sk && sk.skm) return { key: _importPrivate(suite.kem, sk.skm, sk.pkm), pkm: sk.pkm };
374
375
  // A raw scalar alone cannot be imported (an EC private key needs its public
375
376
  // point for the JWK), so the serialized private form is { skm, pkm }; reject a
376
- // bare buffer, and fail any other bad key closed rather than let node's
377
- // createPublicKey throw a raw error out of the setup path.
377
+ // bare buffer, and fail any other bad key closed; a raw createPublicKey error
378
+ // must not escape the setup path.
378
379
  if (Buffer.isBuffer(sk)) throw _err("hpke/bad-key", "a serialized private key must be provided as { skm, pkm } (raw scalar + public point), not a bare buffer");
379
380
  try {
380
381
  var pkm = _exportPublic(suite.kem, nodeCrypto.createPublicKey(sk));
@@ -389,9 +390,9 @@ function _recipPrivate(suite, sk) {
389
390
  // psk / auth / auth-psk. An unknown mode must fail closed, never key-schedule
390
391
  // with an out-of-registry mode byte.
391
392
  // The sender and the recipient read the same option object from OPPOSITE ends, so each direction
392
- // gets its own table rather than sharing their union. A union recognises every name at both ends and
393
- // so accepts the one that cannot do anything there: `senderPublicKey` handed to setupS, or
394
- // `senderKey` handed to setupR, is silently ignored -- which is the exact silence these tables exist
393
+ // gets its own table. A shared union recognises every name at both ends and so accepts the one that
394
+ // cannot do anything there: `senderPublicKey` handed to setupS, or
395
+ // `senderKey` handed to setupR, is silently ignored, which is the exact silence these tables exist
395
396
  // to remove, in a wider form. An option that means nothing where it was passed is a misunderstanding
396
397
  // worth reporting, and usually a misdirected auth-mode setup.
397
398
  //
@@ -491,8 +492,8 @@ function _setupS(ids, pkR, opts) {
491
492
  * @related pki.hpke.setupS, pki.hpke.open
492
493
  *
493
494
  * Establish the recipient HPKE context from the sender's encapsulated key `enc`
494
- * and the recipient KEM private key (a node KeyObject or `{ skm, pkm }` -- the
495
- * raw private scalar plus its public point),
495
+ * and the recipient KEM private key (a node KeyObject, or `{ skm, pkm }` holding
496
+ * the raw private scalar plus its public point),
496
497
  * recovering the same shared secret and key schedule. The returned `context`
497
498
  * `.open(aad, ct)` decrypts and `.export(ctx, L)` derives secrets. `opts` mirrors
498
499
  * `setupS` (mode / info / psk / pskId), with `opts.senderPublicKey` for the auth
@@ -2,20 +2,20 @@
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
4
  //
5
- // @internal -- NOT on pki.*. HTTP Digest access authentication (RFC 7616): parse an UNTRUSTED
5
+ // @internal -- not on pki.*. HTTP Digest access authentication (RFC 7616): parse an untrusted
6
6
  // WWW-Authenticate challenge (fail-closed, quoted-string-honoring) and compute the Authorization
7
7
  // response header (byte-exact A1/A2/KD). Prefix-agnostic: every thrown code comes from the caller's
8
- // E(code, msg) FACTORY (never a defineClass class -- feedback_guard_error_factory_not_class) and its
8
+ // E(code, msg) factory, never a defineClass class (which would crash on the error path), plus its
9
9
  // policy.codes map, so pki.est composes it now and pki.acme / pki.cmp can reuse it as config, not a fork.
10
10
  //
11
- // The challenge is attacker-shaped: a comma inside a quoted value is NOT a delimiter (the _hasBasicChallenge
12
- // substring-scan bug class), a missing realm / nonce is REJECTED not defaulted, and an unsupported / weak
13
- // algorithm or a no-qop challenge fails closed rather than downgrading (feedback_guards_fail_closed_not_guess).
11
+ // The challenge is attacker-shaped: a comma inside a quoted value is not a delimiter (the _hasBasicChallenge
12
+ // substring-scan bug class), a missing realm or nonce is rejected and not defaulted, and an unsupported or
13
+ // weak algorithm, or a no-qop challenge, fails closed instead of downgrading.
14
14
 
15
15
  var crypto = require("crypto");
16
16
  var constants = require("./constants");
17
17
 
18
- // Algorithm registry (a data row, not a switch -- Hard rule #2): rank orders the RFC 7616 sec. 3.7
18
+ // Algorithm registry, a data row and not a switch: rank orders the RFC 7616 sec. 3.7
19
19
  // "most secure the client can use" selection (SHA-512-256 > SHA-256 > MD5); an unknown algorithm ranks 0.
20
20
  var ALGS = {
21
21
  "SHA-512-256": { hash: "sha512-256", rank: 3, sess: false }, "SHA-512-256-SESS": { hash: "sha512-256", rank: 3, sess: true },
@@ -33,7 +33,7 @@ function H(hash, s) { return crypto.createHash(hash).update(s, "latin1").digest(
33
33
  function KD(hash, secret, data) { return H(hash, secret + ":" + data); }
34
34
  function _qstr(v) { return "\"" + String(v).replace(/(["\\])/g, "\\$1") + "\""; }
35
35
 
36
- // Split a header at commas that are OUTSIDE a quoted-string (a quoted comma is a literal, not a delimiter).
36
+ // Split a header at commas outside a quoted-string (a quoted comma is a literal, not a delimiter).
37
37
  function _commaSplitOutsideQuotes(s) {
38
38
  var out = [], buf = "", inQ = false, esc = false;
39
39
  for (var i = 0; i < s.length; i++) {
@@ -47,7 +47,7 @@ function _commaSplitOutsideQuotes(s) {
47
47
  out.push(buf);
48
48
  return out;
49
49
  }
50
- // Index of the first '=' OUTSIDE a quoted-string, or -1.
50
+ // Index of the first '=' outside a quoted-string, or -1.
51
51
  function _firstEqOutsideQuotes(s) {
52
52
  var inQ = false, esc = false;
53
53
  for (var i = 0; i < s.length; i++) {
@@ -60,7 +60,7 @@ function _firstEqOutsideQuotes(s) {
60
60
  return -1;
61
61
  }
62
62
  function _unq(s) { return s.slice(1, -1).replace(/\\(.)/g, "$1"); } // only ever called on a well-formed quoted-string (see _closedQuote)
63
- // A well-formed quoted-string: opens with ", closes with an UNESCAPED " that is the LAST character (nothing trails
63
+ // A well-formed quoted-string: opens with ", closes with an unescaped " as the last character (nothing trails
64
64
  // it). An unterminated / trailing-garbage quoted-string is malformed (RFC 7230 sec. 3.2.6) and rejected.
65
65
  function _closedQuote(s) {
66
66
  if (s.length < 2 || s.charAt(0) !== "\"") return false;
@@ -74,7 +74,7 @@ function _closedQuote(s) {
74
74
  return false;
75
75
  }
76
76
  // A control octet (RFC 7230 sec. 3.2.6 quoted-string / qdtext forbids CTL except HTAB). Scanned with charCodeAt
77
- // rather than a control-char regex literal (which eslint no-control-regex correctly refuses).
77
+ // instead of a control-char regex literal (which eslint no-control-regex correctly refuses).
78
78
  function _hasCtl(s) {
79
79
  for (var i = 0; i < s.length; i++) { var c = s.charCodeAt(i); if ((c < 0x20 && c !== 0x09) || c === 0x7f) return true; }
80
80
  return false;
@@ -119,7 +119,7 @@ function _parseParams(paramText, E, code) {
119
119
  }
120
120
  return map;
121
121
  }
122
- // Structural validation of ONE Digest challenge's params (RFC 7616 sec. 3.3). Throws on any violation.
122
+ // Structural validation of one Digest challenge's params (RFC 7616 sec. 3.3). Throws on any violation.
123
123
  function _validateDigest(paramText, E, code) {
124
124
  var p = _parseParams(paramText, E, code);
125
125
  if (!p.realm || !p.realm.quoted || p.realm.value === "") throw E(code, "a Digest challenge requires a non-empty quoted realm (RFC 7616 sec. 3.3)");
@@ -129,38 +129,38 @@ function _validateDigest(paramText, E, code) {
129
129
  if (p.qop) {
130
130
  if (!p.qop.quoted) throw E(code, "the Digest qop must be a quoted list (RFC 7616 sec. 3.3)");
131
131
  qop = p.qop.value.split(",").map(function (x) { return x.replace(/^\s+|\s+$/g, "").toLowerCase(); }).filter(Boolean);
132
- // A qop directive that is PRESENT but lists no value (qop="" / qop=", ,") is malformed -- it is NOT the
133
- // absent-qop (RFC 2069) case. Reject it rather than silently collapsing to a no-qop response the server
134
- // cannot accept (RFC 7616 sec. 3.3). Only an OMITTED qop directive selects the legacy no-qop path.
132
+ // A qop directive that is present but lists no value (qop="" / qop=", ,") is malformed, and is not the
133
+ // absent-qop (RFC 2069) case. Reject it instead of silently collapsing to a no-qop response the server
134
+ // cannot accept (RFC 7616 sec. 3.3). Only an omitted qop directive selects the legacy no-qop path.
135
135
  if (qop.length === 0) throw E(code, "a present Digest qop directive must list at least one value (RFC 7616 sec. 3.3)");
136
136
  }
137
- // domain (RFC 7616 sec. 3.3): a QUOTED, space-separated list of URIs defining the protection space. An
138
- // UNQUOTED domain is malformed and rejected (fail closed like realm / nonce / qop) rather than silently
139
- // widened -- widening a mis-encoded scope to "the whole server" would send credentials MORE broadly than the
140
- // server intended. Surfaced as an array of the listed URIs, or null when omitted / quoted-empty -- which per
137
+ // domain (RFC 7616 sec. 3.3): a quoted, space-separated list of URIs defining the protection space. An
138
+ // unquoted domain is malformed and rejected (fail closed like realm / nonce / qop) instead of silently
139
+ // widened: widening a mis-encoded scope to "the whole server" would send credentials more broadly than the
140
+ // server intended. Surfaced as an array of the listed URIs, or null when omitted or quoted-empty, which per
141
141
  // the RFC means "all URIs on the responding server" (the answer may be reused for any same-origin URI, sec. 3.5).
142
142
  if (p.domain && !p.domain.quoted) throw E(code, "the Digest domain must be a quoted-string (RFC 7616 sec. 3.3)");
143
143
  var domain = (p.domain && p.domain.value.replace(/^\s+|\s+$/g, "") !== "")
144
144
  ? p.domain.value.replace(/^\s+|\s+$/g, "").split(/\s+/) : null;
145
- // charset (RFC 7616 sec. 3.3): the ONLY permitted value is the UNQUOTED token "UTF-8". A quoted charset, or any
146
- // other value, is malformed -- answering it would hash the credentials in the wrong encoding -- so it is
145
+ // charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
146
+ // other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
147
147
  // rejected (and thus skipped during multi-offer selection in favour of a conforming offer).
148
148
  if (p.charset && (p.charset.quoted || String(p.charset.value).toUpperCase() !== "UTF-8")) throw E(code, "the Digest charset must be the unquoted token UTF-8 (RFC 7616 sec. 3.3)");
149
149
  // stale (RFC 7616 sec. 3.3) is the unquoted token "true" or "false". A quoted or otherwise invalid value
150
- // (stale=maybe) is malformed and rejected (thus skipped in selection) rather than parsed as a false flag that
150
+ // (stale=maybe) is malformed and rejected (thus skipped in selection), never parsed as a false flag that
151
151
  // could shadow a conforming offer.
152
152
  if (p.stale) {
153
153
  var sv = String(p.stale.value).toLowerCase();
154
154
  if (p.stale.quoted || (sv !== "true" && sv !== "false")) throw E(code, "the Digest stale directive must be the unquoted token true or false (RFC 7616 sec. 3.3)");
155
155
  }
156
156
  // userhash (RFC 7616 sec. 3.3) is likewise the unquoted token "true" or "false". A quoted or otherwise invalid
157
- // value is malformed and rejected (skipped in selection) rather than silently changing how the username is sent.
157
+ // value is malformed and rejected (skipped in selection), never silently changing how the username is sent.
158
158
  if (p.userhash) {
159
159
  var uhv = String(p.userhash.value).toLowerCase();
160
160
  if (p.userhash.quoted || (uhv !== "true" && uhv !== "false")) throw E(code, "the Digest userhash directive must be the unquoted token true or false (RFC 7616 sec. 3.3)");
161
161
  }
162
- // opaque (RFC 7616 sec. 3.3) is a quoted-string echoed back verbatim. An UNQUOTED opaque is malformed and
163
- // rejected (skipped in selection) rather than accepted, where it could shadow a conforming offer.
162
+ // opaque (RFC 7616 sec. 3.3) is a quoted-string echoed back verbatim. An unquoted opaque is malformed and
163
+ // rejected (skipped in selection), never accepted, where it could shadow a conforming offer.
164
164
  if (p.opaque && !p.opaque.quoted) throw E(code, "the Digest opaque directive must be a quoted-string (RFC 7616 sec. 3.3)");
165
165
  return {
166
166
  scheme: "Digest", realm: p.realm.value, nonce: p.nonce.value, qop: qop, domain: domain,
@@ -177,9 +177,9 @@ function _validateDigest(paramText, E, code) {
177
177
  // Selection is policy-aware: a challenge answer() would refuse under the active policy (an unsupported /
178
178
  // MD5-when-disallowed algorithm, or a qop this client cannot satisfy) is ranked BELOW every usable one, so a
179
179
  // higher-algorithm-rank but unusable offer (e.g. a SHA-512-256 no-qop challenge under the default policy)
180
- // never shadows a lower-ranked usable one (e.g. SHA-256 with qop="auth") -- RFC 7616 sec. 3.3. When NO
180
+ // never shadows a lower-ranked usable one (e.g. SHA-256 with qop="auth"), per RFC 7616 sec. 3.3. When no
181
181
  // challenge is usable the highest-algorithm-rank one is still returned, so answer() reports the specific
182
- // policy reason (which opt to set) rather than a generic "no challenge".
182
+ // policy reason (which opt to set) in place of a generic "no challenge".
183
183
  function parseChallenge(www, E, code, policy) {
184
184
  var pol = policy || {};
185
185
  var codes = pol.codes || DEFAULT_CODES;
@@ -210,9 +210,9 @@ function parseChallenge(www, E, code, policy) {
210
210
  // so it must not shadow a weaker offer that does apply. Only checked when the caller supplies the request
211
211
  // context; otherwise every offer is treated as applicable.
212
212
  var applicable = pol.requestTarget === undefined ? true : inProtectionSpace(parsed, pol.requestOrigin, pol.requestTarget);
213
- // A RETRYABLE offer (only meaningful in the rejection context) is stale=true, carries a FRESH nonce, AND is
214
- // for the SAME realm whose credential was just rejected -- the conditions under which the caller re-answers.
215
- // A stale offer for a DIFFERENT realm is a new protection space (not a retry), and a stale offer repeating
213
+ // A retryable offer (only meaningful in the rejection context) is stale=true, carries a fresh nonce, and is
214
+ // for the same realm whose credential was just rejected: the conditions under which the caller re-answers.
215
+ // A stale offer for a different realm is a new protection space (not a retry), and a stale offer repeating
216
216
  // the prior nonce cannot re-answer, so neither must out-rank a weaker genuinely retryable offer (RFC 7616 sec. 3.3).
217
217
  var retryable = preferStale && !!parsed.stale && parsed.nonce !== pol.priorNonce && parsed.realm === pol.priorRealm;
218
218
  // Usability dominates; then an offer that APPLIES to this request; then (in a rejection) a retryable offer;
@@ -296,7 +296,7 @@ function answer(challenge, params, E) {
296
296
  var realm = challenge.realm, nonce = challenge.nonce;
297
297
  // Normalize UTF-8 credentials to NFC before hashing (RFC 7616 sec. 4): a server hashes the normalized form,
298
298
  // so a canonically-decomposed username / password (e + combining accent vs the precomposed char) must be
299
- // composed first or HA1 disagrees. The SAME normalized username feeds A1, userhash, and username*.
299
+ // composed first or HA1 disagrees. That one normalized username feeds A1, userhash, and username*.
300
300
  var isUtf8 = String(challenge.charset || "").toUpperCase() === "UTF-8";
301
301
  var pUser = params.username == null ? "" : String(params.username);
302
302
  var pPass = params.password == null ? "" : String(params.password);
@@ -317,8 +317,8 @@ function answer(challenge, params, E) {
317
317
  var response = useQop
318
318
  ? KD(alg.hash, HA1, nonce + ":" + nc + ":" + cnonce + ":" + useQop + ":" + HA2)
319
319
  : KD(alg.hash, HA1, nonce + ":" + HA2);
320
- // The username field (RFC 7616 sec. 3.4). userhash=true sends H(username:realm) for privacy (sec. 3.4.4) --
321
- // an ASCII hex value; A1 above still used the REAL username. Otherwise a charset=UTF-8 username containing
320
+ // The username field (RFC 7616 sec. 3.4). userhash=true sends H(username:realm) for privacy (sec. 3.4.4),
321
+ // an ASCII hex value; A1 above still used the actual username. Otherwise a charset=UTF-8 username containing
322
322
  // non-ASCII characters MUST be carried in the extended `username*` form (RFC 5987 / RFC 8187 percent-encoded
323
323
  // UTF-8), because a server reads the legacy quoted `username` as ISO-8859-1 and could not resolve the account;
324
324
  // `username` and `username*` MUST NOT both appear. An ASCII (or userhash) username uses the legacy quoted form.
@@ -341,9 +341,9 @@ function answer(challenge, params, E) {
341
341
  }
342
342
 
343
343
  // Parse one domain URI entry (RFC 7616 sec. 3.3) into { origin, full }: an abs_path ("/a", "/a?x=1") is
344
- // relative to the responding server -> origin null (any same-origin request); an ABSOLUTE URI carries its own
345
- // authority -> origin "scheme://authority" (lowercased, default port normalized) and matches ONLY a request to
346
- // that SAME origin, since an entry to a different host names a protection space on a different server. `full` is
344
+ // relative to the responding server -> origin null (any same-origin request); an absolute URI carries its own
345
+ // authority -> origin "scheme://authority" (lowercased, default port normalized) and matches only a request to
346
+ // that same origin, since an entry to a different host names a protection space on a different server. `full` is
347
347
  // the pathname + query the request URI is prefix-compared against. A relative / unparseable absolute entry -> null.
348
348
  function _domainEntry(d) {
349
349
  d = String(d == null ? "" : d);
@@ -4,8 +4,8 @@
4
4
  //
5
5
  // @internal -- the shared HTTP Retry-After parser (RFC 7231 sec. 7.1.3): a delay-seconds count OR an
6
6
  // HTTP-date (sec. 7.1.1.1). The enrollment protocol clients (pki.est now, pki.acme next) all surface a
7
- // server's Retry-After as a bounded delay -- never sleeping on it in a single-step call -- so the
8
- // delay-seconds | HTTP-date grammar and the one-year ceiling live in ONE place both compose, error-
7
+ // server's Retry-After as a bounded delay, never sleeping on it in a single-step call, so the
8
+ // delay-seconds | HTTP-date grammar and the one-year ceiling live in one place both compose, error-
9
9
  // factory-parameterized so each domain keeps its own typed verdict. A malformed / overflowing value
10
10
  // fails closed (the caller's E(code, ...)); the value is never returned unparsed.
11
11
 
@@ -26,7 +26,7 @@ var HTTP_DATE_FORMS = [
26
26
  new RegExp("^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) " + MONTH + " [ \\d]\\d \\d{2}:\\d{2}:\\d{2} \\d{4}$"),
27
27
  ];
28
28
 
29
- // Expand an obsolete RFC 850 two-digit year: HTTP interprets it RELATIVE TO the receipt year -- the full
29
+ // Expand an obsolete RFC 850 two-digit year: HTTP interprets it relative to the receipt year, as the full
30
30
  // year with these two digits within [refYear-50, refYear+49] (moved to the past only when it would
31
31
  // otherwise be more than 50 years ahead). A fixed cutoff would misdate a year near the century boundary.
32
32
  // With no receipt time, fall back to the RFC 6265 fixed rule (< 70 -> 2000s, else 1900s).
@@ -34,15 +34,15 @@ function _rfc850Year(yy, refMs) {
34
34
  if (typeof refMs !== "number" || !isFinite(refMs)) return yy + (yy < 70 ? 2000 : 1900);
35
35
  var refYear = new Date(refMs).getUTCFullYear();
36
36
  var full = Math.floor(refYear / 100) * 100 + yy;
37
- // Move to the PREVIOUS century ONLY when the same-century year would be more than 50 years ahead; never
37
+ // Move to the previous century only when the same-century year would be more than 50 years ahead; never
38
38
  // advance a past year to the future (an old two-digit year stays in the past).
39
39
  if (full - refYear > 50) full -= 100;
40
40
  return full;
41
41
  }
42
42
 
43
43
  // Parse an HTTP-date to epoch ms, or NaN when its grammar, calendar, or time is invalid. Built from the
44
- // extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC -- the asctime form carries
45
- // no GMT token, so delegating would parse it in LOCAL time -- and the round-trip check rejects an
44
+ // extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC, the asctime form carries
45
+ // no GMT token so delegating would parse it in local time, and the round-trip check rejects an
46
46
  // impossible date / time (Date.UTC normalizes Feb 31 -> Mar 2 just as Date.parse would).
47
47
  function httpDateMs(s, refMs) {
48
48
  if (!HTTP_DATE_FORMS.some(function (re) { return re.test(s); })) return NaN;
@@ -71,20 +71,20 @@ function httpDateMs(s, refMs) {
71
71
  // retryAfterDate (epoch ms) plus, when opts.now (epoch ms) is given, a bounded retryAfterSeconds. Either
72
72
  // form beyond the one-year ceiling, or a value that is neither, fails closed via opts.E(opts.code, ...).
73
73
  // opts.cap (seconds) clamps a value above the cap to the cap instead of rejecting it; opts.lenient surfaces
74
- // an otherwise-rejected value as a null retryAfterSeconds -- both for a caller (e.g. an ARI poll cadence)
75
- // for whom the value is advisory and must not discard the response. Never slept on here -- the value is
76
- // SURFACED for the caller to decide.
74
+ // an otherwise-rejected value as a null retryAfterSeconds. Both exist for a caller (e.g. an ARI poll
75
+ // cadence) for whom the value is advisory and must not discard the response. Nothing sleeps here; the
76
+ // value is surfaced for the caller to decide.
77
77
  function parse(value, opts) {
78
78
  opts = opts || {};
79
79
  var raStr = String(value).trim();
80
80
  var out = { retryAfterSeconds: null, retryAfterDate: null };
81
81
  if (/^\d+$/.test(raStr)) {
82
82
  var n = parseInt(raStr, 10);
83
- // opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay ABOVE its
84
- // ceiling reduced to the ceiling, not rejected -- so a valid-but-huge value never discards the response.
83
+ // opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay above its
84
+ // ceiling reduced to the ceiling, not rejected, so a valid-but-huge value never discards the response.
85
85
  if (typeof opts.cap === "number" && n > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
86
- // opts.lenient: a caller for whom the value is purely advisory wants an UNPARSEABLE one surfaced as
87
- // null (retryAfterSeconds stays null) rather than a hard reject that would discard the whole response.
86
+ // opts.lenient: a caller for whom the value is purely advisory wants an unparseable one surfaced as
87
+ // null (retryAfterSeconds stays null) instead of a hard reject that would discard the whole response.
88
88
  if (!Number.isSafeInteger(n) || n > MAX_RETRY_AFTER_SECONDS) { if (opts.lenient) return out; throw _fail(opts, "the Retry-After delay is out of the supported range (0.." + MAX_RETRY_AFTER_SECONDS + " seconds)"); }
89
89
  out.retryAfterSeconds = n;
90
90
  return out;
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * @intro
12
12
  * The shared, fail-closed `node:https` transport the enrollment protocol clients
13
- * drive -- `pki.est` now, `pki.acme` and `pki.cmp` next. This is the ONLY module in
13
+ * drive: `pki.est` now, `pki.acme` and `pki.cmp` next. This is the only module in
14
14
  * the toolkit that opens a socket; every protocol layer stays transport-agnostic and
15
15
  * composes it (or an injected substitute) through one contract:
16
16
  * `transport(request) -> Promise<{ status, headers, body, tls }>`. The first three are
@@ -21,7 +21,7 @@
21
21
  * channel -- `{ protocol, cipher, peerCertificate }` -- which is the one fact a caller
22
22
  * cannot recover from the response bytes. An INJECTED substitute should return it too:
23
23
  * `pki.est.serverkeygen` asserts the negotiated cipher can protect the private key it
24
- * is about to accept, and a transport that reports no cipher is trusted rather than
24
+ * is about to accept, and a transport that reports no cipher is trusted instead of
25
25
  * refused (so a loopback test channel works), which means omitting the field silently
26
26
  * skips that assertion.
27
27
  *
@@ -29,7 +29,7 @@
29
29
  * transport. Trust is EXPLICIT and fail-closed: a request is refused unless it
30
30
  * carries an https URL and either a `tls.anchors` set (an Explicit trust-anchor
31
31
  * database, mapped to the node `ca` option) or an explicit `tls.useSystemStore`
32
- * opt-in to node's bundled roots. `rejectUnauthorized` is ALWAYS on -- there is no
32
+ * opt-in to node's bundled roots. `rejectUnauthorized` is always on: there is no
33
33
  * code path that disables server-certificate verification. The response body is
34
34
  * bounded WHILE it streams: the accumulator aborts the socket the instant the running
35
35
  * total crosses `maxResponseBytes`, before a byte reaches a decoder. A protocol
@@ -64,8 +64,8 @@ function _budget(value, key, dflt, max, E, code) {
64
64
  return guard.limits.cap(value, key, dflt, { E: E, code: code, min: 1, max: max, label: key });
65
65
  }
66
66
 
67
- // Node's `ca` option expects PEM; a raw DER certificate Buffer -- the toolkit's native trust-anchor
68
- // form (e.g. what pki.est.cacerts / parseCertsOnly return) -- is silently ignored there, so the
67
+ // Node's `ca` option expects PEM. A raw DER certificate Buffer, the toolkit's native trust-anchor
68
+ // form (e.g. what pki.est.cacerts / parseCertsOnly return), is silently ignored there, so the
69
69
  // connection would fail server authentication despite a valid anchor. Wrap a DER anchor as PEM
70
70
  // (a Buffer already carrying PEM text, or a PEM string, passes through unchanged).
71
71
  function _pemifyAnchor(a) {
@@ -86,7 +86,7 @@ function _pemifyAnchors(anchors) {
86
86
  }
87
87
 
88
88
  // The OS system certificate store plus node's bundled roots (cached; the read is not free), for a
89
- // tls.useSystemStore opt-in. Node's default (no `ca`) is ONLY the bundled Mozilla set, so a server
89
+ // tls.useSystemStore opt-in. Node's default (no `ca`) is just the bundled Mozilla set, so a server
90
90
  // chaining to an OS-installed enterprise CA would fail without this. On a node too old for
91
91
  // tls.getCACertificates the cache stays empty and the caller falls back to node's default store.
92
92
  var _systemCaCache = null;
@@ -96,7 +96,7 @@ function _systemCa() {
96
96
  if (typeof nodeTls.getCACertificates === "function") {
97
97
  ["system", "bundled"].forEach(function (t) {
98
98
  // allow:swallow-unverified a store TYPE unsupported on this node is skipped; the other type
99
- // (and, if both fail, node's default-store fallback) still applies -- a best-effort store load.
99
+ // (and, if both fail, node's default-store fallback) still applies. Best-effort store load.
100
100
  try { var c = nodeTls.getCACertificates(t); if (Array.isArray(c)) out = out.concat(c); } catch (_e) { /* unsupported type */ }
101
101
  });
102
102
  }
@@ -107,14 +107,14 @@ function _systemCa() {
107
107
  // Classify a bare IP string (no brackets) as a private / loopback / link-local / reserved destination an
108
108
  // untrusted URL must not reach: RFC 1918 + loopback + this-network + multicast/reserved + 169.254 (cloud
109
109
  // metadata) + CGNAT for IPv4, and loopback / unspecified / IPv4-mapped / ULA (fc00::/7) + link-local (fe80::/10)
110
- // for IPv6. This is the SAME range set the AIA literal pre-check applies (pki.path.build reuses it), enforced
111
- // here at DNS-RESOLUTION time so a hostname pointing AT an internal address is caught too. A malformed IP fails
112
- // CLOSED (net.isIP === 0 -> not a v4/v6 arm -> the caller treats a non-IP as un-judgeable, never as public).
110
+ // for IPv6. This is the same range set the AIA literal pre-check applies (pki.path.build reuses it), enforced
111
+ // here at DNS-resolution time so a hostname pointing at an internal address is caught too. A malformed IP fails
112
+ // closed (net.isIP === 0 -> not a v4/v6 arm -> the caller treats a non-IP as un-judgeable, never as public).
113
113
  function _isBlockedIp(ip) {
114
114
  var fam = nodeNet.isIP(ip);
115
115
  if (fam === 4) {
116
- // net.isIP === 4 guarantees exactly four octets 0..255. Block the COMPLETE IANA special-purpose /
117
- // non-global set (RFC 6890) so an untrusted destination can reach ONLY globally-routable public space.
116
+ // net.isIP === 4 guarantees exactly four octets 0..255. Block the complete IANA special-purpose /
117
+ // non-global set (RFC 6890) so an untrusted destination can reach only globally-routable public space.
118
118
  var o = ip.split("."), a = +o[0], b = +o[1], c = +o[2];
119
119
  return a === 0 || a === 10 || a === 127 || a >= 224 || // this-network / RFC1918 10/8 / loopback / multicast 224/4 + reserved 240/4 + broadcast
120
120
  (a === 100 && b >= 64 && b <= 127) || // 100.64/10 CGNAT
@@ -146,17 +146,18 @@ function _isBlockedIp(ip) {
146
146
  return false;
147
147
  }
148
148
 
149
- // A DNS-rebinding-safe SSRF filter, installed as node's `lookup` ONLY when a request opts into
150
- // blockPrivateAddresses (the AIA fetch of an untrusted-cert URL). node connects to EXACTLY the address this
151
- // returns -- no second resolution -- so checking the resolved address here PINS it, closing the resolve/connect
152
- // TOCTOU. Any private / loopback / link-local result fails the lookup, which surfaces as transport/blocked-address.
149
+ // A DNS-rebinding-safe SSRF filter, installed as node's `lookup` only when a request opts into
150
+ // blockPrivateAddresses (the AIA fetch of an untrusted-cert URL). node connects to exactly the address this
151
+ // returns, with no second resolution, so checking the resolved address here pins it and closes the
152
+ // resolve/connect TOCTOU. Any private, loopback or link-local result fails the lookup, which surfaces as
153
+ // transport/blocked-address.
153
154
  function _blockedAddrErr(hostname, address) {
154
155
  var e = new Error("refusing to connect to " + hostname + " -> " + address + " (private / loopback / link-local address blocked)");
155
156
  e.pkiBlockedAddress = true;
156
157
  return e;
157
158
  }
158
- // Built over an injectable resolver (defaults to nodeDns.lookup) so every branch -- a resolve error, the
159
- // options.all array shape, a blocked result, a permitted result -- is unit-testable without a live DNS.
159
+ // Built over an injectable resolver (defaults to nodeDns.lookup) so every branch (a resolve error, the
160
+ // options.all array shape, a blocked result, a permitted result) is unit-testable without a live DNS.
160
161
  function _makeGuardedLookup(lookupFn) {
161
162
  return function guardedLookup(hostname, options, callback) {
162
163
  lookupFn(hostname, options || {}, function (err, address, family) {
@@ -211,7 +212,7 @@ function _classifyError(e, C) {
211
212
  *
212
213
  * @opts
213
214
  * - `tls.anchors` -- Explicit trust anchor(s): a DER/PEM Buffer, an array, or PEM string(s) (node `ca`).
214
- * - `tls.useSystemStore` -- boolean; the ONLY opt-in to node's bundled CA store (default false).
215
+ * - `tls.useSystemStore` -- boolean; the one opt-in to node's bundled CA store (default false).
215
216
  * - `tls.cert` / `tls.key` -- client certificate + key for mutual-TLS re-enrollment.
216
217
  * - `tls.minVersion` -- 'TLSv1.2' (default) or 'TLSv1.3'; never below the floor.
217
218
  * - `tls.servername` / `tls.checkServerIdentity` -- SNI + RFC 6125 identity; may tighten, never disable.