@blamejs/pki 0.4.15 → 0.5.0

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 (59) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/MIGRATING.md +2 -2
  3. package/README.md +142 -137
  4. package/index.js +4 -0
  5. package/lib/acme.js +73 -1
  6. package/lib/asn1-der.js +2 -0
  7. package/lib/attrcert-sign.js +4 -0
  8. package/lib/cbor-det.js +32 -16
  9. package/lib/cmc-build.js +880 -0
  10. package/lib/cmc-verify.js +657 -0
  11. package/lib/cmp-build.js +8 -7
  12. package/lib/cmp-verify.js +11 -1
  13. package/lib/cms-sign.js +170 -8
  14. package/lib/cms-verify.js +80 -14
  15. package/lib/crl-sign.js +22 -0
  16. package/lib/crmf-sign.js +5 -2
  17. package/lib/csr-sign.js +3 -0
  18. package/lib/ct.js +72 -0
  19. package/lib/est.js +828 -32
  20. package/lib/framework-error.js +13 -0
  21. package/lib/guard-bytes.js +37 -1
  22. package/lib/guard-range.js +23 -1
  23. package/lib/http-transport.js +9 -3
  24. package/lib/inspect.js +28 -5
  25. package/lib/jose.js +15 -0
  26. package/lib/lint.js +4 -0
  27. package/lib/merkle.js +5 -5
  28. package/lib/ocsp.js +139 -11
  29. package/lib/oid.js +69 -1
  30. package/lib/path-validate.js +27 -4
  31. package/lib/pkcs12-build.js +12 -0
  32. package/lib/schema-all.js +19 -1
  33. package/lib/schema-attrcert.js +27 -0
  34. package/lib/schema-c509.js +6 -0
  35. package/lib/schema-cmc.js +791 -0
  36. package/lib/schema-cmp.js +25 -0
  37. package/lib/schema-cms.js +17 -1
  38. package/lib/schema-crl.js +23 -1
  39. package/lib/schema-crmf.js +13 -0
  40. package/lib/schema-csr.js +11 -0
  41. package/lib/schema-csrattrs.js +6 -0
  42. package/lib/schema-engine.js +6 -2
  43. package/lib/schema-ocsp.js +41 -0
  44. package/lib/schema-pkcs12.js +16 -0
  45. package/lib/schema-pkcs8.js +8 -0
  46. package/lib/schema-smime.js +4 -4
  47. package/lib/schema-tsp.js +32 -1
  48. package/lib/schema-x509.js +14 -1
  49. package/lib/shbs.js +12 -4
  50. package/lib/sigstore.js +4 -0
  51. package/lib/smime.js +28 -7
  52. package/lib/tls-cert-compress.js +15 -3
  53. package/lib/trust.js +27 -4
  54. package/lib/tsp-sign.js +41 -6
  55. package/lib/vendor/README.md +19 -19
  56. package/lib/webauthn.js +895 -26
  57. package/lib/x509-sign.js +3 -0
  58. package/package.json +1 -1
  59. package/sbom.cdx.json +6 -6
package/lib/est.js CHANGED
@@ -40,9 +40,12 @@
40
40
  * certificates are in any order", RFC 5272 sec. 4.1) -- `findIssuedCert` picks
41
41
  * the issued certificate by a public-key match, never a positional guess. The
42
42
  * serverkeygen encrypted-key part's EnvelopedData is surfaced structurally
43
- * (ciphertext raw, decryption external). /fullcmc is recognized and rejected
44
- * with a precise `est/fullcmc-not-supported` (deferred to the CMC format
45
- * module). DER-only where DER, fail-closed everywhere.
43
+ * (ciphertext raw, decryption external). A /fullcmc response is CLASSIFIED:
44
+ * a 200 may carry either arm RFC 7030 sec. 4.3.2 permits (`certs-only` or
45
+ * `CMC-response`), and a 404 or 501 is the distinct `not-implemented`
46
+ * verdict -- this service absent, rather than a transport fault. Reading the
47
+ * CMC message itself is the CMC module's job. DER-only where DER,
48
+ * fail-closed everywhere.
46
49
  *
47
50
  * @card
48
51
  * EST (RFC 7030 / 8951 / 9908) client -- the cacerts / simpleenroll / simplereenroll
@@ -60,6 +63,12 @@ var pkcs8 = require("./schema-pkcs8");
60
63
  var key = require("./key");
61
64
  var csr = require("./schema-csr");
62
65
  var csrattrsFmt = require("./schema-csrattrs"); // aliased: the `csrattrs` name is the verb + the public export
66
+ var cmcVerify = require("./cmc-verify"); // /fullcmc interprets its response through the CMC layer
67
+ var cmcFmt = require("./schema-cmc"); // to read back which keys the submitted request asked to certify
68
+ var OID_CMC_TRANSACTION_ID = oid.byName("id-cmc-transactionId");
69
+ var OID_CMC_SENDER_NONCE = oid.byName("id-cmc-senderNonce");
70
+ var OID_CMC_DATA_RETURN = oid.byName("id-cmc-dataReturn");
71
+ var crmfFmt = require("./schema-crmf");
63
72
  var frameworkError = require("./framework-error");
64
73
  var guard = require("./guard-all");
65
74
  var httpTransport = require("./http-transport");
@@ -94,6 +103,7 @@ var OPERATIONS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serve
94
103
  * decoded DER against `DER_MAX_BYTES` (`est/too-large`).
95
104
  *
96
105
  * @example
106
+ * var der = pki.asn1.build.sequence([pki.asn1.build.integer(1n)]);
97
107
  * var roundTripped = pki.est.transferDecode(pki.est.transferEncode(der));
98
108
  */
