@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
package/lib/est.js CHANGED
@@ -63,9 +63,9 @@ var x509 = require("./schema-x509");
63
63
  var pkcs8 = require("./schema-pkcs8");
64
64
  var key = require("./key");
65
65
  var csr = require("./schema-csr");
66
- var csrattrsFmt = require("./schema-csrattrs"); // aliased: the `csrattrs` name is the verb + the public export
67
- var cmcVerify = require("./cmc-verify"); // /fullcmc interprets its response through the CMC layer
68
- var cmcFmt = require("./schema-cmc"); // to read back which keys the submitted request asked to certify
66
+ var csrattrsFmt = require("./schema-csrattrs");
67
+ var cmcVerify = require("./cmc-verify");
68
+ var cmcFmt = require("./schema-cmc");
69
69
  var OID_CMC_TRANSACTION_ID = oid.byName("id-cmc-transactionId");
70
70
  var OID_CMC_SENDER_NONCE = oid.byName("id-cmc-senderNonce");
71
71
  var OID_CMC_DATA_RETURN = oid.byName("id-cmc-dataReturn");
@@ -76,9 +76,7 @@ var intrinsic = require("./guard-intrinsic");
76
76
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
77
77
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
78
78
  var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
79
- var pkix = require("./schema-pkix"); // shared base64-whitespace strip + JS-\s predicate
80
- // Own-key membership through an operation taken at load: written out it reads two replaceable
81
- // properties to answer one question, and here it decides which fields of a response are copied.
79
+ var pkix = require("./schema-pkix");
82
80
  var _hasOwn = intrinsic.hasOwn;
83
81
  var httpTransport = require("./http-transport");
84
82
  var httpDigest = require("./http-digest");
@@ -90,12 +88,9 @@ var ID_SIGNED_DATA = oid.byName("signedData");
90
88
  var OID_CHALLENGE_PASSWORD = oid.byName("challengePassword");
91
89
  var OID_DECRYPT_KEY_ID = oid.byName("decryptKeyID");
92
90
 
93
- // Charset tables built once from the shared pkix.charTable / pkix.allCharsIn char-scan home.
94
- var _DIGITS_TABLE = pkix.charTable("0123456789"); // /^[0-9]+$/
95
- var _LABEL_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-"); // /^[A-Za-z0-9._~-]+$/
91
+ var _DIGITS_TABLE = pkix.charTable("0123456789");
92
+ var _LABEL_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-");
96
93
 
97
- // Trailing-slash strip (/\/+$/ -> ""): the length after dropping a run of trailing "/" (0x2f); the
98
- // caller slices to it, so the removed regex read is traded for the slice that replaces it.
99
94
  function _stripTrailingSlashEnd(s) { var end = s.length; while (end > 0 && _charCodeAt(s, end - 1) === 0x2f) end -= 1; return end; }
100
95
  var OID_ASYMM_DECRYPT_KEY_ID = oid.byName("asymmDecryptKeyID");
101
96
  var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
@@ -103,19 +98,6 @@ var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
103
98
 
104
99
  var OPERATIONS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serverkeygen", "csrattrs"];
105
100
 
106
- // ---- the option surface each verb accepts -----------------------------------
107
- //
108
- // A misspelled option reads as an omission, never as a value: nothing is out of range and
109
- // nothing fails to parse, so the caller who asked for something stricter gets the looser default
110
- // and is told nothing. That is worst here, where the options carry the security posture of a
111
- // network exchange -- a misspelled `tls` leaves the anchors unset (the no-anchors refusal names
112
- // the missing pin, so this one is caught), a misspelled `strict` accepts the extra certificates it
113
- // was set to reject, a misspelled `expectedRecipientKeyId` drops a recipient pin on a
114
- // server-generated private key, and a misspelled `oldCert` fails the re-enrollment outright.
115
- //
116
- // Every network verb shares the client surface, because every one of them goes through _client and
117
- // the redirect / authentication plumbing it drives. The per-verb tables extend it instead of
118
- // restating it, so a key added to the client reaches every verb at once and none of them drifts.
119
101
  var CLIENT_OPTS = {
120
102
  transport: 1, tls: 1, label: 1, timeout: 1, maxResponseBytes: 1, maxRedirects: 1, now: 1,
121
103
  auth: 1, username: 1, password: 1, allowCrossOriginRedirect: 1,
@@ -126,13 +108,9 @@ function _withClient(extra) {
126
108
  Object.keys(extra || {}).forEach(function (k) { out[k] = 1; });
127
109
  return out;
128
110
  }
129
- // `strict` is enroll-only: _certsResult reads it after the /cacerts branch has already returned, so
130
- // accepting it on cacerts would advertise a check that cannot run there.
131
111
  var CACERTS_OPTS = _withClient(null);
132
112
  var SIMPLEENROLL_OPTS = _withClient({ strict: 1 });
133
113
  var SIMPLEREENROLL_OPTS = _withClient({ strict: 1, oldCert: 1 });
134
- // expectedRecipientKind is deliberately absent: it is derived from the CSR's own advertised attribute, never
135
- // taken from the caller, so listing it would offer a pin that nothing reads.
136
114
  var SERVERKEYGEN_OPTS = _withClient({
137
115
  requestedEncryption: 1, expectedRecipientKeyId: 1, expectedRecipientIssuerSerial: 1,
138
116
  });
@@ -141,7 +119,6 @@ var FULLCMC_OPTS = _withClient({
141
119
  transactionId: 1, senderNonce: 1, dataReturn: 1,
142
120
  responderCerts: 1, responseRecipient: 1, allowUnverifiedResponse: 1, allowUnboundResponse: 1,
143
121
  });
144
- // The two verbs that take options without going near the network.
145
122
  var CLASSIFY_OPTS = { op: 1, now: 1 };
146
123
  var PATHS_OPTS = { label: 1 };
147
124
  var PARSE_SERVERKEYGEN_OPTS = {
@@ -156,7 +133,6 @@ function _knownOpts(opts, known, verb) {
156
133
  });
157
134
  }
158
135
 
159
- // ---- the RFC 8951 sec. 3/3.1 transfer codec (CTE-header-blind) -----------
160
136
 
161
137
  /**
162
138
  * @primitive pki.est.transferDecode
@@ -177,29 +153,17 @@ function _knownOpts(opts, known, verb) {
177
153
  * var roundTripped = pki.est.transferDecode(pki.est.transferEncode(der));
178
154
  */
179
155
  function transferDecode(body) {
180
- // The pre-decode ceiling: the largest base64 that could yield a DER_MAX_BYTES
181
- // document, plus a generous line-wrapping allowance (CRLF at 64/76-char lines is
182
- // ~3%; 1/8 leaves ample margin) so a normally-wrapped near-limit body is not
183
- // rejected before the real DER_MAX_BYTES limit is enforced on the decode below.
184
- // guard.text.decode caps the raw byte length BEFORE the latin1 copy.
185
156
  var b64Len = Math.ceil(constants.LIMITS.DER_MAX_BYTES * 4 / 3);
186
157
  var cap = b64Len + Math.ceil(b64Len / 8) + constants.BYTES.kib(64);
187
158
  var s = guard.text.decode(body, cap, EstError, {
188
159
  charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the EST payload",
189
160
  });
190
161
  var stripped = pkix.stripBase64Whitespace(s);
191
- // Strict canonical RFC 4648 base64 (RFC 8951 sec. 3.1) via the shared encoding
192
- // guard: the alphabet + whole-4-group + canonical round-trip fail a malformed
193
- // body closed instead of silently truncating to a shorter, different DER.
194
162
  var der = guard.encoding.base64(stripped, null, E, "est/bad-base64", "the EST payload");
195
163
  if (der.length > constants.LIMITS.DER_MAX_BYTES) throw E("est/too-large", "the decoded EST DER exceeds the size cap");
196
164
  return der;
197
165
  }
198
166
 
