@blamejs/pki 0.6.13 → 0.6.15

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +1 -1
  3. package/index.js +1 -1
  4. package/lib/acme.js +59 -815
  5. package/lib/asn1-der.js +7 -245
  6. package/lib/attrcert-sign.js +12 -260
  7. package/lib/attrcert-verify.js +1 -7
  8. package/lib/byte-reader.js +0 -8
  9. package/lib/byte-writer.js +0 -11
  10. package/lib/cbor-det.js +0 -54
  11. package/lib/cmc-build.js +15 -357
  12. package/lib/cmc-verify.js +6 -239
  13. package/lib/cmp-build.js +41 -317
  14. package/lib/cmp-session.js +73 -760
  15. package/lib/cmp-verify.js +32 -488
  16. package/lib/cms-compress.js +3 -32
  17. package/lib/cms-decrypt.js +29 -294
  18. package/lib/cms-encrypt.js +14 -234
  19. package/lib/cms-sign.js +29 -324
  20. package/lib/cms-verify.js +9 -500
  21. package/lib/composite-kem.js +2 -102
  22. package/lib/composite-sig.js +1 -77
  23. package/lib/constants.js +0 -249
  24. package/lib/crl-sign.js +9 -237
  25. package/lib/crl-verify.js +1 -10
  26. package/lib/crmf-sign.js +19 -198
  27. package/lib/crmf-verify.js +1 -12
  28. package/lib/csr-sign.js +3 -34
  29. package/lib/csr-verify.js +1 -12
  30. package/lib/ct.js +28 -253
  31. package/lib/edwards-point.js +10 -44
  32. package/lib/est.js +28 -764
  33. package/lib/framework-error.js +4 -182
  34. package/lib/guard-all.js +1 -58
  35. package/lib/guard-async.js +1 -36
  36. package/lib/guard-bytes.js +6 -650
  37. package/lib/guard-compress.js +5 -102
  38. package/lib/guard-crypto.js +1 -40
  39. package/lib/guard-der.js +2 -93
  40. package/lib/guard-encoding.js +2 -60
  41. package/lib/guard-header.js +1 -36
  42. package/lib/guard-identifier.js +7 -531
  43. package/lib/guard-intrinsic.js +1 -150
  44. package/lib/guard-json.js +10 -73
  45. package/lib/guard-limits.js +1 -77
  46. package/lib/guard-list.js +1 -49
  47. package/lib/guard-name.js +3 -209
  48. package/lib/guard-parsed.js +7 -335
  49. package/lib/guard-range.js +1 -71
  50. package/lib/guard-secret.js +1 -102
  51. package/lib/guard-text.js +1 -64
  52. package/lib/guard-time.js +1 -97
  53. package/lib/hpke.js +3 -105
  54. package/lib/http-digest.js +14 -142
  55. package/lib/http-retry-after.js +1 -47
  56. package/lib/http-transport.js +21 -131
  57. package/lib/inspect.js +30 -284
  58. package/lib/ip-utils.js +6 -47
  59. package/lib/jose.js +7 -142
  60. package/lib/key.js +2 -67
  61. package/lib/lint.js +9 -194
  62. package/lib/merkle.js +1 -27
  63. package/lib/mime.js +9 -82
  64. package/lib/ocsp-request-verify.js +1 -14
  65. package/lib/ocsp-verify.js +1 -82
  66. package/lib/ocsp.js +11 -169
  67. package/lib/oid.js +5 -337
  68. package/lib/path-validate.js +103 -1492
  69. package/lib/pbes2.js +2 -89
  70. package/lib/pkcs12-build.js +31 -379
  71. package/lib/pki-build.js +64 -290
  72. package/lib/rc2.js +2 -18
  73. package/lib/rfc3339.js +4 -27
  74. package/lib/scep.js +59 -233
  75. package/lib/schema-all.js +0 -122
  76. package/lib/schema-attrcert.js +4 -146
  77. package/lib/schema-c509.js +153 -802
  78. package/lib/schema-cmc.js +5 -220
  79. package/lib/schema-cmp.js +2 -223
  80. package/lib/schema-cms.js +13 -427
  81. package/lib/schema-crl.js +5 -68
  82. package/lib/schema-crmf.js +6 -167
  83. package/lib/schema-csr.js +1 -54
  84. package/lib/schema-csrattrs.js +2 -87
  85. package/lib/schema-engine.js +0 -152
  86. package/lib/schema-ocsp.js +0 -117
  87. package/lib/schema-pkcs12.js +2 -93
  88. package/lib/schema-pkcs8.js +3 -35
  89. package/lib/schema-pkix.js +24 -655
  90. package/lib/schema-smime.js +0 -54
  91. package/lib/schema-tsp.js +3 -99
  92. package/lib/schema-x509.js +4 -63
  93. package/lib/shbs.js +11 -95
  94. package/lib/sign-scheme.js +2 -97
  95. package/lib/sigstore.js +9 -169
  96. package/lib/sleep.js +2 -8
  97. package/lib/smime.js +15 -408
  98. package/lib/tls-cert-compress.js +0 -62
  99. package/lib/trust.js +19 -185
  100. package/lib/tsp-sign.js +16 -270
  101. package/lib/validator-all.js +1 -30
  102. package/lib/validator-attcert.js +2 -46
  103. package/lib/validator-cose.js +2 -153
  104. package/lib/validator-keydesc.js +1 -37
  105. package/lib/validator-sig.js +1 -46
  106. package/lib/validator-tls.js +1 -88
  107. package/lib/validator-tpm.js +19 -122
  108. package/lib/verify-seam.js +1 -24
  109. package/lib/webauthn-mds.js +3 -400
  110. package/lib/webauthn.js +12 -833
  111. package/lib/webcrypto.js +5 -485
  112. package/lib/x509-sign.js +4 -108
  113. package/package.json +1 -1
  114. package/sbom.cdx.json +6 -6