99
109
  function transferDecode(body) {
@@ -128,6 +138,7 @@ function transferDecode(body) {
128
138
  * (senders need not insert whitespace, RFC 8951 sec. 3.1).
129
139
  *
130
140
  * @example
141
+ * var der = pki.asn1.build.sequence([pki.asn1.build.integer(1n)]);
131
142
  * var body = pki.est.transferEncode(der);
132
143
  */
133
144
  function transferEncode(der) {
@@ -144,8 +155,14 @@ function transferEncode(der) {
144
155
  function _multipartBoundary(contentType) {
145
156
  var ct = String(contentType || "");
146
157
  if (!/^multipart\/mixed\s*(;|$)/i.test(ct)) return null;
147
- var m = /;\s*boundary\s*=\s*("([^"]+)"|([^;\s]+))/i.exec(ct);
148
- return m ? (m[2] !== undefined ? m[2] : m[3]) : null;
158
+ // Two boundaries name two different splits of the same body; there is no reading
159
+ // of that header, so it is refused rather than resolved by taking the first.
160
+ var bp = _ctParam(ct, "boundary");
161
+ if (bp.duplicated) {
162
+ throw E("est/bad-multipart",
163
+ "the multipart Content-Type declares more than one boundary, so where the parts begin is ambiguous (RFC 2045 sec. 5.1)");
164
+ }
165
+ return bp.value;
149
166
  }
150
167
 
151
168
  // Split a multipart/mixed body into its parts, each { headers, contentType,
@@ -211,6 +228,16 @@ function splitMultipartMixed(body, contentType) {
211
228
  * `EstError` (`est/not-certs-only`, `est/no-certificates`).
212
229
  *
213
230
  * @example
231
+ * var b = pki.asn1.build;
232
+ * var pair = await pki.key.generate("Ed25519");
233
+ * var certDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(pair.publicKey),
234
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
235
+ * { key: await pki.key.export(pair.privateKey) });
236
+ * // the certs-only Simple PKI Response shape (RFC 7030 sec. 4.1.3): a SignedData
237
+ * // v1 over id-data with no eContent, the certificates, and an EMPTY signerInfos
238
+ * var caCertsDer = b.sequence([b.oid("1.2.840.113549.1.7.2"), b.explicit(0, b.sequence([
239
+ * b.integer(1n), b.set([]), b.sequence([b.oid("1.2.840.113549.1.7.1")]),
240
+ * b.contextConstructed(0, certDer), b.set([])]))]);
214
241
  * var r = pki.est.parseCertsOnly(caCertsDer);
215
242
  * r.certificates; // -> [Buffer, ...] raw, unordered
216
243
  */
@@ -333,20 +360,39 @@ function _splitContentTypeParams(s) {
333
360
  // like application/pkcs8evil yields that literal, not application/pkcs8), and
334
361
  // parameters are read token-by-token honoring quoted-strings so a smime-type-like
335
362
  // substring inside another quoted parameter value is NOT taken as smime-type.
336
- function _partMediaType(contentType) {
363
+ // A named Content-Type parameter, plus whether the header declared it MORE THAN
364
+ // ONCE. RFC 2045 sec. 5.1 gives a parameter at most one value, so a header stating
365
+ // two is ambiguous -- and taking the first would let the position of a duplicate,
366
+ // not the sender, decide how the body is read. `smime-type` selects which response
367
+ // arm this is and `boundary` decides where the parts begin, so in both cases the
368
+ // choice is the whole answer. Reported rather than thrown here: each boundary
369
+ // refuses in its own terms, and the error path must not let a bad label displace
370
+ // the HTTP fault it arrived with.
371
+ function _ctParam(contentType, name) {
337
372
  var segs = _splitContentTypeParams(String(contentType || ""));
338
- var mediaMatch = /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i.exec(segs[0]);
339
- var smimeType = null;
373
+ var value = null, count = 0;
340
374
  for (var i = 1; i < segs.length; i++) {
341
375
  var eq = segs[i].indexOf("=");
342
376
  if (eq === -1) continue;
343
- if (segs[i].slice(0, eq).trim().toLowerCase() !== "smime-type") continue;
377
+ if (segs[i].slice(0, eq).trim().toLowerCase() !== name) continue;
378
+ count++;
379
+ if (count > 1) continue;
344
380
  var val = segs[i].slice(eq + 1).trim();
345
381
  if (val.length >= 2 && val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') val = val.slice(1, -1);
346
- smimeType = val.toLowerCase();
347
- break;
382
+ value = val;
348
383
  }
349
- return { media: mediaMatch ? mediaMatch[1].toLowerCase() : null, smimeType: smimeType };
384
+ return { value: value, duplicated: count > 1 };
385
+ }
386
+
387
+ function _partMediaType(contentType) {
388
+ var segs = _splitContentTypeParams(String(contentType || ""));
389
+ var mediaMatch = /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i.exec(segs[0]);
390
+ var st = _ctParam(contentType, "smime-type");
391
+ return {
392
+ media: mediaMatch ? mediaMatch[1].toLowerCase() : null,
393
+ smimeType: st.value === null ? null : st.value.toLowerCase(),
394
+ ambiguous: st.duplicated,
395
+ };
350
396
  }
351
397
 
352
398
  function parseServerKeygenResponse(body, contentType, opts) {
@@ -356,6 +402,13 @@ function parseServerKeygenResponse(body, contentType, opts) {
356
402
  var keyPart = null, certPart = null, encrypted = false;
357
403
  for (var i = 0; i < parts.length; i++) {
358
404
  var pt = _partMediaType(parts[i].contentType);
405
+ // Same rule on a PART header: smime-type tells the encrypted key part from the
406
+ // cleartext one and from the certificate part, so two of them leave which part
407
+ // this is undecided.
408
+ if (pt.ambiguous) {
409
+ throw E("est/bad-multipart",
410
+ "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)");
411
+ }
359
412
  if (pt.media === "application/pkcs8") { keyPart = parts[i]; encrypted = false; }
360
413
  else if (pt.media === "application/pkcs7-mime" && pt.smimeType === "server-generated-key") { keyPart = parts[i]; encrypted = true; }
361
414
  // The certificate part exactly matches the /simpleenroll response
@@ -412,19 +465,37 @@ function parseServerKeygenResponse(body, contentType, opts) {
412
465
  // PKI message type (CMC-response, ...) is not accepted; cacerts mandates only the
413
466
  // media type (sec. 4.1.3). Matched token-wise, not by prefix, so a look-alike like
414
467
  // "application/pkcs7-mimeevil" is rejected.
468
+ // smimeTypes is a LIST for every row that constrains it, rather than a scalar on some
469
+ // rows and a list on others: /fullcmc legitimately answers with either arm, and one
470
+ // shape for the field keeps the comparison below single-branch.
415
471
  var CONTENT_TYPE_BY_OP = {
416
472
  cacerts: { media: "application/pkcs7-mime" },
417
- simpleenroll: { media: "application/pkcs7-mime", smimeType: "certs-only" },
418
- simplereenroll: { media: "application/pkcs7-mime", smimeType: "certs-only" },
473
+ simpleenroll: { media: "application/pkcs7-mime", smimeTypes: ["certs-only"] },
474
+ simplereenroll: { media: "application/pkcs7-mime", smimeTypes: ["certs-only"] },
475
+ // RFC 7030 sec. 4.3.2: a 200 /fullcmc response is application/pkcs7-mime whose
476
+ // smime-type is EITHER certs-only (the CA answered with a Simple PKI Response) or
477
+ // CMC-response (a Full PKI Response). Both are conforming and the CA chooses.
478
+ // Spelled lowercase here and compared case-insensitively: RFC 5273 sec. 3 prose
479
+ // writes "CMC-Request"/"CMC-Response" while its own Table 1 and RFC 7030 write
480
+ // "CMC-request"/"CMC-response", so a case-sensitive match would reject a
481
+ // conforming server depending on which sentence its author read.
482
+ fullcmc: { media: "application/pkcs7-mime", smimeTypes: ["certs-only", "cmc-response"] },
419
483
  serverkeygen: { media: "multipart/mixed" },
420
484
  csrattrs: { media: "application/csrattrs" },
421
485
  };
422
486
 
423
- // The operations whose responses this client classifies. /fullcmc is a real EST
424
- // path (paths() emits its URL) but its CMC response is deferred to the CMC
425
- // module, so classifying one is an explicit unsupported-operation fault rather
426
- // than a silently-accepted 200 whose content-type went unchecked.
427
- var CLASSIFIABLE_OPS = ["cacerts", "simpleenroll", "simplereenroll", "serverkeygen", "csrattrs"];
487
+ // The operations whose responses this client classifies.
488
+ var CLASSIFIABLE_OPS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serverkeygen", "csrattrs"];
489
+
490
+ // RFC 7030 sec. 4.3.2: on /fullcmc a 404 OR a 501 means "this service is not
491
+ // implemented" -- a distinct, non-error verdict the caller can act on.
492
+ //
493
+ // That makes THREE meanings for 404 across this classifier, so they are listed
494
+ // together rather than as branches accumulated one release at a time:
495
+ // /csrattrs 404 -> none-available (sec. 4.5.2)
496
+ // /fullcmc 404 -> not-implemented (sec. 4.3.2), and 501 likewise
497
+ // every other op -> est/http-error
498
+ var NOT_IMPLEMENTED_OPS = { fullcmc: 1 };
428
499
 
429
500
  /**
430
501
  * @primitive pki.est.classifyResponse
@@ -455,20 +526,40 @@ function classifyResponse(status, headers, body, opts) {
455
526
  opts = opts || {};
456
527
  var op = opts.op;
457
528
  // Fail closed on an operation whose response this client cannot validate:
458
- // /fullcmc is recognized-but-deferred, and any other named op is a typo. An
529
+ // A named op this client cannot validate is a typo, not a pass. An
459
530
  // absent op is the caller opting out of the content-type gate (generic
460
531
  // status handling) and stays permissive.
461
- if (op === "fullcmc") throw E("est/fullcmc-not-supported", "the /fullcmc operation is recognized but its CMC response is not supported by this client (RFC 7030 sec. 4.3)");
462
532
  if (op !== undefined && op !== null && CLASSIFIABLE_OPS.indexOf(op) === -1) throw E("est/unsupported-operation", "unrecognized EST operation " + JSON.stringify(op));
463
- var h = {};
464
- Object.keys(headers || {}).forEach(function (k) { h[k.toLowerCase()] = headers[k]; });
533
+ // Normalized ONCE, through the reader that refuses a field carried under two
534
+ // spellings. Folding the map with last-value-wins here while another stage reads
535
+ // the exact key is how the two could pick different values out of the same
536
+ // response -- and the whole point of this function is to decide what the body is.
537
+ // Content-Type, Retry-After and Location are SINGLETON fields: each carries one
538
+ // value that decides one thing -- how to read the body, how long to wait, where to
539
+ // go -- so two of them is an ambiguity, refused rather than resolved by order.
540
+ // WWW-Authenticate is a LIST field (RFC 9110 sec. 11.6.1: a challenge per element),
541
+ // where repetition is the field working as defined; its values are combined the way
542
+ // sec. 5.3 permits a recipient to combine list-field lines, so every challenge the
543
+ // server offered reaches the scheme scan.
544
+ var h = {
545
+ "content-type": _ciHeader(headers, "content-type"),
546
+ "retry-after": _ciHeader(headers, "retry-after"),
547
+ "location": _ciHeader(headers, "location"),
548
+ "www-authenticate": _ciHeaderList(headers, "www-authenticate"),
549
+ };
465
550
  if (status === 200) {
466
551
  var spec = CONTENT_TYPE_BY_OP[op];
467
552
  var ct = h["content-type"] || "";
468
553
  if (spec) {
469
554
  var pt = _partMediaType(ct);
470
- if (pt.media !== spec.media || (spec.smimeType && pt.smimeType !== spec.smimeType)) {
471
- throw E("est/bad-content-type", "a 200 " + op + " response must carry content-type " + spec.media + (spec.smimeType ? "; smime-type=" + spec.smimeType : "") + ", got " + JSON.stringify(ct));
555
+ // _partMediaType already lowercases smimeType, and the table's entries are
556
+ // lowercase, so this comparison is case-insensitive on both sides.
557
+ // A duplicated smime-type is not a value the whitelist can be applied to:
558
+ // one of the two might be on it while the other is not, and matching the
559
+ // first would pass a header that also declares something else.
560
+ var smimeOk = !pt.ambiguous && (!spec.smimeTypes || spec.smimeTypes.indexOf(pt.smimeType) !== -1);
561
+ if (pt.media !== spec.media || !smimeOk) {
562
+ 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));
472
563
  }
473
564
  }
474
565
  return { status: "ok", contentType: ct };
@@ -483,8 +574,12 @@ function classifyResponse(status, headers, body, opts) {
483
574
  var parsed = retryAfter.parse(raStr, { now: opts.now, E: E, code: "est/bad-retry-after" });
484
575
  return { status: "retry", retryAfter: raStr, retryAfterSeconds: parsed.retryAfterSeconds, retryAfterDate: parsed.retryAfterDate };
485
576
  }
577
+ // 501 is meaningful ONLY as the /fullcmc not-implemented signal; on every other
578
+ // operation it stays an ordinary 5xx and falls through to est/http-error below.
579
+ if (status === 501 && NOT_IMPLEMENTED_OPS[op]) return { status: "not-implemented", httpStatus: status };
486
580
  if (status === 204 || status === 404) {
487
581
  if (op === "csrattrs") return { status: "none-available" };
582
+ if (status === 404 && NOT_IMPLEMENTED_OPS[op]) return { status: "not-implemented", httpStatus: status };
488
583
  throw E("est/http-error", "HTTP " + status + " is not a valid " + op + " response");
489
584
  }
490
585
  if (status >= 300 && status < 400) return { status: "redirect", location: h["location"] || null };
@@ -808,8 +903,17 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
808
903
  // The transport contract returns lowercased headers, but the injectable seam only promises
809
904
  // { status, headers, body }; normalize here so a redirect Location / WWW-Authenticate from an
810
905
  // injected transport using ordinary HTTP casing is read correctly (never missed as absent).
811
- var h = {};
812
- Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
906
+ //
907
+ // Through the SAME accessors classifyResponse uses, not a fold of its own: a
908
+ // last-value-wins fold keeps one line of a field that arrived under two
909
+ // spellings, so a usable challenge sent before an unusable one would vanish
910
+ // and the exchange would be refused for offering nothing this client speaks.
911
+ // Location is a singleton, so a second one is an ambiguity and refused;
912
+ // WWW-Authenticate is a list, so every challenge is kept.
913
+ var h = {
914
+ location: _ciHeader(res.headers, "location"),
915
+ "www-authenticate": _ciHeaderList(res.headers, "www-authenticate"),
916
+ };
813
917
  var status = res.status;
814
918
  if (status >= 300 && status < 400) {
815
919
  if (redirects >= budgets.maxRedirects) throw E("est/too-many-redirects", "the redirect chain exceeded maxRedirects=" + budgets.maxRedirects + " (RFC 7030 sec. 3.2.1)");
@@ -960,6 +1064,662 @@ function _certsResult(op, res, opts, csrSpki) {
960
1064
  return { certificate: issued, chain: chain, certificates: parsed.certificates };
961
1065
  }
962
1066
 
1067
+ // ---- /fullcmc (RFC 7030 sec. 4.3) ---------------------------------------
1068
+
1069
+ /**
1070
+ * @primitive pki.est.fullcmc
1071
+ * @signature pki.est.fullcmc(baseUrl, request, opts?) -> Promise<verdict | { retry, retryAfterSeconds }>
1072
+ * @since 0.4.16
1073
+ * @status experimental
1074
+ * @spec RFC 7030, RFC 8951, RFC 5273, RFC 5272
1075
+ * @related pki.cmc.build, pki.cmc.verify, pki.est.simpleenroll
1076
+ *
1077
+ * Enroll through the full CMC message layer: POST a Full PKI Request (from `pki.cmc.build`, as a
1078
+ * DER Buffer or a PEM `CMS` block) to `<baseUrl>/.well-known/est/fullcmc` as
1079
+ * `application/pkcs7-mime; smime-type=CMC-request`, base64 per RFC 8951, over the shared
1080
+ * `pki.transport`.
1081
+ *
1082
+ * A 200 answers with EITHER `smime-type=certs-only` (a Simple PKI Response) or
1083
+ * `smime-type=CMC-response` (a Full PKI Response) -- RFC 7030 sec. 4.3.2 names both, and the label
1084
+ * must agree with the bytes. Either way the result is the `pki.cmc.verify` verdict shape, so a
1085
+ * caller reads one `outcome` (`issued` / `pending` / `confirm-required` / `pop-required` /
1086
+ * `rejected`) regardless of which arm the server chose. Pass `transactionId` / `senderNonce` /
1087
+ * `dataReturn` to have the exchange bound to the request that was sent.
1088
+ *
1089
+ * A 404 **or** a 501 is the distinct `est/not-implemented` verdict -- support for this verb is
1090
+ * OPTIONAL on both sides (sec. 4.3). A 202 surfaces its Retry-After rather than sleeping. A
1091
+ * rejection carries a CMC response (sec. 4.3.2 makes it a MUST), which is decoded and attached to
1092
+ * a typed `est/cmc-failed` as `err.cmc` and `err.httpStatus` -- but a body that cannot be read
1093
+ * never masks the HTTP fault it arrived with.
1094
+ *
1095
+ * On the `certs-only` arm the issued certificates are identified by PUBLIC-KEY MATCH against the
1096
+ * requests that were submitted -- the only identification RFC 5272 sec. 4.1 sanctions, since "the
1097
+ * certificates are in any order" -- and EVERY certification request in the message must be answered
1098
+ * before the exchange reads as `issued` -- a key wanted by N requests needs N certificates, so a
1099
+ * bag that answers only some of them, or none, is a refusal rather than a partial success. That arm
1100
+ * carries no controls, so it cannot echo a Transaction Identifier, Sender Nonce or Data Return: a
1101
+ * request that sent those asked for replay binding it cannot provide -- the key match is not one,
1102
+ * since an old response for the same key still matches -- and it is refused as
1103
+ * `est/unbound-response` rather than accepted with silently none of what was asked for. They are
1104
+ * surfaced as `issuedCertificates` (with `certificate` the first), distinct from `certificates`,
1105
+ * which is the whole returned bag including any chain. Where the requested keys are distinct that
1106
+ * list is in request order; where several requests deliberately SHARE one key it is not, and does
1107
+ * not claim to be -- the public key is the only identification sec. 4.1 sanctions, so when it is
1108
+ * shared nothing in the response says which of those requests a given certificate answers. That arm reports `signatureVerified: false`: a certs-only body is
1109
+ * a degenerate SignedData with no signers by definition, so its security rests on the authenticated
1110
+ * TLS channel, not on a signature.
1111
+ *
1112
+ * Every EST transport gate holds unchanged, including on a bootstrap enrollment: https-only, an
1113
+ * explicit trust anchor required, redirect and size bounds. A Publish Trust Anchors control in the
1114
+ * response is SURFACED, never acted on (RFC 5272 sec. 6.15 makes accepting one a manual decision).
1115
+ *
1116
+ * @opts
1117
+ * - `transport` / `tls` / `label` / `timeout` / `maxResponseBytes` / `maxRedirects` / `now` -- as pki.est.cacerts.
1118
+ * - `transactionId` / `senderNonce` / `dataReturn` -- what the request sent, for the exchange binding.
1119
+ * - `responderCerts` -- EXTRA certificates for CMC signer lookup, for a response that does not carry
1120
+ * its own signer; the certificates the response carries are searched either way. The carrier's
1121
+ * signature MUST be verified (RFC 5272 sec. 3.2.1.3.4), so a `CMC-response` whose signer is found
1122
+ * nowhere and which does not name the opt-out below is refused.
1123
+ * - `responseRecipient` -- key material for a response carried in AuthenticatedData, in the shape
1124
+ * `pki.cms.decrypt` takes. Its MAC is then checked and the verdict reports
1125
+ * `signatureVerified: true`, rather than the carrier being reachable only unauthenticated.
1126
+ * - `allowUnverifiedResponse` -- accept a `CMC-response` whose signer certificate cannot be found,
1127
+ * without checking its signature; the verdict then reports `signatureVerified: false`. For an
1128
+ * unauthenticated bootstrap only, and it never excuses a signature that is present and wrong.
1129
+ * - `username` / `password` / `allowCrossOriginRedirect` -- as pki.est.simpleenroll.
1130
+ * @example
1131
+ * var pair = await pki.key.generate("Ed25519");
1132
+ * var key = await pki.key.export(pair.privateKey);
1133
+ * var spki = await pki.key.export(pair.publicKey);
1134
+ * var cert = await pki.x509.sign({ subject: "device.example", subjectPublicKey: spki,
1135
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
1136
+ * var csr = await pki.csr.sign({ subject: "device.example", subjectPublicKey: spki }, { key: key });
1137
+ * var request = await pki.cmc.build({ requests: [{ tcr: csr }] }, { cert: cert, key: key });
1138
+ * // a 202 means the CA queued the request -- the verb surfaces the delay, never sleeps
1139
+ * var r = await pki.est.fullcmc("https://ca.example", request,
1140
+ * { transport: function () { return Promise.resolve({ status: 202, headers: { "retry-after": "60" }, body: "" }); } });
1141
+ * r.retry && r.retryAfterSeconds; // 60
1142
+ */
1143
+ function fullcmc(baseUrl, request, opts) {
1144
+ opts = opts || {};
1145
+ // Everything this exchange MEANS is captured synchronously, before a single
1146
+ // deferred turn: the request bytes (copied), the keys they ask to have
1147
+ // certified, and the state the response will be checked against. Doing it
1148
+ // inside the promise body below would be a turn too late -- a caller that
1149
+ // mutates the buffer or flips allowUnverifiedResponse on the line after this
1150
+ // call has already changed them by then, which is the easiest version of the
1151
+ // race to hit and the one that would skip the signature check.
1152
+ var der, wanted, sent;
1153
+ try {
1154
+ if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw E("est/bad-input", "pki.est.fullcmc options must be an object");
1155
+ der = _cmcRequestDer(request);
1156
+ // Confirm this IS a Full PKI Request before any of it goes over the wire. The
1157
+ // bytes are about to be labelled `smime-type=CMC-request`, so a PKIResponse or
1158
+ // an unparseable blob handed in by mistake would be POSTed to a CA under a
1159
+ // label that does not describe it -- and, since only an `issued` outcome is
1160
+ // correlated, could still come back as a pending or rejected verdict, making
1161
+ // the mistake look like a protocol answer. A caller error belongs at the entry
1162
+ // point, not on the network.
1163
+ wanted = _requestedPublicKeys(der);
1164
+ // The exchange state is READ BACK OUT OF THE REQUEST, not taken on the caller's
1165
+ // word. A value supplied here that the request does not actually carry would
1166
+ // have the response checked against a binding this exchange never sent -- and a
1167
+ // replayed response echoing that value would satisfy it. What the request says
1168
+ // is the only thing that can bind the answer to it.
1169
+ sent = _cmcSent(opts, der);
1170
+ // The TRANSPORT options are pinned here too, not only the binding state. They
1171
+ // decide where this request goes and what trust it goes under -- transport,
1172
+ // tls, credentials, redirect policy.
1173
+ opts = _shallowCopy(opts);
1174
+ // And the request is STARTED here, synchronously, rather than in a deferred
1175
+ // turn. That is what actually closes the window: a copy only reaches as deep
1176
+ // as it is written, so nested config (opts.tls and its anchors) would still be
1177
+ // the caller's to change while the request sat queued. Handing the options to
1178
+ // _client before yielding leaves no turn in which any of them, at any depth,
1179
+ // can differ from what this call was made with.
1180
+ var body = transferEncode(der);
1181
+ return _client("fullcmc", "POST", baseUrl, body,
1182
+ { accept: "application/pkcs7-mime", "content-type": "application/pkcs7-mime; smime-type=CMC-request" }, opts)
1183
+ .then(function (res) { return _fullcmcResult(res, opts, wanted, sent); });
1184
+ } catch (e) {
1185
+ return Promise.reject(e); // the surface stays promise-rejecting, never throwing
1186
+ }
1187
+ }
1188
+
1189
+ /**
1190
+ * Tie a returned certificate bag back to the requests it claims to answer, for
1191
+ * BOTH response arms -- the rule is one rule, so it lives in one place rather than
1192
+ * being restated per arm where one arm can quietly lose it.
1193
+ *
1194
+ * Every certification request the message submitted must be answered before the
1195
+ * exchange reads as an issuance: a bag holding a CA chain, someone else's
1196
+ * certificate, or only some of the requested keys is a partial or unrelated answer,
1197
+ * not a success. Matching is by public key, the only identification RFC 5272
1198
+ * sec. 4.1 sanctions ("the certificates are in any order" -- position means
1199
+ * nothing).
1200
+ *
1201
+ * Each certificate is CONSUMED as it is matched, so two requests that share a
1202
+ * public key -- CSRs for different subjects, say -- need two certificates rather
1203
+ * than matching the same one twice and reporting an unanswered request as answered.
1204
+ */
1205
+ function _correlateIssued(bag, wanted, arm) {
1206
+ if (!wanted.length) {
1207
+ throw E("est/no-issued-cert",
1208
+ "this /fullcmc request declared no certification request whose key a returned certificate could be " +
1209
+ "matched against, so a " + arm + " response cannot be read as an issuance (RFC 5272 sec. 4.1)");
1210
+ }
1211
+ // Counted PER DISTINCT KEY, because "how many certificates should match this
1212
+ // key?" is answered by how many requests asked for it. Two requests may share a
1213
+ // public key deliberately -- different subjects for one key -- and a CA
1214
+ // answering both returns two certificates for it. Rejecting the second as
1215
+ // ambiguous would refuse the complete, correct response; requiring only one
1216
+ // would let a half-answer pass. So the count must agree exactly.
1217
+ var byKey = {};
1218
+ wanted.forEach(function (k) {
1219
+ var hex = k.toString("hex");
1220
+ if (!byKey[hex]) byKey[hex] = { key: k, want: 0, have: null };
1221
+ byKey[hex].want += 1;
1222
+ });
1223
+ Object.keys(byKey).forEach(function (hex) {
1224
+ var e = byKey[hex];
1225
+ e.have = _allMatching(bag || [], e.key);
1226
+ if (e.have.length < e.want) {
1227
+ throw E("est/no-issued-cert",
1228
+ "the /fullcmc " + arm + " response carried " + e.have.length + " certificate(s) for a public key " +
1229
+ e.want + " certification request(s) asked to have certified, so it does not answer this request " +
1230
+ "(RFC 5272 sec. 4.1)");
1231
+ }
1232
+ if (e.have.length > e.want) {
1233
+ throw E("est/ambiguous-issued-cert",
1234
+ "the /fullcmc response carried more certificates matching a submitted request key than there were " +
1235
+ "requests for it; the issued certificate is ambiguous (RFC 5272 sec. 4.1)");
1236
+ }
1237
+ });
1238
+ // Each certificate handed out once. Where the requested keys are distinct this
1239
+ // lands in request order; where several requests share ONE key it cannot, and
1240
+ // does not pretend to -- the public key is the only identification RFC 5272
1241
+ // sec. 4.1 sanctions, so when it is shared nothing in the response says which
1242
+ // of those requests a given certificate answers. What is established either way
1243
+ // is the set: every requested key is answered, and by exactly as many
1244
+ // certificates as asked for it.
1245
+ return wanted.map(function (k) { return byKey[k.toString("hex")].have.shift(); });
1246
+ }
1247
+
1248
+ // Every certificate in the bag whose subject public key is `key`, found through
1249
+ // the same public-key match the single-certificate case uses so the two cannot
1250
+ // disagree about what "matches" means.
1251
+ function _allMatching(bag, key) {
1252
+ var rest = bag.slice(), out = [], hit;
1253
+ while ((hit = findIssuedCert(rest, key)) !== null) {
1254
+ out.push(hit);
1255
+ rest = rest.filter(function (c) { return c !== hit; });
1256
+ }
1257
+ return out;
1258
+ }
1259
+
1260
+ // The public keys the submitted Full PKI Request asked to have certified. A
1261
+ // certs-only answer carries no status and no request reference, so these are the
1262
+ // ONLY thing a returned certificate can be tied back to -- and RFC 5272 sec. 4.1
1263
+ // forbids identifying the issued certificate positionally ("the certificates are
1264
+ // in any order"). Both key-bearing request arms are read; an `orm` arm carries no
1265
+ // key and contributes none, and an arm that will not parse contributes none rather
1266
+ // than failing the exchange -- the caller still gets a refusal downstream, because
1267
+ // an empty set cannot match anything.
1268
+ function _requestedPublicKeys(der) {
1269
+ var out = [], body;
1270
+ try { body = cmcFmt.parse(der); }
1271
+ catch (e) {
1272
+ throw E("est/bad-input",
1273
+ "pki.est.fullcmc requires a Full PKI Request (id-cct-PKIData); this input did not parse as one", e);
1274
+ }
1275
+ if (body.kind !== "pkiData") {
1276
+ throw E("est/bad-input",
1277
+ "pki.est.fullcmc sends a Full PKI Request (id-cct-PKIData); this input is a " + body.kind);
1278
+ }
1279
+ (body.requests || []).forEach(function (r) {
1280
+ // A key-bearing arm whose key cannot be read must NOT quietly contribute
1281
+ // nothing. The CMC parser validates these arms only far enough to find their
1282
+ // identity, so a malformed one reaches here; dropping it would leave the
1283
+ // correlation blind to a request that was still sent, and a response covering
1284
+ // only the readable arms would then read as a complete issuance. Refused at
1285
+ // the entry point, like any other request this verb cannot stand behind.
1286
+ var before = out.length;
1287
+ var keyBearing = !!(r.certificationRequestBytes || r.certReqMsgBytes);
1288
+ try {
1289
+ if (r.certificationRequestBytes) {
1290
+ var spki = csr.parse(r.certificationRequestBytes).subjectPublicKeyInfo;
1291
+ if (spki && Buffer.isBuffer(spki.bytes)) out.push(spki.bytes);
1292
+ if (out.length === before) _unreadableArm();
1293
+ return;
1294
+ }
1295
+ if (r.certReqMsgBytes) {
1296
+ // pki.schema.crmf reads CertReqMessages (SEQUENCE OF CertReqMsg); the CMC
1297
+ // arm holds ONE CertReqMsg, so it is wrapped rather than decoded here --
1298
+ // the CRMF rules stay in the CRMF parser instead of being restated.
1299
+ var msgs = crmfFmt.parse(asn1.build.sequence([r.certReqMsgBytes])).messages;
1300
+ var msg0 = msgs && msgs[0];
1301
+ var tmpl = msg0 && msg0.certReq && msg0.certReq.certTemplate;
1302
+ // RFC 4211 sec. 4.1: the requested key may be absent from the CertTemplate
1303
+ // and carried in the signature POP's POPOSigningKeyInput instead -- a form
1304
+ // this toolkit's own CRMF parser accepts and surfaces. Reading only the
1305
+ // template would refuse a conforming request before it was ever sent.
1306
+ var pk = (tmpl && tmpl.publicKey) ||
1307
+ (msg0 && msg0.popo && msg0.popo.poposkInput && msg0.popo.poposkInput.publicKey);
1308
+ var pkBytes = Buffer.isBuffer(pk) ? pk : (pk && pk.bytes);
1309
+ if (Buffer.isBuffer(pkBytes)) out.push(pkBytes);
1310
+ if (out.length === before) _unreadableArm();
1311
+ }
1312
+ } catch (e) {
1313
+ if (!keyBearing) return; // an orm arm carries no key and is not expected to
1314
+ throw (e && e.code === "est/bad-input") ? e : E("est/bad-input",
1315
+ "a certification request in this Full PKI Request could not be read, so a response could not be " +
1316
+ "tied back to it; pki.est.fullcmc will not send a request it cannot check the answer to", e);
1317
+ }
1318
+ });
1319
+ return out;
1320
+ }
1321
+
1322
+ function _unreadableArm() {
1323
+ throw E("est/bad-input", "a certification request in this Full PKI Request declares no readable public key");
1324
+ }
1325
+
1326
+ // What the caller retained, threaded to pki.cmc.verify -- the exchange binding
1327
+ // AND the carrier-authentication inputs. Built in one place so the success and
1328
+ // rejection arms cannot drift apart about what this exchange is.
1329
+ // Captured SYNCHRONOUSLY, before the request goes out, and with the byte buffers
1330
+ // copied. Everything here is read again only after the transport resolves, so
1331
+ // reading the caller's live options at that point would let an object mutated
1332
+ // during the round trip decide the checks -- and flipping allowUnverifiedResponse
1333
+ // mid-flight would skip the signature check the default posture requires.
1334
+ // pki.cmc.verify freezes its own inputs too; this is the wrapper's half of the
1335
+ // same rule, because by the time it calls verify the damage would already be done.
1336
+ function _cmcSent(opts, der) {
1337
+ var carried = _requestBinding(der);
1338
+ // A caller-supplied value must AGREE with the request. Silently preferring one
1339
+ // over the other would leave the response checked against something the request
1340
+ // may never have carried; refusing the disagreement keeps the two honest.
1341
+ _assertAgrees("transactionId", opts.transactionId, carried.transactionId);
1342
+ _assertAgrees("senderNonce", opts.senderNonce, carried.senderNonce);
1343
+ _assertAgrees("dataReturn", opts.dataReturn, carried.dataReturn);
1344
+ return {
1345
+ transactionId: carried.transactionId,
1346
+ senderNonce: _copyBytes(carried.senderNonce),
1347
+ dataReturn: _copyBytes(carried.dataReturn),
1348
+ bodyPartIDs: carried.bodyPartIDs,
1349
+ // The nested paths travel with the flat identifiers: a status may name a body part inside a
1350
+ // nested message this request carried, and forwarding only the flat set would leave every
1351
+ // such reference unconfirmable and therefore refused.
1352
+ bodyPartPaths: carried.bodyPartPaths,
1353
+ certs: Array.isArray(opts.responderCerts) ? opts.responderCerts.map(_copyBytes) : opts.responderCerts,
1354
+ // The AuthenticatedData carrier's key material. Without this the verb could
1355
+ // reach that carrier only through the unauthenticated opt-out -- the capability
1356
+ // would exist one layer down and be unreachable from the one operators call.
1357
+ recipient: _copyRecipient(opts.responseRecipient),
1358
+ allowUnverified: opts.allowUnverifiedResponse === true,
1359
+ };
1360
+ }
1361
+
1362
+ // EVERY byte form the parsers and crypto routines downstream accept, not just the two most
1363
+ // common: a DataView or a bare ArrayBuffer arrives by the same door as a Buffer, and the values
1364
+ // this copies -- the nonce the response must echo, the certificates that authenticate it, the
1365
+ // recipient key material -- are all read after the transport resolves. Covering only Buffer and
1366
+ // Uint8Array would leave those forms aliased across the request's whole flight, which is the same
1367
+ // window this exists to close, open for exactly the inputs that came in by the wider door.
1368
+ // A DataView is copied over its OWN window, not the whole backing buffer it happens to sit in.
1369
+ function _copyBytes(v) {
1370
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v);
1371
+ if (ArrayBuffer.isView(v)) return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength));
1372
+ if (v instanceof ArrayBuffer) return Buffer.from(new Uint8Array(v));
1373
+ return v;
1374
+ }
1375
+
1376
+ // The exchange-binding controls the request ACTUALLY carries (RFC 5272 sec. 6.6
1377
+ // Transaction Identifier / Sender Nonce, sec. 6.4 Data Return). These are what the
1378
+ // response has to echo, so they are read from the bytes going out rather than from
1379
+ // a parallel claim that could disagree with them.
1380
+ function _requestBinding(der) {
1381
+ var out = { transactionId: undefined, senderNonce: undefined, dataReturn: undefined, bodyPartIDs: undefined,
1382
+ bodyPartPaths: undefined };
1383
+ // Parsed WITHOUT a fallback. The caller reaches this only after the request has
1384
+ // already been confirmed to be a Full PKI Request, so a parse failure here is
1385
+ // impossible rather than expected -- and defaulting to "no binding" if it ever
1386
+ // became possible is the wrong answer anyway: a request whose controls could not
1387
+ // be read would face no echo requirement at all, which is a weaker exchange than
1388
+ // the one being sent. Letting the typed decode error propagate keeps it closed.
1389
+ var body = cmcFmt.parse(der);
1390
+ // Every identifier this request actually carries. A status in the response names
1391
+ // the body parts it is ABOUT, and a status naming a body part that was never
1392
+ // sent is not an answer to this request -- so the client keeps the set it can
1393
+ // check that claim against, the same way it keeps the transaction and nonce.
1394
+ // Collected from all four sequences because RFC 5272 sec. 3.2.1 draws every
1395
+ // identifier from ONE space; a status may legitimately reference a control or a
1396
+ // content info, not only a certification request.
1397
+ var ids = [];
1398
+ var paths = [];
1399
+ [body.requests, body.controls, body.cmsSequence, body.otherMsgs].forEach(function (list) {
1400
+ (list || []).forEach(function (el) {
1401
+ if (el && el.bodyPartID != null) { ids.push(el.bodyPartID); paths.push([el.bodyPartID]); }
1402
+ });
1403
+ });
1404
+ // A cmsSequence element carries a NESTED CMS message, which may itself be a Full PKI
1405
+ // Request (sec. 3.2.1), and a response identifies a body part inside one by the path
1406
+ // [outer, inner...]. This client composed that nested message, so it can confirm such a
1407
+ // path -- but only by reading the message back, which is what this does. A nested message
1408
+ // this parser cannot read back yields no retained path, so a status naming a part inside
1409
+ // it is still refused: what cannot be read cannot be confirmed.
1410
+ _collectNestedPaths(body.cmsSequence, [], paths, 0);
1411
+ out.bodyPartIDs = ids;
1412
+ out.bodyPartPaths = paths;
1413
+ (body.controls || []).forEach(function (c) {
1414
+ var field = c.attrType === OID_CMC_TRANSACTION_ID ? "transactionId"
1415
+ : c.attrType === OID_CMC_SENDER_NONCE ? "senderNonce"
1416
+ : c.attrType === OID_CMC_DATA_RETURN ? "dataReturn" : null;
1417
+ if (!field) return; // not a binding control
1418
+ // Duplicates are ambiguous, and ambiguity here decides what the response must
1419
+ // echo: taking the last would bind the answer to one of two values the request
1420
+ // sent, chosen arbitrarily. pki.cmc.verify already refuses duplicate binding
1421
+ // controls on the response; the request side is held to the same rule rather
1422
+ // than shipping a message whose own binding cannot be read one way.
1423
+ if (out[field] !== undefined) {
1424
+ throw E("est/bad-input",
1425
+ "the Full PKI Request carries more than one " + field + " control, so there is no single value " +
1426
+ "the response can be bound to (RFC 5272 sec. 6.6 / 6.4)");
1427
+ }
1428
+ // A binding control that is PRESENT but unreadable must not decay into
1429
+ // "absent". Doing so would drop the replay check for a request that carries
1430
+ // the control -- the response would then face no echo requirement at all,
1431
+ // which is a weaker exchange than the one being sent. An unreadable security
1432
+ // control is a refusal, not a default.
1433
+ var v = c.values && c.values.length === 1 ? c.values[0] : null;
1434
+ if (!v) {
1435
+ throw E("est/bad-input",
1436
+ "the Full PKI Request's " + field + " control must carry exactly one value (RFC 5272 sec. 6.6 / 6.4)");
1437
+ }
1438
+ try {
1439
+ out[field] = field === "transactionId"
1440
+ ? asn1.read.integer(asn1.decode(v))
1441
+ : asn1.read.octetString(asn1.decode(v));
1442
+ } catch (e) {
1443
+ throw E("est/bad-input",
1444
+ "the Full PKI Request's " + field + " control could not be read, so the response could not be " +
1445
+ "checked against it; a request is not sent under a binding that cannot be enforced", e);
1446
+ }
1447
+ });
1448
+ return out;
1449
+ }
1450
+
1451
+ // Every body-part path reachable inside the nested messages this request carries, so a status
1452
+ // naming one can be confirmed rather than refused outright. Descends only into a nested message
1453
+ // this parser reads back as a PKIData: anything else yields no path, and a status naming a part
1454
+ // inside it stays a refusal, because a reference that cannot be checked is not one to accept.
1455
+ // The depth cap bounds the walk; a request nested deeper than this is composed by nobody in
1456
+ // practice, and stopping simply leaves those paths unretained -- which refuses, not accepts.
1457
+ var NESTED_PATH_DEPTH_CAP = 8;
1458
+ function _collectNestedPaths(list, prefix, out, depth) {
1459
+ if (depth >= NESTED_PATH_DEPTH_CAP) return;
1460
+ (list || []).forEach(function (el) {
1461
+ if (!el || el.bodyPartID == null || !el.contentInfoBytes) return;
1462
+ var inner;
1463
+ // A nested element that cannot be read back as a Full PKI Request contributes no path, by
1464
+ // either route: the read throws, or it succeeds and yields some other content type. Both land
1465
+ // on the same refusal downstream -- a status naming a part inside it finds no retained path --
1466
+ // so neither can widen what the response may report on. Nothing is absorbed here that a
1467
+ // verdict then rests on; the absence of a path IS the fail-closed answer.
1468
+ try { inner = cmcFmt.parse(el.contentInfoBytes); }
1469
+ 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; }
1470
+ if (!inner || inner.kind !== "pkiData") return;
1471
+ var path = prefix.concat([el.bodyPartID]);
1472
+ [inner.requests, inner.controls, inner.cmsSequence, inner.otherMsgs].forEach(function (l) {
1473
+ (l || []).forEach(function (e2) { if (e2 && e2.bodyPartID != null) out.push(path.concat([e2.bodyPartID])); });
1474
+ });
1475
+ _collectNestedPaths(inner.cmsSequence, path, out, depth + 1);
1476
+ });
1477
+ }
1478
+
1479
+ function _assertAgrees(name, supplied, carried) {
1480
+ if (supplied == null) return;
1481
+ var same;
1482
+ if (Buffer.isBuffer(supplied) || supplied instanceof Uint8Array) {
1483
+ same = Buffer.isBuffer(carried) && Buffer.from(supplied).equals(carried);
1484
+ } else {
1485
+ // Through the shared authoring guard, not a bare BigInt(): a `number` above
1486
+ // Number.MAX_SAFE_INTEGER has already lost the digits that distinguish it from
1487
+ // its neighbour, so converting it here would compare a value the caller never
1488
+ // wrote. A transaction identifier is an unbounded INTEGER on the wire; large
1489
+ // ones are passed as a bigint, and this says so rather than rounding.
1490
+ same = carried != null &&
1491
+ guard.range.authoredInteger(supplied, E, "est/bad-input", "opts." + name) === BigInt(carried);
1492
+ }
1493
+ if (!same) {
1494
+ throw E("est/bad-input",
1495
+ "opts." + name + " does not match the " + name + " control the Full PKI Request carries; the " +
1496
+ "response is checked against what was sent, so the two must agree (RFC 5272 sec. 6.6 / 6.4)");
1497
+ }
1498
+ }
1499
+
1500
+ // The recipient descriptor, one level deep with its byte values copied -- key
1501
+ // material is read after the round trip like everything else in this snapshot.
1502
+ function _copyRecipient(r) {
1503
+ if (!r || typeof r !== "object") return r;
1504
+ var out = {}, k;
1505
+ for (k in r) { if (Object.prototype.hasOwnProperty.call(r, k)) out[k] = _copyBytes(r[k]); }
1506
+ return out;
1507
+ }
1508
+
1509
+ // Own enumerable properties, one level, plus a copy of `auth`. The extra level is
1510
+ // not decoration: credentials are the one nested object read AFTER an await --
1511
+ // _drive reads the scheme, the username and password, and the Digest policy only
1512
+ // once a 401 comes back -- so leaving it shared would let an options object
1513
+ // mutated during the round trip change which credentials go out, or turn on a
1514
+ // digest algorithm the request did not start with. Everything else here is either
1515
+ // read before the request leaves or is a value the caller means to own (a
1516
+ // transport function, a tls config).
1517
+ function _shallowCopy(o) {
1518
+ var out = {}, k;
1519
+ for (k in o) { if (Object.prototype.hasOwnProperty.call(o, k)) out[k] = o[k]; }
1520
+ if (out.auth && typeof out.auth === "object") {
1521
+ var a = {}, ak;
1522
+ for (ak in out.auth) { if (Object.prototype.hasOwnProperty.call(out.auth, ak)) a[ak] = out.auth[ak]; }
1523
+ out.auth = a;
1524
+ }
1525
+ return out;
1526
+ }
1527
+
1528
+ // The Full PKI Request bytes: a DER Buffer, or a PEM CMS block. Anything else is
1529
+ // a caller mistake caught before the transport is touched.
1530
+ function _cmcRequestDer(request) {
1531
+ // COPIED, not aliased: these bytes are parsed now (for the requested keys) and
1532
+ // transmitted later, so sharing the caller's buffer would let the two disagree.
1533
+ if (Buffer.isBuffer(request)) return Buffer.from(request);
1534
+ if (request instanceof Uint8Array) return Buffer.from(request);
1535
+ if (typeof request === "string") return cms.pemDecode(request);
1536
+ throw E("est/bad-input", "pki.est.fullcmc requires the Full PKI Request as DER bytes or a PEM CMS block");
1537
+ }
1538
+
1539
+ // The result function for /fullcmc. Deliberately NOT _certsResult: the two differ
1540
+ // on the point RFC 7030 sec. 4.2.3 and sec. 4.3.2 disagree about. On the sibling
1541
+ // enroll verbs a Simple PKI Response on an error only MAY be present; on
1542
+ // /fullcmc "A CMC response with the content-type of application/pkcs7-mime MUST
1543
+ // be included in the response data for any CMC error response", so the CMC
1544
+ // verdict is decoded and surfaced. What must NOT happen is the decode failing and
1545
+ // taking the HTTP fault with it -- an unreadable error body leaves the fault
1546
+ // reported as itself.
1547
+ function _fullcmcResult(res, opts, wanted, sent) {
1548
+ // classifyResponse is the single authority on what an HTTP status means here,
1549
+ // and it THROWS est/http-error on a 4xx/5xx rather than returning a verdict.
1550
+ // So the rejection path runs from its exception: the mandated CMC response is
1551
+ // decoded and, if readable, REPLACES the fault with a richer typed one. If it
1552
+ // cannot be read, the original fault is rethrown untouched -- an unreadable
1553
+ // error body is a worse fault than the status, never a way to lose it.
1554
+ var verdict;
1555
+ try {
1556
+ verdict = classifyResponse(res.status, res.headers, res.body, { op: "fullcmc", now: opts.now });
1557
+ } catch (httpFault) {
1558
+ // ONLY an actual rejection status earns the richer error. classifyResponse also
1559
+ // throws when a NON-error response fails its own validation -- a 200 with a
1560
+ // missing or unrecognized smime-type, a 202 with no Retry-After -- and those
1561
+ // faults are about the response being malformed, not about the CA refusing.
1562
+ // Letting a signed CMC body replace them would let a malformed 200 answer as
1563
+ // though it were a clean rejection, and the validation it failed would never be
1564
+ // reported. The status is read from the response, not from the fault, so a
1565
+ // future fault type cannot quietly opt itself into the upgrade.
1566
+ if (!(res.status >= 400 && res.status <= 599)) throw httpFault;
1567
+ return _tryDecodeCmcFault(res, sent).then(function (cmcErr) { throw cmcErr || httpFault; });
1568
+ }
1569
+
1570
+ if (verdict.status === "not-implemented") {
1571
+ // Takes precedence over any body: the service is absent, so there is no CMC
1572
+ // verdict to read (RFC 7030 sec. 4.3.2 -- 404 or 501).
1573
+ throw E("est/not-implemented",
1574
+ "the EST server does not implement /fullcmc (HTTP " + verdict.httpStatus + "; RFC 7030 sec. 4.3.2)");
1575
+ }
1576
+ if (verdict.status === "retry") {
1577
+ return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
1578
+ }
1579
+ if (verdict.status !== "ok") {
1580
+ throw E("est/http-error",
1581
+ "an EST /fullcmc response must be HTTP 200 or 202 (RFC 7030 sec. 4.3.2), got " + res.status);
1582
+ }
1583
+
1584
+ var bodyLen = Buffer.isBuffer(res.body) ? res.body.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
1585
+ if (bodyLen === 0) throw E("est/empty-body", "a 200 /fullcmc response carried an empty body (RFC 7030 sec. 4.3.2)");
1586
+ var der = transferDecode(res.body);
1587
+
1588
+ // FC5 admits two smime-types, and the LABEL must agree with the BYTES: a
1589
+ // certs-only label over a Full PKI Response (or the reverse) is a server
1590
+ // mislabelling its own body, and accepting either shape under either label
1591
+ // would make the content-type check decorative.
1592
+ // Through the shared case-insensitive accessor: HTTP header names are
1593
+ // case-insensitive (RFC 9110 sec. 5.1), the classifier already reads them that
1594
+ // way, and a second lookup that guessed at two spellings would disagree with it
1595
+ // for a conformant server -- routing a certs-only body down the wrong arm.
1596
+ var pt200 = _partMediaType(_ciHeader(res.headers, "content-type"));
1597
+ // A header declaring BOTH arms selects neither. Taking the first would let the
1598
+ // order of two labels decide which shape the body is read as, on a header whose
1599
+ // whole job here is to say which one it is.
1600
+ if (pt200.ambiguous) {
1601
+ throw E("est/bad-content-type",
1602
+ "the 200 /fullcmc Content-Type declares more than one smime-type, so which response arm this is " +
1603
+ "cannot be told from it (RFC 7030 sec. 4.3.2)");
1604
+ }
1605
+ var smimeType = pt200.smimeType;
1606
+ if (smimeType === "certs-only") {
1607
+ // A certs-only body is a degenerate certificates-only SignedData: it carries no
1608
+ // CMC controls, so it CANNOT echo a Transaction Identifier, Sender Nonce or
1609
+ // Data Return. A client that sent those asked for replay binding, and the
1610
+ // public-key correlation below is not one -- an old response for the same key
1611
+ // still matches. Accepting this arm would silently give none of what was asked
1612
+ // for, so it is refused; a caller who does not need the binding simply does not
1613
+ // send the controls.
1614
+ var unecho = ["transactionId", "senderNonce", "dataReturn"].filter(function (k) {
1615
+ return sent[k] != null;
1616
+ });
1617
+ if (unecho.length) {
1618
+ throw E("est/unbound-response",
1619
+ "the request carried " + unecho.join(" / ") + ", which a certs-only response has no controls to " +
1620
+ "echo, so the replay binding it asked for cannot be checked (RFC 5272 sec. 6.6 / 6.4)");
1621
+ }
1622
+ var certs = parseCertsOnly(der); // throws est/not-certs-only on a Full PKI Response
1623
+ // A certs-only body says nothing about WHICH request it answers -- no status,
1624
+ // no body-part reference. So the issued certificate is identified the one way
1625
+ // RFC 5272 sec. 4.1 sanctions, by public-key match against what was submitted,
1626
+ // exactly as /simpleenroll and /serverkeygen already do. Without this, a bag
1627
+ // holding only a CA chain -- or a certificate for someone else's key -- reads
1628
+ // as a successful issuance for this request.
1629
+ var issuedCerts = _correlateIssued(certs.certificates, wanted, "certs-only");
1630
+ var issued = issuedCerts[0];
1631
+ // `signatureVerified: false` is stated, not omitted. A certs-only body is a
1632
+ // DEGENERATE certificates-only SignedData -- RFC 5652 sec. 5.2 defines it with
1633
+ // an empty signerInfos set -- so there is no signature here to check, and
1634
+ // demanding one would reject every conformant server. What this arm must not
1635
+ // do is leave the field undefined while the cmc-response arm sets it: a caller
1636
+ // reading `verdict.signatureVerified` would then get a different KIND of answer
1637
+ // depending on which arm the server happened to choose. RFC 7030 secures this
1638
+ // arm through the authenticated TLS channel instead, and the certificates stay
1639
+ // `trusted: false` for the caller to run through pki.path.validate.
1640
+ return { outcome: "issued", certificate: issued, issuedCertificates: issuedCerts,
1641
+ certificates: certs.certificates, crls: certs.crls,
1642
+ controls: [], statuses: [], publishTrustAnchors: null, trusted: false,
1643
+ signatureVerified: false };
1644
+ }
1645
+ // smime-type=cmc-response: the Full PKI Response, interpreted into one verdict.
1646
+ // pki.cmc.verify owns the transaction binding and the status reduction; this
1647
+ // verb only supplies what the caller retained.
1648
+ return cmcVerify.verify(der, sent).then(function (verdict) {
1649
+ // An `issued` verdict is a claim about THIS request, so it is held to the same
1650
+ // correlation as the certs-only arm. A status control saying success while the
1651
+ // bag holds a CA chain, someone else's certificate, or nothing at all does not
1652
+ // make an issuance -- and the status is the server's word, whereas the key
1653
+ // match is checkable. Only `issued` is correlated: pending / pop-required /
1654
+ // confirm-required / rejected are not claims that a certificate was issued.
1655
+ if (verdict.outcome !== "issued") return verdict;
1656
+ // A request that asked for no certificate cannot have one correlated to it. An
1657
+ // orm-only Full PKI Request is exactly that: the other-message arm carries no
1658
+ // certification request and no key, and RFC 5272 lets a server answer it
1659
+ // successfully with its result in the cmsSequence or otherMsgSequence rather
1660
+ // than as an issuance. Reading `issued` there as "a certificate must match"
1661
+ // would make this verb unable to carry that exchange at all. The response's own
1662
+ // certificate bag is still surfaced raw, untrusted, for pki.path.validate.
1663
+ //
1664
+ // The certs-only arm keeps the opposite rule, and deliberately: that body is
1665
+ // nothing BUT a claim of certificate issuance, so with nothing to match it
1666
+ // against there is no way to say it answers this request.
1667
+ if (!wanted.length) { verdict.issuedCertificates = []; return verdict; }
1668
+ var issuedCerts = _correlateIssued(verdict.certificates, wanted, "CMC-response");
1669
+ verdict.certificate = issuedCerts[0];
1670
+ verdict.issuedCertificates = issuedCerts;
1671
+ return verdict;
1672
+ });
1673
+ }
1674
+
1675
+ // Decode the CMC response a rejection MUST carry, returning a typed est/cmc-failed
1676
+ // with the verdict attached -- or null when it cannot be read, so the caller
1677
+ // reports the HTTP fault instead of an asn1/* leak from a body that was never
1678
+ // going to parse.
1679
+ function _tryDecodeCmcFault(res, sent) {
1680
+ return Promise.resolve().then(function () {
1681
+ var pt = _partMediaType(_ciHeader(res.headers, "content-type"));
1682
+ if (pt.media !== "application/pkcs7-mime") return null;
1683
+ // A header declaring two smime-types cannot vouch for the body either. This
1684
+ // path returns NULL rather than throwing, so the caller reports the HTTP fault
1685
+ // the response actually carried -- replacing a real server error with a
1686
+ // content-type complaint would hide the thing the operator needs to see.
1687
+ if (pt.ambiguous) return null;
1688
+ // The LABEL must agree with the bytes here too. The success path refuses a
1689
+ // certs-only label over a Full PKI Response and the reverse; accepting any
1690
+ // pkcs7-mime on the error path would make that agreement decorative, and would
1691
+ // read a CMC verdict out of a body the server said was something else. RFC 7030
1692
+ // sec. 4.3.2 makes the rejection body a CMC-response.
1693
+ if (String(pt.smimeType || "").toLowerCase() !== "cmc-response") return null;
1694
+ // The SAME binding the success path applies. An error response that does not
1695
+ // echo this exchange's transaction / nonce is not this request's answer, and
1696
+ // attaching it would let a replayed or unrelated failure be reported as the
1697
+ // verdict for the request just sent. A binding failure makes verify reject,
1698
+ // which lands in the catch below and leaves the HTTP fault reported.
1699
+ return cmcVerify.verify(transferDecode(res.body), sent).then(function (verdict) {
1700
+ // Only an actual REJECTION becomes the richer error. A CMC body inside an
1701
+ // HTTP failure that says the request was issued is the server contradicting
1702
+ // itself, and reporting it as "the server rejected the Full PKI Request:
1703
+ // issued" would hand the caller a successful outcome wrapped in a rejection.
1704
+ // Returning null there leaves the HTTP fault standing, which is the honest
1705
+ // report of what happened. RFC 7030 sec. 4.3.2 makes the body a MUST for a
1706
+ // rejection, which is the case this upgrade exists for.
1707
+ if (verdict.outcome !== "rejected") return null;
1708
+ var e = E("est/cmc-failed",
1709
+ "the EST server rejected the Full PKI Request: " + verdict.outcome +
1710
+ (verdict.failInfo ? " (" + verdict.failInfo + ")" : "") + " [HTTP " + res.status + "]");
1711
+ e.cmc = verdict;
1712
+ e.httpStatus = res.status;
1713
+ return e;
1714
+ });
1715
+ // EVERY way the attempt can fail lands here, not just the verify rejection:
1716
+ // transferDecode throws synchronously on a non-base64 body, which is exactly
1717
+ // the shape FC7a is about. Returning null makes the caller rethrow the ORIGINAL
1718
+ // HTTP fault -- nothing is absorbed into a verdict, and the richer error is
1719
+ // only ever an upgrade, never a substitute that loses the status.
1720
+ }).then(null, function () { return null; });
1721
+ }
1722
+
963
1723
  function _enroll(op, baseUrl, csrInput, opts) {
964
1724
  var csrDer = _csrDer(csrInput);
965
1725
  var spki = csr.parse(csrDer).subjectPublicKeyInfo;
@@ -986,7 +1746,7 @@ function _enroll(op, baseUrl, csrInput, opts) {
986
1746
  * anchor on the next call.
987
1747
  *
988
1748
  * @opts
989
- * - `transport` -- an injected transport(request) -> {status, headers, body}; default pki.transport.https.
1749
+ * - `transport` -- an injected transport(request) -> {status, headers, body, tls}; default pki.transport.https.
990
1750
  * - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity }.
991
1751
  * - `label` -- an OPTIONAL CA label path segment; `timeout` / `maxResponseBytes` / `maxRedirects` -- budgets.
992
1752
  * - `now` -- receipt time (epoch ms) to render a 202 Retry-After HTTP-date as seconds.
@@ -1114,12 +1874,47 @@ function _assertConfidentialCipher(res) {
1114
1874
  // A case-insensitive header lookup: the injectable transport seam only promises { status, headers, body }, so a
1115
1875
  // hostile / non-Node transport may deliver "Content-Type" in any casing -- read it case-insensitively (classifyResponse
1116
1876
  // normalizes internally, but the multipart boundary is read from the header directly here).
1877
+ // A header, read case-insensitively (RFC 9110 sec. 5.1) -- and refused when the map
1878
+ // carries the SAME field under more than one spelling. HTTP header names are
1879
+ // case-insensitive, so `content-type` and `Content-Type` are one field with two
1880
+ // values; a reader that prefers the exact key and a reader that lowercases into a
1881
+ // map keep different ones, and this verb has both. Two stages that disagree about
1882
+ // which response arm a body is can accept a response the header declares twice
1883
+ // over. That is the same ambiguity a repeated Content-Type PARAMETER raises, at the
1884
+ // level of the field itself, and it gets the same answer: refused, not resolved by
1885
+ // which spelling was reached first.
1886
+ // A LIST field, read case-insensitively and combined across every spelling it
1887
+ // arrived under. RFC 9110 sec. 5.3 lets a recipient join multiple lines of a
1888
+ // list-based field into one comma-separated value, and sec. 11.6.1 makes
1889
+ // WWW-Authenticate exactly that -- one challenge per element. Refusing repetition
1890
+ // here would reject a server offering both Digest and Basic, which is not an
1891
+ // ambiguity but the field doing its job.
1892
+ function _ciHeaderList(headers, name) {
1893
+ headers = headers || {};
1894
+ var lname = name.toLowerCase(), keys = Object.keys(headers), parts = [];
1895
+ for (var i = 0; i < keys.length; i++) {
1896
+ if (keys[i].toLowerCase() !== lname) continue;
1897
+ var v = headers[keys[i]];
1898
+ if (v == null) continue;
1899
+ parts.push(Array.isArray(v) ? v.join(", ") : String(v));
1900
+ }
1901
+ return parts.length ? parts.join(", ") : null;
1902
+ }
1903
+
1117
1904
  function _ciHeader(headers, name) {
1118
1905
  headers = headers || {};
1119
- if (headers[name] !== undefined) return headers[name];
1120
- var lname = name.toLowerCase(), keys = Object.keys(headers);
1121
- for (var i = 0; i < keys.length; i++) { if (keys[i].toLowerCase() === lname) return headers[keys[i]]; }
1122
- return null;
1906
+ var lname = name.toLowerCase(), keys = Object.keys(headers), found = null, n = 0;
1907
+ for (var i = 0; i < keys.length; i++) {
1908
+ if (keys[i].toLowerCase() !== lname) continue;
1909
+ n += 1;
1910
+ if (n === 1) found = headers[keys[i]];
1911
+ }
1912
+ if (n > 1) {
1913
+ throw E("est/bad-content-type",
1914
+ "the response carries more than one " + lname + " header field, so what the body is cannot be told " +
1915
+ "from it (RFC 9110 sec. 5.1: field names are case-insensitive)");
1916
+ }
1917
+ return n === 0 ? null : found;
1123
1918
  }
1124
1919
 
1125
1920
  async function _serverkeygenResult(res, opts, derived) {
@@ -1285,4 +2080,5 @@ module.exports = {
1285
2080
  smimeCapabilitiesAttr: smimeCapabilitiesAttr,
1286
2081
  buildEnrollAttributes: buildEnrollAttributes,
1287
2082
  reenrollGuard: reenrollGuard,
2083
+ fullcmc: fullcmc,
1288
2084
  };