199
- // The byte length of a transport response body for the size-cap and empty-body checks: a null / absent body
200
- // is 0, a string its UTF-8 byte length, and any BufferSource (a Buffer / Uint8Array / ArrayBuffer / DataView
201
- // from an injected transport) its byte count through the byte guard -- never a String()-coerced comma list.
202
- // res.body is read ONCE so a lying getter on an injected response cannot change it between the arms.
203
167
  function _bodyLen(res) {
204
168
  var body = res.body;
205
169
  return body == null ? 0 : (typeof body === "string" ? Buffer.byteLength(body, "utf8")
@@ -223,25 +187,20 @@ function _bodyLen(res) {
223
187
  */
224
188
  function transferEncode(der) {
225
189
  if (!Buffer.isBuffer(der)) throw E("est/bad-input", "transferEncode requires a DER Buffer");
226
- // Re-view: a detached-backed Buffer reads as zero-length and would encode "".
227
190
  der = guard.bytes.view(der, EstError, "est/bad-input", "transferEncode DER input");
228
191
  return der.toString("base64");
229
192
  }
230
193
 
231
- // ---- the multipart/mixed splitter (/serverkeygen) -----------------------
232
194
 
233
- // Case-insensitive ASCII prefix test: does `s` begin with the uppercase literal `litUpper`?
234
195
  function _ciStartsWith(s, litUpper) {
235
196
  if (s.length < litUpper.length) return false;
236
197
  for (var k = 0; k < litUpper.length; k++) {
237
198
  var c = _charCodeAt(s, k);
238
- if (c >= 97 && c <= 122) c -= 32; // fold lowercase -> uppercase
199
+ if (c >= 97 && c <= 122) c -= 32;
239
200
  if (c !== _charCodeAt(litUpper, k)) return false;
240
201
  }
241
202
  return true;
242
203
  }
243
- // /^multipart\/mixed\s*(;|$)/i by character scan: the media type "multipart/mixed" (case-insensitive),
244
- // then JS \s* whitespace, then a `;` parameter separator or end of the header value.
245
204
  var _MULTIPART_MIXED = "MULTIPART/MIXED";
246
205
  function _isMultipartMixedPrefix(ct) {
247
206
  if (!_ciStartsWith(ct, _MULTIPART_MIXED)) return false;
@@ -250,13 +209,9 @@ function _isMultipartMixedPrefix(ct) {
250
209
  return p === ct.length || _charCodeAt(ct, p) === 0x3b;
251
210
  }
252
211
 
253
- // Extract the boundary from a `multipart/mixed; boundary=...` content-type,
254
- // tolerating whitespace before the semicolon (erratum 5779 REJECTED the ban).
255
212
  function _multipartBoundary(contentType) {
256
213
  var ct = String(contentType || "");
257
214
  if (!_isMultipartMixedPrefix(ct)) return null;
258
- // Two boundaries name two different splits of the same body; there is no reading
259
- // of that header, so it is refused rather than resolved by taking the first.
260
215
  var bp = _ctParam(ct, "boundary");
261
216
  if (bp.duplicated) {
262
217
  throw E("est/bad-multipart",
@@ -265,10 +220,6 @@ function _multipartBoundary(contentType) {
265
220
  return bp.value;
266
221
  }
267
222
 
268
- // A boundary delimiter at a candidate line start `ls`: `--boundary`, an optional `--` close marker, optional
269
- // SP/HTAB transport-padding, then a CRLF/LF or end-of-text. Returns { end, close } (end = the index past the
270
- // trailing line terminator) or null. `(--)?` is taken greedily without backtracking loss: a `-` can be neither
271
- // padding nor a terminator, so if the close marker is present it is the only possible parse.
272
223
  function _matchDelimAt(text, ls, boundary) {
273
224
  var n = text.length, q = ls, blen = boundary.length, k;
274
225
  if (q + 2 > n || _charCodeAt(text, q) !== 0x2d || _charCodeAt(text, q + 1) !== 0x2d) return null;
@@ -284,11 +235,6 @@ function _matchDelimAt(text, ls, boundary) {
284
235
  if (_charCodeAt(text, q) === 0x0d && q + 1 < n && _charCodeAt(text, q + 1) === 0x0a) return { end: q + 2, close: close };
285
236
  return null;
286
237
  }
287
- // Every delimiter in `text`, replacing the global /(?:^|\r?\n)--boundary(--)?[ \t]*(?:\r?\n|$)/g scan: a
288
- // delimiter begins the text or follows a CRLF/LF (which the match CONSUMES), so `at` is the index of that
289
- // leading terminator (0 at the start) and `bodyStart` is the index past the trailing one. The leftmost match
290
- // at or after the previous `bodyStart` is taken, so two adjacent delimiters share one terminator and only the
291
- // first is found -- the same non-overlapping behavior the regex has.
292
238
  function _findMultipartMarks(text, boundary) {
293
239
  var marks = [], n = text.length, searchStart = 0, p;
294
240
  while (searchStart <= n) {
@@ -306,8 +252,6 @@ function _findMultipartMarks(text, boundary) {
306
252
  }
307
253
  return marks;
308
254
  }
309
- // Strip the part-body framing the regexes /^(\r?\n){2}/ and /\r?\n$/ removed: the two leading line terminators
310
- // (all-or-nothing) that follow the header/body separator, and one trailing terminator.
311
255
  function _stripTwoLeadingEol(seg, from) {
312
256
  var q = from;
313
257
  for (var t = 0; t < 2; t++) {
@@ -324,8 +268,6 @@ function _stripOneTrailingEol(seg, end) {
324
268
  }
325
269
  return end;
326
270
  }
327
- // Unfold folded MIME header lines (/\r?\n(?=[ \t])/g -> ""): a CRLF/LF immediately followed by SP/HTAB is a
328
- // FOLD, not a header break, so the terminator is dropped while the whitespace stays.
329
271
  function _unfoldHeaders(raw) {
330
272
  var out = "", last = 0, i = 0, n = raw.length;
331
273
  while (i < n) {
@@ -340,7 +282,6 @@ function _unfoldHeaders(raw) {
340
282
  }
341
283
  return out + _strSlice(raw, last, n);
342
284
  }
343
- // Split on CRLF/LF (.split(/\r?\n/)) by character scan, preserving JS split semantics (leading/trailing "").
344
285
  function _splitCrlf(s) {
345
286
  var out = [], last = 0, i = 0, n = s.length;
346
287
  while (i < n) {
@@ -353,11 +294,6 @@ function _splitCrlf(s) {
353
294
  return out;
354
295
  }
355
296
 
356
- // Split a multipart/mixed body into its parts, each { headers, contentType,
357
- // body }. A boundary delimiter is `--boundary` only at the START OF A LINE (body
358
- // start or after CRLF), optional transport-padding, then CRLF (a part) or `--`
359
- // (the close delimiter) -- matching the raw substring would treat `--boundaryX`,
360
- // which is NOT a delimiter (RFC 2046), as one. The preamble/epilogue are ignored.
361
297
  function splitMultipartMixed(body, contentType) {
362
298
  var boundary = _multipartBoundary(contentType);
363
299
  if (!boundary) throw E("est/bad-multipart", "a serverkeygen response must be multipart/mixed with a boundary (RFC 7030 sec. 4.4.2)");
@@ -370,16 +306,13 @@ function splitMultipartMixed(body, contentType) {
370
306
  if (closeAt === -1) throw E("est/bad-multipart", "the multipart body is missing its terminal boundary (RFC 2046)");
371
307
  var parts = [];
372
308
  for (var i = 0; i < closeAt; i++) {
373
- var seg = _strSlice(text, marks[i].bodyStart, marks[i + 1].at); // between this delimiter and the next
309
+ var seg = _strSlice(text, marks[i].bodyStart, marks[i + 1].at);
374
310
  var sep = seg.indexOf("\r\n\r\n");
375
311
  if (sep === -1) sep = seg.indexOf("\n\n");
376
312
  if (sep === -1) throw E("est/bad-multipart", "a multipart part is missing its header/body separator");
377
313
  var rawHeaders = _strSlice(seg, 0, sep);
378
314
  var partBody = _strSlice(seg, _stripTwoLeadingEol(seg, sep), _stripOneTrailingEol(seg, seg.length));
379
315
  var headers = {};
380
- // Unfold MIME part headers (RFC 5322 sec. 2.2.3 / RFC 2046): a CRLF immediately followed by whitespace is a
381
- // FOLD, not a header break, so it is removed before the split -- a Content-Type whose smime-type parameter
382
- // continues on the next line must be read whole, not truncated.
383
316
  var lines = _splitCrlf(_unfoldHeaders(rawHeaders));
384
317
  for (var li = 0; li < lines.length; li++) {
385
318
  var line = lines[li];
@@ -393,7 +326,6 @@ function splitMultipartMixed(body, contentType) {
393
326
  return parts;
394
327
  }
395
328
 
396
- // ---- the certs-only Simple PKI Response validator -----------------------
397
329
 
398
330
  /**
399
331
  * @primitive pki.est.parseCertsOnly
@@ -426,15 +358,9 @@ function splitMultipartMixed(body, contentType) {
426
358
  * r.certificates; // -> [Buffer, ...] raw, unordered
427
359
  */
428
360
  function parseCertsOnly(der) {
429
- // The certs-only Simple PKI Response shape is a CMS concern shared with AIA path building; the reader lives
430
- // in schema-cms. The "est" prefix keeps the exact est/* codes (est/not-certs-only, est/no-certificates, ...).
431
361
  return cms.parseCertsOnly(der, E, "est");
432
362
  }
433
363
 
434
- // Pick the issued certificate from a certs-only response by matching its public
435
- // key against the CSR / SPKI the client submitted -- the ONLY sanctioned
436
- // identification (positional guessing is forbidden, RFC 5272 sec. 4.1). `target`
437
- // is an SPKI object (with a `bytes` subjectPublicKey field) or a raw Buffer.
438
364
  function findIssuedCert(certs, target) {
439
365
  var want = Buffer.isBuffer(target) ? target : (target && target.bytes);
440
366
  if (!Buffer.isBuffer(want)) return null;
@@ -447,24 +373,14 @@ function findIssuedCert(certs, target) {
447
373
  return null;
448
374
  }
449
375
 
450
- // ---- /serverkeygen ------------------------------------------------------
451
376
 
452
- // Collect every key-identifier (subjectKeyIdentifier / KEKIdentifier) a set of
453
- // RecipientInfos names, across the ktri / kari / kekri / kemri arms -- the
454
- // byte identifiers a client's advertised decryptKeyID would match.
455
- // The key-identifier bytes each RecipientInfo arm names, filtered by MECHANISM (RFC 7030 sec. 4.4.1): an
456
- // ASYMMETRIC decryption key (AsymmetricDecryptKeyIdentifier) is named by a subjectKeyIdentifier on a
457
- // KeyTrans / KeyAgree / KEM arm, a SYMMETRIC key (DecryptKeyIdentifier) by the keyIdentifier of a KEK arm.
458
- // Filtering keeps a symmetric KEK arm from satisfying an advertised asymmetric key (and vice-versa) merely
459
- // because the identifier bytes coincide -- the returned key would be unusable with the mechanism requested.
460
- // kind: "asymmetric" | "symmetric" | undefined (any, for a direct caller that did not state the mechanism).
461
377
  function _recipientKeyIds(recipientInfos, kind) {
462
378
  var ids = [];
463
379
  function push(v) { if (Buffer.isBuffer(v)) ids.push(v); }
464
380
  (recipientInfos || []).forEach(function (r) {
465
381
  if (kind !== "symmetric") {
466
382
  if (r.rid) push(r.rid.subjectKeyIdentifier);
467
- if (r.kemri && r.kemri.rid) push(r.kemri.rid.subjectKeyIdentifier); // KEMRecipientInfo (RFC 9629, under the ori arm)
383
+ if (r.kemri && r.kemri.rid) push(r.kemri.rid.subjectKeyIdentifier);
468
384
  (r.recipientEncryptedKeys || []).forEach(function (rek) { if (rek.rid) push(rek.rid.subjectKeyIdentifier); });
469
385
  }
470
386
  if (kind !== "asymmetric") {
@@ -474,9 +390,6 @@ function _recipientKeyIds(recipientInfos, kind) {
474
390
  return ids;
475
391
  }
476
392
 
477
- // The { issuer (raw DN bytes), serialNumber } every issuerAndSerialNumber recipient
478
- // arm names -- the form a server MAY use after mapping an AsymmetricDecryptKeyIdentifier
479
- // to a certificate (RFC 7030 sec. 4.4.2), so a key-id-only match would miss it.
480
393
  function _recipientIssuerSerials(recipientInfos) {
481
394
  var out = [];
482
395
  function push(rid) { if (rid && rid.issuer && Buffer.isBuffer(rid.issuer.bytes) && rid.serialNumber != null) out.push({ issuer: rid.issuer.bytes, serialNumber: rid.serialNumber }); }
@@ -488,45 +401,22 @@ function _recipientIssuerSerials(recipientInfos) {
488
401
  return out;
489
402
  }
490
403
 
491
- // The advertised recipient (a decryptKeyID / asymmDecryptKeyID key identifier
492
- // and/or an issuer+serial) matches SOME RecipientInfo arm -- either identifier
493
- // form the server may have used to name the same requested key.
494
404
  function _recipientMatches(recipientInfos, opts) {
495
405
  if (Buffer.isBuffer(opts.expectedRecipientKeyId) &&
496
406
  _recipientKeyIds(recipientInfos, opts.expectedRecipientKind).some(function (id) { return id.equals(opts.expectedRecipientKeyId); })) return true;
497
407
  var ias = opts.expectedRecipientIssuerSerial;
498
408
  if (ias && Buffer.isBuffer(ias.issuer) && ias.serialNumber != null) {
499
- // Validate the caller-supplied serial BEFORE BigInt() so a bad value is a typed est/bad-input, never a
500
- // raw SyntaxError / RangeError leaking from the codec (the single choke point for both the serverkeygen
501
- // verb and the public parseServerKeygenResponse).
502
409
  var sn = ias.serialNumber, want;
503
410
  if (typeof sn === "bigint" && sn >= 0n) want = sn;
504
411
  else if (typeof sn === "number" && Number.isSafeInteger(sn) && sn >= 0) want = BigInt(sn);
505
412
  else if (typeof sn === "string" && pkix.allCharsIn(sn, _DIGITS_TABLE)) want = BigInt(sn);
506
413
  else throw E("est/bad-input", "expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
507
- // An issuerAndSerialNumber names an ASYMMETRIC recipient -- a certificate identity the server derived from
508
- // an AsymmetricDecryptKeyIdentifier (RFC 7030 sec. 4.4.2); a symmetric KEK recipient has none. So a SYMMETRIC
509
- // request must NOT be satisfied by an asymmetric arm's issuer+serial, or the client would accept ciphertext
510
- // undecryptable with the symmetric key it requested. The key-id branch applies the same mechanism filter.
511
414
  if (opts.expectedRecipientKind !== "symmetric" &&
512
415
  _recipientIssuerSerials(recipientInfos).some(function (r) { return r.issuer.equals(ias.issuer) && r.serialNumber === want; })) return true;
513
416
  }
514
417
  return false;
515
418
  }
516
419
 
517
- // Parse a /serverkeygen response: exactly two parts -- a private-key part
518
- // (application/pkcs8 cleartext PrivateKeyInfo, or application/pkcs7-mime;
519
- // smime-type=server-generated-key EnvelopedData) and a certificate part (the
520
- // enroll-response shape). opts.requestedEncryption asserts the key part is
521
- // encrypted (a cleartext part -> est/expected-encrypted-key). The advertised
522
- // recipient may be given as opts.expectedRecipientKeyId (a Buffer, the
523
- // decryptKeyID / asymmDecryptKeyID) and/or opts.expectedRecipientIssuerSerial
524
- // ({ issuer: raw-DN Buffer, serialNumber }, the form a server MAY use after
525
- // mapping that identifier to a certificate); when either is given, a RecipientInfo
526
- // of the encrypted key must match one of them (else est/recipient-mismatch) -- so
527
- // a response encrypted to a DIFFERENT recipient fails closed rather than passing.
528
- // Split a content-type on `;` that are OUTSIDE a quoted-string (RFC 2045),
529
- // so a `;` inside a parameter value does not create a spurious parameter.
530
420
  function _splitContentTypeParams(s) {
531
421
  var segs = [], cur = "", inQuote = false;
532
422
  for (var i = 0; i < s.length; i++) {
@@ -539,19 +429,6 @@ function _splitContentTypeParams(s) {
539
429
  return segs;
540
430
  }
541
431
 
542
- // Split a part's content-type into its base media type (lowercased) and its
543
- // smime-type parameter. The media type must be the EXACT token (a look-alike
544
- // like application/pkcs8evil yields that literal, not application/pkcs8), and
545
- // parameters are read token-by-token honoring quoted-strings so a smime-type-like
546
- // substring inside another quoted parameter value is NOT taken as smime-type.
547
- // A named Content-Type parameter, plus whether the header declared it MORE THAN
548
- // ONCE. RFC 2045 sec. 5.1 gives a parameter at most one value, so a header stating
549
- // two is ambiguous -- and taking the first would let the position of a duplicate,
550
- // not the sender, decide how the body is read. `smime-type` selects which response
551
- // arm this is and `boundary` decides where the parts begin, so in both cases the
552
- // choice is the whole answer. Reported rather than thrown here: each boundary
553
- // refuses in its own terms, and the error path must not let a bad label displace
554
- // the HTTP fault it arrived with.
555
432
  function _ctParam(contentType, name) {
556
433
  var segs = _splitContentTypeParams(String(contentType || ""));
557
434
  var value = null, count = 0;
@@ -568,8 +445,6 @@ function _ctParam(contentType, name) {
568
445
  return { value: value, duplicated: count > 1 };
569
446
  }
570
447
 
571
- // A media type `type/subtype` by character scan (RFC 2045 sec. 5.1 restricted token): the first character
572
- // of each part is alphanumeric, the rest add !#$&^_.+- (case-insensitive), exactly one `/` separates them.
573
448
  var _MT_FIRST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
574
449
  var _MT_REST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$&^_.+-");
575
450
  function _isMediaToken(s, from, to) {
@@ -577,8 +452,6 @@ function _isMediaToken(s, from, to) {
577
452
  for (var i = from + 1; i < to; i++) { if (!_MT_REST[_charCodeAt(s, i)]) return false; }
578
453
  return true;
579
454
  }
580
- // /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i -> the lowercased media type, or null:
581
- // trim JS whitespace, require a single `/` splitting two restricted tokens.
582
455
  function _mediaType(seg) {
583
456
  var a = 0, b = seg.length, slash = -1, i;
584
457
  while (a < b && pkix.isJsWhitespace(_charCodeAt(seg, a))) a += 1;
@@ -606,44 +479,23 @@ function parseServerKeygenResponse(body, contentType, opts) {
606
479
  var keyPart = null, certPart = null, encrypted = false;
607
480
  for (var i = 0; i < parts.length; i++) {
608
481
  var pt = _partMediaType(parts[i].contentType);
609
- // Same rule on a PART header: smime-type tells the encrypted key part from the
610
- // cleartext one and from the certificate part, so two of them leave which part
611
- // this is undecided.
612
482
  if (pt.ambiguous) {
613
483
  throw E("est/bad-multipart",
614
484
  "a serverkeygen response part declares more than one smime-type, so which part it is cannot be told from it (RFC 2045 sec. 5.1)");
615
485
  }
616
486
  if (pt.media === "application/pkcs8") { keyPart = parts[i]; encrypted = false; }
617
487
  else if (pt.media === "application/pkcs7-mime" && pt.smimeType === "server-generated-key") { keyPart = parts[i]; encrypted = true; }
618
- // The certificate part exactly matches the /simpleenroll response
619
- // (RFC 7030 sec. 4.4.2), so it too MUST be smime-type=certs-only.
620
488
  else if (pt.media === "application/pkcs7-mime" && pt.smimeType === "certs-only") certPart = parts[i];
621
489
  else throw E("est/bad-multipart", "unrecognized serverkeygen part content-type " + JSON.stringify(parts[i].contentType));
622
490
  }
623
491
  if (!keyPart || !certPart) throw E("est/bad-multipart", "a serverkeygen response needs one key part and one certificate part");
624
492
  if (opts.requestedEncryption && !encrypted) throw E("est/expected-encrypted-key", "encryption was requested but the private-key part is cleartext (RFC 7030 sec. 4.4.2)");
625
- // The reverse is also a mismatch: the CSR advertised NO key-encryption key (requestedEncryption explicitly
626
- // false), yet the server returned an EnvelopedData. The client holds nothing to decrypt it, so an unsolicited
627
- // encrypted key is an unusable credential, not a success (RFC 7030 sec. 4.4.2). (An undefined requestedEncryption
628
- // -- a low-level caller not stating the mode -- stays permissive.)
629
493
  if (opts.requestedEncryption === false && encrypted) throw E("est/unexpected-encrypted-key", "the server returned an encrypted key but the CSR advertised no DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier to decrypt it (RFC 7030 sec. 4.4.2)");
630
494
  var out = { certificates: parseCertsOnly(transferDecode(certPart.body)).certificates };
631
495
  if (encrypted) {
632
- // The server-generated key MUST be a CMS EnvelopedData (RFC 7030 sec. 4.4.2);
633
- // a SignedData or any other CMS content type under the encrypted label is a
634
- // structurally invalid key part, not a success. The EnvelopedData is surfaced
635
- // structurally -- the ciphertext stays opaque and the RecipientInfo arms feed
636
- // the coherence check the caller makes.
637
496
  var parsedKey = cms.parse(transferDecode(keyPart.body));
638
497
  if (parsedKey.contentTypeName !== "envelopedData") throw E("est/bad-key-part", "a server-generated encrypted key part must be a CMS EnvelopedData (RFC 7030 sec. 4.4.2), got " + JSON.stringify(parsedKey.contentTypeName));
639
- // The EnvelopedData MUST encapsulate a CMS SignedData holding the private key
640
- // (RFC 7030 sec. 4.4.2) -- validate the inner encryptedContentInfo content type,
641
- // not just the outer ContentInfo, so a key wrapped around id-data or anything
642
- // else fails closed before the caller decrypts it.
643
498
  if (!parsedKey.encryptedContentInfo || parsedKey.encryptedContentInfo.contentType !== ID_SIGNED_DATA) throw E("est/bad-key-part", "a server-generated encrypted key's EnvelopedData must encapsulate a CMS SignedData (RFC 7030 sec. 4.4.2)");
644
- // A detached OR empty EnvelopedData carries no ciphertext to decrypt -- there is
645
- // no key to recover, so it is not a valid key response (the shared CMS assert
646
- // rejects both null and zero-length, matching the CRMF / PKCS#12 siblings).
647
499
  cms.assertAttachedCiphertext(parsedKey.encryptedContentInfo, E, "est/bad-key-part", "a server-generated encrypted key's EnvelopedData");
648
500
  if (Buffer.isBuffer(opts.expectedRecipientKeyId) || opts.expectedRecipientIssuerSerial) {
649
501
  if (!_recipientMatches(parsedKey.recipientInfos, opts)) throw E("est/recipient-mismatch", "the server-generated key is not encrypted to the advertised recipient (RFC 7030 sec. 4.4.2)");
@@ -652,53 +504,23 @@ function parseServerKeygenResponse(body, contentType, opts) {
652
504
  } else {
653
505
  var keyDer = transferDecode(keyPart.body);
654
506
  out.privateKey = pkcs8.parse(keyDer);
655
- // The raw cleartext PrivateKeyInfo DER is surfaced so a semantic layer can bind the key to its
656
- // certificate (the public-key coherence check the verb runs). Structural parsing itself never
657
- // does the crypto derivation -- mirroring parseCertsOnly, which splits certs while the enroll
658
- // verb picks the issued one by public-key match.
659
507
  out.privateKeyDer = keyDer;
660
508
  }
661
509
  return out;
662
510
  }
663
511
 
664
- // ---- the HTTP response classifier --------------------------------------
665
512
 
666
- // The required 200-response content-type per operation: the EXACT media-type
667
- // token plus, where the RFC mandates it, the smime-type parameter. simpleenroll /
668
- // simplereenroll require smime-type=certs-only (RFC 7030 sec. 4.2.3) so a different
669
- // PKI message type (CMC-response, ...) is not accepted; cacerts mandates only the
670
- // media type (sec. 4.1.3). Matched token-wise, not by prefix, so a look-alike like
671
- // "application/pkcs7-mimeevil" is rejected.
672
- // smimeTypes is a LIST for every row that constrains it, rather than a scalar on some
673
- // rows and a list on others: /fullcmc legitimately answers with either arm, and one
674
- // shape for the field keeps the comparison below single-branch.
675
513
  var CONTENT_TYPE_BY_OP = {
676
514
  cacerts: { media: "application/pkcs7-mime" },
677
515
  simpleenroll: { media: "application/pkcs7-mime", smimeTypes: ["certs-only"] },
678
516
  simplereenroll: { media: "application/pkcs7-mime", smimeTypes: ["certs-only"] },
679
- // RFC 7030 sec. 4.3.2: a 200 /fullcmc response is application/pkcs7-mime whose
680
- // smime-type is EITHER certs-only (the CA answered with a Simple PKI Response) or
681
- // CMC-response (a Full PKI Response). Both are conforming and the CA chooses.
682
- // Spelled lowercase here and compared case-insensitively: RFC 5273 sec. 3 prose
683
- // writes "CMC-Request"/"CMC-Response" while its own Table 1 and RFC 7030 write
684
- // "CMC-request"/"CMC-response", so a case-sensitive match would reject a
685
- // conforming server depending on which sentence its author read.
686
517
  fullcmc: { media: "application/pkcs7-mime", smimeTypes: ["certs-only", "cmc-response"] },
687
518
  serverkeygen: { media: "multipart/mixed" },
688
519
  csrattrs: { media: "application/csrattrs" },
689
520
  };
690
521
 
691
- // The operations whose responses this client classifies.
692
522
  var CLASSIFIABLE_OPS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serverkeygen", "csrattrs"];
693
523
 
694
- // RFC 7030 sec. 4.3.2: on /fullcmc a 404 OR a 501 means "this service is not
695
- // implemented" -- a distinct, non-error verdict the caller can act on.
696
- //
697
- // That makes THREE meanings for 404 across this classifier, so they are listed
698
- // together rather than as branches accumulated one release at a time:
699
- // /csrattrs 404 -> none-available (sec. 4.5.2)
700
- // /fullcmc 404 -> not-implemented (sec. 4.3.2), and 501 likewise
701
- // every other op -> est/http-error
702
524
  var NOT_IMPLEMENTED_OPS = { fullcmc: 1 };
703
525
 
704
526
  /**
@@ -730,22 +552,7 @@ function classifyResponse(status, headers, body, opts) {
730
552
  opts = opts || {};
731
553
  _knownOpts(opts, CLASSIFY_OPTS, "classifyResponse");
732
554
  var op = opts.op;
733
- // Fail closed on an operation whose response this client cannot validate:
734
- // A named op this client cannot validate is a typo, not a pass. An
735
- // absent op is the caller opting out of the content-type gate (generic
736
- // status handling) and stays permissive.
737
555
  if (op !== undefined && op !== null && CLASSIFIABLE_OPS.indexOf(op) === -1) throw E("est/unsupported-operation", "unrecognized EST operation " + JSON.stringify(op));
738
- // Normalized ONCE, through the reader that refuses a field carried under two
739
- // spellings. Folding the map with last-value-wins here while another stage reads
740
- // the exact key is how the two could pick different values out of the same
741
- // response -- and the whole point of this function is to decide what the body is.
742
- // Content-Type, Retry-After and Location are SINGLETON fields: each carries one
743
- // value that decides one thing -- how to read the body, how long to wait, where to
744
- // go -- so two of them is an ambiguity, refused rather than resolved by order.
745
- // WWW-Authenticate is a LIST field (RFC 9110 sec. 11.6.1: a challenge per element),
746
- // where repetition is the field working as defined; its values are combined the way
747
- // sec. 5.3 permits a recipient to combine list-field lines, so every challenge the
748
- // server offered reaches the scheme scan.
749
556
  var h = {
750
557
  "content-type": _ciHeader(headers, "content-type"),
751
558
  "retry-after": _ciHeader(headers, "retry-after"),
@@ -757,11 +564,6 @@ function classifyResponse(status, headers, body, opts) {
757
564
  var ct = h["content-type"] || "";
758
565
  if (spec) {
759
566
  var pt = _partMediaType(ct);
760
- // _partMediaType already lowercases smimeType, and the table's entries are
761
- // lowercase, so this comparison is case-insensitive on both sides.
762
- // A duplicated smime-type is not a value the whitelist can be applied to:
763
- // one of the two might be on it while the other is not, and matching the
764
- // first would pass a header that also declares something else.
765
567
  var smimeOk = !pt.ambiguous && (!spec.smimeTypes || spec.smimeTypes.indexOf(pt.smimeType) !== -1);
766
568
  if (pt.media !== spec.media || !smimeOk) {
767
569
  throw E("est/bad-content-type", "a 200 " + op + " response must carry content-type " + spec.media + (spec.smimeTypes ? "; smime-type=" + spec.smimeTypes.join(" or ") : "") + ", got " + JSON.stringify(ct));
@@ -773,14 +575,9 @@ function classifyResponse(status, headers, body, opts) {
773
575
  var ra = h["retry-after"];
774
576
  if (ra === undefined || ra === null || String(ra).trim() === "") throw E("est/missing-retry-after", "an HTTP 202 EST response must include Retry-After (RFC 7030 sec. 4.2.3)");
775
577
  var raStr = String(ra).trim();
776
- // Retry-After is delay-seconds OR an HTTP-date (RFC 7231 sec. 7.1.3), surfaced (never slept on)
777
- // as a bounded retryAfterSeconds and/or an absolute retryAfterDate via the shared parser; neither
778
- // form -> fail closed rather than a retry verdict with no delay.
779
578
  var parsed = retryAfter.parse(raStr, { now: opts.now, E: E, code: "est/bad-retry-after" });
780
579
  return { status: "retry", retryAfter: raStr, retryAfterSeconds: parsed.retryAfterSeconds, retryAfterDate: parsed.retryAfterDate };
781
580
  }
782
- // 501 is meaningful ONLY as the /fullcmc not-implemented signal; on every other
783
- // operation it stays an ordinary 5xx and falls through to est/http-error below.
784
581
  if (status === 501 && NOT_IMPLEMENTED_OPS[op]) return { status: "not-implemented", httpStatus: status };
785
582
  if (status === 204 || status === 404) {
786
583
  if (op === "csrattrs") return { status: "none-available" };
@@ -789,17 +586,10 @@ function classifyResponse(status, headers, body, opts) {
789
586
  }
790
587
  if (status >= 300 && status < 400) return { status: "redirect", location: h["location"] || null };
791
588
  if (status >= 400) {
792
- // Decode only a bounded prefix for the message -- a huge error body must not
793
- // be materialized as a full string just to show its first 512 characters.
794
- // toString(enc, start, end) bounds the decode WITHOUT constructing a subarray
795
- // view, so a detached body reads as "" here rather than throwing a raw
796
- // TypeError that would mask the est/http-error verdict.
797
589
  var text;
798
590
  if (Buffer.isBuffer(body)) {
799
591
  text = body.toString("utf8", 0, 512);
800
592
  } else if (guard.bytes.isByteSource(body)) {
801
- // A byte source from an injected transport (a Uint8Array / ArrayBuffer): decode a bounded prefix through
802
- // the byte guard rather than String()-coercing it to a "48,130,..." comma list.
803
593
  try { text = guard.bytes.source(body, EstError, "est/bad-input", "the EST error body").toString("utf8", 0, 512); }
804
594
  catch (_e) { /* allow:swallow-unverified a detached / unreadable error body reads as an empty snippet rather than masking the est/http-error verdict with a raw TypeError */ text = ""; }
805
595
  } else {
@@ -810,7 +600,6 @@ function classifyResponse(status, headers, body, opts) {
810
600
  return { status: "unexpected", httpStatus: status };
811
601
  }
812
602
 
813
- // ---- operation-path builder ---------------------------------------------
814
603
 
815
604
  /**
816
605
  * @primitive pki.est.paths
@@ -838,10 +627,6 @@ function paths(baseUrl, opts) {
838
627
  var prefix = base.slice(0, _stripTrailingSlashEnd(base)) + "/.well-known/est";
839
628
  if (opts.label != null) {
840
629
  var label = String(opts.label);
841
- // A label is ONE URL path segment: unreserved characters only (RFC 3986),
842
- // never a dot-segment or an operation name. Rejecting rather than
843
- // percent-encoding keeps a reserved char (`/` `?` `#` `%`) or `..` from
844
- // silently retargeting the request to a different resource.
845
630
  if (label === "" || label === "." || label === ".." || !pkix.allCharsIn(label, _LABEL_TABLE) || OPERATIONS.indexOf(label) !== -1) {
846
631
  throw E("est/bad-label", "an EST CA label must be a single path segment of unreserved characters, not '.' / '..' or an operation name (RFC 7030 sec. 3.2.2)");
847
632
  }
@@ -852,17 +637,10 @@ function paths(baseUrl, opts) {
852
637
  return out;
853
638
  }
854
639
 
855
- // ---- builders: the CSR attributes EST adds ------------------------------
856
640
 
857
641
  function _attr(typeOid, valueNodes) { return asn1.build.sequence([asn1.build.oid(typeOid), asn1.build.set(valueNodes)]); }
858
642
 
859
- // the RFC 5929 tls-unique channel-binding bytes -> a challengePassword
860
- // attribute whose value is their RFC 4648 base64 (SIZE 1..255). The builder
861
- // takes caller-supplied binding bytes and never fakes one.
862
643
  function challengePasswordFromTlsUnique(channelBinding) {
863
- // The count comes from the guard, so an own `length` on the caller's Buffer cannot report a
864
- // full one as empty. `length` is shadowable on any instance, and `Object.defineProperty(buf,
865
- // "length", { get: function () { return 0; } })` had this refuse bytes that were there.
866
644
  if (!guard.bytes.isByteSource(channelBinding)) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
867
645
  var _cb = guard.bytes.source(channelBinding, EstError, "est/bad-input", "the channel-binding bytes");
868
646
  if (_cb.length === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
@@ -871,7 +649,6 @@ function challengePasswordFromTlsUnique(channelBinding) {
871
649
  return _attr(OID_CHALLENGE_PASSWORD, [asn1.build.printable(b64)]);
872
650
  }
873
651
 
874
- // the out-of-band key-encryption-key identifiers (OCTET STRING values).
875
652
  function decryptKeyIdentifierAttr(keyId) {
876
653
  if (!Buffer.isBuffer(keyId)) throw E("est/bad-input", "decryptKeyIdentifierAttr requires the key-identifier bytes");
877
654
  return _attr(OID_DECRYPT_KEY_ID, [asn1.build.octetString(keyId)]);
@@ -880,8 +657,6 @@ function asymmetricDecryptKeyIdentifierAttr(keyId) {
880
657
  if (!Buffer.isBuffer(keyId)) throw E("est/bad-input", "asymmetricDecryptKeyIdentifierAttr requires the key-identifier bytes");
881
658
  return _attr(OID_ASYMM_DECRYPT_KEY_ID, [asn1.build.octetString(keyId)]);
882
659
  }
883
- // SMIMECapabilities ::= SEQUENCE OF SMIMECapability { capabilityID OID,
884
- // parameters ANY OPTIONAL }.
885
660
  function smimeCapabilitiesAttr(capabilities) {
886
661
  if (!Array.isArray(capabilities) || capabilities.length === 0) throw E("est/bad-input", "smimeCapabilitiesAttr requires a non-empty capability list");
887
662
  var caps = capabilities.map(function (c) {
@@ -892,15 +667,6 @@ function smimeCapabilitiesAttr(capabilities) {
892
667
  return _attr(OID_SMIME_CAPABILITIES, [asn1.build.sequence(caps)]);
893
668
  }
894
669
 
895
- // Derive the enroll-request attribute plan from a parsed CsrAttrs.
896
- // When a template is present the plan derives from the TEMPLATE ONLY and ignores
897
- // every other element (RFC 9908 sec. 4). The challengePassword OID is an
898
- // INSTRUCTION ("include tls-unique") -> a channelBindingRequired flag, never a
899
- // password value (the RFC 7030 example that echoed it is "NOT CORRECT",
900
- // RFC 9908 sec. 4). Every item the plan does not model into a specific field --
901
- // including a REGISTERED-but-unmodeled instruction like a bare signature-algorithm
902
- // OID (RFC 8951) -- is surfaced on `unhandled` ({ kind, oid, name }) so the client
903
- // never silently drops a server requirement.
904
670
  function buildEnrollAttributes(csrattrsParsed) {
905
671
  var items = (csrattrsParsed && csrattrsParsed.items) || [];
906
672
  var template = null;
@@ -914,14 +680,7 @@ function buildEnrollAttributes(csrattrsParsed) {
914
680
  if (it.oid === OID_CHALLENGE_PASSWORD) plan.channelBindingRequired = true;
915
681
  else if (it.kind === "attribute" && it.extensions) plan.extensions = it.extensions;
916
682
  else if (it.kind === "attribute" && it.isKeyType) {
917
- // EVERY key type the parser accepts is a key-type constraint (RSA / EC /
918
- // Ed25519 / ML-DSA / ...). The non-template form carries EXACTLY ONE key-type
919
- // attribute (RFC 9908 sec. 3.2); a second is an ambiguous server instruction,
920
- // not a last-one-wins override -- fail closed rather than pick one by order.
921
683
  if (plan.keyType) throw E("est/ambiguous-key-type", "a non-template CsrAttrs response must carry exactly one key-type attribute (RFC 9908 sec. 3.2)");
922
- // Surface the raw values too: a key type this planner does not decode into
923
- // curve / keySize (Ed25519, ML-DSA, ...) may still carry RFC 9908 parameters,
924
- // so never silently drop them.
925
684
  plan.keyType = { type: it.name, curve: it.curve || null, keySize: it.keySize || null, values: it.values || [] };
926
685
  }
927
686
  else plan.unhandled.push({ kind: it.kind, oid: it.oid, name: it.name });
@@ -931,11 +690,6 @@ function buildEnrollAttributes(csrattrsParsed) {
931
690
 
932
691
  var SAN_OID = oid.byName("subjectAltName");
933
692
 
934
- // Pull the SubjectAltName extension's raw extnValue (the DER GeneralNames) off a
935
- // parsed extension list -- a certificate's `.extensions` or a CSR's
936
- // extensionRequest `.extensions` -- as { critical, value }, or null when no SAN
937
- // is present. Both fields matter: "identical" (RFC 7030 sec. 4.2.2) covers the
938
- // criticality flag, not just the GeneralNames bytes.
939
693
  function _san(extList) {
940
694
  if (!Array.isArray(extList)) return null;
941
695
  for (var i = 0; i < extList.length; i++) {
@@ -944,11 +698,6 @@ function _san(extList) {
944
698
  return null;
945
699
  }
946
700
 
947
- // The requested extensions a CSR carries in its extensionRequest attribute
948
- // (RFC 2985 sec. 5.4.2), decoded by the CSR parser, or null when absent. A CSR
949
- // carrying more than one extensionRequest is structurally legal (no SET-OF
950
- // uniqueness) but AMBIGUOUS for the re-enroll SAN comparison -- fail closed
951
- // rather than trusting the DER-first one while a later one requests a different SAN.
952
701
  function _csrRequestedExtensions(parsedCsr) {
953
702
  var attrs = (parsedCsr && parsedCsr.attributes) || [];
954
703
  var extReqOid = oid.byName("extensionRequest");
@@ -960,16 +709,9 @@ function _csrRequestedExtensions(parsedCsr) {
960
709
  return found;
961
710
  }
962
711
 
963
- // A re-enroll CSR MUST reuse the old certificate's Subject and SubjectAltName
964
- // extension byte-identically (RFC 7030 sec. 4.2.2). Returns the old cert's
965
- // subject + SAN for verbatim reuse in the new CSR; on a caller-supplied new CSR,
966
- // compares both by raw DER (a rendered DN two different string encodings can
967
- // share is NOT enough) and throws est/reenroll-subject-mismatch or
968
- // est/reenroll-san-mismatch on any divergence (a SAN present on one side and
969
- // absent on the other is a mismatch, not an accept).
970
712
  function reenrollGuard(oldCertDer, newCsrDer) {
971
713
  var oldCert = x509.parse(oldCertDer);
972
- var oldSubject = oldCert.subject.rdns; // the parsed subject; raw region surfaced below
714
+ var oldSubject = oldCert.subject.rdns;
973
715
  var oldSubjectDn = oldCert.subject.dn;
974
716
  var oldSubjectBytes = oldCert.subject.bytes;
975
717
  var oldSan = _san(oldCert.extensions);
@@ -984,22 +726,16 @@ function reenrollGuard(oldCertDer, newCsrDer) {
984
726
  return { subjectDn: oldSubjectDn, subjectAltName: oldSan };
985
727
  }
986
728
 
987
- // ---- the thin RFC 7030 client: network verbs over the shared pki.transport ----
988
729
 
989
730
  var DEFAULT_TIMEOUT = constants.TIME.seconds(30);
990
731
  var MAX_TIMEOUT = constants.TIME.seconds(600);
991
732
 
992
- // The DER of a caller-supplied CSR: a DER Buffer as-is, or a PEM "CERTIFICATE REQUEST"
993
- // decoded. Any other input is a config-time est/bad-input.
994
733
  function _csrDer(input) {
995
734
  if (guard.bytes.isByteSource(input)) return guard.bytes.snapshotSource(input, EstError, "est/bad-input", "a CSR");
996
735
  if (typeof input === "string") return csr.pemDecode(input);
997
736
  throw E("est/bad-input", "a CSR must be a DER BufferSource or a PEM CERTIFICATE REQUEST string");
998
737
  }
999
738
 
1000
- // Parse + validate the request URL BEFORE any transport is called: a URL that will not
1001
- // parse is est/bad-url, a non-https URL is est/insecure-url (RFC 7030 sec. 3.3). Runs
1002
- // even when a transport is injected, so an insecure URL never reaches the wire.
1003
739
  function _parseUrl(urlStr) {
1004
740
  var url;
1005
741
  try { url = new URL(String(urlStr)); }
@@ -1008,18 +744,11 @@ function _parseUrl(urlStr) {
1008
744
  return url;
1009
745
  }
1010
746
 
1011
- // Map opts.tls (operator-facing) to the transport request.tls shape (anchors -> ca).
1012
- // rejectUnauthorized is NOT here -- the transport forces it on unconditionally.
1013
747
  function _tlsForRequest(opts) {
1014
748
  var t = opts.tls || {};
1015
749
  return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
1016
750
  }
1017
751
 
1018
- // Resolve a redirect target: reject a scheme downgrade (est/insecure-redirect) or a
1019
- // cross-origin redirect on a non-GET/HEAD method without opt-in
1020
- // (est/cross-origin-redirect); a Location that will not parse is est/bad-url. Same-origin
1021
- // (and cross-origin GET/HEAD) are followed -- the transport re-runs every TLS check on the
1022
- // new connection (RFC 7030 sec. 3.2.1).
1023
752
  function _redirectTarget(current, location, method, opts) {
1024
753
  if (location == null || String(location).trim() === "") throw E("est/http-error", "a redirect response carried no Location header");
1025
754
  var resolved;
@@ -1033,14 +762,6 @@ function _redirectTarget(current, location, method, opts) {
1033
762
  return resolved;
1034
763
  }
1035
764
 
1036
- // Does WWW-Authenticate advertise the given auth-scheme as a TOKEN? The quoted-string contents are blanked
1037
- // first (a quoted auth-param may contain a comma + the scheme name, e.g. `Digest realm="x, Basic required"`,
1038
- // which must NOT be read as a Basic challenge), then the scheme is matched at the start or after a
1039
- // comma-separated challenge, followed by whitespace / a comma / end -- never a substring, so `Digest
1040
- // realm="basic"` is not taken as Basic. `scheme` is always our own literal ("Basic" / "Digest").
1041
- // Blank the CONTENTS of every complete quoted-string (/"(?:[^"\\]|\\.)*"/g -> `""`) by character scan: a
1042
- // `\` escapes the next character, so `\"` does not close; an unterminated final quote is left as written
1043
- // (the regex only matches a closed quoted-string).
1044
765
  function _blankQuotedStrings(s) {
1045
766
  var out = "", last = 0, i = 0, n = s.length;
1046
767
  while (i < n) {
@@ -1054,9 +775,6 @@ function _blankQuotedStrings(s) {
1054
775
  }
1055
776
  return out + _strSlice(s, last, n);
1056
777
  }
1057
- // Is `scheme` (our own literal, case-insensitive) offered as a challenge TOKEN in the blanked header?
1058
- // (?:^|,)\s*SCHEME(?=[\s,]|$): at the start or after a comma, optional whitespace, the scheme, then a
1059
- // whitespace / comma / end boundary -- never a substring, so `Digest realm="basic"` is not read as Basic.
1060
778
  function _schemeOffered(s, scheme) {
1061
779
  var n = s.length, slen = scheme.length, p, k;
1062
780
  for (p = 0; p <= n; p++) {
@@ -1080,9 +798,6 @@ function _schemeOffered(s, scheme) {
1080
798
  function _offersScheme(www, scheme) {
1081
799
  return _schemeOffered(_blankQuotedStrings(String(www || "")), scheme);
1082
800
  }
1083
- // The chosen HTTP auth scheme: an explicit opts.auth.scheme wins; a legacy opts.username / opts.password
1084
- // WITHOUT opts.auth means Basic (backwards compatible); otherwise none. There is NO scheme:"auto" -- a
1085
- // Basic<->Digest downgrade is never silently chosen (a MITM stripping Digest to Basic must not succeed).
1086
801
  function _authScheme(opts) {
1087
802
  if (opts.auth && opts.auth.scheme != null) {
1088
803
  var sc = String(opts.auth.scheme).toLowerCase();
@@ -1094,60 +809,34 @@ function _authScheme(opts) {
1094
809
  }
1095
810
  function _authUser(opts) { return opts.auth && opts.auth.username !== undefined ? opts.auth.username : opts.username; }
1096
811
  function _authPass(opts) { return opts.auth && opts.auth.password !== undefined ? opts.auth.password : opts.password; }
1097
- // Credentials present -- an EMPTY username is allowed (RFC 7030 sec. 3.2.3), so "defined" (not "truthy").
1098
812
  function _hasCreds(opts) { return _authUser(opts) !== undefined || _authPass(opts) !== undefined; }
1099
813
  var DIGEST_CODES = { unsupportedAlgorithm: "est/digest-unsupported-algorithm", weakAlgorithm: "est/digest-weak-algorithm", noQop: "est/digest-no-qop", badChallenge: "est/digest-bad-challenge" };
1100
- // The Digest answer policy, from opts.auth. The SAME object drives challenge SELECTION (parseChallenge) and
1101
- // the ANSWER (answer), so the client never selects a challenge under one policy then answers under another.
1102
814
  function _digestPolicy(opts) { return { allowMD5: !!(opts.auth && opts.auth.allowMD5), allowLegacyQop: !!(opts.auth && opts.auth.allowLegacyQop), codes: DIGEST_CODES }; }
1103
815
 
1104
- // The redirect-follow + HTTP-auth-retry loop. Returns the terminal transport response
1105
- // ({status, headers, body}) -- a 2xx, a 202, or a non-401 4xx/5xx. A 3xx is followed
1106
- // (bounded by maxRedirects); a 401 is answered ONCE with Basic credentials, and ONLY when
1107
- // the caller supplied them and the challenge is Basic (RFC 7030 sec. 3.2.3) -- the server
1108
- // was authenticated by the transport's rejectUnauthorized TLS before any credential is
1109
- // transmitted (no credential ever precedes server authorization).
1110
816
  function _drive(method, url, body, headers, opts, transport, budgets) {
1111
817
  var redirects = 0;
1112
818
  var authTried = false;
1113
- var staleRetries = 0; // bounded re-answers to a Digest stale=true re-challenge (a fresh nonce), never an open loop
1114
- var authSpaces = 0; // distinct Digest protection spaces (realms) entered beyond the first -- bounded so a server cannot loop with an endless stream of new realms
1115
- var lastDigestNonce = null; // the nonce just answered -- a stale=true retry MUST carry a DIFFERENT (fresh) nonce
1116
- var initialOrigin = url.origin; // the origin the caller intended to authenticate to
1117
- // The per-hop TLS: the origin-specific identity (mTLS cert/key + pinned SNI) is sent ONLY to the caller's
1118
- // configured origin; a cross-origin hop gets a narrowed copy with those stripped (checkServerIdentity kept).
1119
- // Derived FRESH from budgets.tls each hop (the ACME _tlsFor model) so a redirect back to the original origin
1120
- // restores its identity rather than permanently losing the SNI at the first cross-origin boundary.
819
+ var staleRetries = 0;
820
+ var authSpaces = 0;
821
+ var lastDigestNonce = null;
822
+ var initialOrigin = url.origin;
1121
823
  function _tlsFor(u) {
1122
824
  var t = budgets.tls;
1123
825
  if (t && u.origin !== initialOrigin) {
1124
- // Every cross-origin hop overrides the client identity with an explicit null, unconditionally: an
1125
- // omitted field falls back to the transport's own configured defaults (http-transport resolves
1126
- // reqTls.cert !== undefined ? reqTls.cert : tlsDefaults.cert), so the override must fire even when the
1127
- // request-level tls carries no credential, or a transport default would still reach the new origin.
1128
826
  t = Object.assign({}, t);
1129
827
  t.cert = null; t.key = null; t.servername = null;
1130
828
  }
1131
829
  return t;
1132
830
  }
1133
- // The per-hop Authorization, sent ONLY to the caller's origin (a cross-origin hop travels unauthenticated; a
1134
- // redirect back to the origin restores it -- mirrors _tlsFor). A Basic credential is target-INDEPENDENT, so it
1135
- // is cached whole in authValue. A Digest response is bound to the request method + target, so the CHALLENGE is
1136
- // cached and the answer is (re)computed for the CURRENT method / uri on every hop -- a same-origin redirect (or
1137
- // a 303 POST->GET) then gets a target-correct answer instead of a stale one the server would reject.
1138
831
  var authValue = null;
1139
832
  var digestChallenge = null;
1140
- var digestNcByNonce = Object.create(null); // nonce string -> count of requests answered under THAT nonce, tracked PER NONCE so a nonce reissued after others were used never repeats an nc (RFC 7616 sec. 3.4); bounded by the total answered 401s; a null-proto map so an attacker-chosen nonce (e.g. "__proto__") is an ordinary key
1141
- var digestSent = false; // whether the LAST request actually carried a Digest Authorization -- the DIRECT signal that a following 401 rejects a credential, vs. is a fresh challenge for a request sent unauthenticated
833
+ var digestNcByNonce = Object.create(null);
834
+ var digestSent = false;
1142
835
  function _headersFor(u) {
1143
836
  digestSent = false;
1144
837
  if (u.origin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
1145
838
  var hh = Object.assign({}, headers);
1146
839
  if (digestChallenge) {
1147
- // Preemptively reuse the cached answer only WITHIN the challenge's protection space (RFC 7616 sec. 3.5):
1148
- // a same-origin redirect to a URI outside the challenge's `domain` is sent unauthenticated, so the target
1149
- // resource's own challenge drives a fresh authentication instead of receiving an Authorization computed
1150
- // for the wrong protection space.
1151
840
  if (!httpDigest.inProtectionSpace(digestChallenge, u.origin, u.pathname + u.search)) return headers;
1152
841
  var nkey = digestChallenge.nonce;
1153
842
  digestNcByNonce[nkey] = (digestNcByNonce[nkey] || 0) + 1;
@@ -1159,21 +848,8 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
1159
848
  function step() {
1160
849
  return transport({ method: method, url: url.href, headers: _headersFor(url), body: body, tls: _tlsFor(url), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
1161
850
  res = res || {};
1162
- // Measure an injected string body as UTF-8 -- the byte width the body is decoded/transfer-decoded at,
1163
- // and what the real socket transport counts -- so a non-ASCII body (multi-byte chars are ~half the
1164
- // count under latin1) cannot undercount past the DoS cap and reach body handling.
1165
851
  var blen = _bodyLen(res);
1166
852
  if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
1167
- // The transport contract returns lowercased headers, but the injectable seam only promises
1168
- // { status, headers, body }; normalize here so a redirect Location / WWW-Authenticate from an
1169
- // injected transport using ordinary HTTP casing is read correctly (never missed as absent).
1170
- //
1171
- // Through the SAME accessors classifyResponse uses, not a fold of its own: a
1172
- // last-value-wins fold keeps one line of a field that arrived under two
1173
- // spellings, so a usable challenge sent before an unusable one would vanish
1174
- // and the exchange would be refused for offering nothing this client speaks.
1175
- // Location is a singleton, so a second one is an ambiguity and refused;
1176
- // WWW-Authenticate is a list, so every challenge is kept.
1177
853
  var h = {
1178
854
  location: _ciHeader(res.headers, "location"),
1179
855
  "www-authenticate": _ciHeaderList(res.headers, "www-authenticate"),
@@ -1181,53 +857,32 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
1181
857
  var status = res.status;
1182
858
  if (status >= 300 && status < 400) {
1183
859
  if (redirects >= budgets.maxRedirects) throw E("est/too-many-redirects", "the redirect chain exceeded maxRedirects=" + budgets.maxRedirects + " (RFC 7030 sec. 3.2.1)");
1184
- // 303 See Other directs a non-GET/HEAD request to retrieve the target with GET and no body
1185
- // (RFC 7231 sec. 6.4.4); drop the request body + its entity content-type before following, so
1186
- // the CSR is not re-POSTed to the redirect target as a duplicate enrollment.
1187
860
  if (status === 303 && method !== "GET" && method !== "HEAD") {
1188
861
  method = "GET";
1189
862
  body = null;
1190
863
  if (headers["content-type"]) { headers = Object.assign({}, headers); delete headers["content-type"]; }
1191
864
  }
1192
865
  url = _redirectTarget(url, h.location, method, opts);
1193
- // Credentials MUST NOT cross an origin boundary: the HTTP Basic Authorization header and the
1194
- // ORIGIN-SPECIFIC TLS identity (mTLS cert/key + pinned SNI) are BOTH scoped per hop (_headersFor /
1195
- // _tlsFor), so each is absent off-origin but restored on a hop back to the caller's configured origin.
1196
866
  redirects += 1;
1197
867
  return step();
1198
868
  }
1199
869
  if (status === 401) {
1200
- // Answer a challenge ONLY on the origin the caller targeted: a 401 arriving after a cross-origin
1201
- // redirect is a different server, and the client MUST NOT send its credentials there (RFC 7030 sec. 3.6).
1202
870
  if (url.origin !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
1203
871
  var www = String(h["www-authenticate"] || "");
1204
- // Bound the attacker-controlled challenge header BEFORE any scheme scan / copy (_offersScheme, the parser):
1205
- // an injected transport without its own header limit must not cause unbounded allocation or work despite
1206
- // the parser's own cap being applied later (CWE-400). The cap matches the Digest parser's.
1207
872
  if (www.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E("est/auth-required", "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap (RFC 7030 sec. 3.2.3)");
1208
873
  var scheme = _authScheme(opts);
1209
874
  if (scheme === null) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
1210
875
  if (scheme === "digest") {
1211
876
  if (!_offersScheme(www, "Digest")) throw E("est/auth-required", "opts.auth.scheme is \"digest\" but the server offered no Digest challenge: " + www);
1212
877
  if (!_hasCreds(opts)) throw E("est/auth-required", "Digest authentication requires opts.auth.username / password (RFC 7030 sec. 3.2.3)");
1213
- // In a credential-rejection context (this request carried a Digest answer), bias selection toward a
1214
- // RETRYABLE stale=true offer so a stronger stale=false offer does not shadow one that could re-answer.
1215
878
  var selPol = _digestPolicy(opts);
1216
879
  selPol.preferStale = digestSent;
1217
- selPol.priorNonce = lastDigestNonce; // a retryable stale offer must carry a nonce DIFFERENT from the one just rejected
1218
- selPol.priorRealm = digestChallenge && digestChallenge.realm; // ...and be for the SAME realm that was rejected (a different realm is a new space, not a retry)
1219
- selPol.requestOrigin = url.origin; // prefer an offer whose protection space actually covers THIS request --
1220
- selPol.requestTarget = url.pathname + url.search; // one scoped elsewhere cannot authenticate it and must not shadow one that can
880
+ selPol.priorNonce = lastDigestNonce;
881
+ selPol.priorRealm = digestChallenge && digestChallenge.realm;
882
+ selPol.requestOrigin = url.origin;
883
+ selPol.requestTarget = url.pathname + url.search;
1221
884
  var ch = httpDigest.parseChallenge(www, E, "est/digest-bad-challenge", selPol);
1222
885
  if (!ch) throw E("est/auth-required", "the server offered no usable Digest challenge: " + www);
1223
- // Whether this 401 REJECTS a credential turns on the DIRECT signal (RFC 7616 sec. 3.3 / 3.5): did the
1224
- // request that received it actually CARRY a Digest answer for THIS protection space? A request sent
1225
- // unauthenticated -- the first attempt, or one whose target lay outside the cached challenge's domain --
1226
- // has not authenticated, so its 401 (even in the same realm) is a FRESH challenge to answer, never a
1227
- // rejection. A credentialed request refused in the SAME realm IS a rejection of that credential: only a
1228
- // stale=true re-challenge carrying a FRESH nonce may retry (a repeated nonce with stale=true is a
1229
- // self-contradictory / hostile server -> terminate). Any fresh authentication -- the first, a newly
1230
- // entered protection space, or an unauthenticated resource -- is bounded (maxRedirects) against a loop.
1231
886
  if (digestSent && digestChallenge && ch.realm === digestChallenge.realm) {
1232
887
  if (!(ch.stale && ch.nonce !== lastDigestNonce && staleRetries < budgets.maxStaleRetries)) throw E("est/auth-required", "the server rejected the credentialed Digest request (RFC 7030 sec. 3.2.3)");
1233
888
  staleRetries += 1;
@@ -1235,26 +890,15 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
1235
890
  if (authTried && authSpaces >= budgets.maxRedirects) throw E("est/auth-required", "the server demanded Digest authentication for too many distinct protection spaces (RFC 7616 sec. 3.3)");
1236
891
  if (authTried) authSpaces += 1;
1237
892
  authTried = true;
1238
- staleRetries = 0; // a fresh authentication (new space / unauthenticated resource) restarts the stale budget
893
+ staleRetries = 0;
1239
894
  }
1240
895
  lastDigestNonce = ch.nonce;
1241
- // Cache the CHALLENGE, not a fixed answer: _headersFor computes the response for the CURRENT method /
1242
- // request-target on each hop, so a same-origin redirect (or a 303 POST->GET) gets a target-correct answer
1243
- // rather than a stale one. A policy fault (MD5 / no-qop / unsupported) surfaces from _headersFor on the
1244
- // immediate re-send (the transport is not reached, so a rejected leg still proves calls did not advance).
1245
- // The nonce-count is tracked PER NONCE (digestNcByNonce), so a nonce reissued after other nonces were
1246
- // used in between resumes its own count -- a (nonce, nc) pair is never replayed, without a reset here.
1247
896
  digestChallenge = ch;
1248
897
  return step();
1249
898
  }
1250
- // Basic (default / explicit): the one-shot semantics unchanged.
1251
899
  if (authTried) throw E("est/auth-required", "the server rejected the credentialed request (RFC 7030 sec. 3.2.3)");
1252
900
  if (!_offersScheme(www, "Basic")) throw E("est/auth-required", "the server requires an unsupported HTTP authentication scheme (only Basic and Digest are supported): " + www);
1253
- // An explicit auth.scheme:"basic" with no credentials must fail closed too (an empty username stays legal),
1254
- // never transmit a "Basic Og==" (base64 of ":") the operator never supplied.
1255
901
  if (!_hasCreds(opts)) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
1256
- // Establish the credential as origin-scoped state (_headersFor attaches it only on the initial origin),
1257
- // never a mutation of the shared headers that would leak across a cross-origin redirect.
1258
902
  authValue = "Basic " + Buffer.from((_authUser(opts) || "") + ":" + (_authPass(opts) || ""), "utf8").toString("base64");
1259
903
  authTried = true;
1260
904
  return step();
@@ -1265,12 +909,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
1265
909
  return step();
1266
910
  }
1267
911
 
1268
- // The shared client entry: build the op URL, validate it + the trust config + the budgets
1269
- // (ALL before any transport call so a config fault proves the transport was never reached),
1270
- // then drive the request through the redirect/auth loop.
1271
912
  function _client(op, method, baseUrl, body, headers, opts) {
1272
- // paths() concatenates onto the base string, so a query / fragment on the base URL would capture
1273
- // the operation path (sending the request -- possibly an enrollment CSR -- to the wrong resource).
1274
913
  var base;
1275
914
  try { base = new URL(String(baseUrl)); }
1276
915
  catch (e) { throw E("est/bad-url", "the EST base URL did not parse: " + String(baseUrl), e); }
@@ -1290,45 +929,29 @@ function _client(op, method, baseUrl, body, headers, opts) {
1290
929
  maxRedirects: guard.limits.cap(opts.maxRedirects, "maxRedirects", 5, { E: E, code: "est/bad-input", min: 0, max: 32 }),
1291
930
  maxStaleRetries: guard.limits.cap(opts.auth && opts.auth.maxStaleRetries, "maxStaleRetries", 1, { E: E, code: "est/bad-input", min: 0, max: 8 }),
1292
931
  };
1293
- _authScheme(opts); // config-time: a bad opts.auth.scheme throws est/bad-input here, never deferred to a 401
932
+ _authScheme(opts);
1294
933
  return _drive(method, url, body, Object.assign({}, headers), opts, transport, budgets);
1295
934
  }
1296
935
 
1297
- // Turn a terminal response into the verb result: classifyResponse gates status +
1298
- // content-type (202 -> a surfaced retry, never slept; 4xx/5xx -> est/http-error), then an
1299
- // empty 200 is rejected and the certs-only body is decoded + validated. For an enroll, the
1300
- // issued certificate is the public-key match (findIssuedCert), never a positional guess;
1301
- // the remaining certificates are the chain.
1302
936
  function _certsResult(op, res, opts, csrSpki) {
1303
937
  var verdict = classifyResponse(res.status, res.headers, res.body, { op: op, now: opts.now });
1304
938
  if (verdict.status === "retry") {
1305
- // 202 Retry-After is an ENROLLMENT response (RFC 7030 sec. 4.2.3); a successful /cacerts is 200
1306
- // only (sec. 4.1.3), so a 202 there is a nonconforming server -> fail closed rather than "retry".
1307
939
  if (op === "cacerts") throw E("est/http-error", "a /cacerts response must be HTTP 200, not 202 (RFC 7030 sec. 4.1.3)");
1308
940
  return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
1309
941
  }
1310
- // Only the RFC-defined success statuses are accepted: a non-standard 2xx (201, 206, ...) that the
1311
- // classifier reports as "unexpected" must NOT have its body decoded and accepted as certificates.
1312
942
  if (verdict.status !== "ok") throw E("est/http-error", "an EST " + op + " response must be HTTP 200 or 202 (RFC 7030 sec. 4.1.3 / 4.2.3), got " + res.status);
1313
- // Measure the body as UTF-8 for parity with the transport cap above (this check only distinguishes empty
1314
- // from non-empty, where the encoding is immaterial, but the shape is kept uniform so no site undercounts).
1315
943
  var bodyLen = _bodyLen(res);
1316
944
  if (bodyLen === 0) throw E("est/empty-body", "a 200 " + op + " response carried an empty body (RFC 7030 sec. 4.1.3 / 4.2.3)");
1317
945
  var parsed = parseCertsOnly(transferDecode(res.body));
1318
946
  if (op === "cacerts") return { certificates: parsed.certificates, crls: parsed.crls };
1319
947
  var issued = findIssuedCert(parsed.certificates, csrSpki);
1320
948
  if (!issued) throw E("est/issued-cert-not-found", "no returned certificate matched the submitted CSR public key (RFC 5272 sec. 4.1)");
1321
- // The chain is every OTHER response certificate (by reference, so a byte-identical duplicate is
1322
- // NOT silently collapsed into the issued cert). Exactly one certificate may match the submitted
1323
- // public key -- a second match (a renewed cert sharing the key, or a duplicate entry) makes the
1324
- // issued certificate ambiguous, since the certs-only set has no issuance ordering (RFC 5272 sec. 4.1).
1325
949
  var chain = parsed.certificates.filter(function (c) { return c !== issued; });
1326
950
  if (findIssuedCert(chain, csrSpki)) throw E("est/ambiguous-issued-cert", "the enroll response carried more than one certificate matching the submitted CSR public key; the issued certificate is ambiguous (RFC 5272 sec. 4.1)");
1327
951
  if (opts.strict && chain.length > 0) throw E("est/unexpected-certs", "strict: the enroll response carried " + parsed.certificates.length + " certificates, expected exactly the issued one");
1328
952
  return { certificate: issued, chain: chain, certificates: parsed.certificates };
1329
953
  }
1330
954
 
1331
- // ---- /fullcmc (RFC 7030 sec. 4.3) ---------------------------------------
1332
955
 
1333
956
  /**
1334
957
  * @primitive pki.est.fullcmc
@@ -1422,83 +1045,30 @@ function _certsResult(op, res, opts, csrSpki) {
1422
1045
  */
1423
1046
  function fullcmc(baseUrl, request, opts) {
1424
1047
  opts = opts || {};
1425
- // Everything this exchange MEANS is captured synchronously, before a single
1426
- // deferred turn: the request bytes (copied), the keys they ask to have
1427
- // certified, and the state the response will be checked against. Doing it
1428
- // inside the promise body below would be a turn too late -- a caller that
1429
- // mutates the buffer or flips allowUnverifiedResponse on the line after this
1430
- // call has already changed them by then, which is the easiest version of the
1431
- // race to hit and the one that would skip the signature check.
1432
1048
  var der, wanted, sent;
1433
1049
  try {
1434
1050
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw E("est/bad-input", "pki.est.fullcmc options must be an object");
1435
- // Inside the try, so the refusal is a REJECTION like every other failure of this verb: it is
1436
- // documented as Promise-returning, and a synchronous throw escapes the `.catch(...)` a caller
1437
- // has already written. It stays in the synchronous capture rather than moving into a later turn,
1438
- // because that is what closes the race described above.
1439
1051
  _knownOpts(opts, FULLCMC_OPTS, "fullcmc");
1440
1052
  der = _cmcRequestDer(request);
1441
- // Confirm this IS a Full PKI Request before any of it goes over the wire. The
1442
- // bytes are about to be labeled `smime-type=CMC-request`, so a PKIResponse or
1443
- // an unparseable blob handed in by mistake would be POSTed to a CA under a
1444
- // label that does not describe it -- and, since only an `issued` outcome is
1445
- // correlated, could still come back as a pending or rejected verdict, making
1446
- // the mistake look like a protocol answer. A caller error belongs at the entry
1447
- // point, not on the network.
1448
1053
  wanted = _requestedPublicKeys(der);
1449
- // The exchange state is READ BACK OUT OF THE REQUEST, not taken on the caller's
1450
- // word. A value supplied here that the request does not actually carry would
1451
- // have the response checked against a binding this exchange never sent -- and a
1452
- // replayed response echoing that value would satisfy it. What the request says
1453
- // is the only thing that can bind the answer to it.
1454
1054
  sent = _cmcSent(opts, der);
1455
- // The TRANSPORT options are pinned here too, not only the binding state. They
1456
- // decide where this request goes and what trust it goes under -- transport,
1457
- // tls, credentials, redirect policy.
1458
1055
  opts = _shallowCopy(opts);
1459
- // And the request is STARTED here, synchronously, rather than in a deferred
1460
- // turn. That is what actually closes the window: a copy only reaches as deep
1461
- // as it is written, so nested config (opts.tls and its anchors) would still be
1462
- // the caller's to change while the request sat queued. Handing the options to
1463
- // _client before yielding leaves no turn in which any of them, at any depth,
1464
- // can differ from what this call was made with.
1465
1056
  var body = transferEncode(der);
1466
1057
  return _client("fullcmc", "POST", baseUrl, body,
1467
1058
  { accept: "application/pkcs7-mime", "content-type": "application/pkcs7-mime; smime-type=CMC-request" }, opts)
1468
1059
  .then(function (res) { return _fullcmcResult(res, opts, wanted, sent); });
1469
1060
  } catch (e) {
1470
- return Promise.reject(e); // the surface stays promise-rejecting, never throwing
1061
+ return Promise.reject(e);
1471
1062
  }
1472
1063
  }
1473
1064
 
1474
- /**
1475
- * Tie a returned certificate bag back to the requests it claims to answer, for
1476
- * BOTH response arms -- the rule is one rule, so it lives in one place rather than
1477
- * being restated per arm where one arm can quietly lose it.
1478
- *
1479
- * Every certification request the message submitted must be answered before the
1480
- * exchange reads as an issuance: a bag holding a CA chain, someone else's
1481
- * certificate, or only some of the requested keys is a partial or unrelated answer,
1482
- * not a success. Matching is by public key, the only identification RFC 5272
1483
- * sec. 4.1 sanctions ("the certificates are in any order" -- position means
1484
- * nothing).
1485
- *
1486
- * Each certificate is CONSUMED as it is matched, so two requests that share a
1487
- * public key -- CSRs for different subjects, say -- need two certificates rather
1488
- * than matching the same one twice and reporting an unanswered request as answered.
1489
- */
1065
+
1490
1066
  function _correlateIssued(bag, wanted, arm) {
1491
1067
  if (!wanted.length) {
1492
1068
  throw E("est/no-issued-cert",
1493
1069
  "this /fullcmc request declared no certification request whose key a returned certificate could be " +
1494
1070
  "matched against, so a " + arm + " response cannot be read as an issuance (RFC 5272 sec. 4.1)");
1495
1071
  }
1496
- // Counted PER DISTINCT KEY, because "how many certificates should match this
1497
- // key?" is answered by how many requests asked for it. Two requests may share a
1498
- // public key deliberately -- different subjects for one key -- and a CA
1499
- // answering both returns two certificates for it. Rejecting the second as
1500
- // ambiguous would refuse the complete, correct response; requiring only one
1501
- // would let a half-answer pass. So the count must agree exactly.
1502
1072
  var byKey = {};
1503
1073
  wanted.forEach(function (k) {
1504
1074
  var hex = k.toString("hex");
@@ -1520,19 +1090,9 @@ function _correlateIssued(bag, wanted, arm) {
1520
1090
  "requests for it; the issued certificate is ambiguous (RFC 5272 sec. 4.1)");
1521
1091
  }
1522
1092
  });
1523
- // Each certificate handed out once. Where the requested keys are distinct this
1524
- // lands in request order; where several requests share ONE key it cannot, and
1525
- // does not pretend to -- the public key is the only identification RFC 5272
1526
- // sec. 4.1 sanctions, so when it is shared nothing in the response says which
1527
- // of those requests a given certificate answers. What is established either way
1528
- // is the set: every requested key is answered, and by exactly as many
1529
- // certificates as asked for it.
1530
1093
  return wanted.map(function (k) { return byKey[k.toString("hex")].have.shift(); });
1531
1094
  }
1532
1095
 
1533
- // Every certificate in the bag whose subject public key is `key`, found through
1534
- // the same public-key match the single-certificate case uses so the two cannot
1535
- // disagree about what "matches" means.
1536
1096
  function _allMatching(bag, key) {
1537
1097
  var rest = bag.slice(), out = [], hit;
1538
1098
  while ((hit = findIssuedCert(rest, key)) !== null) {
@@ -1542,14 +1102,6 @@ function _allMatching(bag, key) {
1542
1102
  return out;
1543
1103
  }
1544
1104
 
1545
- // The public keys the submitted Full PKI Request asked to have certified. A
1546
- // certs-only answer carries no status and no request reference, so these are the
1547
- // ONLY thing a returned certificate can be tied back to -- and RFC 5272 sec. 4.1
1548
- // forbids identifying the issued certificate positionally ("the certificates are
1549
- // in any order"). Both key-bearing request arms are read; an `orm` arm carries no
1550
- // key and contributes none, and an arm that will not parse contributes none rather
1551
- // than failing the exchange -- the caller still gets a refusal downstream, because
1552
- // an empty set cannot match anything.
1553
1105
  function _requestedPublicKeys(der) {
1554
1106
  var out = [], body;
1555
1107
  try { body = cmcFmt.parse(der); }
@@ -1562,12 +1114,6 @@ function _requestedPublicKeys(der) {
1562
1114
  "pki.est.fullcmc sends a Full PKI Request (id-cct-PKIData); this input is a " + body.kind);
1563
1115
  }
1564
1116
  (body.requests || []).forEach(function (r) {
1565
- // A key-bearing arm whose key cannot be read must NOT quietly contribute
1566
- // nothing. The CMC parser validates these arms only far enough to find their
1567
- // identity, so a malformed one reaches here; dropping it would leave the
1568
- // correlation blind to a request that was still sent, and a response covering
1569
- // only the readable arms would then read as a complete issuance. Refused at
1570
- // the entry point, like any other request this verb cannot stand behind.
1571
1117
  var before = out.length;
1572
1118
  var keyBearing = !!(r.certificationRequestBytes || r.certReqMsgBytes);
1573
1119
  try {
@@ -1578,16 +1124,9 @@ function _requestedPublicKeys(der) {
1578
1124
  return;
1579
1125
  }
1580
1126
  if (r.certReqMsgBytes) {
1581
- // pki.schema.crmf reads CertReqMessages (SEQUENCE OF CertReqMsg); the CMC
1582
- // arm holds ONE CertReqMsg, so it is wrapped rather than decoded here --
1583
- // the CRMF rules stay in the CRMF parser instead of being restated.
1584
1127
  var msgs = crmfFmt.parse(asn1.build.sequence([r.certReqMsgBytes])).messages;
1585
1128
  var msg0 = msgs && msgs[0];
1586
1129
  var tmpl = msg0 && msg0.certReq && msg0.certReq.certTemplate;
1587
- // RFC 4211 sec. 4.1: the requested key may be absent from the CertTemplate
1588
- // and carried in the signature POP's POPOSigningKeyInput instead -- a form
1589
- // this toolkit's own CRMF parser accepts and surfaces. Reading only the
1590
- // template would refuse a conforming request before it was ever sent.
1591
1130
  var pk = (tmpl && tmpl.publicKey) ||
1592
1131
  (msg0 && msg0.popo && msg0.popo.poposkInput && msg0.popo.poposkInput.publicKey);
1593
1132
  var pkBytes = Buffer.isBuffer(pk) ? pk : (pk && pk.bytes);
@@ -1595,7 +1134,7 @@ function _requestedPublicKeys(der) {
1595
1134
  if (out.length === before) _unreadableArm();
1596
1135
  }
1597
1136
  } catch (e) {
1598
- if (!keyBearing) return; // an orm arm carries no key and is not expected to
1137
+ if (!keyBearing) return;
1599
1138
  throw (e && e.code === "est/bad-input") ? e : E("est/bad-input",
1600
1139
  "a certification request in this Full PKI Request could not be read, so a response could not be " +
1601
1140
  "tied back to it; pki.est.fullcmc will not send a request it cannot check the answer to", e);
@@ -1608,21 +1147,8 @@ function _unreadableArm() {
1608
1147
  throw E("est/bad-input", "a certification request in this Full PKI Request declares no readable public key");
1609
1148
  }
1610
1149
 
1611
- // What the caller retained, threaded to pki.cmc.verify -- the exchange binding
1612
- // AND the carrier-authentication inputs. Built in one place so the success and
1613
- // rejection arms cannot drift apart about what this exchange is.
1614
- // Captured SYNCHRONOUSLY, before the request goes out, and with the byte buffers
1615
- // copied. Everything here is read again only after the transport resolves, so
1616
- // reading the caller's live options at that point would let an object mutated
1617
- // during the round trip decide the checks -- and flipping allowUnverifiedResponse
1618
- // mid-flight would skip the signature check the default posture requires.
1619
- // pki.cmc.verify freezes its own inputs too; this is the wrapper's half of the
1620
- // same rule, because by the time it calls verify the damage would already be done.
1621
1150
  function _cmcSent(opts, der) {
1622
1151
  var carried = _requestBinding(der);
1623
- // A caller-supplied value must AGREE with the request. Silently preferring one
1624
- // over the other would leave the response checked against something the request
1625
- // may never have carried; refusing the disagreement keeps the two honest.
1626
1152
  _assertAgrees("transactionId", opts.transactionId, carried.transactionId);
1627
1153
  _assertAgrees("senderNonce", opts.senderNonce, carried.senderNonce);
1628
1154
  _assertAgrees("dataReturn", opts.dataReturn, carried.dataReturn);
@@ -1631,63 +1157,23 @@ function _cmcSent(opts, der) {
1631
1157
  senderNonce: _copyBytes(carried.senderNonce),
1632
1158
  dataReturn: _copyBytes(carried.dataReturn),
1633
1159
  bodyPartIDs: carried.bodyPartIDs,
1634
- // The nested paths travel with the flat identifiers: a status may name a body part inside a
1635
- // nested message this request carried, and forwarding only the flat set would leave every
1636
- // such reference unconfirmable and therefore refused.
1637
1160
  bodyPartPaths: carried.bodyPartPaths,
1638
1161
  certs: Array.isArray(opts.responderCerts) ? opts.responderCerts.map(_copyBytes) : opts.responderCerts,
1639
- // The AuthenticatedData carrier's key material. Without this the verb could
1640
- // reach that carrier only through the unauthenticated opt-out -- the capability
1641
- // would exist one layer down and be unreachable from the one operators call.
1642
1162
  recipient: _copyRecipient(opts.responseRecipient),
1643
1163
  allowUnverified: opts.allowUnverifiedResponse === true,
1644
- // The binding opt-out travels the same way. Without it the capability would
1645
- // exist one layer down and be unreachable from the verb operators call: a
1646
- // request built with no Transaction Identifier, Sender Nonce or Data Return
1647
- // carries nothing for the response to echo, so pki.cmc.verify would refuse
1648
- // every answer to it and no option here could say that was intended.
1649
1164
  allowUnbound: opts.allowUnboundResponse === true,
1650
1165
  };
1651
1166
  }
1652
1167
 
1653
- // EVERY byte form the parsers and crypto routines downstream accept, not just the two most
1654
- // common: a DataView or a bare ArrayBuffer arrives by the same door as a Buffer, and the values
1655
- // this copies -- the nonce the response must echo, the certificates that authenticate it, the
1656
- // recipient key material -- are all read after the transport resolves. Covering only Buffer and
1657
- // Uint8Array would leave those forms aliased across the request's whole flight, which is the same
1658
- // window this exists to close, open for exactly the inputs that came in by the wider door.
1659
- // A DataView is copied over its OWN window, not the whole backing buffer it happens to sit in.
1660
- // One call, because guard.bytes decides what a byte form is and where its bytes are. The three
1661
- // hand-rolled branches this replaces read `v.buffer`, `v.byteOffset` and `v.byteLength` off the
1662
- // value, and all three are accessors a caller's subclass can answer: one that lied copied a
1663
- // different store than the array held, so the bytes checked here and the bytes sent later were
1664
- // two different reads.
1665
1168
  function _copyBytes(v) {
1666
1169
  if (!guard.bytes.isByteSource(v)) return v;
1667
1170
  return guard.bytes.snapshotSource(v, EstError, "est/bad-input", "a byte field of the request");
1668
1171
  }
1669
1172
 
1670
- // The exchange-binding controls the request ACTUALLY carries (RFC 5272 sec. 6.6
1671
- // Transaction Identifier / Sender Nonce, sec. 6.4 Data Return). These are what the
1672
- // response has to echo, so they are read from the bytes going out rather than from
1673
- // a parallel claim that could disagree with them.
1674
1173
  function _requestBinding(der) {
1675
1174
  var out = { transactionId: undefined, senderNonce: undefined, dataReturn: undefined, bodyPartIDs: undefined,
1676
1175
  bodyPartPaths: undefined };
1677
- // Parsed WITHOUT a fallback. The caller reaches this only after the request has
1678
- // already been confirmed to be a Full PKI Request, so a parse failure here is
1679
- // impossible rather than expected -- and defaulting to "no binding" if it ever
1680
- // became possible is the wrong answer anyway: a request whose controls could not
1681
- // be read would face no echo requirement at all, which is a weaker exchange than
1682
- // the one being sent. Letting the typed decode error propagate keeps it closed.
1683
1176
  var body = cmcFmt.parse(der);
1684
- // Every identifier this request actually carries. A status in the response names
1685
- // the body parts it is ABOUT, and a status naming a body part that was never
1686
- // sent is not an answer to this request -- so the client keeps the set it can
1687
- // check that claim against, the same way it keeps the transaction and nonce.
1688
- // Collected from all four sequences because RFC 5272 sec. 3.2.1 draws every
1689
- // identifier from ONE space; a status may legitimately reference a control or a
1690
- // content info, not only a certification request.
1691
1177
  var ids = [];
1692
1178
  var paths = [];
1693
1179
  [body.requests, body.controls, body.cmsSequence, body.otherMsgs].forEach(function (list) {
@@ -1695,12 +1181,6 @@ function _requestBinding(der) {
1695
1181
  if (el && el.bodyPartID != null) { ids.push(el.bodyPartID); paths.push([el.bodyPartID]); }
1696
1182
  });
1697
1183
  });
1698
- // A cmsSequence element carries a NESTED CMS message, which may itself be a Full PKI
1699
- // Request (sec. 3.2.1), and a response identifies a body part inside one by the path
1700
- // [outer, inner...]. This client composed that nested message, so it can confirm such a
1701
- // path -- but only by reading the message back, which is what this does. A nested message
1702
- // this parser cannot read back yields no retained path, so a status naming a part inside
1703
- // it is still refused: what cannot be read cannot be confirmed.
1704
1184
  _collectNestedPaths(body.cmsSequence, [], paths, 0);
1705
1185
  out.bodyPartIDs = ids;
1706
1186
  out.bodyPartPaths = paths;
@@ -1708,22 +1188,12 @@ function _requestBinding(der) {
1708
1188
  var field = c.attrType === OID_CMC_TRANSACTION_ID ? "transactionId"
1709
1189
  : c.attrType === OID_CMC_SENDER_NONCE ? "senderNonce"
1710
1190
  : c.attrType === OID_CMC_DATA_RETURN ? "dataReturn" : null;
1711
- if (!field) return; // not a binding control
1712
- // Duplicates are ambiguous, and ambiguity here decides what the response must
1713
- // echo: taking the last would bind the answer to one of two values the request
1714
- // sent, chosen arbitrarily. pki.cmc.verify already refuses duplicate binding
1715
- // controls on the response; the request side is held to the same rule rather
1716
- // than shipping a message whose own binding cannot be read one way.
1191
+ if (!field) return;
1717
1192
  if (out[field] !== undefined) {
1718
1193
  throw E("est/bad-input",
1719
1194
  "the Full PKI Request carries more than one " + field + " control, so there is no single value " +
1720
1195
  "the response can be bound to (RFC 5272 sec. 6.6 / 6.4)");
1721
1196
  }
1722
- // A binding control that is PRESENT but unreadable must not decay into
1723
- // "absent". Doing so would drop the replay check for a request that carries
1724
- // the control -- the response would then face no echo requirement at all,
1725
- // which is a weaker exchange than the one being sent. An unreadable security
1726
- // control is a refusal, not a default.
1727
1197
  var v = c.values && c.values.length === 1 ? c.values[0] : null;
1728
1198
  if (!v) {
1729
1199
  throw E("est/bad-input",
@@ -1742,23 +1212,12 @@ function _requestBinding(der) {
1742
1212
  return out;
1743
1213
  }
1744
1214
 
1745
- // Every body-part path reachable inside the nested messages this request carries, so a status
1746
- // naming one can be confirmed rather than refused outright. Descends only into a nested message
1747
- // this parser reads back as a PKIData: anything else yields no path, and a status naming a part
1748
- // inside it stays a refusal, because a reference that cannot be checked is not one to accept.
1749
- // The depth cap bounds the walk; a request nested deeper than this is composed by nobody in
1750
- // practice, and stopping simply leaves those paths unretained -- which refuses, not accepts.
1751
1215
  var NESTED_PATH_DEPTH_CAP = 8;
1752
1216
  function _collectNestedPaths(list, prefix, out, depth) {
1753
1217
  if (depth >= NESTED_PATH_DEPTH_CAP) return;
1754
1218
  (list || []).forEach(function (el) {
1755
1219
  if (!el || el.bodyPartID == null || !el.contentInfoBytes) return;
1756
1220
  var inner;
1757
- // A nested element that cannot be read back as a Full PKI Request contributes no path, by
1758
- // either route: the read throws, or it succeeds and yields some other content type. Both land
1759
- // on the same refusal downstream -- a status naming a part inside it finds no retained path --
1760
- // so neither can widen what the response may report on. Nothing is absorbed here that a
1761
- // verdict then rests on; the absence of a path IS the fail-closed answer.
1762
1221
  try { inner = cmcFmt.parse(el.contentInfoBytes); }
1763
1222
  catch (_e) { /* allow:swallow-unverified the outer request must itself parse for this to run, which already rejects a malformed nested ContentInfo; a payload that survives that and still fails to read back lands on the same no-path refusal the non-pkiData route below takes, which the vectors drive */ return; }
1764
1223
  if (!inner || inner.kind !== "pkiData") return;
@@ -1776,11 +1235,6 @@ function _assertAgrees(name, supplied, carried) {
1776
1235
  if (guard.bytes.isByteSource(supplied)) {
1777
1236
  same = Buffer.isBuffer(carried) && guard.bytes.source(supplied, EstError, "est/bad-input", "opts.senderNonce").equals(carried);
1778
1237
  } else {
1779
- // Through the shared authoring guard, not a bare BigInt(): a `number` above
1780
- // Number.MAX_SAFE_INTEGER has already lost the digits that distinguish it from
1781
- // its neighbor, so converting it here would compare a value the caller never
1782
- // wrote. A transaction identifier is an unbounded INTEGER on the wire; large
1783
- // ones are passed as a bigint, and this says so rather than rounding.
1784
1238
  same = carried != null &&
1785
1239
  guard.range.authoredInteger(supplied, E, "est/bad-input", "opts." + name) === BigInt(carried);
1786
1240
  }
@@ -1791,8 +1245,6 @@ function _assertAgrees(name, supplied, carried) {
1791
1245
  }
1792
1246
  }
1793
1247
 
1794
- // The recipient descriptor, one level deep with its byte values copied -- key
1795
- // material is read after the round trip like everything else in this snapshot.
1796
1248
  function _copyRecipient(r) {
1797
1249
  if (!r || typeof r !== "object") return r;
1798
1250
  var out = {}, k;
@@ -1800,14 +1252,6 @@ function _copyRecipient(r) {
1800
1252
  return out;
1801
1253
  }
1802
1254
 
1803
- // Own enumerable properties, one level, plus a copy of `auth`. The extra level is
1804
- // not decoration: credentials are the one nested object read AFTER an await --
1805
- // _drive reads the scheme, the username and password, and the Digest policy only
1806
- // once a 401 comes back -- so leaving it shared would let an options object
1807
- // mutated during the round trip change which credentials go out, or turn on a
1808
- // digest algorithm the request did not start with. Everything else here is either
1809
- // read before the request leaves or is a value the caller means to own (a
1810
- // transport function, a tls config).
1811
1255
  function _shallowCopy(o) {
1812
1256
  var out = {}, k;
1813
1257
  for (k in o) { if (_hasOwn(o, k)) out[k] = o[k]; }
@@ -1819,11 +1263,7 @@ function _shallowCopy(o) {
1819
1263
  return out;
1820
1264
  }
1821
1265
 
1822
- // The Full PKI Request bytes: a DER Buffer, or a PEM CMS block. Anything else is
1823
- // a caller mistake caught before the transport is touched.
1824
1266
  function _cmcRequestDer(request) {
1825
- // COPIED, not aliased: these bytes are parsed now (for the requested keys) and
1826
- // transmitted later, so sharing the caller's buffer would let the two disagree.
1827
1267
  if (guard.bytes.isByteSource(request)) {
1828
1268
  return guard.bytes.snapshotSource(request, EstError, "est/bad-input", "the Full PKI Request");
1829
1269
  }
@@ -1831,40 +1271,16 @@ function _cmcRequestDer(request) {
1831
1271
  throw E("est/bad-input", "pki.est.fullcmc requires the Full PKI Request as DER bytes or a PEM CMS block");
1832
1272
  }
1833
1273
 
1834
- // The result function for /fullcmc. Deliberately NOT _certsResult: the two differ
1835
- // on the point RFC 7030 sec. 4.2.3 and sec. 4.3.2 disagree about. On the sibling
1836
- // enroll verbs a Simple PKI Response on an error only MAY be present; on
1837
- // /fullcmc "A CMC response with the content-type of application/pkcs7-mime MUST
1838
- // be included in the response data for any CMC error response", so the CMC
1839
- // verdict is decoded and surfaced. What must NOT happen is the decode failing and
1840
- // taking the HTTP fault with it -- an unreadable error body leaves the fault
1841
- // reported as itself.
1842
1274
  function _fullcmcResult(res, opts, wanted, sent) {
1843
- // classifyResponse is the single authority on what an HTTP status means here,
1844
- // and it THROWS est/http-error on a 4xx/5xx rather than returning a verdict.
1845
- // So the rejection path runs from its exception: the mandated CMC response is
1846
- // decoded and, if readable, REPLACES the fault with a richer typed one. If it
1847
- // cannot be read, the original fault is rethrown untouched -- an unreadable
1848
- // error body is a worse fault than the status, never a way to lose it.
1849
1275
  var verdict;
1850
1276
  try {
1851
1277
  verdict = classifyResponse(res.status, res.headers, res.body, { op: "fullcmc", now: opts.now });
1852
1278
  } catch (httpFault) {
1853
- // ONLY an actual rejection status earns the richer error. classifyResponse also
1854
- // throws when a NON-error response fails its own validation -- a 200 with a
1855
- // missing or unrecognized smime-type, a 202 with no Retry-After -- and those
1856
- // faults are about the response being malformed, not about the CA refusing.
1857
- // Letting a signed CMC body replace them would let a malformed 200 answer as
1858
- // though it were a clean rejection, and the validation it failed would never be
1859
- // reported. The status is read from the response, not from the fault, so a
1860
- // future fault type cannot quietly opt itself into the upgrade.
1861
1279
  if (!(res.status >= 400 && res.status <= 599)) throw httpFault;
1862
1280
  return _tryDecodeCmcFault(res, sent).then(function (cmcErr) { throw cmcErr || httpFault; });
1863
1281
  }
1864
1282
 
1865
1283
  if (verdict.status === "not-implemented") {
1866
- // Takes precedence over any body: the service is absent, so there is no CMC
1867
- // verdict to read (RFC 7030 sec. 4.3.2 -- 404 or 501).
1868
1284
  throw E("est/not-implemented",
1869
1285
  "the EST server does not implement /fullcmc (HTTP " + verdict.httpStatus + "; RFC 7030 sec. 4.3.2)");
1870
1286
  }
@@ -1880,18 +1296,7 @@ function _fullcmcResult(res, opts, wanted, sent) {
1880
1296
  if (bodyLen === 0) throw E("est/empty-body", "a 200 /fullcmc response carried an empty body (RFC 7030 sec. 4.3.2)");
1881
1297
  var der = transferDecode(res.body);
1882
1298
 
1883
- // FC5 admits two smime-types, and the LABEL must agree with the BYTES: a
1884
- // certs-only label over a Full PKI Response (or the reverse) is a server
1885
- // mislabeling its own body, and accepting either shape under either label
1886
- // would make the content-type check decorative.
1887
- // Through the shared case-insensitive accessor: HTTP header names are
1888
- // case-insensitive (RFC 9110 sec. 5.1), the classifier already reads them that
1889
- // way, and a second lookup that guessed at two spellings would disagree with it
1890
- // for a conformant server -- routing a certs-only body down the wrong arm.
1891
1299
  var pt200 = _partMediaType(_ciHeader(res.headers, "content-type"));
1892
- // A header declaring BOTH arms selects neither. Taking the first would let the
1893
- // order of two labels decide which shape the body is read as, on a header whose
1894
- // whole job here is to say which one it is.
1895
1300
  if (pt200.ambiguous) {
1896
1301
  throw E("est/bad-content-type",
1897
1302
  "the 200 /fullcmc Content-Type declares more than one smime-type, so which response arm this is " +
@@ -1899,13 +1304,6 @@ function _fullcmcResult(res, opts, wanted, sent) {
1899
1304
  }
1900
1305
  var smimeType = pt200.smimeType;
1901
1306
  if (smimeType === "certs-only") {
1902
- // A certs-only body is a degenerate certificates-only SignedData: it carries no
1903
- // CMC controls, so it CANNOT echo a Transaction Identifier, Sender Nonce or
1904
- // Data Return. A client that sent those asked for replay binding, and the
1905
- // public-key correlation below is not one -- an old response for the same key
1906
- // still matches. Accepting this arm would silently give none of what was asked
1907
- // for, so it is refused; a caller who does not need the binding simply does not
1908
- // send the controls.
1909
1307
  var unecho = ["transactionId", "senderNonce", "dataReturn"].filter(function (k) {
1910
1308
  return sent[k] != null;
1911
1309
  });
@@ -1914,12 +1312,6 @@ function _fullcmcResult(res, opts, wanted, sent) {
1914
1312
  "the request carried " + unecho.join(" / ") + ", which a certs-only response has no controls to " +
1915
1313
  "echo, so the replay binding it asked for cannot be checked (RFC 5272 sec. 6.6 / 6.4)");
1916
1314
  }
1917
- // Reaching here means the request asked for no binding, so nothing whatever
1918
- // ties this bag of certificates to this exchange. The public-key correlation
1919
- // below is explicitly not a binding, since an old response for the same key
1920
- // matches just as well. pki.cmc.verify takes this posture on the other arm,
1921
- // and it is taken here too: one arm quietly accepting what the other refuses
1922
- // would make which rule applied depend on the smime-type the server chose.
1923
1315
  if (sent.allowUnbound !== true) {
1924
1316
  throw E("est/unbound-response",
1925
1317
  "nothing ties this certs-only response to the request just sent: a certs-only body carries no " +
@@ -1927,30 +1319,9 @@ function _fullcmcResult(res, opts, wanted, sent) {
1927
1319
  "answers with a CMC-response, or pass `allowUnboundResponse: true` to accept a bag of certificates " +
1928
1320
  "that could answer any earlier request for the same key (RFC 5272 sec. 6.6)");
1929
1321
  }
1930
- var certs = parseCertsOnly(der); // throws est/not-certs-only on a Full PKI Response
1931
- // A certs-only body says nothing about WHICH request it answers -- no status,
1932
- // no body-part reference. So the issued certificate is identified the one way
1933
- // RFC 5272 sec. 4.1 sanctions, by public-key match against what was submitted,
1934
- // exactly as /simpleenroll and /serverkeygen already do. Without this, a bag
1935
- // holding only a CA chain -- or a certificate for someone else's key -- reads
1936
- // as a successful issuance for this request.
1322
+ var certs = parseCertsOnly(der);
1937
1323
  var issuedCerts = _correlateIssued(certs.certificates, wanted, "certs-only");
1938
1324
  var issued = issuedCerts[0];
1939
- // `signatureVerified: false` is stated, not omitted. A certs-only body is a
1940
- // DEGENERATE certificates-only SignedData -- RFC 5652 sec. 5.2 defines it with
1941
- // an empty signerInfos set -- so there is no signature here to check, and
1942
- // demanding one would reject every conformant server. What this arm must not
1943
- // do is leave the field undefined while the cmc-response arm sets it: a caller
1944
- // reading `verdict.signatureVerified` would then get a different KIND of answer
1945
- // depending on which arm the server happened to choose. RFC 7030 secures this
1946
- // arm through the authenticated TLS channel instead, and the certificates stay
1947
- // `trusted: false` for the caller to run through pki.path.validate.
1948
- // `bound` and `boundToRequest` are stated here for the same reason
1949
- // `signatureVerified` is: leaving them undefined while the CMC-response arm
1950
- // sets them would give a caller a different kind of answer depending on which
1951
- // arm the server chose. Every one is false and cannot be otherwise, because a
1952
- // certs-only body has no controls to echo. That is what the refusal above
1953
- // makes the caller acknowledge before reading this verdict at all.
1954
1325
  return { outcome: "issued", certificate: issued, issuedCertificates: issuedCerts,
1955
1326
  certificates: certs.certificates, crls: certs.crls,
1956
1327
  controls: [], statuses: [], publishTrustAnchors: null, trusted: false,
@@ -1958,28 +1329,8 @@ function _fullcmcResult(res, opts, wanted, sent) {
1958
1329
  bound: { transactionId: false, senderNonce: false, dataReturn: false, bodyPartIDs: false },
1959
1330
  boundToRequest: false };
1960
1331
  }
1961
- // smime-type=cmc-response: the Full PKI Response, interpreted into one verdict.
1962
- // pki.cmc.verify owns the transaction binding and the status reduction; this
1963
- // verb only supplies what the caller retained.
1964
1332
  return cmcVerify.verify(der, sent).then(function (verdict) {
1965
- // An `issued` verdict is a claim about THIS request, so it is held to the same
1966
- // correlation as the certs-only arm. A status control saying success while the
1967
- // bag holds a CA chain, someone else's certificate, or nothing at all does not
1968
- // make an issuance -- and the status is the server's word, whereas the key
1969
- // match is checkable. Only `issued` is correlated: pending / pop-required /
1970
- // confirm-required / rejected are not claims that a certificate was issued.
1971
1333
  if (verdict.outcome !== "issued") return verdict;
1972
- // A request that asked for no certificate cannot have one correlated to it. An
1973
- // orm-only Full PKI Request is exactly that: the other-message arm carries no
1974
- // certification request and no key, and RFC 5272 lets a server answer it
1975
- // successfully with its result in the cmsSequence or otherMsgSequence rather
1976
- // than as an issuance. Reading `issued` there as "a certificate must match"
1977
- // would make this verb unable to carry that exchange at all. The response's own
1978
- // certificate bag is still surfaced raw, untrusted, for pki.path.validate.
1979
- //
1980
- // The certs-only arm keeps the opposite rule, and deliberately: that body is
1981
- // nothing BUT a claim of certificate issuance, so with nothing to match it
1982
- // against there is no way to say it answers this request.
1983
1334
  if (!wanted.length) { verdict.issuedCertificates = []; return verdict; }
1984
1335
  var issuedCerts = _correlateIssued(verdict.certificates, wanted, "CMC-response");
1985
1336
  verdict.certificate = issuedCerts[0];
@@ -1988,38 +1339,13 @@ function _fullcmcResult(res, opts, wanted, sent) {
1988
1339
  });
1989
1340
  }
1990
1341
 
1991
- // Decode the CMC response a rejection MUST carry, returning a typed est/cmc-failed
1992
- // with the verdict attached -- or null when it cannot be read, so the caller
1993
- // reports the HTTP fault instead of an asn1/* leak from a body that was never
1994
- // going to parse.
1995
1342
  function _tryDecodeCmcFault(res, sent) {
1996
1343
  return Promise.resolve().then(function () {
1997
1344
  var pt = _partMediaType(_ciHeader(res.headers, "content-type"));
1998
1345
  if (pt.media !== "application/pkcs7-mime") return null;
1999
- // A header declaring two smime-types cannot vouch for the body either. This
2000
- // path returns NULL rather than throwing, so the caller reports the HTTP fault
2001
- // the response actually carried -- replacing a real server error with a
2002
- // content-type complaint would hide the thing the operator needs to see.
2003
1346
  if (pt.ambiguous) return null;
2004
- // The LABEL must agree with the bytes here too. The success path refuses a
2005
- // certs-only label over a Full PKI Response and the reverse; accepting any
2006
- // pkcs7-mime on the error path would make that agreement decorative, and would
2007
- // read a CMC verdict out of a body the server said was something else. RFC 7030
2008
- // sec. 4.3.2 makes the rejection body a CMC-response.
2009
1347
  if (String(pt.smimeType || "").toLowerCase() !== "cmc-response") return null;
2010
- // The SAME binding the success path applies. An error response that does not
2011
- // echo this exchange's transaction / nonce is not this request's answer, and
2012
- // attaching it would let a replayed or unrelated failure be reported as the
2013
- // verdict for the request just sent. A binding failure makes verify reject,
2014
- // which lands in the catch below and leaves the HTTP fault reported.
2015
1348
  return cmcVerify.verify(transferDecode(res.body), sent).then(function (verdict) {
2016
- // Only an actual REJECTION becomes the richer error. A CMC body inside an
2017
- // HTTP failure that says the request was issued is the server contradicting
2018
- // itself, and reporting it as "the server rejected the Full PKI Request:
2019
- // issued" would hand the caller a successful outcome wrapped in a rejection.
2020
- // Returning null there leaves the HTTP fault standing, which is the honest
2021
- // report of what happened. RFC 7030 sec. 4.3.2 makes the body a MUST for a
2022
- // rejection, which is the case this upgrade exists for.
2023
1349
  if (verdict.outcome !== "rejected") return null;
2024
1350
  var e = E("est/cmc-failed",
2025
1351
  "the EST server rejected the Full PKI Request: " + verdict.outcome +
@@ -2028,11 +1354,6 @@ function _tryDecodeCmcFault(res, sent) {
2028
1354
  e.httpStatus = res.status;
2029
1355
  return e;
2030
1356
  });
2031
- // EVERY way the attempt can fail lands here, not just the verify rejection:
2032
- // transferDecode throws synchronously on a non-base64 body, which is exactly
2033
- // the shape FC7a is about. Returning null makes the caller rethrow the ORIGINAL
2034
- // HTTP fault -- nothing is absorbed into a verdict, and the richer error is
2035
- // only ever an upgrade, never a substitute that loses the status.
2036
1357
  }).then(null, function () { return null; });
2037
1358
  }
2038
1359
 
@@ -2079,10 +1400,6 @@ function _enroll(op, baseUrl, csrInput, opts) {
2079
1400
  function cacerts(baseUrl, opts) {
2080
1401
  opts = opts || {};
2081
1402
  return Promise.resolve().then(function () {
2082
- // Inside the promise, like every other refusal these verbs make. They are documented as
2083
- // Promise-returning, so a caller writes `.catch(...)` -- and a check that throws synchronously
2084
- // escapes that catch entirely, turning a misspelled option into an uncaught exception rather
2085
- // than the rejection the caller is already handling.
2086
1403
  _knownOpts(opts, CACERTS_OPTS, "cacerts");
2087
1404
  return _client("cacerts", "GET", baseUrl, null, { accept: "application/pkcs7-mime" }, opts);
2088
1405
  }).then(function (res) { return _certsResult("cacerts", res, opts, null); });
@@ -2156,25 +1473,17 @@ function simplereenroll(baseUrl, csrInput, opts) {
2156
1473
  return Promise.resolve().then(function () {
2157
1474
  _knownOpts(opts, SIMPLEREENROLL_OPTS, "simplereenroll");
2158
1475
  if (!opts.oldCert) throw E("est/bad-input", "simplereenroll requires opts.oldCert (the certificate being renewed, RFC 7030 sec. 4.2.2)");
2159
- reenrollGuard(opts.oldCert, _csrDer(csrInput)); // est/reenroll-* on mismatch, BEFORE the POST
1476
+ reenrollGuard(opts.oldCert, _csrDer(csrInput));
2160
1477
  return _enroll("simplereenroll", baseUrl, csrInput, opts);
2161
1478
  });
2162
1479
  }
2163
1480
 
2164
- // ---- /serverkeygen (RFC 7030 sec. 4.4) ----------------------------------
2165
1481
 
2166
- // Derive the server-generated-key ENCRYPTION coherence from the CSR the caller actually POSTs (RFC 7030
2167
- // sec. 4.4.1): a DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute names the key-encryption
2168
- // key the server must encrypt the generated private key to. Binding the expected recipient to the CSR (not a
2169
- // free-floating opts boolean) closes the drift where a cleartext key rides past a caller who forgot the flag.
2170
1482
  function _serverkeygenEncryptionFromCsr(csrDer) {
2171
1483
  var attrs = csr.parse(csrDer).attributes || [];
2172
1484
  var keyId = null, kind = null;
2173
1485
  for (var i = 0; i < attrs.length; i++) {
2174
1486
  if (attrs[i].type !== OID_DECRYPT_KEY_ID && attrs[i].type !== OID_ASYMM_DECRYPT_KEY_ID) continue;
2175
- // DecryptKeyIdentifier names a SYMMETRIC key-encryption key; AsymmetricDecryptKeyIdentifier an ASYMMETRIC
2176
- // one (RFC 7030 sec. 4.4.1). The mechanism is preserved so the returned key part is matched to a compatible
2177
- // RecipientInfo arm, not merely one whose identifier bytes coincide.
2178
1487
  var thisKind = attrs[i].type === OID_ASYMM_DECRYPT_KEY_ID ? "asymmetric" : "symmetric";
2179
1488
  var vals = attrs[i].values || [];
2180
1489
  if (vals.length !== 1) throw E("est/bad-input", "a serverkeygen key-identifier attribute must carry exactly one value (RFC 7030 sec. 4.4.1)");
@@ -2188,20 +1497,11 @@ function _serverkeygenEncryptionFromCsr(csrDer) {
2188
1497
  return { requestedEncryption: keyId !== null, expectedRecipientKeyId: keyId, expectedRecipientKind: kind };
2189
1498
  }
2190
1499
 
2191
- // The delivered private key rides TLS confidentiality alone: a NULL / anonymous / EXPORT suite MUST NOT be
2192
- // used to carry it (RFC 7030 sec. 4.4, sec. 6). Assert (never reconfigure) the negotiated cipher when the
2193
- // transport surfaces it; an injected transport that reports no cipher is trusted (the test/loopback channel).
2194
- // The weak-cipher token scan (RFC 3986/2119-free ASCII, by character code): a "word" character is
2195
- // [A-Za-z0-9_] (regex \w), and a boundary (\b) sits where a word character meets a non-word one or a
2196
- // string edge. `_subAt`/`_containsSub` test a literal token at / anywhere in an already-uppercased name.
2197
1500
  function _isWordChar(c) { return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95; }
2198
1501
  function _subAt(s, sub, i) { var m = sub.length; if (i + m > s.length) return false; for (var k = 0; k < m; k++) { if (_charCodeAt(s, i + k) !== _charCodeAt(sub, k)) return false; } return true; }
2199
1502
  function _containsSub(s, sub) { for (var i = 0; i + sub.length <= s.length; i++) { if (_subAt(s, sub, i)) return true; } return false; }
2200
1503
  function _wbBefore(s, i) { return i === 0 || !_isWordChar(_charCodeAt(s, i - 1)); }
2201
1504
  function _wbAfter(s, j) { return j === s.length || !_isWordChar(_charCodeAt(s, j)); }
2202
- // /NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/ over the uppercased name: the three plain substrings, then
2203
- // EXP at a word boundary followed by a separator OR a digit run (EXP-RC4-MD5, EXP1024-RC4-SHA -- a bare
2204
- // \bEXP\b misses EXP1024), then ADH / AECDH each bounded on both sides.
2205
1505
  function _hasWeakCipherToken(name) {
2206
1506
  if (_containsSub(name, "NULL") || _containsSub(name, "ANON") || _containsSub(name, "EXPORT")) return true;
2207
1507
  var i, n = name.length;
@@ -2224,24 +1524,6 @@ function _assertConfidentialCipher(res) {
2224
1524
  if (_hasWeakCipherToken(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
2225
1525
  }
2226
1526
 
2227
- // A case-insensitive header lookup: the injectable transport seam only promises { status, headers, body }, so a
2228
- // hostile / non-Node transport may deliver "Content-Type" in any casing -- read it case-insensitively (classifyResponse
2229
- // normalizes internally, but the multipart boundary is read from the header directly here).
2230
- // A header, read case-insensitively (RFC 9110 sec. 5.1) -- and refused when the map
2231
- // carries the SAME field under more than one spelling. HTTP header names are
2232
- // case-insensitive, so `content-type` and `Content-Type` are one field with two
2233
- // values; a reader that prefers the exact key and a reader that lowercases into a
2234
- // map keep different ones, and this verb has both. Two stages that disagree about
2235
- // which response arm a body is can accept a response the header declares twice
2236
- // over. That is the same ambiguity a repeated Content-Type PARAMETER raises, at the
2237
- // level of the field itself, and it gets the same answer: refused, not resolved by
2238
- // which spelling was reached first.
2239
- // A LIST field, read case-insensitively and combined across every spelling it
2240
- // arrived under. RFC 9110 sec. 5.3 lets a recipient join multiple lines of a
2241
- // list-based field into one comma-separated value, and sec. 11.6.1 makes
2242
- // WWW-Authenticate exactly that -- one challenge per element. Refusing repetition
2243
- // here would reject a server offering both Digest and Basic, which is not an
2244
- // ambiguity but the field doing its job.
2245
1527
  function _ciHeaderList(headers, name) {
2246
1528
  headers = headers || {};
2247
1529
  var lname = name.toLowerCase(), keys = Object.keys(headers), parts = [];
@@ -2272,27 +1554,17 @@ function _ciHeader(headers, name) {
2272
1554
 
2273
1555
  async function _serverkeygenResult(res, opts, derived) {
2274
1556
  var verdict = classifyResponse(res.status, res.headers, res.body, { op: "serverkeygen", now: opts.now });
2275
- // A 202 is an enrollment posture (RFC 7030 sec. 4.4 treats /serverkeygen as an enroll): surfaced, never slept.
2276
1557
  if (verdict.status === "retry") return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
2277
1558
  if (verdict.status !== "ok") throw E("est/http-error", "an EST serverkeygen response must be HTTP 200 or 202 (RFC 7030 sec. 4.4.2), got " + res.status);
2278
1559
  var bodyLen = _bodyLen(res);
2279
1560
  if (bodyLen === 0) throw E("est/empty-body", "a 200 serverkeygen response carried an empty body (RFC 7030 sec. 4.4.2)");
2280
1561
  _assertConfidentialCipher(res);
2281
- // parseServerKeygenResponse does the per-part transfer-decode; the recipient coherence is bound to the CSR.
2282
1562
  var out = parseServerKeygenResponse(res.body, _ciHeader(res.headers, "content-type"), {
2283
1563
  requestedEncryption: derived.requestedEncryption,
2284
1564
  expectedRecipientKeyId: derived.expectedRecipientKeyId,
2285
1565
  expectedRecipientKind: derived.expectedRecipientKind,
2286
1566
  expectedRecipientIssuerSerial: opts.expectedRecipientIssuerSerial,
2287
1567
  });
2288
- // Bind a CLEARTEXT server-generated key to its certificate (RFC 7030 sec. 4.4.2): the CA generated
2289
- // this pair and issued a certificate over its PUBLIC half, so the delivered PrivateKeyInfo's public key
2290
- // MUST equal EXACTLY ONE returned certificate's SubjectPublicKeyInfo -- a key unrelated to every
2291
- // certificate is an unusable / mis-associated credential, and a key matching more than one leaves the
2292
- // issued certificate ambiguous. Mirrors simpleenroll's findIssuedCert public-key match; the encrypted
2293
- // key stays opaque, so its recipient coherence is bound in the parser instead. The public half is
2294
- // derived through the key engine (never re-serialized here); a key whose public half cannot be derived
2295
- // is not bindable and fails closed.
2296
1568
  if (out.privateKeyDer) {
2297
1569
  var spki;
2298
1570
  try { spki = await key.publicFromPrivate(out.privateKeyDer); }
@@ -2356,10 +1628,6 @@ function serverkeygen(baseUrl, csrInput, opts) {
2356
1628
  var s = eis.serialNumber;
2357
1629
  if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && pkix.allCharsIn(s, _DIGITS_TABLE)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
2358
1630
  }
2359
- // A recipient expectation implies the key part MUST be encrypted to that recipient; if the CSR advertised no
2360
- // key-encryption attribute the coherence check below never runs, so a compromised CA could deliver the key
2361
- // to a recipient IT controls (or cleartext) while the caller believes its pin was enforced. Refuse the
2362
- // contradiction at config time (RFC 7030 sec. 4.4.1) -- the caller must advertise the KEK in the CSR.
2363
1631
  if ((opts.expectedRecipientKeyId !== undefined || opts.expectedRecipientIssuerSerial != null) && !derived.requestedEncryption) {
2364
1632
  throw E("est/bad-input", "a recipient expectation (expectedRecipientKeyId / expectedRecipientIssuerSerial) implies an encrypted key, but the CSR advertised no DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute (RFC 7030 sec. 4.4.1)");
2365
1633
  }
@@ -2368,17 +1636,13 @@ function serverkeygen(baseUrl, csrInput, opts) {
2368
1636
  });
2369
1637
  }
2370
1638
 
2371
- // ---- /csrattrs GET (RFC 7030 sec. 4.5, RFC 9908) ------------------------
2372
1639
 
2373
1640
  function _csrattrsResult(res, opts) {
2374
1641
  var verdict = classifyResponse(res.status, res.headers, res.body, { op: "csrattrs", now: opts.now });
2375
- // 204 / 404 = "CSR Attributes Response not available" -- a valid NONE outcome, not an error (RFC 7030 sec. 4.5.2).
2376
1642
  if (verdict.status === "none-available") return { available: false, attrs: null };
2377
- // A 202 is nonconforming for a policy GET (sec. 4.5.2 lists only 200 / 204 / 404).
2378
1643
  if (verdict.status === "retry") throw E("est/http-error", "a /csrattrs response must be HTTP 200, 204, or 404, not 202 (RFC 7030 sec. 4.5.2)");
2379
1644
  if (verdict.status !== "ok") throw E("est/http-error", "an EST csrattrs response must be HTTP 200 / 204 / 404 (RFC 7030 sec. 4.5.2), got " + res.status);
2380
1645
  var bodyLen = _bodyLen(res);
2381
- // An empty HTTP body is distinct from a valid EMPTY CsrAttrs (`30 00` / base64 `MAA=`), which parses as {items:[]}.
2382
1646
  if (bodyLen === 0) throw E("est/empty-body", "a 200 csrattrs response carried an empty body (RFC 7030 sec. 4.5.2)");
2383
1647
  var attrs = csrattrsFmt.parse(transferDecode(res.body));
2384
1648
  return { available: true, attrs: attrs, plan: buildEnrollAttributes(attrs) };