@@ -1,36 +1,17 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- not on pki.*. HTTP Digest access authentication (RFC 7616): parse an untrusted
6
- // WWW-Authenticate challenge (fail-closed, quoted-string-honoring) and compute the Authorization
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 (which would crash on the error path), plus its
9
- // policy.codes map, so pki.est composes it now and pki.acme / pki.cmp can reuse it as config, not a fork.
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 or nonce is rejected and not defaulted, and an unsupported or
13
- // weak algorithm, or a no-qop challenge, fails closed instead of downgrading.
4
+ // @internal
14
5
 
15
6
  var crypto = require("crypto");
16
7
  var constants = require("./constants");
17
8
  var intrinsic = require("./guard-intrinsic");
18
- // Own-key membership through an operation taken at load. Written out, the question reads two
19
- // replaceable properties to answer one thing, and here it decides whether an auth-param repeats --
20
- // answering false lets the second copy of a nonce overwrite the first.
21
9
  var _hasOwn = intrinsic.hasOwn;
22
10
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
23
11
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
24
12
  var _fromCharCode = intrinsic.fromCharCode;
25
- var pkix = require("./schema-pkix"); // the shared JS-\s code-point predicate (pkix.isJsWhitespace)
13
+ var pkix = require("./schema-pkix");
26
14
 
27
- // Algorithm registry, a data row and not a switch: rank orders the RFC 7616 sec. 3.7
28
- // "most secure the client can use" selection (SHA-512-256 > SHA-256 > MD5); an unknown algorithm ranks 0.
29
- //
30
- // Null prototype, because the gate on an unknown algorithm is `if (!ALGS[name])`. With an ordinary
31
- // object literal that question is asked of the prototype chain too, so an algorithm named for any
32
- // member of Object.prototype answers with a truthy value the table never carried: the
33
- // unsupported-algorithm refusal is skipped and the descriptor's own fields read as undefined.
34
15
  var ALGS = intrinsic.assign(intrinsic.create(null), {
35
16
  "SHA-512-256": { hash: "sha512-256", rank: 3, sess: false }, "SHA-512-256-SESS": { hash: "sha512-256", rank: 3, sess: true },
36
17
  "SHA-256": { hash: "sha256", rank: 2, sess: false }, "SHA-256-SESS": { hash: "sha256", rank: 2, sess: true },
@@ -41,18 +22,14 @@ var DEFAULT_CODES = {
41
22
  noQop: "digest/no-qop", badChallenge: "digest/bad-challenge",
42
23
  };
43
24
 
44
- // H = lowercase-hex digest; the octet string is fed as latin1 so a UTF-8-encoded credential (charset=UTF-8)
45
- // is hashed over its exact bytes. KD(secret, data) = H(secret ":" data).
46
25
  function H(hash, s) { return crypto.createHash(hash).update(s, "latin1").digest("hex"); }
47
26
  function KD(hash, secret, data) { return H(hash, secret + ":" + data); }
48
- // Quote a value: wrap in double quotes, backslash-escaping any " or \ (matching /(["\\])/g -> "\\$1").
49
27
  function _qstr(v) {
50
28
  var s = String(v), out = "";
51
29
  for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === 34 || c === 92) out += "\\"; out += _fromCharCode(c); }
52
30
  return "\"" + out + "\"";
53
31
  }
54
32
 
55
- // Split a header at commas outside a quoted-string (a quoted comma is a literal, not a delimiter).
56
33
  function _commaSplitOutsideQuotes(s) {
57
34
  var out = [], buf = "", inQ = false, esc = false;
58
35
  for (var i = 0; i < s.length; i++) {
@@ -66,7 +43,6 @@ function _commaSplitOutsideQuotes(s) {
66
43
  out.push(buf);
67
44
  return out;
68
45
  }
69
- // Index of the first '=' outside a quoted-string, or -1.
70
46
  function _firstEqOutsideQuotes(s) {
71
47
  var inQ = false, esc = false;
72
48
  for (var i = 0; i < s.length; i++) {
@@ -78,15 +54,11 @@ function _firstEqOutsideQuotes(s) {
78
54
  }
79
55
  return -1;
80
56
  }
81
- // Unquote a well-formed quoted-string (only ever called after _closedQuote): drop the surrounding
82
- // quotes and each escaping backslash (matching s.slice(1,-1).replace(/\\(.)/g, "$1")).
83
57
  function _unq(s) {
84
- var out = "", last = 1, n = s.length; // skip the opening quote at 0
58
+ var out = "", last = 1, n = s.length;
85
59
  for (var i = 1; i < n - 1; i++) { if (_charCodeAt(s, i) === 92) { out += _strSlice(s, last, i); last = i + 1; i += 1; } }
86
- return out + _strSlice(s, last, n - 1); // trailing content, excluding the closing quote
60
+ return out + _strSlice(s, last, n - 1);
87
61
  }
88
- // A well-formed quoted-string: opens with ", closes with an unescaped " as the last character (nothing trails
89
- // it). An unterminated / trailing-garbage quoted-string is malformed (RFC 7230 sec. 3.2.6) and rejected.
90
62
  function _closedQuote(s) {
91
63
  if (s.length < 2 || s.charAt(0) !== "\"") return false;
92
64
  var esc = false;
@@ -98,41 +70,31 @@ function _closedQuote(s) {
98
70
  }
99
71
  return false;
100
72
  }
101
- // A control octet (RFC 7230 sec. 3.2.6 quoted-string / qdtext forbids CTL except HTAB). Scanned with charCodeAt
102
- // instead of a control-char regex literal (which eslint no-control-regex correctly refuses).
103
73
  function _hasCtl(s) {
104
74
  for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if ((c < 0x20 && c !== 0x09) || c === 0x7f) return true; }
105
75
  return false;
106
76
  }
107
77
 
108
- // Split a WWW-Authenticate header into [{ scheme, paramText }]. A comma-separated segment whose pre-'='
109
- // text is "token WS token" (a scheme name before the first auth-param key), or a bare token with no '=',
110
- // STARTS a new challenge; a "key=value" segment is a param continuation of the current challenge.
111
- // The leading token when `pre` is "token WS more" (/^(\S+)\s+(\S[\s\S]*)$/) -- a scheme name before the
112
- // first auth-param -- else null. \s is the shared JS-\s predicate (RFC 7230 OWS is a subset).
113
78
  function _matchTokenRest(pre) {
114
79
  var n = pre.length, p = 0;
115
- while (p < n && !pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1; // ^(\S+)
80
+ while (p < n && !pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1;
116
81
  if (p === 0) return null;
117
82
  var tokEnd = p;
118
- if (p >= n || !pkix.isJsWhitespace(_charCodeAt(pre, p))) return null; // \s+
83
+ if (p >= n || !pkix.isJsWhitespace(_charCodeAt(pre, p))) return null;
119
84
  while (p < n && pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1;
120
- if (p >= n || pkix.isJsWhitespace(_charCodeAt(pre, p))) return null; // (\S[\s\S]*)$ -- at least one non-\s
85
+ if (p >= n || pkix.isJsWhitespace(_charCodeAt(pre, p))) return null;
121
86
  return _strSlice(pre, 0, tokEnd);
122
87
  }
123
- // The index past a leading "OWS token OWS" (/^\s*\S+\s+/), or 0 when the string does not begin that way.
124
- // The caller slices from this index (the slice that replaces the removed .replace).
125
88
  function _stripTokenIndex(seg) {
126
89
  var n = seg.length, p = 0;
127
- while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // ^\s*
90
+ while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
128
91
  var ts = p;
129
- while (p < n && !pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // \S+
92
+ while (p < n && !pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
130
93
  if (p === ts) return 0;
131
94
  var ws = p;
132
- while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // \s+ (required)
95
+ while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
133
96
  return p === ws ? 0 : p;
134
97
  }
135
- // String.prototype.split(/\s+/): the pieces between \s runs (a leading/trailing run yields a "" element).
136
98
  function _splitWhitespace(s) {
137
99
  var out = [], i = 0, n = s.length, ps = 0;
138
100
  while (i < n) {
@@ -155,13 +117,12 @@ function _splitChallenges(s) {
155
117
  var eq = _firstEqOutsideQuotes(seg);
156
118
  var pre = (eq < 0 ? seg : _strSlice(seg, 0, eq)).trim();
157
119
  var scheme = _matchTokenRest(pre);
158
- if (eq < 0) { cur = { scheme: seg, paramText: "" }; out.push(cur); } // a bare scheme token (e.g. "Basic")
159
- else if (scheme !== null) { cur = { scheme: scheme, paramText: _strSlice(seg, _stripTokenIndex(seg)) }; out.push(cur); } // "scheme firstKey=..."
160
- else if (cur) { cur.paramText = cur.paramText ? cur.paramText + "," + seg : seg; } // "key=value" continuation
120
+ if (eq < 0) { cur = { scheme: seg, paramText: "" }; out.push(cur); }
121
+ else if (scheme !== null) { cur = { scheme: scheme, paramText: _strSlice(seg, _stripTokenIndex(seg)) }; out.push(cur); }
122
+ else if (cur) { cur.paramText = cur.paramText ? cur.paramText + "," + seg : seg; }
161
123
  }
162
124
  return out;
163
125
  }
164
- // Parse a challenge's params into { key(lower): { value, quoted } }; a repeated key is malformed (fail closed).
165
126
  function _parseParams(paramText, E, code) {
166
127
  var segs = _commaSplitOutsideQuotes(paramText), map = Object.create(null);
167
128
  for (var i = 0; i < segs.length; i++) {
@@ -175,14 +136,11 @@ function _parseParams(paramText, E, code) {
175
136
  if (quoted && !_closedQuote(rawVal)) throw E(code, "an unterminated or trailing-garbage Digest quoted-string (RFC 7230 sec. 3.2.6)");
176
137
  if (_hasOwn(map, key)) throw E(code, "repeated Digest auth-param " + JSON.stringify(key));
177
138
  var value = quoted ? _unq(rawVal) : rawVal;
178
- // Reject a control octet in any value (CR / LF / NUL / ...): it is a MUST-reject per RFC 7230 qdtext AND a
179
- // header-injection sink -- an unescaped CR/LF reflected into the outgoing Authorization would split the header.
180
139
  if (_hasCtl(value)) throw E(code, "a Digest auth-param value contains a control character (RFC 7230 sec. 3.2.6)");
181
140
  map[key] = { value: value, quoted: quoted };
182
141
  }
183
142
  return map;
184
143
  }
185
- // Structural validation of one Digest challenge's params (RFC 7616 sec. 3.3). Throws on any violation.
186
144
  function _validateDigest(paramText, E, code) {
187
145
  var p = _parseParams(paramText, E, code);
188
146
  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)");
@@ -192,38 +150,20 @@ function _validateDigest(paramText, E, code) {
192
150
  if (p.qop) {
193
151
  if (!p.qop.quoted) throw E(code, "the Digest qop must be a quoted list (RFC 7616 sec. 3.3)");
194
152
  qop = p.qop.value.split(",").map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean);
195
- // A qop directive that is present but lists no value (qop="" / qop=", ,") is malformed, and is not the
196
- // absent-qop (RFC 2069) case. Reject it instead of silently collapsing to a no-qop response the server
197
- // cannot accept (RFC 7616 sec. 3.3). Only an omitted qop directive selects the legacy no-qop path.
198
153
  if (qop.length === 0) throw E(code, "a present Digest qop directive must list at least one value (RFC 7616 sec. 3.3)");
199
154
  }
200
- // domain (RFC 7616 sec. 3.3): a quoted, space-separated list of URIs defining the protection space. An
201
- // unquoted domain is malformed and rejected (fail closed like realm / nonce / qop) instead of silently
202
- // widened: widening a mis-encoded scope to "the whole server" would send credentials more broadly than the
203
- // server intended. Surfaced as an array of the listed URIs, or null when omitted or quoted-empty, which per
204
- // the RFC means "all URIs on the responding server" (the answer may be reused for any same-origin URI, sec. 3.5).
205
155
  if (p.domain && !p.domain.quoted) throw E(code, "the Digest domain must be a quoted-string (RFC 7616 sec. 3.3)");
206
156
  var domain = (p.domain && p.domain.value.trim() !== "")
207
157
  ? _splitWhitespace(p.domain.value.trim()) : null;
208
- // charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
209
- // other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
210
- // rejected (and thus skipped during multi-offer selection in favor of a conforming offer).
211
158
  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)");
212
- // stale (RFC 7616 sec. 3.3) is the unquoted token "true" or "false". A quoted or otherwise invalid value
213
- // (stale=maybe) is malformed and rejected (thus skipped in selection), never parsed as a false flag that
214
- // could shadow a conforming offer.
215
159
  if (p.stale) {
216
160
  var sv = String(p.stale.value).toLowerCase();
217
161
  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)");
218
162
  }
219
- // userhash (RFC 7616 sec. 3.3) is likewise the unquoted token "true" or "false". A quoted or otherwise invalid
220
- // value is malformed and rejected (skipped in selection), never silently changing how the username is sent.
221
163
  if (p.userhash) {
222
164
  var uhv = String(p.userhash.value).toLowerCase();
223
165
  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)");
224
166
  }
225
- // opaque (RFC 7616 sec. 3.3) is a quoted-string echoed back verbatim. An unquoted opaque is malformed and
226
- // rejected (skipped in selection), never accepted, where it could shadow a conforming offer.
227
167
  if (p.opaque && !p.opaque.quoted) throw E(code, "the Digest opaque directive must be a quoted-string (RFC 7616 sec. 3.3)");
228
168
  return {
229
169
  scheme: "Digest", realm: p.realm.value, nonce: p.nonce.value, qop: qop, domain: domain,
@@ -235,21 +175,9 @@ function _validateDigest(paramText, E, code) {
235
175
  };
236
176
  }
237
177
 
238
- // parseChallenge(www, E, code, policy?) -> the most-secure USABLE structurally-valid Digest challenge,
239
- // OR null (no Digest challenge present), OR throw E(code) (a Digest challenge present but none valid).
240
- // Selection is policy-aware: a challenge answer() would refuse under the active policy (an unsupported /
241
- // MD5-when-disallowed algorithm, or a qop this client cannot satisfy) is ranked BELOW every usable one, so a
242
- // higher-algorithm-rank but unusable offer (e.g. a SHA-512-256 no-qop challenge under the default policy)
243
- // never shadows a lower-ranked usable one (e.g. SHA-256 with qop="auth"), per RFC 7616 sec. 3.3. When no
244
- // challenge is usable the highest-algorithm-rank one is still returned, so answer() reports the specific
245
- // policy reason (which opt to set) in place of a generic "no challenge".
246
178
  function parseChallenge(www, E, code, policy) {
247
179
  var pol = policy || {};
248
180
  var codes = pol.codes || DEFAULT_CODES;
249
- // preferStale marks a CREDENTIAL-REJECTION context (a 401 answering a credentialed request in the same space):
250
- // there, a stale=true offer is RETRYABLE with a fresh nonce while a stale=false offer is a terminal rejection,
251
- // so a retryable offer must outrank a stronger non-retryable one (RFC 7616 sec. 3.3). In the initial context
252
- // it is not set and stale plays no part.
253
181
  var preferStale = !!pol.preferStale;
254
182
  var raw = String(www == null ? "" : www);
255
183
  if (raw.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E(code, "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap");
@@ -259,8 +187,6 @@ function parseChallenge(www, E, code, policy) {
259
187
  if (challenges[i].scheme.toLowerCase() !== "digest") continue;
260
188
  sawDigest = true;
261
189
  var parsed;
262
- // A MALFORMED offer is skipped, not fatal: parseChallenge's contract is to throw only when NO valid Digest
263
- // offer exists (RFC 7616 sec. 3.3), so a bad offer alongside a good one still authenticates on the good one.
264
190
  try { parsed = _validateDigest(challenges[i].paramText, E, code); }
265
191
  catch (_e) {
266
192
  continue;
@@ -268,18 +194,8 @@ function parseChallenge(www, E, code, policy) {
268
194
  var alg = ALGS[parsed.algorithm];
269
195
  var rank = alg ? alg.rank : 0;
270
196
  var usable = _rejection(parsed, pol, codes) === null;
271
- // APPLICABLE: the offer's own protection space covers the CURRENT request target (RFC 7616 sec. 3.5). An
272
- // offer whose `domain` excludes this request cannot authenticate it (the caller would omit the credential),
273
- // so it must not shadow a weaker offer that does apply. Only checked when the caller supplies the request
274
- // context; otherwise every offer is treated as applicable.
275
197
  var applicable = pol.requestTarget === undefined ? true : inProtectionSpace(parsed, pol.requestOrigin, pol.requestTarget);
276
- // A retryable offer (only meaningful in the rejection context) is stale=true, carries a fresh nonce, and is
277
- // for the same realm whose credential was just rejected: the conditions under which the caller re-answers.
278
- // A stale offer for a different realm is a new protection space (not a retry), and a stale offer repeating
279
- // the prior nonce cannot re-answer, so neither must out-rank a weaker genuinely retryable offer (RFC 7616 sec. 3.3).
280
198
  var retryable = preferStale && !!parsed.stale && parsed.nonce !== pol.priorNonce && parsed.realm === pol.priorRealm;
281
- // Usability dominates; then an offer that APPLIES to this request; then (in a rejection) a retryable offer;
282
- // then the stronger algorithm.
283
199
  if (best === null ||
284
200
  (usable && !bestUsable) ||
285
201
  (usable === bestUsable && applicable && !bestApplicable) ||
@@ -293,22 +209,17 @@ function parseChallenge(www, E, code, policy) {
293
209
  return null;
294
210
  }
295
211
 
296
- // Credential octets for the given charset (RFC 7616 sec. 4): UTF-8 -> the exact UTF-8 bytes; else the
297
- // string as-is (ASCII / latin1). H() hashes over latin1, so a UTF-8 credential is fed as its octet string.
298
212
  function _octets(s, charset) {
299
213
  var v = s == null ? "" : String(s);
300
214
  return (charset && String(charset).toUpperCase() === "UTF-8") ? Buffer.from(v, "utf8").toString("latin1") : v;
301
215
  }
302
216
 
303
- // Does the string carry any non-ASCII code unit (> U+007F)? Such a UTF-8 username is sent via `username*`.
304
217
  function _hasNonAscii(s) {
305
218
  s = String(s == null ? "" : s);
306
219
  for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) > 0x7F) return true; }
307
220
  return false;
308
221
  }
309
222
 
310
- // RFC 5987 / RFC 8187 percent-encoding of a string's UTF-8 octets: attr-char (ALPHA / DIGIT / "!#$&+-.^_`|~")
311
- // is kept literal, every other octet is %XX (upper-hex). Used for the Digest `username*` extended value.
312
223
  function _pctEncodeUtf8(s) {
313
224
  var bytes = Buffer.from(String(s == null ? "" : s), "utf8");
314
225
  var out = "";
@@ -325,11 +236,6 @@ function _pctEncodeUtf8(s) {
325
236
  return out;
326
237
  }
327
238
 
328
- // The policy gates a parsed challenge must clear to be answerable (RFC 7616 sec. 3.4): a supported
329
- // algorithm, MD5 only when allowed, and a qop this client can satisfy (auth / auth-int, or no-qop only when
330
- // allowLegacyQop). Returns a { code, msg } rejection or null. This is the SINGLE definition both answer()
331
- // and parseChallenge() consult, so challenge SELECTION can never rank a challenge as usable that answer()
332
- // would then refuse (or vice-versa) -- the two cannot drift.
333
239
  function _rejection(challenge, pol, codes) {
334
240
  var alg = ALGS[challenge.algorithm];
335
241
  if (!alg) return { code: codes.unsupportedAlgorithm, msg: "unsupported Digest algorithm " + challenge.algorithm + " (supported: SHA-512-256, SHA-256, MD5)" };
@@ -342,7 +248,6 @@ function _rejection(challenge, pol, codes) {
342
248
  return null;
343
249
  }
344
250
 
345
- // answer(challenge, { method, uri, username, password, body, policy, rng }, E) -> the Authorization header value.
346
251
  function answer(challenge, params, E) {
347
252
  var pol = params.policy || {};
348
253
  var codes = pol.codes || DEFAULT_CODES;
@@ -351,15 +256,7 @@ function answer(challenge, params, E) {
351
256
  var alg = ALGS[challenge.algorithm];
352
257
  var useQop = challenge.qop.indexOf("auth") !== -1 ? "auth" : (challenge.qop.indexOf("auth-int") !== -1 ? "auth-int" : null);
353
258
  var cnonce = String((params.rng || function () { return crypto.randomBytes(18).toString("base64"); })());
354
- // The realm is used with its incoming header OCTETS unchanged (RFC 7616 sec. 4). The realm arrives from the
355
- // wire, so a transport that exposes header bytes as Latin-1 (the Node default) already presents the server's
356
- // UTF-8 octets one-per-char; re-encoding them as UTF-8 (as user / pass, which come from the caller as Unicode
357
- // strings, correctly do) would DOUBLE-encode and make the digest disagree with the server. H() hashes over
358
- // Latin-1, so the raw realm string contributes exactly the octets the server used.
359
259
  var realm = challenge.realm, nonce = challenge.nonce;
360
- // Normalize UTF-8 credentials to NFC before hashing (RFC 7616 sec. 4): a server hashes the normalized form,
361
- // so a canonically-decomposed username / password (e + combining accent vs the precomposed char) must be
362
- // composed first or HA1 disagrees. That one normalized username feeds A1, userhash, and username*.
363
260
  var isUtf8 = String(challenge.charset || "").toUpperCase() === "UTF-8";
364
261
  var pUser = params.username == null ? "" : String(params.username);
365
262
  var pPass = params.password == null ? "" : String(params.password);
@@ -372,19 +269,11 @@ function answer(challenge, params, E) {
372
269
  ? params.method + ":" + params.uri + ":" + H(alg.hash, params.body == null ? "" : params.body)
373
270
  : params.method + ":" + params.uri;
374
271
  var HA2 = H(alg.hash, A2);
375
- // The nonce-count: a (nonce, nc) pair MUST NOT be reused (RFC 7616 sec. 3.4), so a request that REUSES a nonce
376
- // (a same-origin redirect, an auth-int retry) increments nc; a fresh nonce restarts at 1. The caller supplies
377
- // the count for this nonce (default 1); formatted as 8 lowercase hex.
378
272
  var ncNum = (typeof params.nc === "number" && params.nc >= 1) ? Math.floor(params.nc) : 1;
379
273
  var nc = _strSlice("0000000" + ncNum.toString(16), -8);
380
274
  var response = useQop
381
275
  ? KD(alg.hash, HA1, nonce + ":" + nc + ":" + cnonce + ":" + useQop + ":" + HA2)
382
276
  : KD(alg.hash, HA1, nonce + ":" + HA2);
383
- // The username field (RFC 7616 sec. 3.4). userhash=true sends H(username:realm) for privacy (sec. 3.4.4),
384
- // an ASCII hex value; A1 above still used the actual username. Otherwise a charset=UTF-8 username containing
385
- // non-ASCII characters MUST be carried in the extended `username*` form (RFC 5987 / RFC 8187 percent-encoded
386
- // UTF-8), because a server reads the legacy quoted `username` as ISO-8859-1 and could not resolve the account;
387
- // `username` and `username*` MUST NOT both appear. An ASCII (or userhash) username uses the legacy quoted form.
388
277
  var parts;
389
278
  if (!challenge.userhash && isUtf8 && _hasNonAscii(pUser)) {
390
279
  parts = ["username*=UTF-8''" + _pctEncodeUtf8(pUser)];
@@ -394,8 +283,6 @@ function answer(challenge, params, E) {
394
283
  }
395
284
  parts.push("realm=" + _qstr(realm), "nonce=" + _qstr(nonce), "uri=" + _qstr(params.uri), "algorithm=" + challenge.algorithm);
396
285
  if (useQop) { parts.push("qop=" + useQop); parts.push("nc=" + nc); }
397
- // The cnonce directive is emitted whenever the server needs it to recompute the response: under a qop, and
398
- // also under a -sess algorithm whose A1 folds in the cnonce even with no qop (RFC 7616 sec. 3.4.2 / 3.9.1).
399
286
  if (useQop || alg.sess) { parts.push("cnonce=" + _qstr(cnonce)); }
400
287
  parts.push("response=" + _qstr(response));
401
288
  if (challenge.opaque != null) parts.push("opaque=" + _qstr(challenge.opaque));
@@ -403,28 +290,13 @@ function answer(challenge, params, E) {
403
290
  return "Digest " + parts.join(", ");
404
291
  }
405
292
 
406
- // Parse one domain URI entry (RFC 7616 sec. 3.3) into { origin, full }: an abs_path ("/a", "/a?x=1") is
407
- // relative to the responding server -> origin null (any same-origin request); an absolute URI carries its own
408
- // authority -> origin "scheme://authority" (lowercased, default port normalized) and matches only a request to
409
- // that same origin, since an entry to a different host names a protection space on a different server. `full` is
410
- // the pathname + query the request URI is prefix-compared against. A relative / unparseable absolute entry -> null.
411
293
  function _domainEntry(d) {
412
294
  d = String(d == null ? "" : d);
413
295
  if (d.charAt(0) === "/") return { origin: null, full: d };
414
- // An absolute entry is parsed through the URL constructor so its origin is NORMALIZED the same way the
415
- // request's URL.origin is -- an explicit default port (https :443 / http :80) is dropped, so a domain naming
416
- // "https://h:443/x" still matches a request whose origin normalizes to "https://h". A relative / unparseable
417
- // entry throws and never matches (fail closed).
418
296
  try { var u = new URL(d); return { origin: u.origin.toLowerCase(), full: (u.pathname || "/") + (u.search || "") }; }
419
297
  catch (_e) { return null; }
420
298
  }
421
299
 
422
- // inProtectionSpace(challenge, requestOrigin, requestPathAndSearch) -> is the request within the challenge's
423
- // protection space? An absent / empty domain means "all URIs on the responding server" (RFC 7616 sec. 3.3), so
424
- // any same-origin request matches; otherwise membership is by LITERAL URI PREFIX (RFC 7616 sec. 3.5 / RFC 2617
425
- // sec. 1.2): the request's pathname + query must have a listed domain URI as a prefix, and for an absolute entry
426
- // the same origin. The query is part of the prefix, so /enroll?tenant=a does not cover /enroll?tenant=b while a
427
- // query-less /enroll covers its whole subtree (including /enroll?...). Used to scope where a cached answer is reused.
428
300
  function inProtectionSpace(challenge, requestOrigin, requestPathAndSearch) {
429
301
  var domain = challenge && challenge.domain;
430
302
  if (!domain || !domain.length) return true;
@@ -433,7 +305,7 @@ function inProtectionSpace(challenge, requestOrigin, requestPathAndSearch) {
433
305
  for (var i = 0; i < domain.length; i++) {
434
306
  var e = _domainEntry(domain[i]);
435
307
  if (e === null) continue;
436
- if (e.origin !== null && e.origin !== origin) continue; // an absolute entry to a DIFFERENT origin is a different protection space
308
+ if (e.origin !== null && e.origin !== origin) continue;
437
309
  if (full.indexOf(e.full) === 0) return true;
438
310
  }
439
311
  return false;
@@ -1,66 +1,41 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- the shared HTTP Retry-After parser (RFC 7231 sec. 7.1.3): a delay-seconds count OR an
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-
9
- // factory-parameterized so each domain keeps its own typed verdict. A malformed / overflowing value
10
- // fails closed (the caller's E(code, ...)); the value is never returned unparsed.
4
+ // @internal
11
5
 
12
6
  var constants = require("./constants");
13
7
 
14
- // The largest delay this parser surfaces as a number: a generous one-year ceiling that keeps the
15
- // value a safe integer and rejects an overflowing / nonsensical delay-seconds or far-future date.
16
8
  var MAX_RETRY_AFTER_SECONDS = constants.TIME.days(365) / constants.TIME.seconds(1);
17
9
 
18
- // The three HTTP-date forms (RFC 7231 sec. 7.1.1.1): IMF-fixdate (the required form), and the obsolete
19
- // rfc850-date / asctime-date a recipient must still accept. Gating Date.parse on this grammar keeps its
20
- // permissiveness (an ISO string like "2026-07-10") from passing as a valid Retry-After header.
21
10
  var MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
22
11
  var DOW_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
23
12
  var DOW_LONG = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
24
13
 
25
- // The grammar is scanned character by character (no regex): each form is a fixed layout of a
26
- // day-of-week name, delimiters, an all-digit field, a three-letter month name, and an HH:MM:SS time.
27
14
  function _isDigit(ch) { return ch >= "0" && ch <= "9"; }
28
- // The two-digit / four-digit integer at `i`, or -1 when those positions are not all digits.
29
15
  function _digits2(s, i) { return _isDigit(s.charAt(i)) && _isDigit(s.charAt(i + 1)) ? (s.charCodeAt(i) - 48) * 10 + (s.charCodeAt(i + 1) - 48) : -1; }
30
16
  function _digits4(s, i) { for (var k = 0; k < 4; k++) if (!_isDigit(s.charAt(i + k))) return -1; return parseInt(s.slice(i, i + 4), 10); }
31
- // The length of a name from `list` matching `s` at `i` (the day-of-week alternation), or -1.
32
17
  function _matchName(s, i, list) { for (var j = 0; j < list.length; j++) { if (s.slice(i, i + list[j].length) === list[j]) return list[j].length; } return -1; }
33
- // The three-letter month at `i` when it is a real month name, else null.
34
18
  function _monthAt(s, i) { return _matchName(s, i, MONTHS) === -1 ? null : s.slice(i, i + 3); }
35
- // An "HH:MM:SS" time at `i` -> { hh, mi, ss, next } (next = i + 8), or null.
36
19
  function _timeAt(s, i) {
37
20
  var hh = _digits2(s, i); if (hh < 0 || s.charAt(i + 2) !== ":") return null;
38
21
  var mi = _digits2(s, i + 3); if (mi < 0 || s.charAt(i + 5) !== ":") return null;
39
22
  var ss = _digits2(s, i + 6); if (ss < 0) return null;
40
23
  return { hh: hh, mi: mi, ss: ss, next: i + 8 };
41
24
  }
42
- // A non-empty run of ASCII digits (matching /^\d+$/, the delay-seconds form).
43
25
  function _isAllDigits(s) {
44
26
  if (s.length === 0) return false;
45
27
  for (var i = 0; i < s.length; i++) if (!_isDigit(s.charAt(i))) return false;
46
28
  return true;
47
29
  }
48
30
 
49
- // Expand an obsolete RFC 850 two-digit year: HTTP interprets it relative to the receipt year, as the full
50
- // year with these two digits within [refYear-50, refYear+49] (moved to the past only when it would
51
- // otherwise be more than 50 years ahead). A fixed cutoff would misdate a year near the century boundary.
52
- // With no receipt time, fall back to the RFC 6265 fixed rule (< 70 -> 2000s, else 1900s).
53
31
  function _rfc850Year(yy, refMs) {
54
32
  if (typeof refMs !== "number" || !isFinite(refMs)) return yy + (yy < 70 ? 2000 : 1900);
55
33
  var refYear = new Date(refMs).getUTCFullYear();
56
34
  var full = Math.floor(refYear / 100) * 100 + yy;
57
- // Move to the previous century only when the same-century year would be more than 50 years ahead; never
58
- // advance a past year to the future (an old two-digit year stays in the past).
59
35
  if (full - refYear > 50) full -= 100;
60
36
  return full;
61
37
  }
62
38
 
63
- // IMF-fixdate (the required form): "Sun, 06 Nov 1994 08:49:37 GMT".
64
39
  function _scanImf(s) {
65
40
  var p = _matchName(s, 0, DOW_SHORT);
66
41
  if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
@@ -70,8 +45,6 @@ function _scanImf(s) {
70
45
  var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
71
46
  return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
72
47
  }
73
- // rfc850-date (obsolete, still accepted): "Sunday, 06-Nov-94 08:49:37 GMT" -- a two-digit year expanded
74
- // relative to the receipt year.
75
48
  function _scanRfc850(s, refMs) {
76
49
  var p = _matchName(s, 0, DOW_LONG);
77
50
  if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
@@ -81,7 +54,6 @@ function _scanRfc850(s, refMs) {
81
54
  var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
82
55
  return { day: dd, monName: mo, year: _rfc850Year(yy, refMs), hh: t.hh, mi: t.mi, ss: t.ss };
83
56
  }
84
- // asctime-date (obsolete, still accepted): "Sun Nov 6 08:49:37 1994" -- a space-padded one/two-digit day.
85
57
  function _scanAsctime(s) {
86
58
  var p = _matchName(s, 0, DOW_SHORT);
87
59
  if (p === -1 || s.charAt(p) !== " ") return null; p += 1;
@@ -94,10 +66,6 @@ function _scanAsctime(s) {
94
66
  return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
95
67
  }
96
68
 
97
- // Parse an HTTP-date to epoch ms, or NaN when its grammar, calendar, or time is invalid. Built from the
98
- // extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC, the asctime form carries
99
- // no GMT token so delegating would parse it in local time, and the round-trip check rejects an
100
- // impossible date / time (Date.UTC normalizes Feb 31 -> Mar 2 just as Date.parse would).
101
69
  function httpDateMs(s, refMs) {
102
70
  var f = _scanImf(s) || _scanRfc850(s, refMs) || _scanAsctime(s);
103
71
  if (!f) return NaN;
@@ -110,25 +78,13 @@ function httpDateMs(s, refMs) {
110
78
  return when;
111
79
  }
112
80
 
113
- // parse(value, opts) -> { retryAfterSeconds, retryAfterDate }. A PRESENT Retry-After value (the caller
114
- // gates absence with its own code) is a delay-seconds integer -> retryAfterSeconds, or an HTTP-date ->
115
- // retryAfterDate (epoch ms) plus, when opts.now (epoch ms) is given, a bounded retryAfterSeconds. Either
116
- // form beyond the one-year ceiling, or a value that is neither, fails closed via opts.E(opts.code, ...).
117
- // opts.cap (seconds) clamps a value above the cap to the cap instead of rejecting it; opts.lenient surfaces
118
- // an otherwise-rejected value as a null retryAfterSeconds. Both exist for a caller (e.g. an ARI poll
119
- // cadence) for whom the value is advisory and must not discard the response. Nothing sleeps here; the
120
- // value is surfaced for the caller to decide.
121
81
  function parse(value, opts) {
122
82
  opts = opts || {};
123
83
  var raStr = String(value).trim();
124
84
  var out = { retryAfterSeconds: null, retryAfterDate: null };
125
85
  if (_isAllDigits(raStr)) {
126
86
  var n = parseInt(raStr, 10);
127
- // opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay above its
128
- // ceiling reduced to the ceiling, not rejected, so a valid-but-huge value never discards the response.
129
87
  if (typeof opts.cap === "number" && n > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
130
- // opts.lenient: a caller for whom the value is purely advisory wants an unparseable one surfaced as
131
- // null (retryAfterSeconds stays null) instead of a hard reject that would discard the whole response.
132
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)"); }
133
89
  out.retryAfterSeconds = n;
134
90
  return out;
@@ -137,8 +93,6 @@ function parse(value, opts) {
137
93
  if (isNaN(when)) { if (opts.lenient) return out; throw _fail(opts, "a Retry-After must be delay-seconds or a valid HTTP-date (RFC 7231 sec. 7.1.1.1/7.1.3), got " + JSON.stringify(raStr)); }
138
94
  out.retryAfterDate = when;
139
95
  if (typeof opts.now === "number" && isFinite(opts.now)) {
140
- // Round the remaining whole-second delay UP (a sub-second date must not retry before the requested
141
- // time), clamping a past date to 0.
142
96
  var d = Math.max(0, Math.ceil((when - opts.now) / constants.TIME.seconds(1)));
143
97
  if (typeof opts.cap === "number" && d > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
144
98
  if (d > MAX_RETRY_AFTER_SECONDS) { if (opts.lenient) return out; throw _fail(opts, "the Retry-After date is beyond the supported horizon (" + MAX_RETRY_AFTER_SECONDS + " seconds)"); }