@blamejs/pki 0.6.14 → 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 (112) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/lib/acme.js +59 -815
  3. package/lib/asn1-der.js +7 -245
  4. package/lib/attrcert-sign.js +12 -260
  5. package/lib/attrcert-verify.js +1 -7
  6. package/lib/byte-reader.js +0 -8
  7. package/lib/byte-writer.js +0 -11
  8. package/lib/cbor-det.js +0 -54
  9. package/lib/cmc-build.js +15 -357
  10. package/lib/cmc-verify.js +6 -239
  11. package/lib/cmp-build.js +41 -317
  12. package/lib/cmp-session.js +73 -760
  13. package/lib/cmp-verify.js +32 -488
  14. package/lib/cms-compress.js +3 -32
  15. package/lib/cms-decrypt.js +29 -294
  16. package/lib/cms-encrypt.js +14 -234
  17. package/lib/cms-sign.js +29 -324
  18. package/lib/cms-verify.js +9 -500
  19. package/lib/composite-kem.js +2 -102
  20. package/lib/composite-sig.js +1 -77
  21. package/lib/constants.js +0 -249
  22. package/lib/crl-sign.js +9 -237
  23. package/lib/crl-verify.js +1 -10
  24. package/lib/crmf-sign.js +19 -198
  25. package/lib/crmf-verify.js +1 -12
  26. package/lib/csr-sign.js +3 -34
  27. package/lib/csr-verify.js +1 -12
  28. package/lib/ct.js +28 -253
  29. package/lib/edwards-point.js +10 -44
  30. package/lib/est.js +28 -764
  31. package/lib/framework-error.js +4 -182
  32. package/lib/guard-all.js +1 -58
  33. package/lib/guard-async.js +1 -36
  34. package/lib/guard-bytes.js +6 -650
  35. package/lib/guard-compress.js +5 -102
  36. package/lib/guard-crypto.js +1 -40
  37. package/lib/guard-der.js +2 -93
  38. package/lib/guard-encoding.js +2 -60
  39. package/lib/guard-header.js +1 -36
  40. package/lib/guard-identifier.js +7 -531
  41. package/lib/guard-intrinsic.js +1 -150
  42. package/lib/guard-json.js +10 -73
  43. package/lib/guard-limits.js +1 -77
  44. package/lib/guard-list.js +1 -49
  45. package/lib/guard-name.js +3 -209
  46. package/lib/guard-parsed.js +7 -335
  47. package/lib/guard-range.js +1 -71
  48. package/lib/guard-secret.js +1 -102
  49. package/lib/guard-text.js +1 -64
  50. package/lib/guard-time.js +1 -97
  51. package/lib/hpke.js +3 -105
  52. package/lib/http-digest.js +14 -142
  53. package/lib/http-retry-after.js +1 -47
  54. package/lib/http-transport.js +21 -131
  55. package/lib/inspect.js +30 -284
  56. package/lib/ip-utils.js +6 -47
  57. package/lib/jose.js +7 -142
  58. package/lib/key.js +2 -67
  59. package/lib/lint.js +9 -194
  60. package/lib/merkle.js +1 -27
  61. package/lib/mime.js +9 -82
  62. package/lib/ocsp-request-verify.js +1 -14
  63. package/lib/ocsp-verify.js +1 -82
  64. package/lib/ocsp.js +11 -169
  65. package/lib/oid.js +5 -337
  66. package/lib/path-validate.js +103 -1492
  67. package/lib/pbes2.js +2 -89
  68. package/lib/pkcs12-build.js +31 -379
  69. package/lib/pki-build.js +64 -290
  70. package/lib/rc2.js +2 -18
  71. package/lib/rfc3339.js +4 -27
  72. package/lib/scep.js +8 -252
  73. package/lib/schema-all.js +0 -122
  74. package/lib/schema-attrcert.js +4 -146
  75. package/lib/schema-c509.js +153 -802
  76. package/lib/schema-cmc.js +5 -220
  77. package/lib/schema-cmp.js +2 -223
  78. package/lib/schema-cms.js +13 -427
  79. package/lib/schema-crl.js +5 -68
  80. package/lib/schema-crmf.js +6 -167
  81. package/lib/schema-csr.js +1 -54
  82. package/lib/schema-csrattrs.js +2 -87
  83. package/lib/schema-engine.js +0 -152
  84. package/lib/schema-ocsp.js +0 -117
  85. package/lib/schema-pkcs12.js +2 -93
  86. package/lib/schema-pkcs8.js +3 -35
  87. package/lib/schema-pkix.js +24 -655
  88. package/lib/schema-smime.js +0 -54
  89. package/lib/schema-tsp.js +3 -99
  90. package/lib/schema-x509.js +4 -63
  91. package/lib/shbs.js +11 -95
  92. package/lib/sign-scheme.js +2 -97
  93. package/lib/sigstore.js +9 -169
  94. package/lib/sleep.js +2 -8
  95. package/lib/smime.js +15 -408
  96. package/lib/tls-cert-compress.js +0 -62
  97. package/lib/trust.js +19 -185
  98. package/lib/tsp-sign.js +16 -270
  99. package/lib/validator-all.js +1 -30
  100. package/lib/validator-attcert.js +2 -46
  101. package/lib/validator-cose.js +2 -153
  102. package/lib/validator-keydesc.js +1 -37
  103. package/lib/validator-sig.js +1 -46
  104. package/lib/validator-tls.js +1 -88
  105. package/lib/validator-tpm.js +19 -122
  106. package/lib/verify-seam.js +1 -24
  107. package/lib/webauthn-mds.js +3 -400
  108. package/lib/webauthn.js +12 -833
  109. package/lib/webcrypto.js +5 -485
  110. package/lib/x509-sign.js +4 -108
  111. package/package.json +1 -1
  112. package/sbom.cdx.json +6 -6
@@ -1,22 +1,10 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- the pki.cmp.session implementation. The operator-facing @module pki.cmp home is
4
+ // @internal
6
5
  // lib/cmp-build.js; this file adds the @primitive pki.cmp.session block and re-exports the cmp message
7
- // surface (build / transfer / wellKnownUrl / verify) so the whole pki.cmp namespace wires through here
8
- // (index.js requires this file as `cmp`).
9
- //
10
- // pki.cmp.session -- the stateful CMP enrollment-transaction orchestrator (the pki.acme.client analogue).
11
- // It composes the shipped message layer (build / transfer / verify) into a single enroll(request): mint a
12
- // stable transactionID, build + protect + transfer a request, verify every response's protection before
13
- // reading its body, chain the nonces (recipNonce echoes the peer's senderNonce, a fresh senderNonce per
14
- // request) and the transactionID (RFC 9810 sec. 5.1.1 anti-replay / anti-interleave), interpret the
15
- // CertResponse PKIStatus (grant -> extract the cert, waiting -> a bounded pollReq/pollRep loop, rejection
16
- // -> a terminal verdict), and confirm (certConf -> pkiConf, unless implicitConfirm was granted). The
17
- // single invariant is transfer -> verify (fail-closed) -> only then read the body off the verdict.
18
6
 
19
- var cmp = require("./cmp-verify"); // re-exports build / transfer / wellKnownUrl / verify (the whole message layer)
7
+ var cmp = require("./cmp-verify");
20
8
  var asn1 = require("./asn1-der");
21
9
  var oid = require("./oid");
22
10
  var x509 = require("./schema-x509");
@@ -36,25 +24,17 @@ var retryAfter = require("./http-retry-after");
36
24
 
37
25
  var CmpError = frameworkError.CmpError;
38
26
  function _err(code, message, cause) { return new CmpError(code, message, cause); }
39
- var OID_IMPLICIT_CONFIRM = oid.byName("implicitConfirm"); // classify a granted implicitConfirm by its IMMUTABLE OID
27
+ var OID_IMPLICIT_CONFIRM = oid.byName("implicitConfirm");
40
28
 
41
- // The certification-path build/validate engine, injected by path-validate (the crl/ocsp/cmp-verify seam) so
42
- // the session can validate the issued leaf certificate's signature + chain; x509.parse is structural only.
43
29
  var _engine = null;
44
30
  function setEngine(engine) { _engine = engine; }
45
31
 
46
- // The signature algorithms whose OID conveys no message hash: EdDSA (the hash is the scheme) and the FIPS
47
- // PQC signatures (ML-DSA / SLH-DSA, internally hashed). Only these may take the certConf SHA-256 + explicit
48
- // hashAlg fallback (RFC 9810 sec. 5.3.18); any other indeterminate/non-signature AlgorithmIdentifier (e.g.
49
- // rsaEncryption, an ML-KEM OID) is refused. Resolved to a dotted-OID set at load (never a runtime lookup).
50
32
  var HASHLESS_SIG_OIDS = {};
51
33
  (function () {
52
34
  var names = ["Ed25519", "Ed448", "id-ml-dsa-44", "id-ml-dsa-65", "id-ml-dsa-87"];
53
35
  ["sha2", "shake"].forEach(function (h) { ["128s", "128f", "192s", "192f", "256s", "256f"].forEach(function (s) { names.push("id-slh-dsa-" + h + "-" + s); }); });
54
36
  names.forEach(function (n) { var o = oid.byName(n); if (o) HASHLESS_SIG_OIDS[o] = 1; });
55
37
  })();
56
- // Hash-conveying signature algorithms -> the certConf hash, dispatched by the IMMUTABLE OID (never the mutable
57
- // display name, which pki.oid.register can override). Resolved to a dotted-OID map at load.
58
38
  var SIG_OID_TO_HASH = {};
59
39
  [["sha256WithRSAEncryption", "SHA-256"], ["sha384WithRSAEncryption", "SHA-384"], ["sha512WithRSAEncryption", "SHA-512"],
60
40
  ["ecdsaWithSHA256", "SHA-256"], ["ecdsaWithSHA384", "SHA-384"], ["ecdsaWithSHA512", "SHA-512"]].forEach(function (row) {
@@ -62,20 +42,11 @@ var SIG_OID_TO_HASH = {};
62
42
  });
63
43
  var OID_RSASSA_PSS = oid.byName("rsassaPss");
64
44
  var OID_RSA_ENCRYPTION = oid.byName("rsaEncryption");
65
- // A CRL states the point it speaks for in this extension (RFC 5280 sec. 5.2.5); taken at load, by
66
- // identifier, so a later pki.oid.register cannot rename the extension the scope is read from.
67
45
  var OID_IDP = oid.byName("issuingDistributionPoint");
68
- // The two extensions that decide whether a certificate can act as an authority, taken at load and
69
- // matched by identifier. pki.oid.register replaces an OID's display name, so a test asking whether
70
- // an extension is NAMED basicConstraints reads a present one as absent after a rename, and every
71
- // valid root key update is refused.
72
46
  var OID_BASIC_CONSTRAINTS = oid.byName("basicConstraints");
73
47
  var OID_KEY_USAGE = oid.byName("keyUsage");
74
- // Message-digest OID -> the WebCrypto digest name, for the RSASSA-PSS hashAlgorithm parameter, dispatched by
75
- // the immutable OID (never oid.name, which pki.oid.register can rename). Resolved to a dotted-OID map at load.
76
48
  var HASH_OID_TO_DIGEST = {};
77
49
  [["sha256", "SHA-256"], ["sha384", "SHA-384"], ["sha512", "SHA-512"]].forEach(function (row) { var o = oid.byName(row[0]); if (o) HASH_OID_TO_DIGEST[o] = row[1]; });
78
- // A composite signature's declared prehash (COMPOSITE_ALGS[oid].ph) -> the certConf hashAlg field value.
79
50
  var COMPOSITE_PH_HASHALG = { "SHA-256": "sha256", "SHA-384": "sha384", "SHA-512": "sha512" };
80
51
 
81
52
  var KNOWN_SESSION_OPTS = {
@@ -85,34 +56,15 @@ var KNOWN_SESSION_OPTS = {
85
56
  acceptCert: 1, senderKID: 1, recipKID: 1, expectedSender: 1,
86
57
  };
87
58
 
88
- // The read-only transcript retains at most this many responses' worth of payload bytes (a response is bounded by
89
- // the transport's maxResponseBytes). Bounding it PROPORTIONALLY to the per-response cap keeps the ceiling above
90
- // any single legitimate response while still preventing a polling loop from multiplying a padded-response flood
91
- // across legs into memory exhaustion; a tightened maxResponseBytes tightens the transcript ceiling in step.
92
59
  var TRANSCRIPT_RETAIN_RESPONSES = 2;
93
60
  var DEFAULT_MAX_POLLS = 20;
94
- var DEFAULT_MAX_TOTAL_WAIT = retryAfter.MAX_RETRY_AFTER_SECONDS; // the shared retry-after ceiling (seconds)
95
- var DEFAULT_CERT_REQ_ID = 0; // the CRMF single-request certReqId when the caller supplies none (ir / cr / kur)
96
- var P10CR_CERT_REQ_ID = -1; // a PKCS#10 request has no CRMF id; a conforming cp identifies it with the -1 sentinel (RFC 9483)
97
- var ENROLL_ARMS = { ir: 1, cr: 1, kur: 1, p10cr: 1 }; // the initial request arms enroll() accepts
98
- // The response body arm each enrollment request is answered by (RFC 9810 sec. 5.3.2/5.3.4): ir->ip, cr/p10cr
99
- // ->cp, kur->kup. A response on a different cert-response arm than the request is misrouted, never confirmed.
61
+ var DEFAULT_MAX_TOTAL_WAIT = retryAfter.MAX_RETRY_AFTER_SECONDS;
62
+ var DEFAULT_CERT_REQ_ID = 0;
63
+ var P10CR_CERT_REQ_ID = -1;
64
+ var ENROLL_ARMS = { ir: 1, cr: 1, kur: 1, p10cr: 1 };
100
65
  var RESPONSE_ARM = { ir: "ip", cr: "cp", kur: "kup", p10cr: "cp", rr: "rp", genm: "genp" };
101
- // A pollReq for a non-enrollment operation refers to the WHOLE message rather than to a
102
- // CertResponse element inside it. RFC 9483 sec. 4.4: an entity "sends a poll request with certReqId
103
- // 0 if referring to the CertResponse element contained in the ip/cp/kup message, else -1 to refer
104
- // to the whole message". A revocation or a support message has no CertResponse element, so the
105
- // second half of that sentence is the one that applies and the id is -1. The 0 is not a default for
106
- // polling; it is the id OF an element these operations do not carry.
107
66
  var WHOLE_MESSAGE_CERT_REQ_ID = -1;
108
67
 
109
- // The four support messages RFC 9483 sec. 4.3 defines, keyed by the name a caller asks for.
110
- // `requestOid` and `responseOid` differ only for rootCaCert (sec. 4.3.2), which is the reason
111
- // the pair is a table rather than one field: a response echoing the request OID is a different
112
- // operation's answer, and only the table can say so. `value` is absent for an operation whose
113
- // request carries no infoValue (sec. 4.3.1 and sec. 4.3.3 both say it MUST be absent).
114
- // The table is null-prototype: the key comes from a caller's request object, so a plain literal
115
- // would answer `toString` or `constructor` with an inherited value and dispatch on it.
116
68
  var INFO_OPS = Object.assign(Object.create(null), {
117
69
  caCerts: { name: "caCerts", requestOid: "caCerts", responseOid: "caCerts", value: false, read: "readCaCerts" },
118
70
  rootCaCert: { name: "rootCaCert", requestOid: "rootCaCert", responseOid: "rootCaKeyUpdate", value: "cert", read: "readRootCaKeyUpdate" },
@@ -120,45 +72,9 @@ var INFO_OPS = Object.assign(Object.create(null), {
120
72
  crlUpdate: { name: "crlUpdate", requestOid: "crlStatusList", responseOid: "crls", value: "crlStatus", read: "readCrls" },
121
73
  });
122
74
  var KNOWN_REVOKE_KEYS = { certificate: 1, certDetails: 1, reason: 1 };
123
- // The CRLReason names a revocation may state, read off the same shared table pki.crl.sign and
124
- // pki.cmp.build encode from, so the three cannot drift apart on what a reason is called. Checked at
125
- // this door so a mistyped name reads as the request-shape error it is, before the transport engages.
126
- // A keySpec algId (RFC 9483 sec. 4.3.3, RFC 9480 sec. 2.16) MUST "give an algorithm other than RSA",
127
- // and that is the ONLY constraint the responder is held to: the control names a public-key type an
128
- // entity MAY be asked to generate, one AttributeTypeAndValue per algorithm the CA supports, and the
129
- // entity picks one it can do. So the check refuses an algId ONLY when it names an RSA algorithm (whose
130
- // requirement is stated with rsaKeyLen instead); every other value -- a non-RSA algorithm this toolkit
131
- // knows, or an OID it does not recognize -- is surfaced to the caller. Refusing an unrecognized
132
- // algorithm would break interop with a CA offering one newer than this registry AND drop the other
133
- // algorithms the same keySpec offers, since one bad element fails the whole exchange.
134
- //
135
- // "RSA" is decided by OID FAMILY, not a hand-list. An RSA-DEDICATED arc -- one whose members are RSA --
136
- // is matched by its prefix, so a standardized member this registry has not named is caught with the ones
137
- // it has: PKCS#1 (rsaEncryption, PSS, OAEP, every sha*/md*WithRSAEncryption), the TeleTrusT rsaSignature
138
- // arc (RSA with SHA-1 and the RIPEMD family), the TeleTrusT signatureScheme arc (the ISO/IEC 9796-2 RSA
139
- // signature-with-message-recovery schemes -- deterministic 1.3.36.3.4.2 and randomized .4.3 -- plus ISO/IEC
140
- // 9796-1 on .4.1), and the BSI TR-03110 id-TA-RSA arc (EAC Terminal Authentication, RSA v1.5/PSS over
141
- // SHA-1/256/512). Each prefix is the arc a registered member sits directly under, read from the registry,
142
- // so no dotted-decimal literal appears here and pki.oid.register cannot move it. The strict-descendant
143
- // match skips an arc NODE itself, so a scheme's bare arc node (the 9796 1.3.36.3.4.1/.2/.3 nodes) is named
144
- // off-arc below; the generic signatureScheme PARENT (1.3.36.3.4) and the sibling authentication arc
145
- // (1.3.36.3.5, whose .5.3 children are ECC) are NOT RSA-dedicated, so neither is prefix-matched. The PKCS#1
146
- // arc also carries two auxiliary identifiers that are NOT RSA algorithms --
147
- // mgf1 and pSpecified, the mask-generation and OAEP-label parameters -- so those are excluded from the
148
- // match by name (RSA_ARC_EXCLUDE) and surfaced. RSA also sits off those arcs on ones it SHARES with non-RSA
149
- // algorithms, where no prefix can isolate it, so those are listed by name: id-rsa-kem (S/MIME) and
150
- // id-kem-rsa (ISO 18033-2); the RSASSA-PKCS1-v1_5-with-SHA-3 set on the NIST signature arc, shared with
151
- // ML-DSA and SLH-DSA; RSASSA-PSS-with-SHAKE (RFC 8692) on the PKIX arc; every RSA identifier the OIW SIG
152
- // assigned on the OIW Secsig arc (the md4/md5-WithRSA, rsaSignature, mdc2/sha-WithRSA, rsaKeyTransport,
153
- // and md2/md5/sha1-WithRSASignature identifiers), which is shared with DES, DSA, DH, and the hashes so no
154
- // prefix isolates it; and the X.500 directory RSA OIDs on the 2.5.8 arc -- rsa (2.5.8.1.1), distinct from
155
- // PKCS#1 rsaEncryption, and sqMod-nWithRSA / mdc2WithRSA on the signatureAlgorithm sub-arc; and the
156
- // Chinese GM/T sm3WithRSAEncryption (1.2.156.10197.1.504), an RSA-over-SM3 signature on the SM arc it
157
- // shares with SM2 and SM3. A composite signature naming RSA as one half is a composite key, not an RSA
158
- // one, and lives on the PKIX arc, so it is outside all of these.
159
75
  var RSA_ARCS = ["rsaEncryption", "rsaSignatureWithripemd160", "id-TA-RSA-v1-5-SHA-256",
160
76
  "sigS-ISO9796-1-DFUE", "sigS-ISO9796-2Withrsa", "sigS-ISO9796-2rndWithrsa"].map(function (n) {
161
- return oid.toArcs(oid.byName(n)).slice(0, -1); // the RSA-dedicated arc a registered member sits directly under
77
+ return oid.toArcs(oid.byName(n)).slice(0, -1);
162
78
  });
163
79
  var RSA_OFF_ARC = Object.create(null);
164
80
  ["id-rsa-kem", "id-kem-rsa",
@@ -168,50 +84,25 @@ var RSA_OFF_ARC = Object.create(null);
168
84
  "md4WithRSA", "md5WithRSA", "md4WithRSAEncryption", "rsaSignature", "mdc2WithRSASignature",
169
85
  "shaWithRSAEncryption", "rsaKeyTransport",
170
86
  "md2WithRSASignature", "md5WithRSASignature", "sha1WithRSASignature",
171
- "rsa", // the X.509 directory rsa public-key OID (2.5.8.1.1), distinct from PKCS#1 rsaEncryption
172
- "sqMod-nWithRSA", "mdc2WithRSA", // the X.500 directory signatureAlgorithm RSA OIDs (2.5.8.3.1, 2.5.8.3.100)
173
- "sm3WithRSAEncryption", // the Chinese GM/T RSA-with-SM3 signature (1.2.156.10197.1.504)
174
- // The TeleTrusT signatureScheme (ISO/IEC 9796-2) sub-arcs are prefix-matched in RSA_ARCS, but the strict-
175
- // descendant match skips each arc NODE itself, so the three bare nodes are named here (1.3.36.3.4.1/.2/.3).
87
+ "rsa",
88
+ "sqMod-nWithRSA", "mdc2WithRSA",
89
+ "sm3WithRSAEncryption",
176
90
  "sigS-ISO9796-1", "sigS-ISO9796-2", "sigS-ISO9796-2rnd",
177
91
  ].forEach(function (n) { var d = oid.byName(n); if (d) RSA_OFF_ARC[d] = 1; });
178
- // The PKCS#1 arc carries two identifiers that are NOT RSA algorithms: mgf1 (the mask-generation function
179
- // RSA-PSS and RSA-OAEP parametrize) and pSpecified (the OAEP label source). The prefix match would sweep
180
- // them in, but a keySpec algId naming one states no RSA key requirement -- the entity does not generate an
181
- // "mgf1 key" -- so they are surfaced, not refused, and are excluded from the arc match by name.
182
92
  var RSA_ARC_EXCLUDE = Object.create(null);
183
93
  ["mgf1", "pSpecified"].forEach(function (n) { var d = oid.byName(n); if (d) RSA_ARC_EXCLUDE[d] = 1; });
184
94
 
185
95
  var CRL_REASON_NAMES = Object.create(null);
186
96
  Object.keys(constants.NAMES.CRL_REASON).forEach(function (v) { CRL_REASON_NAMES[constants.NAMES.CRL_REASON[v]] = 1; });
187
97
 
188
- // PKIStatus codes (RFC 9810 sec. 5.2.3): 0 accepted, 1 grantedWithMods, 2 rejection, 3 waiting.
189
98
  function _isGranted(code) { return code === 0 || code === 1; }
190
- // The PKIStatus name surfaced to an acceptance policy so it can distinguish a clean grant from a modified one.
191
99
  var PKI_STATUS_NAMES = { 0: "accepted", 1: "grantedWithMods", 2: "rejection", 3: "waiting" };
192
100
 
193
- // certReqId equality across the encode side (a number or bigint in the request) and the decode side (a BigInt
194
- // off the wire): compare as BigInts so two distinct large ids above 2^53 never collide under Number rounding.
195
101
  function _certReqIdEq(a, b) { return a != null && b != null && BigInt(a) === BigInt(b); }
196
102
 
197
- // Normalize a certificate-list option (the constructor + cmp.verify accept a lone Buffer / PEM OR an array) to
198
- // an array the path engine requires; a copy so a caller's array is never mutated by the caPubs append.
199
103
  function _asCertList(v) { return v == null ? [] : (Array.isArray(v) ? v.slice() : [v]); }
200
104
 
201
- // Append session-supplied pool material (a CA-delivered caPubs, a cached protection-signer chain part-derived
202
- // from the response's unsigned extraCerts) to a `base` pool (the caller's own intermediates) up to the path
203
- // builder's candidate ceiling, deduped against each other AND the base's Buffer entries so a copy of an existing
204
- // candidate never spends a slot. The base is never truncated (if it alone exceeds the ceiling, that is a genuine
205
- // config error path.build reports). So neither a legitimate caPubs nor a meddler's extraCerts flood can push a
206
- // valid caller pool over the ceiling and fail an otherwise-valid grant. Used for both the response signer path
207
- // (cmp.verify) and the issued-leaf path (path.build), the two places session material joins a caller pool.
208
105
  function _boundedPool(base, added) {
209
- // Dedup the base (the priority pool) first: duplicate copies would otherwise inflate its length, drive the
210
- // remaining room to zero, and evict genuinely needed added material even though the distinct candidate count is
211
- // small (path.build dedups internally, so dropping exact duplicates is behavior-preserving). An uncanonicalizable
212
- // entry is kept as-is (path.build judges it) and cannot dedup an added cert. The base holds the priority material
213
- // (a signed response's own delivered issuers + the cached chain, or the grant's caPubs) so it is never truncated;
214
- // the added caller pool fills the remaining room to the candidate ceiling.
215
106
  var ceiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
216
107
  var seen = Object.create(null), distinctBase = [];
217
108
  base.forEach(function (c) { var k = _certIdentity(c); if (k == null) { distinctBase.push(c); return; } if (!seen[k]) { seen[k] = 1; distinctBase.push(c); } });
@@ -221,82 +112,35 @@ function _boundedPool(base, added) {
221
112
  added.forEach(function (c) { var k = _certIdentity(c); if (k != null && seen[k]) return; if (k != null) seen[k] = 1; useful.push(c); });
222
113
  return distinctBase.concat(useful.slice(0, room));
223
114
  }
224
- // A canonical byte identity for a certificate in any form path.build accepts (a DER Buffer, a PEM string, or
225
- // an already-parsed certificate object), so a caller intermediate supplied as PEM or parsed still dedups against
226
- // a byte-identical DER caPubs / cached certificate and does not spend a scarce candidate slot on a copy. The
227
- // identity is the tbsCertificate bytes plus the signature (mirroring cmp.verify's _certKey): a meddler controls
228
- // the unsigned extraCerts ordering, so a corrupted-signature copy sharing a valid issuer's TBS must not collapse
229
- // onto it and evict the valid one. Returns null when the identity cannot be derived; a non-deduped entry is safe
230
- // (a redundant slot), a wrong merge (dropping a distinct or the only valid certificate) is not.
231
- // The identity comes from the same derivation every other certificate door uses: the bytes the
232
- // parser recorded, never the fields of the object handed in. This is a dedupe, not a verdict,
233
- // and no collision attack on it is apparent, since carrying another certificate's exact tbsBytes and
234
- // signature means being that certificate. But "no attack is apparent" is the reasoning that put a
235
- // completeness-only door on nine deciding boundaries, so it is not the reasoning this uses: one
236
- // derivation for certificates, everywhere, and the exceptions have to argue for themselves.
237
- //
238
- // Failure returns null instead of throwing, which is this function's own contract and is why the
239
- // door is wrapped: an underivable identity is a redundant pool slot, while a wrong merge (dropping
240
- // a distinct or the only valid certificate) is not. So a rebuilt entry simply does not dedupe.
241
115
  function _certIdentity(cert) {
242
116
  try {
243
117
  var p = guard.parsed.acceptDerived(cert, "certificate", x509.parse, _err, "cmp/bad-input", "a pool certificate");
244
118
  if (!guard.parsed.isCert(p)) return null;
245
119
  return p.tbsBytes.toString("base64") + "|" + p.signatureValue.bytes.toString("base64");
246
120
  } catch (_e) {
247
- return null; // underivable: kept as its own slot, never merged onto another certificate's
121
+ return null;
248
122
  }
249
123
  }
250
124
 
251
- // A canonical identity for a SubjectPublicKeyInfo: the algorithm OID + the AlgorithmIdentifier parameters +
252
- // the raw subjectPublicKey BIT STRING. The parameters are part of the key identity (an EC curve OID, an
253
- // RSASSA-PSS constraint set) and are kept, with one exception: rsaEncryption, whose parameters are NULL or
254
- // omitted, both naming the same key. So the issued-cert key-match compares keys (with their constraints), not byte
255
- // encodings, and neither rejects an equivalent rsaEncryption re-encoding nor accepts a constraint-changed key.
256
125
  function _spkiKeyIdentity(spkiDer) {
257
- var node = asn1.decode(spkiDer); // SEQUENCE { AlgorithmIdentifier { OID, params? }, BIT STRING }
126
+ var node = asn1.decode(spkiDer);
258
127
  var algId = node.children[0];
259
128
  var algOid = asn1.read.oid(algId.children[0]);
260
129
  var pn = algId.children[1];
261
130
  var params;
262
131
  if (algOid === OID_RSA_ENCRYPTION) {
263
- // rsaEncryption parameters MUST be absent or a NULL (RFC 3279 sec. 2.3.1). Normalize only those two
264
- // equivalent forms to "" so a NULL-vs-omitted re-encoding matches. Any other value (a malformed empty OCTET
265
- // STRING the parser/importer may tolerate, or a changed parameter) keeps its bytes, so a parameter-changed
266
- // certificate a stricter consumer rejects gets a distinct identity and the key-match refuses it.
267
132
  params = (pn == null || _isDerNull(pn)) ? "" : pn.bytes.toString("latin1");
268
133
  } else {
269
- // Every other algorithm's parameters ARE part of the key identity (an EC curve OID, an RSASSA-PSS constraint set).
270
134
  params = pn ? pn.bytes.toString("latin1") : "";
271
135
  }
272
136
  return algOid + "|" + params + "|" + node.children[1].bytes.toString("latin1");
273
137
  }
274
- // A well-formed DER NULL node (universal tag 5, empty content) -- the only rsaEncryption parameter form treated
275
- // as equivalent to an absent parameter.
276
138
  function _isDerNull(pn) { return pn.tagClass === "universal" && pn.tagNumber === 5 && pn.content.length === 0; }
277
139
 
278
- // The bounded, distinct, parseable extraCerts of an already-verified response, cached so a later leg that
279
- // omits extraCerts can rebuild the signer path, mirroring cmp.verify's own extraCerts bounding: dedup, drop
280
- // any non-X.509 entry, cap at MAX_EXTRA_CERTS, and stop after MAX_EXTRA_SCAN entries. So a meddler appending a
281
- // flood of unsigned certs cannot make an otherwise-valid enrollment fail when the cache reaches path.build.
282
140
  var MAX_EXTRA_CERTS = 32, MAX_EXTRA_SCAN = 256;
283
- // The caller intermediates pool is capped below the path-builder candidate ceiling, reserving room for the CA's
284
- // own authenticated material (a response's extraCerts + the cached signer chain, or the grant's caPubs + cache,
285
- // each <= MAX_EXTRA_CERTS). So the authenticated certs and the whole caller pool always fit in one candidate pool:
286
- // no priority attempt has to choose between them, and a chain assembled from both sources validates in one build.
287
- var CAPUBS_MAX = 2 * MAX_EXTRA_CERTS; // the leaf's authenticated issuer material (caPubs), bounded so caPubs + cached chain + the caller pool fit under the ceiling
288
- // The caller intermediates pool is capped below the path-builder ceiling by the room the session's own material
289
- // can occupy. A signature session reserves CAPUBS_MAX (a grant's caPubs, leaf validation) plus MAX_EXTRA_CERTS (the
290
- // response's extraCerts + cached signer chain, the signer-path pool). A MAC session authenticates the response by
291
- // the shared secret and never adds a signer chain (its cache gate is isSig), so it reserves CAPUBS_MAX alone; its
292
- // sole pool is _validateLeaf's caPubs + caller. So every candidate pool the session builds holds the whole caller
293
- // pool and all authenticated material at once: one build, no priority choice, no retry.
294
- var SESSION_MAX_INTERMEDIATES = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX - MAX_EXTRA_CERTS; // signature
295
- var SESSION_MAX_INTERMEDIATES_MAC = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX; // MAC (no signer chain reserved)
296
- // A verify verdict whose failure a DIFFERENT candidate pool or the cached/prebound signer might still recover:
297
- // extraCerts is outside the protected part, so a signer that did not resolve, a wrong-key/wrong-subject decoy
298
- // selected first, or an untrusted chain can all be retried. A transaction-integrity failure (transactionID /
299
- // recipNonce mismatch) is deliberately absent: it is decoy-independent, and re-running would only mask the desync.
141
+ var CAPUBS_MAX = 2 * MAX_EXTRA_CERTS;
142
+ var SESSION_MAX_INTERMEDIATES = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX - MAX_EXTRA_CERTS;
143
+ var SESSION_MAX_INTERMEDIATES_MAC = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX;
300
144
  function _isRecoverableVerify(code) {
301
145
  return code === "cmp/signer-cert-not-found" || code === "cmp/protection-failed" ||
302
146
  code === "cmp/sender-mismatch" || code === "cmp/untrusted-signer";
@@ -312,15 +156,12 @@ function _responseExtraCerts(responseBytes) {
312
156
  if (seen[key]) continue;
313
157
  seen[key] = true;
314
158
  try { x509.parse(c); }
315
- catch (_e) { continue; } // drop a malformed entry, mirroring cmp.verify's bounding
316
- out.push(Buffer.from(c)); // COPY out of the parser's subarray -- retaining the slice would pin the whole multi-MB response buffer if this entry is cached (the caPubs accumulator copies for the same reason)
159
+ catch (_e) { continue; }
160
+ out.push(Buffer.from(c));
317
161
  }
318
162
  return out;
319
163
  }
320
164
 
321
- // Normalize a caller-supplied CRMF certReqId to the value the session echoes + matches, the same way
322
- // crmf-sign._certReqId encodes it: a number or bigint is kept; a string (decimal or 0x-hex) is parsed via
323
- // BigInt so a supported form ("5" / "0x5") is not silently replaced by the default; anything else -> `dflt`.
324
165
  function _normalizeCertReqId(cid, dflt) {
325
166
  if (typeof cid === "bigint" || typeof cid === "number") return cid;
326
167
  if (typeof cid === "string") {
@@ -330,9 +171,6 @@ function _normalizeCertReqId(cid, dflt) {
330
171
  return dflt;
331
172
  }
332
173
 
333
- // The hash inside RSASSA-PSS-params (the hashAlgorithm [0] AlgorithmIdentifier OID) -- for id-RSASSA-PSS the
334
- // digest lives in the parameters, not the OID name (RFC 4055). Returns "SHA-256" / "SHA-384" / "SHA-512", or
335
- // null when the params are absent / unreadable / name an unmapped hash (the caller then declares SHA-256).
336
174
  function _pssDigest(paramsBytes) {
337
175
  if (!Buffer.isBuffer(paramsBytes) || paramsBytes.length === 0) return null;
338
176
  try {
@@ -343,31 +181,18 @@ function _pssDigest(paramsBytes) {
343
181
  if (f.tagClass === "context" && f.tagNumber === 0 && f.children && f.children.length === 1) {
344
182
  var algSeq = f.children[0];
345
183
  if (!algSeq.children || algSeq.children.length < 1) return null;
346
- return HASH_OID_TO_DIGEST[asn1.read.oid(algSeq.children[0])] || null; // by immutable OID, not oid.name
184
+ return HASH_OID_TO_DIGEST[asn1.read.oid(algSeq.children[0])] || null;
347
185
  }
348
186
  }
349
187
  } catch (_e) { /* allow:swallow-unverified a malformed PSS-params blob falls back to null -> the caller's declared-SHA-256 path; a display-hash inference never throws */ return null; }
350
188
  return null;
351
189
  }
352
190
 
353
- // The certConf certHash algorithm (RFC 9810 sec. 5.3.18): use the same hash the certificate signature uses.
354
- // If the signatureAlgorithm OID conveys the hash (sha256WithRSAEncryption / ecdsaWithSHA384 / ...) use that
355
- // hash and omit hashAlg. For id-RSASSA-PSS the hash is carried in the params, so decode it and likewise omit
356
- // hashAlg. Only when the hash is genuinely not conveyed (Ed25519 / Ed448; ML-DSA / SLH-DSA) is SHA-256 used
357
- // and declared in the explicit hashAlg field so the CA recomputes certHash under the same stated hash.
358
191
  function _certConfHash(certDer) {
359
192
  var sa;
360
193
  try { sa = x509.parse(certDer).signatureAlgorithm; }
361
- catch (e) { throw _err("cmp/bad-cert-response", "the issued certificate is unexpectedly unparseable at certConf", e); } // unreachable: _leafOf already validated it; re-throw fail-closed
362
- // Dispatch by the IMMUTABLE signatureAlgorithm OID (never the mutable display name). A hashless SIGNATURE
363
- // (EdDSA / ML-DSA / SLH-DSA) -> SHA-256 + an explicit hashAlg; a hash-conveying signature -> its hash, no
364
- // hashAlg; id-RSASSA-PSS -> the hash from its parameters, no hashAlg.
194
+ catch (e) { throw _err("cmp/bad-cert-response", "the issued certificate is unexpectedly unparseable at certConf", e); }
365
195
  if (HASHLESS_SIG_OIDS[sa.oid]) return { digest: "SHA-256", hashAlg: "sha256" };
366
- // A composite signature (draft-ietf-lamps-pq-composite-sigs): compute certHash under the composite's own
367
- // declared prehash digest (COMPOSITE_ALGS[oid].ph) and declare it in the explicit hashAlg. If the prehash is
368
- // not a certConf-representable hash (SHAKE256, which the CMP CertStatus hashAlg cannot name), fail closed:
369
- // substituting SHA-256 would send a certHash under a hash that contradicts the signature's declared prehash,
370
- // which a conforming CA rejects. Refuse, never confirm under a false algorithm (RFC 9810 sec. 5.3.18).
371
196
  var comp = compositeSig.COMPOSITE_ALGS[sa.oid];
372
197
  if (comp) {
373
198
  var ha = COMPOSITE_PH_HASHALG[comp.ph];
@@ -380,9 +205,6 @@ function _certConfHash(certDer) {
380
205
  if (pd) return { digest: pd, hashAlg: null };
381
206
  throw _err("cmp/bad-cert-response", "the issued RSASSA-PSS certificate's hash cannot be resolved from its parameters (RFC 4055); the certConf hash is indeterminate");
382
207
  }
383
- // Any other AlgorithmIdentifier (an unregistered OID, or a registered non-signature / indeterminate one
384
- // such as rsaEncryption or an ML-KEM OID): the required certConf hash cannot be determined. Do not guess
385
- // SHA-256 (a wrong certHash the CA rejects); fail the transaction closed before confirming.
386
208
  throw _err("cmp/bad-cert-response", "the issued certificate's signature algorithm does not determine a certConf hash (an unrecognized or non-signature algorithm); the transaction is refused (RFC 9810 sec. 5.3.18)");
387
209
  }
388
210
 
@@ -483,59 +305,26 @@ function session(opts) {
483
305
  if (opts == null) opts = {};
484
306
  guard.identifier.assertPlainRecord(opts, _err, "cmp/bad-input", "opts");
485
307
  guard.identifier.assertKnownKeys(opts, KNOWN_SESSION_OPTS, _err, "cmp/bad-input", "unknown session opts field ");
486
- // Work on a SHALLOW COPY so a later normalization (e.g. an empty MAC trustAnchors list) never mutates the
487
- // caller's object -- which may be frozen or reused across sessions -- and normalizing a frozen input does not
488
- // throw a raw TypeError. Only top-level fields are reassigned; nested values are read, never mutated.
489
308
  opts = Object.assign({}, opts);
490
309
  if (typeof opts.url !== "string" || !opts.url) throw _err("cmp/bad-input", "opts.url (the CMP endpoint) is required");
491
310
 
492
- // EXACTLY ONE protection flavor -- signature (key + cert) XOR PBMAC1 (mac), mirroring cmp.build / cmp.verify.
493
311
  var isSig = opts.key != null || opts.cert != null;
494
312
  var isMac = opts.mac != null;
495
313
  if (isSig === isMac) throw _err("cmp/bad-input", "supply EXACTLY ONE protection flavor: { key, cert } (signature) OR { mac } (PBMAC1)");
496
314
  if (isSig && (opts.key == null || opts.cert == null)) throw _err("cmp/bad-input", "signature protection requires BOTH opts.key and opts.cert");
497
- // Signature protection can only be AUTHENTICATED against a trust anchor: without one, a response signer is
498
- // resolved from the message's own (unsigned) extraCerts and can be any self-signed key an attacker on the
499
- // transport supplies. Require trustAnchors up front for the signature flavor. A MAC (PBMAC1) session
500
- // authenticates the RESPONSE by the shared secret, so anchors are OPTIONAL there -- but when supplied they
501
- // still validate the ISSUED certificate's own signature + chain before it is confirmed (the MAC authenticates
502
- // the exchange, not the embedded X.509 signature); they are NOT forwarded to the MAC response verify.
503
315
  if (isSig) {
504
316
  var hasAnchors = opts.trustAnchors != null && !(Array.isArray(opts.trustAnchors) && opts.trustAnchors.length === 0);
505
317
  if (!hasAnchors) throw _err("cmp/bad-input", "signature protection requires opts.trustAnchors to authenticate the CA's response signer (RFC 9483 sec. 3.2)");
506
318
  }
507
- // A MAC session's anchors are OPTIONAL (used only to validate the issued certificate's own chain, never the MAC
508
- // response authentication): an empty list means "no issued-cert chain validation", exactly like omitting it. The
509
- // signature branch above already rejects [] outright. Normalize the MAC empty list to absent so _validateLeaf /
510
- // _verifyOpts treat it uniformly -- otherwise the empty trust store reaches _engine.build and rejects the issued
511
- // certificate only in _finish, AFTER the authenticated grant has consumed the one-shot session. This assigns to
512
- // the shallow COPY made above, never the caller's (possibly frozen) options object.
513
319
  if (isMac && Array.isArray(opts.trustAnchors) && opts.trustAnchors.length === 0) opts.trustAnchors = null;
514
- // Parse + validate each supplied anchor + intermediate NOW (through the SAME path-builder normalization the
515
- // verify + issued-cert validation use), so an unusable trust input (a non-certificate string, a malformed
516
- // tuple, junk bytes) is a construction error rather than a failure discovered only after the one-shot
517
- // transaction has already engaged the transport.
518
320
  if (opts.trustAnchors != null && _engine && _engine.toAnchor) _asCertList(opts.trustAnchors).forEach(function (a) { try { _engine.toAnchor(a); } catch (e) { throw _err("cmp/bad-input", "opts.trustAnchors: each entry must be a certificate (DER/PEM/parsed) or a { name, publicKey, algorithm } anchor tuple -- " + ((e && e.message) || e), e); } });
519
321
  if (opts.intermediates != null && _engine && _engine.coerceCert) _asCertList(opts.intermediates).forEach(function (c) { try { _engine.coerceCert(c); } catch (e) { throw _err("cmp/bad-input", "opts.intermediates: each entry must be a certificate (DER/PEM/parsed) -- " + ((e && e.message) || e), e); } });
520
- // opts.expectedSender pins the CA's SIGNER CERTIFICATE (not a subject string): every signed response's
521
- // authenticated header sender must bind to it under cmp.verify's own subject-or-subjectAltName rule, so an
522
- // empty-subject CA named only by a directoryName SAN -- which a subject-string pin reads as an unmatchable null
523
- // -- is bound correctly. Accept the documented DER Buffer / PEM string / already-parsed certificate at
524
- // construction; a non-certificate value would otherwise consume the one-shot transaction before the first
525
- // verify could reject it. The parsed form drives the identity bind; the DER (kept when supplied as bytes/PEM)
526
- // resolves a first response that omits its extraCerts (below).
527
- var _expectedSenderCert = null; // parsed, for the senderBoundToCert identity pin
528
- var _expectedSenderDer = null; // DER, for the omitted-extraCerts signer fallback (null when given the already-parsed form, which retains no source DER)
322
+ var _expectedSenderCert = null;
323
+ var _expectedSenderDer = null;
529
324
  if (opts.expectedSender != null) {
530
325
  var _es = opts.expectedSender;
531
326
  try {
532
- if (_es && Buffer.isBuffer(_es.tbsBytes)) { // the documented already-parsed form (pki.schema.x509.parse output), detected like _certIdentity
533
- // The door's RETURN is what gets pinned, not the object handed in. coerceCert re-derives the
534
- // certificate from the bytes its parser read, so calling it only as a check and then storing
535
- // the caller's object keeps every edit the re-derivation exists to discard: this pin is
536
- // compared against each response signer's subject and SAN, so an edited one would accept a
537
- // different CMP signer than the caller meant to pin. A validator that normalizes has its
538
- // return value as its contract -- using it as a predicate throws that contract away.
327
+ if (_es && Buffer.isBuffer(_es.tbsBytes)) {
539
328
  _expectedSenderCert = (_engine && _engine.coerceCert) ? _engine.coerceCert(_es) : _es;
540
329
  }
541
330
  else if (guard.bytes.isByteSource(_es)) { _expectedSenderDer = guard.bytes.snapshotSource(_es, CmpError, "cmp/bad-input", "opts.expectedSender"); _expectedSenderCert = x509.parse(_expectedSenderDer); }
@@ -546,11 +335,6 @@ function session(opts) {
546
335
  throw _err("cmp/bad-input", "opts.expectedSender must be the CA signer certificate (DER BufferSource / PEM / parsed) so every signed response can be bound to it -- " + ((e && e.message) || e), e);
547
336
  }
548
337
  }
549
- // Reject a DISTINCT intermediate pool exceeding the per-flavor cap (the candidate ceiling MINUS the room reserved
550
- // for the CA's own authenticated material). The caller pool is never truncated, so the whole pool + that material
551
- // must fit under path.build's ceiling; an oversized one would otherwise raise path/bad-input only at the first
552
- // verify, after the one-shot transaction is consumed. A MAC session reserves LESS (no signer chain), so its cap is
553
- // higher. (Duplicates collapse, so the count is by distinct id.)
554
338
  var maxIntermediates = isSig ? SESSION_MAX_INTERMEDIATES : SESSION_MAX_INTERMEDIATES_MAC;
555
339
  if (opts.intermediates != null) {
556
340
  var seenInt = Object.create(null), distinctInt = 0;
@@ -558,102 +342,61 @@ function session(opts) {
558
342
  if (distinctInt > maxIntermediates) throw _err("cmp/bad-input", "opts.intermediates has " + distinctInt + " distinct certificates, exceeding the " + maxIntermediates + " limit (room is reserved below the path-builder ceiling for the CA's own delivered issuer certificates" + (isSig ? " and signer chain" : "") + ")");
559
343
  }
560
344
 
561
- // A certificate-acceptance policy must be a function -- a non-function value (a config typo) would otherwise
562
- // be silently skipped, auto-accepting a grantedWithMods certificate the caller meant to vet. Fail at config.
563
345
  if (opts.acceptCert != null && typeof opts.acceptCert !== "function") throw _err("cmp/bad-input", "opts.acceptCert must be a function (certDer, info) => boolean");
564
- // implicitConfirm must be a boolean -- a truthy non-boolean (the string "false", say) would silently REQUEST
565
- // implicit confirmation and reverse the caller's confirmation policy. Reject a non-boolean at construction.
566
346
  if (opts.implicitConfirm != null && typeof opts.implicitConfirm !== "boolean") throw _err("cmp/bad-input", "opts.implicitConfirm must be a boolean");
567
- // sleep, when supplied, must be a function -- a non-function value would silently fall back to the REAL timer,
568
- // reversing the caller's intent (a bounded / injected sleeper) into a real-time wait. Reject it at construction.
569
347
  if (opts.sleep != null && typeof opts.sleep !== "function") throw _err("cmp/bad-input", "opts.sleep must be a function (delayMs) => Promise");
570
- // The transport, when supplied, must be a function -- a non-function value passes cmp.transfer's LOCAL url/budget
571
- // checks and then throws a raw TypeError when the transport is invoked, before any request is sent. That local
572
- // type error carries none of the typed cmp/* local codes, so the send path would mark the one-shot session
573
- // consumed on it. Reject it at construction so the transport call can only throw AFTER it has engaged the network.
574
348
  if (opts.transport != null && typeof opts.transport !== "function") throw _err("cmp/bad-input", "opts.transport must be a function (url, reqDer, opts) => Promise<{ responseBytes, status }>");
575
- // acceptCert and implicitConfirm are incompatible: under implicit confirmation the CA treats the certificate
576
- // as delivered at grant time, so there is no reject leg to honor a veto -- a policy could only produce a
577
- // misleading rejected verdict for a certificate the CA still considers issued. Refuse the combination up front.
578
349
  if (opts.acceptCert != null && opts.implicitConfirm) throw _err("cmp/bad-input", "opts.acceptCert cannot be combined with opts.implicitConfirm -- implicit confirmation leaves no certConf leg to reject on (drop implicitConfirm to vet a grant)");
579
350
 
580
- // The verify/validation clock, when supplied, must be a valid Date -- else path.build would only reject it while
581
- // verifying a response, AFTER the one-shot transaction has already engaged the transport and been consumed.
582
351
  if (opts.time != null) guard.time.assertValid(opts.time, _err, "cmp/bad-input", "opts.time (the verify/validation clock)");
583
- // Poll budgets, validated at construction (NaN / negative / over-max -> cmp/bad-input, never a disabled bound).
584
352
  var maxPolls = guard.limits.cap(opts.maxPolls, "opts.maxPolls", DEFAULT_MAX_POLLS, { E: _err, code: "cmp/bad-input", min: 1, max: 1000 });
585
353
  var maxTotalWait = guard.limits.cap(opts.maxTotalWait, "opts.maxTotalWait", DEFAULT_MAX_TOTAL_WAIT, { E: _err, code: "cmp/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS });
586
- var sleep = opts.sleep || sleepUtil.sleep; // validated a function (or absent) at construction
354
+ var sleep = opts.sleep || sleepUtil.sleep;
587
355
 
588
- // Transaction identity: mint ONE 128-bit transactionID (stable for the whole transaction), held here.
589
356
  var transactionID = Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(16)));
590
- var lastPeerNonce = null; // recipNonce for the NEXT request := the previous response's senderNonce
591
- var haveResponse = false; // a response has been received -> every subsequent request MUST echo a recipNonce
592
- var cachedSignerCert = null; // the CA's verified signature-protection signer cert, reused when a later leg omits extraCerts
593
- var pinnedSignerCert = null, signerPinned = false; // the parsed signer certificate of the FIRST signed response -- every later leg's authenticated sender must bind to it (a boolean flag, so it is pinned exactly once)
594
- var cachedChain = []; // the VALIDATED signer chain (signer + the intermediates path.build used) from cmp.verify -- INDEPENDENT DER copies, so a later leg rebuilds the signer path from only trusted-path material (not paddable extraCerts) without pinning a response allocation
595
- var caPubsAccum = []; // the AUTHENTICATED caPubs (issuer certs) accumulated across EVERY ip/cp/kup leg -- a CA may deliver the issuing chain in a `waiting` response and omit it from the eventual grant
596
- var caPubsSeen = Object.create(null); // byte-identity dedup for caPubsAccum, bounding cross-leg accumulation (below)
597
- var caPubsBytes = 0; // running total of retained caPubs bytes -- a transaction-wide byte budget (below) bounds it independent of the count cap, since a single certificate can approach the DER limit
598
- var caPubsWaitingCount = 0; // WAITING-leg entries retained at the FRONT of caPubsAccum -- only these are evictable to fit a grant entry; a grant entry (at the back) is the issued leaf's own chain and is never evicted
599
- var activeCertReqId = DEFAULT_CERT_REQ_ID; // the certReqId of the in-flight enrollment; echoed in pollReq / certConf
600
- var expectedRespArm = "ip"; // the response arm the in-flight transaction must be answered by (RESPONSE_ARM)
601
- // Which operation is in flight: it decides how the expected arm's body is read, and, for an error
602
- // body, whether a `waiting` status announces delayed delivery. RFC 9483 sec. 4.4 puts that status
603
- // in the ip/cp/kup of an enrollment and in an error message for every other operation, so the two
604
- // readings are exclusive rather than a leniency applied to both.
357
+ var lastPeerNonce = null;
358
+ var haveResponse = false;
359
+ var cachedSignerCert = null;
360
+ var pinnedSignerCert = null, signerPinned = false;
361
+ var cachedChain = [];
362
+ var caPubsAccum = [];
363
+ var caPubsSeen = Object.create(null);
364
+ var caPubsBytes = 0;
365
+ var caPubsWaitingCount = 0;
366
+ var activeCertReqId = DEFAULT_CERT_REQ_ID;
367
+ var expectedRespArm = "ip";
605
368
  var txnKind = "enroll";
606
- var expectedInfoOp = null; // the INFO_OPS row of an in-flight support message
607
- var crlQuery = null; // what a crlUpdate asked for: the encoded issuer Name and any thisUpdate
608
- var requestedSpki = null; // the SPKI DER of the key the enrollment requested; the issued cert MUST carry it
609
- var inFlight = false, completed = false, started = false; // one transaction per session; `started` = a request may have hit the transport
369
+ var expectedInfoOp = null;
370
+ var crlQuery = null;
371
+ var requestedSpki = null;
372
+ var inFlight = false, completed = false, started = false;
610
373
  var transcript = [];
611
- var transcriptBytes = 0; // running total of RETAINED transcript payload bytes -- bounds a padded-response flood across polling legs (below)
612
- // The transcript-retention ceiling: TRANSCRIPT_RETAIN_RESPONSES times the effective per-response byte cap. A
613
- // non-positive / non-finite maxResponseBytes falls back to the default here (a bad value throws in the transport
614
- // on the first transfer, before any response is recorded) so the ceiling is ALWAYS a positive finite number --
615
- // a NaN ceiling would make `total > ceiling` perpetually false and silently disable the bound.
374
+ var transcriptBytes = 0;
616
375
  var _perRespCap = (typeof opts.maxResponseBytes === "number" && isFinite(opts.maxResponseBytes) && opts.maxResponseBytes > 0) ? opts.maxResponseBytes : constants.LIMITS.HTTP_MAX_RESPONSE_BYTES;
617
376
  var transcriptCap = _perRespCap * TRANSCRIPT_RETAIN_RESPONSES;
618
- // The caPubs byte budget, proportional to the per-response cap and above one full response, so the GRANT leg's
619
- // own caPubs (bounded by one response) always fit after evicting waiting entries -- while the total stays bounded
620
- // (a count-only cap would still admit ~16 GiB of DER-limit-sized certs) independent of the candidate-count cap.
621
377
  var caPubsByteBudget = _perRespCap * TRANSCRIPT_RETAIN_RESPONSES;
622
378
 
623
- // Sender / recipient routing (RFC 9810 sec. 5.1.1). The sender NAMES the requester: signature protection
624
- // defaults it to the signer cert's own subject DN; a MAC transaction (no cert) has no established name, so
625
- // it defaults to the NULL-DN (an empty RDNSequence). The recipient (the CA) defaults to the NULL-DN too --
626
- // the CA is addressed by the endpoint URL, not by a name we can assert. opts.sender / opts.recipient override.
627
379
  var NULL_DN = { directoryName: [] };
628
380
  var defaultSender = NULL_DN;
629
381
  if (isSig) {
630
382
  try {
631
383
  var _signerSubject = x509.parse(opts.cert).subject;
632
- // An EMPTY-subject certificate (RFC 5280 sec. 4.1.2.6) is identified only by its subjectAltName, and a
633
- // conforming peer binds the sender to that SAN (never to the empty subject). Defaulting the sender to the
634
- // empty DN would name the requester with a value no peer accepts, so REQUIRE opts.sender for such a cert.
635
384
  if (_signerSubject.rdns && _signerSubject.rdns.length > 0) defaultSender = { directoryName: _signerSubject.bytes };
636
385
  else if (opts.sender == null) throw _err("cmp/bad-input", "the signature-protection certificate has an empty subject, so it cannot name the request sender -- an empty-subject certificate is identified by its subjectAltName; set opts.sender explicitly (e.g. a subjectAltName identity the CA will bind the sender to)");
637
386
  } catch (e) {
638
387
  if (e && e.isCmpError) throw e;
639
- defaultSender = NULL_DN; // a malformed signer cert -> the sender falls back to a NULL-DN (driven by the unparseable-cert construction vector)
388
+ defaultSender = NULL_DN;
640
389
  }
641
390
  }
642
391
 
643
- // The header the session OWNS on every request (transaction identity + routing), plus the protection opts.
644
392
  function _baseHeader(fresh) {
645
393
  var h = { transactionID: transactionID, senderNonce: fresh };
646
- // The first request carries no recipNonce; every FOLLOW-UP leg MUST echo the previous response's
647
- // senderNonce (RFC 9810 sec. 5.1.1). A prior response that omitted its senderNonce leaves nothing to
648
- // echo -- the anti-replay chain is broken, so fail closed rather than send an unchained request.
649
394
  if (haveResponse) {
650
395
  if (!Buffer.isBuffer(lastPeerNonce) || lastPeerNonce.length === 0) throw _err("cmp/bad-nonce", "the previous CMP response omitted its senderNonce, so this follow-up request cannot echo it as recipNonce (RFC 9810 sec. 5.1.1)");
651
396
  h.recipNonce = lastPeerNonce;
652
397
  }
653
398
  h.sender = opts.sender != null ? opts.sender : defaultSender;
654
399
  h.recipient = opts.recipient != null ? opts.recipient : NULL_DN;
655
- // Key identifiers the endpoint uses to select a credential -- a PBMAC1 shared secret (senderKID) among several,
656
- // or the recipient key (recipKID). Propagated on EVERY leg so the CA resolves the same credential throughout.
657
400
  if (opts.senderKID != null) h.senderKID = opts.senderKID;
658
401
  if (opts.recipKID != null) h.recipKID = opts.recipKID;
659
402
  return h;
@@ -666,24 +409,9 @@ function session(opts) {
666
409
  return o;
667
410
  }
668
411
  function _verifyOpts(fresh, signerCertOverride, extraIntermediates, responseExtra) {
669
- // Verify the RESPONSE protection + bind it to THIS exchange via the opt-in echo checks. The CA's signer
670
- // cert is resolved from the response extraCerts (RFC 9483 sec. 3.3); trustAnchors chain it. A MAC response
671
- // verifies under the shared secret. transactionID + expectRecipNonce fail-close a mismatched response.
672
412
  var o = { transactionID: transactionID, expectRecipNonce: fresh };
673
413
  if (isMac) o.sharedSecret = opts.mac.secret;
674
- // Forward trustAnchors to the RESPONSE verify ONLY for the signature flavor -- the MAC branch of cmp.verify
675
- // rejects them as a stray signature credential. A MAC session's anchors are used solely for issued-cert
676
- // validation (_validateLeaf), never the MAC response authentication (which the shared secret establishes).
677
414
  else if (opts.trustAnchors != null) o.trustAnchors = opts.trustAnchors;
678
- // Give cmp.verify ONE bounded pool that already contains the signer's OWN delivered issuers, so it needs no
679
- // room RESERVED for an internal append (no pre-verify count to estimate, no duplicate to over-reserve). Order
680
- // by priority: the response's extraCerts first (when responseExtra is passed), then the cached signer chain,
681
- // then the broad caller pool fills to the ceiling -- _boundedPool dedups the whole thing, so a response cert
682
- // duplicating a caller one spends no slot, and cmp.verify's re-append of the response extraCerts is all
683
- // duplicates path.build collapses. The signer itself may sit in this pool as a redundant candidate (a slot);
684
- // rather than try to IDENTIFY it among extraCerts (senderKID can select one that is not first), the caller of
685
- // _send retries with responseExtra=null (caller pool at the full ceiling) if this response-prioritized pool
686
- // truncated a needed caller issuer -- the two attempts cover both truncation priorities without identifying the signer.
687
415
  var issuers = Array.isArray(responseExtra) ? responseExtra : [];
688
416
  var extra = [];
689
417
  if (Array.isArray(extraIntermediates)) {
@@ -694,9 +422,6 @@ function session(opts) {
694
422
  var ints = _boundedPool(issuers.concat(extra), _asCertList(opts.intermediates));
695
423
  if (ints.length) o.intermediates = ints;
696
424
  if (opts.time != null) o.time = opts.time;
697
- // An explicit signerCert takes ABSOLUTE precedence in cmp.verify, so it is passed ONLY as a fallback for a
698
- // response that cannot resolve its own signer -- never by default, else a clustered CA that rotates the
699
- // protection cert mid-transaction (a valid later cert in that leg's extraCerts) would verify under the wrong key.
700
425
  if (signerCertOverride != null) o.signerCert = signerCertOverride;
701
426
  return o;
702
427
  }
@@ -706,20 +431,11 @@ function session(opts) {
706
431
  return o;
707
432
  }
708
433
 
709
- // ONE leg: mint a fresh senderNonce, build+protect the request, transfer it, VERIFY the response BEFORE
710
- // reading its body (a !valid verdict is a HARD STOP throw), chain the peer nonce, record both directions.
711
434
  async function _send(bodySpec, arm) {
712
435
  var fresh = Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(16)));
713
436
  var header = Object.assign(_baseHeader(fresh), ENROLL_ARMS[arm] && opts.implicitConfirm ? { generalInfo: [{ infoType: "implicitConfirm" }] } : {});
714
437
  var reqDer = await cmp.build({ header: header, body: bodySpec }, _buildOpts());
715
438
  _recordTranscript({ direction: "out", arm: arm, bytes: reqDer });
716
- // The one-shot `started` flag must flip iff a request MAY have reached the transport, so the caller can fix a
717
- // purely-local config error (url parse, transfer-budget caps, TLS-anchor check) and retry, but a request that
718
- // reached the CA is never silently replayed under the same transactionID/nonce. cmp.transfer runs those local
719
- // checks BEFORE it invokes the transport. A CUSTOM transport, though, can itself throw the SAME cmp/* codes as
720
- // those preflight checks AFTER receiving the request, so the error code cannot distinguish the two -- WRAP it so
721
- // `engaged` flips the instant cmp.transfer hands it the request (past its preflight). The DEFAULT transport never
722
- // reuses the preflight codes for its own network errors, so for it those codes reliably mean a pre-send failure.
723
439
  var engaged = false;
724
440
  var topts = _transferOpts();
725
441
  if (opts.transport != null) topts.transport = function (a) { engaged = true; return opts.transport(a); };
@@ -733,162 +449,76 @@ function session(opts) {
733
449
  if (reached) started = true;
734
450
  throw e;
735
451
  }
736
- started = true; // the transport returned a response -> the transaction has engaged, the session is consumed
737
- // The response's OWN deduped extraCerts (the signer + its delivered issuers) -- _verifyOpts folds them into the
738
- // signer-path pool as priority so cmp.verify needs no reserved room for its internal append. cmp.transfer has
739
- // already parsed res.responseBytes as a PKIMessage (its cmp.parse gate on every return path), so this cannot throw.
452
+ started = true;
740
453
  var responseExtra = _responseExtraCerts(res.responseBytes);
741
- // The response's OWN issuers, the CACHED chain from an earlier authenticated leg, AND the whole caller pool fit
742
- // in ONE candidate pool (SESSION_MAX_INTERMEDIATES reserves room below the ceiling), so a single verify sees every
743
- // candidate -- no priority attempt has to choose between them, and a signer path assembled from any of the three
744
- // sources builds. cachedChain is included in the PRIMARY (not only the fallback) so a same-identity signer
745
- // rotation -- a later leg's extraCerts carries the rotated signer B, but B's issuer was delivered only alongside
746
- // signer A on an earlier leg -- resolves B from its own extraCerts AND chains it via the cached issuer, rather
747
- // than reporting B untrusted and falling through to a fallback that (forcing signer A's key) cannot verify B's
748
- // signature. No signer identification is needed (senderKID may name a cert that is not extraCerts[0]); every
749
- // candidate is simply present. cachedChain is [] before any response has authenticated, so this is inert then.
750
454
  var verdict = await cmp.verify(res.responseBytes, _verifyOpts(fresh, null, cachedChain, responseExtra));
751
- // Fallback: a signature-flavor response's OWN signer resolution can fail FOUR ways that the earlier,
752
- // authenticated signer still recovers, all because extraCerts is OUTSIDE the protected part and a meddler
753
- // controls which cert the resolver selects -- (a) NO signer resolved (a later leg omitted extraCerts once the
754
- // recipient holds the cert, RFC 9483 sec. 3.3) -> signer-cert-not-found; (b) a same-subject decoy with a
755
- // DIFFERENT key selected first -> protection-failed; (c) a decoy carrying the REAL signer's key but a WRONG
756
- // subject -> the signature verifies yet the header sender does not bind to it -> sender-mismatch; (d) a decoy
757
- // carrying the REAL signer's key + identity but chaining to an UNTRUSTED issuer -> verifies yet trust fails
758
- // -> untrusted-signer. All retry against the cached (already trusted) signer + its chain, whose subject IS the
759
- // authenticated sender and whose key is fixed. The response's own certs are still preferred on the FIRST
760
- // attempt, so a clustered CA's legitimately rotated cert wins there; the retry can only rescue a message
761
- // ACTUALLY signed by the cached signer, never a genuinely tampered / mismatched / untrusted one (it re-runs
762
- // every gate against the cached cert -- a wrong-key message stays valid:false, a wrong sender stays mismatched).
763
- // A transaction-integrity failure (transactionID / recipNonce) is NOT retried: it is decoy-independent and a
764
- // real desync, so the cached signer would fail it identically -- retrying would only mask the diagnostic.
765
- // The fallback signer is the already-authenticated cached signer, or -- before any response has succeeded --
766
- // the caller's prebound CA certificate (opts.expectedSender). The latter rescues a FIRST response that omits
767
- // its own extraCerts because the CA assumes the client already holds its certificate (RFC 9483 sec. 3.3): the
768
- // signer cannot resolve from an empty extraCerts, yet the exact certificate is locally provisioned.
769
455
  var usedCachedFallback = false;
770
456
  var fallbackSigner = cachedSignerCert != null ? cachedSignerCert : _expectedSenderDer;
771
457
  var recoverable = _isRecoverableVerify(verdict.code);
772
458
  if ((verdict.valid !== true || verdict.trusted !== true) && isSig && fallbackSigner != null && recoverable) {
773
- // A CACHED-signer fallback re-verifies against material a PRIOR leg authenticated, so THIS response's
774
- // extraCerts are decoy-suspect and must NOT overwrite the cache. A PREBORN fallback (no prior cache, the
775
- // caller's opts.expectedSender) instead verifies this response's OWN extraCerts against the prebound signer,
776
- // so they ARE authenticated -- cache them, else a later extraCerts-less leg retries with an empty chain.
777
459
  usedCachedFallback = cachedSignerCert != null;
778
460
  verdict = await cmp.verify(res.responseBytes, _verifyOpts(fresh, fallbackSigner, cachedChain, responseExtra));
779
461
  }
780
462
  _recordTranscript({ direction: "in", arm: verdict.body ? verdict.body.arm : null, status: res.status, bytes: res.responseBytes, verdict: { valid: verdict.valid, trusted: verdict.trusted, code: verdict.code || null } });
781
463
  if (verdict.valid !== true) throw _err(verdict.code || "cmp/protection-failed", "the CMP response protection did not verify (" + (verdict.reason || "invalid") + ") -- the transaction is NOT advanced", null);
782
- // Cryptographically valid is NOT enough: the signer (signature flavor) must chain to a supplied trust
783
- // anchor, or the shared secret (MAC flavor) must match -- both surface as verdict.trusted. A valid-but-
784
- // untrusted response is an unauthenticated signer; fail closed rather than read a certificate off it.
785
464
  if (verdict.trusted !== true) throw _err(verdict.code || "cmp/untrusted-signer", "the CMP response protection verified but its signer is not trusted (it did not chain to a supplied trust anchor) -- the transaction is NOT advanced", null);
786
- // Bind a SIGNED response to the intended CA IDENTITY, not merely to "any trusted signer". A trust anchor that
787
- // issues to more than one party would otherwise let an on-path holder of ANY chaining digitalSignature cert
788
- // sign a forged response under its OWN subject and still pass the trust gate. The binding reuses cmp.verify's
789
- // OWN sender<->certificate rule (RFC 5280 sec. 7 per-type comparison: a directoryName under the sec. 7.1
790
- // canonical DN comparison, a dNSName / rfc822Name / URI case-folded, every other type by exact DER) so a
791
- // re-encoded-but-equivalent DN (PrintableString<->UTF8String) and an empty-subject CA named only by a
792
- // subjectAltName both compare correctly -- neither a raw byte pin nor a subject-string pin can do that.
793
465
  if (isSig && verdict.signer && verdict.header && verdict.header.sender) {
794
- // (1) opts.expectedSender pinned the CA's signer certificate up front: every signed response's authenticated
795
- // header sender MUST bind to it, enforced from the FIRST response (so a forged first response is caught too).
796
466
  if (_expectedSenderCert && !cmp.senderBoundToCert(verdict.header.sender, _expectedSenderCert)) {
797
467
  throw _err("cmp/untrusted-signer", "the response signer does not match the expected CA identity (opts.expectedSender) -- refusing a response from a different trusted signer", null);
798
468
  }
799
- // (2) The FIRST signed response pins the CA certificate; every later leg's authenticated sender MUST bind to
800
- // it under the same rule -- permitting same-identity certificate/key rotation (the sender identity survives a
801
- // re-encoding) while rejecting a mid-transaction switch to a different trusted signer.
802
- if (!signerPinned) { pinnedSignerCert = x509.parse(verdict.signer.cert); signerPinned = true; } // a boolean flag: the pin is set once, never re-initialized per leg
469
+ if (!signerPinned) { pinnedSignerCert = x509.parse(verdict.signer.cert); signerPinned = true; }
803
470
  else if (!cmp.senderBoundToCert(verdict.header.sender, pinnedSignerCert)) {
804
471
  throw _err("cmp/untrusted-signer", "the response signer identity changed mid-transaction -- a different trusted signer, not a same-identity certificate rotation; the transaction is NOT advanced", null);
805
472
  }
806
473
  }
807
- lastPeerNonce = verdict.senderNonce; // may be absent; _baseHeader enforces its presence before a follow-up leg
474
+ lastPeerNonce = verdict.senderNonce;
808
475
  haveResponse = true;
809
- // Refresh the cached signer + its validated chain material from a response that verified ON ITS OWN OR via the
810
- // PREBORN (opts.expectedSender) fallback -- in both, its extraCerts IS the chain that established trust (the
811
- // most recently authenticated cert + the intermediates that chained it; a clustered CA may rotate its
812
- // protection cert across legs), so a later extraCerts-less leg falls back to the current signer AND can rebuild
813
- // its path. A response that verified only via the CACHED-signer fallback did NOT authenticate its own extraCerts
814
- // (which may hold a meddler's decoy or omit the real intermediate); keep the existing cached signer + chain --
815
- // the material that actually established trust -- rather than overwriting it with that response's untrusted pool.
816
476
  if (isSig && !usedCachedFallback && verdict.signer && Buffer.isBuffer(verdict.signer.cert)) {
817
- var extras = _responseExtraCerts(res.responseBytes); // the response's own delivered extraCerts (bounded)
818
- // Cache the VALIDATED chain cmp.verify assembled (the signer + the intermediates path.build actually used),
819
- // NOT the raw extraCerts. extraCerts is UNSIGNED: a peer can pad it with the real signer plus unrelated
820
- // parseable certificates, and caching that raw list (even newest-first, bounded) would evict the issuer a
821
- // same-identity rotation relied on -- a subsequent bare leg from the rotated signer would then fail to chain.
822
- // verdict.signer.chain holds ONLY certificates on the trusted path, so the establishing issuer always
823
- // survives. Fall back to the delivered extraCerts if a chain is somehow absent (defensive; a trusted verdict
824
- // always carries one). Only refresh when the response delivered its OWN extraCerts (an extraCerts-less leg
825
- // keeps the prior chain).
477
+ var extras = _responseExtraCerts(res.responseBytes);
826
478
  if (extras.length) {
827
- cachedSignerCert = Buffer.from(verdict.signer.cert); // COPY: an extraCerts-resolved signer.cert is a slice of the response, so caching it raw pins that allocation (the chain is copied by cmp.verify; keep this one consistent)
479
+ cachedSignerCert = Buffer.from(verdict.signer.cert);
828
480
  cachedChain = (Array.isArray(verdict.signer.chain) && verdict.signer.chain.length) ? verdict.signer.chain : extras;
829
481
  }
830
482
  }
831
483
  return verdict;
832
484
  }
833
485
 
834
- // Classify a verified response into a transaction transition. For ip/cp/kup, read the FIRST CertResponse
835
- // status; error carries a terminal PKIStatusInfo; pollRep drives the loop; any other arm is unexpected.
836
486
  function _classify(verdict) {
837
487
  var body = verdict.body || {};
838
488
  var arm = body.arm;
839
489
  if (arm === expectedRespArm && txnKind === "revoke") return _classifyRevRep(body);
840
490
  if (arm === expectedRespArm && txnKind === "info") return _classifyGenRep(body);
841
491
  if (arm === expectedRespArm) {
842
- // Select the CertResponse for OUR request by its certReqId (RFC 9810 sec. 5.3.4) -- never blindly
843
- // response[0], so a batch / misrouted response for a different request id is not read as ours.
844
492
  var responses = (body.decoded && body.decoded.response) || [];
845
493
  var resp = null;
846
494
  for (var ri = 0; ri < responses.length; ri++) { if (_certReqIdEq(responses[ri].certReqId, activeCertReqId)) { resp = responses[ri]; break; } }
847
495
  if (!resp) return { state: "unexpected", reason: "a " + arm + " carried no CertResponse for certReqId " + activeCertReqId };
848
496
  var code = resp.status && resp.status.status ? resp.status.status.code : null;
849
497
  var isGrantLeg = _isGranted(code);
850
- // RFC 9483 sec. 4.4: a "waiting" CertResponse must not carry failInfo -- a wait is not a failure. Refuse
851
- // a contradictory failInfo (mirroring the _classifyError waiting check). A granting status carrying
852
- // failInfo is already refused upstream: the parser rejects failInfo alongside a certifiedKeyPair (RFC 9810
853
- // sec. 5.3.4), and a grant with no certificate is refused as a missing issued certificate.
854
498
  if (code === 3 && resp.status && resp.status.failInfo != null) {
855
499
  return { state: "unexpected", reason: "a waiting " + arm + " CertResponse must not carry failInfo (RFC 9483 sec. 4.4)" };
856
500
  }
857
- // Accumulate the AUTHENTICATED caPubs of EVERY ip/cp/kup leg (waiting AND granting): a CA may deliver the
858
- // issuing chain in a `waiting` response and omit it from the grant, so the leaf's issuer must survive the poll.
859
- // Bound + validate + dedup the caPubs AS ACCUMULATED, not only when _finish is reached: an authenticated CA
860
- // can return `waiting` up to the poll budget with large or repeated caPubs, so without a cross-leg cap this
861
- // array would retain unbounded material before validation. Enforce a count AND a byte budget; a byte-identical
862
- // repeat is dropped; a non-X.509 entry is REJECTED (fail-closed -- the leaf validation would otherwise defer
863
- // it to _finish). The GRANT leg's caPubs are the issued leaf's OWN chain (highest priority): they EVICT the
864
- // oldest WAITING entries to fit ALL of them within the budget, so no fixed reserve can starve the leaf's own
865
- // required issuer no matter how many waiting caPubs preceded it (a caPubs field carries no MAX_EXTRA_CERTS cap).
866
501
  if (body.decoded && Array.isArray(body.decoded.caPubs)) body.decoded.caPubs.forEach(function (c) {
867
502
  if (!Buffer.isBuffer(c)) return;
868
503
  var k = c.toString("base64");
869
504
  if (caPubsSeen[k]) {
870
- // Already retained (from a waiting leg). On the GRANT leg it is the issued leaf's OWN material -- PROMOTE
871
- // it to the newest position so a LATER grant entry's eviction cannot drop it while its grant occurrence is
872
- // skipped as a duplicate. (A waiting-leg duplicate keeps its position; only the grant confers priority.)
873
505
  if (isGrantLeg) { for (var pi = 0; pi < caPubsAccum.length; pi++) { if (caPubsAccum[pi].toString("base64") === k) { caPubsAccum.push(caPubsAccum.splice(pi, 1)[0]); if (pi < caPubsWaitingCount) caPubsWaitingCount--; break; } } }
874
506
  return;
875
507
  }
876
508
  try { x509.parse(c); } catch (e) { throw _err("cmp/bad-cert-response", "an authenticated caPubs entry is not a valid X.509 certificate", e); }
877
509
  if (isGrantLeg) {
878
- // Evict ONLY the WAITING entries at the front to fit this grant entry -- never an earlier GRANT entry (the
879
- // issued leaf's own chain). Once no waiting entries remain, this grant entry is dropped by the cap below.
880
510
  while (caPubsWaitingCount > 0 && (caPubsAccum.length >= CAPUBS_MAX || caPubsBytes + c.length > caPubsByteBudget)) {
881
511
  var evicted = caPubsAccum.shift();
882
512
  caPubsBytes -= evicted.length;
883
513
  caPubsWaitingCount--;
884
- delete caPubsSeen[evicted.toString("base64")]; // keep the dedup set == the retained set, so a grant re-delivering an evicted issuer (also seen in a waiting leg) can re-add it
514
+ delete caPubsSeen[evicted.toString("base64")];
885
515
  }
886
516
  }
887
- if (caPubsAccum.length >= CAPUBS_MAX || caPubsBytes + c.length > caPubsByteBudget) return; // no waiting entry left to evict (a grant would evict a grant) OR over budget -> drop
517
+ if (caPubsAccum.length >= CAPUBS_MAX || caPubsBytes + c.length > caPubsByteBudget) return;
888
518
  caPubsSeen[k] = 1;
889
519
  caPubsBytes += c.length;
890
- caPubsAccum.push(Buffer.from(c)); // COPY out of the parser's subarray -- retaining the slice would pin the whole multi-MB response buffer, so caPubsBytes (the cert length) would not bound the real allocation across polls
891
- if (!isGrantLeg) caPubsWaitingCount++; // a waiting entry joins the evictable front region
520
+ caPubsAccum.push(Buffer.from(c));
521
+ if (!isGrantLeg) caPubsWaitingCount++;
892
522
  });
893
523
  if (isGrantLeg) return { state: "granted", resp: resp, code: code };
894
524
  if (code === 3) return { state: "waiting", resp: resp };
@@ -901,36 +531,18 @@ function session(opts) {
901
531
  return { state: "unexpected", reason: "response arm " + JSON.stringify(arm) + " has no transition in a " + txnKind + " transaction" };
902
532
  }
903
533
 
904
- // An error body. For a revocation or a support message it is ALSO how delayed delivery is announced:
905
- // RFC 9483 sec. 4.4 puts the "waiting" status in an ip/cp/kup for ir/cr/kur/p10cr "and for responses
906
- // to other request message types in an error message". An error answering an ENROLLMENT therefore
907
- // stays a rejection -- that operation announces its own delay in its own cert-response arm, so
908
- // reading waiting out of an error there would accept a shape the profile does not produce.
909
534
  function _classifyError(body) {
910
535
  var si = body.decoded && body.decoded.pKIStatusInfo;
911
536
  var code = si && si.status ? si.status.code : null;
912
537
  if (code === 3) {
913
- // sec. 4.4 puts waiting in an ip/cp/kup when answering an enrollment and in an error message for
914
- // anything else, so a waiting error drives the poll loop only for revoke/info. For an enrollment a
915
- // waiting ERROR is off-profile and refused, not coerced to a rejection whose own status reads
916
- // "waiting". A wait is not a failure, so sec. 4.4 forbids failInfo on it -- a waiting error carrying
917
- // one is contradictory and refused rather than polled.
918
538
  if (txnKind === "enroll") throw _err("cmp/bad-error", "an error message answering an enrollment must not carry status waiting -- the profile places enrollment waiting in an ip/cp/kup (RFC 9483 sec. 4.4)");
919
539
  if (si.failInfo != null) throw _err("cmp/bad-error", "an error message with status waiting must not carry failInfo (RFC 9483 sec. 4.4)");
920
540
  return { state: "waiting", resp: { status: si } };
921
541
  }
922
- // sec. 4.2 / 3.6.4: an error message that is not delayed delivery is a rejection, whose status is
923
- // rejection(2). accepted(0), grantedWithMods(1), a status in 4..6, an unknown value, or an absent
924
- // status is malformed for an error message, so it is refused rather than surfaced as a definitive
925
- // rejected verdict a caller would act on -- the same call the rp classifier makes for an unsupported
926
- // status (a trusted "rejected" whose PKIStatusInfo says "accepted" is a contradiction, not a verdict).
927
542
  if (code !== 2) throw _err("cmp/bad-error", "an error message carries status rejection(2), or waiting(3) for a revoke/info delayed delivery (RFC 9483 sec. 4.2); got " + (code == null ? "no status" : code));
928
543
  return { state: "rejected", status: si };
929
544
  }
930
545
 
931
- // RevRepContent. RFC 9483 sec. 4.2: the response to an rr "MUST be an rp message containing a single
932
- // status field", whose positive value is "accepted" and negative "rejection", with failInfo "MUST be
933
- // absent if the status is accepted". A status with no transition here is a hard stop, never a verdict.
934
546
  function _classifyRevRep(body) {
935
547
  var d = body.decoded || {};
936
548
  var list = d.status || [];
@@ -949,9 +561,6 @@ function session(opts) {
949
561
  return { state: "unexpected", reason: "an rp status code " + code + " has no transition in a revocation (RFC 9483 sec. 4.2 allows accepted or rejection)" };
950
562
  }
951
563
 
952
- // GenRepContent. RFC 9483 sec. 4.3: a genp "MUST contain a sequence of one element of type
953
- // InfoTypeAndValue" whose infoType names the operation. The OID is checked against the RESPONSE
954
- // side of the INFO_OPS row, which for rootCaCert is not the OID the request carried.
955
564
  function _classifyGenRep(body) {
956
565
  var items = body.decoded || [];
957
566
  if (items.length !== 1) {
@@ -967,8 +576,6 @@ function session(opts) {
967
576
 
968
577
  function _statusOf(resp) { return resp && resp.status ? resp.status : null; }
969
578
 
970
- // A granted implicitConfirm in the RESPONSE header's generalInfo (RFC 9810 sec. 5.1.1.1) -- classified by
971
- // the IMMUTABLE id-it OID, never the mutable display name.
972
579
  function _implicitConfirmGranted(header) {
973
580
  var gi = header && header.generalInfo;
974
581
  if (!Array.isArray(gi)) return false;
@@ -976,12 +583,6 @@ function session(opts) {
976
583
  return false;
977
584
  }
978
585
 
979
- // Append a transcript entry, bounding the RETAINED payload bytes transaction-wide. A polling enrollment runs
980
- // many legs and an on-path party can pad every otherwise-valid signed response with unsigned extraCerts up to
981
- // the per-response cap; retaining each full payload would multiply that cap across legs into memory exhaustion.
982
- // Once the running total crosses the ceiling, the entry keeps its metadata + `byteLength` but drops the payload
983
- // (`bytes: null, truncated: true`) so the internal reference is released to GC and the transcript stays a
984
- // fixed-size diagnostic. Normal kilobyte messages never reach the ceiling, so the payloads are retained intact.
985
586
  function _recordTranscript(entry) {
986
587
  var len = Buffer.isBuffer(entry.bytes) ? entry.bytes.length : 0;
987
588
  if (transcriptBytes + len > transcriptCap) {
@@ -992,9 +593,6 @@ function session(opts) {
992
593
  transcript.push(entry);
993
594
  }
994
595
 
995
- // A defensive SNAPSHOT of the transcript: a fresh array of shallow-copied entries with copied byte Buffers, so
996
- // freezing or mutating the returned value cannot break the internal transcript (_send keeps appending to it)
997
- // nor mutate a Buffer still shared with an in-flight request/response body.
998
596
  function _transcriptSnapshot() {
999
597
  return transcript.map(function (e) {
1000
598
  var c = Object.assign({}, e);
@@ -1004,17 +602,13 @@ function session(opts) {
1004
602
  });
1005
603
  }
1006
604
  function _terminal(outcome, extra) {
1007
- // Every terminal outcome is reached only through _send, which throws unless the response was both valid
1008
- // AND trusted -- so a returned verdict is always authenticated (trusted:true), surfaced for the caller.
1009
605
  return Object.assign({
1010
606
  outcome: outcome, certificate: null, chain: [], status: null, trusted: true,
1011
- confirmed: false, implicitConfirm: false, transactionID: Buffer.from(transactionID), // a copy: mutating it must not corrupt the session identity
607
+ confirmed: false, implicitConfirm: false, transactionID: Buffer.from(transactionID),
1012
608
  polls: 0, transcript: _transcriptSnapshot(),
1013
609
  }, extra);
1014
610
  }
1015
611
 
1016
- // The bounded pollReq/pollRep loop (RFC 9810 sec. 5.3.22): sleep >= checkAfter each round, re-classify a
1017
- // non-pollRep response as an initial response, END as poll-timeout when maxPolls / maxTotalWait is hit.
1018
612
  async function _pollLoop(lastWaitingResp) {
1019
613
  var polls = 0, waited = 0, lastStatus = _statusOf(lastWaitingResp);
1020
614
  for (;;) {
@@ -1023,9 +617,6 @@ function session(opts) {
1023
617
  polls += 1;
1024
618
  var t = _classify(verdict);
1025
619
  if (t.state === "pollRep") {
1026
- // Select the pollRep entry for OUR request by certReqId (RFC 9810 sec. 5.3.22) -- never entry zero,
1027
- // so an unrelated entry's checkAfter cannot stall us and a pollRep that never mentions our request
1028
- // (its entry absent) is a misrouted response, not a silent zero-delay re-poll.
1029
620
  var entries = t.entries || [];
1030
621
  var entry = null;
1031
622
  for (var pe = 0; pe < entries.length; pe++) { if (_certReqIdEq(entries[pe].certReqId, activeCertReqId)) { entry = entries[pe]; break; } }
@@ -1033,42 +624,24 @@ function session(opts) {
1033
624
  var checkAfter = typeof entry.checkAfter === "number" ? entry.checkAfter : 0;
1034
625
  waited += checkAfter;
1035
626
  if (waited > maxTotalWait) return { timeout: true, polls: polls, status: lastStatus };
1036
- // Do NOT sleep after the FINAL permitted poll: this pollRep answered poll number maxPolls, so the
1037
- // budget is already spent -- sleeping its checkAfter (up to a year) before the loop-top count check
1038
- // would defeat the poll-count bound. Detect exhaustion here, before scheduling the delay.
1039
627
  if (polls >= maxPolls) return { timeout: true, polls: polls, status: lastStatus };
1040
628
  await sleep(checkAfter * constants.TIME.seconds(1));
1041
629
  continue;
1042
630
  }
1043
- if (t.state === "waiting") { lastStatus = _statusOf(t.resp); continue; } // still processing -> keep polling
1044
- // granted / rejected / unexpected -> advance out of the loop, carrying the GRANTING response's header
1045
- // (an implicitConfirm grant / a fresh nonce lives in THIS response, not the initial waiting one).
631
+ if (t.state === "waiting") { lastStatus = _statusOf(t.resp); continue; }
1046
632
  return { done: t, polls: polls, header: verdict.header };
1047
633
  }
1048
634
  }
1049
635
 
1050
- // Confirm delivery (RFC 9810 sec. 5.1.1.1 / 5.3.18): if implicitConfirm was granted, END; else send a
1051
- // certConf (certHash under the cert's signature hash) and require a verified pkiConf, else cmp/bad-confirmation.
1052
- // `info` carries the grant status so an acceptance policy can veto a grantedWithMods certificate.
1053
636
  async function _confirm(certDer, header, info) {
1054
- // Honor an implicitConfirm grant ONLY when the caller REQUESTED it (opts.implicitConfirm): implicit
1055
- // confirmation is negotiated (RFC 9810 sec. 5.1.1.1), so an UNSOLICITED implicitConfirm from a nonconforming
1056
- // server is ignored -- the session performs the explicit certConf the caller expects. No acceptance policy
1057
- // runs here: acceptCert + implicitConfirm is refused at construction (there is no reject leg to honor a veto).
1058
637
  if (opts.implicitConfirm && _implicitConfirmGranted(header)) return { confirmed: true, implicit: true };
1059
- // Consult the caller's acceptance policy before the explicit confirmation. A grantedWithMods response (RFC
1060
- // 9810 sec. 5.2.3) carries a certificate the CA CHANGED (subject / validity / extensions), and an accepting
1061
- // certConf is irrevocable, so the caller may inspect + veto it here. The policy MUST return true to accept;
1062
- // anything else is a veto (a thrown rejection is NOT swallowed -- it propagates). No policy accepts (default).
1063
638
  var accept = true;
1064
639
  if (typeof opts.acceptCert === "function") accept = (await opts.acceptCert(Buffer.from(certDer), info)) === true;
1065
640
  var h = _certConfHash(certDer);
1066
641
  var certHash = Buffer.from(await webcrypto.webcrypto.subtle.digest(h.digest, certDer));
1067
642
  var cs = { certHash: certHash, certReqId: activeCertReqId };
1068
- // A veto sends a REJECTING certConf (CertStatus.statusInfo status rejection, RFC 9810 sec. 5.3.18) so the CA
1069
- // learns the EE declined and does not treat the certificate as delivered; an acceptance omits statusInfo.
1070
643
  if (!accept) cs.statusInfo = { status: 2, statusString: ["the enrolling client rejected the issued certificate"] };
1071
- if (h.hashAlg) cs.hashAlg = h.hashAlg; // declare the hash for a sig alg whose OID does not convey it (PSS / EdDSA / ML-DSA)
644
+ if (h.hashAlg) cs.hashAlg = h.hashAlg;
1072
645
  var verdict = await _send({ certConf: [cs] }, "certConf");
1073
646
  if (!verdict.body || verdict.body.arm !== "pkiconf") throw _err("cmp/bad-confirmation", "expected a pkiConf acknowledgement to the certConf but got " + JSON.stringify(verdict.body && verdict.body.arm) + " (RFC 9810 sec. 5.3.18)");
1074
647
  return { confirmed: accept, implicit: false, rejected: !accept };
@@ -1078,28 +651,16 @@ function session(opts) {
1078
651
  var ckp = resp && resp.certifiedKeyPair;
1079
652
  var cert = ckp && ckp.certificate;
1080
653
  if (!Buffer.isBuffer(cert)) throw _err("cmp/unexpected-arm", "a granted CertResponse carried no plain issued certificate (an encryptedCert form is out of enrollment v1 scope) (RFC 9810 sec. 5.3.4)");
1081
- // A central-key-generation privateKey (the CA generated the key, delivered encrypted) is out of a
1082
- // client-key enrollment session's scope: the caller submitted the key + its proof of possession, so the
1083
- // session would otherwise confirm a certificate whose private key it discarded -- a result the caller cannot use.
1084
654
  if (ckp.privateKey != null) throw _err("cmp/unexpected-arm", "a granted CertResponse carried a server-generated privateKey (central key generation is out of enrollment v1 scope; a session enrolls a client-generated key) (RFC 9810 sec. 5.3.4)");
1085
- // The CMP parser surfaces certifiedKeyPair.certificate as opaque bytes (any non-empty SEQUENCE); a
1086
- // non-conformant / hostile CA could deliver bytes that are not a real X.509 certificate. Validate them
1087
- // BEFORE confirming, so the session never returns outcome:issued with a value pki.schema.x509.parse rejects.
1088
655
  var parsed;
1089
656
  try { parsed = x509.parse(cert); }
1090
657
  catch (e) { throw _err("cmp/bad-cert-response", "the granted CertResponse's certificate is not a valid X.509 certificate", e); }
1091
- // For a client-generated-key enrollment, the issued cert MUST carry the key the request submitted -- else
1092
- // it is a misrouted certificate the caller holds no private key for. Compare the KEY identity (alg OID +
1093
- // key bits), not the raw SPKI bytes, so an equivalent re-encoding (rsaEncryption NULL vs omitted) matches.
1094
658
  if (requestedSpki != null && _spkiKeyIdentity(parsed.subjectPublicKeyInfo.bytes) !== _spkiKeyIdentity(requestedSpki)) {
1095
659
  throw _err("cmp/bad-cert-response", "the issued certificate's public key does not match the requested key -- a misrouted certificate the caller has no private key for");
1096
660
  }
1097
661
  return cert;
1098
662
  }
1099
663
 
1100
- // The SPKI DER the enrollment requested, for the issued-cert key match: an ir/cr/kur certTemplate.publicKey
1101
- // (a SPKI Buffer), or a p10cr CSR's subjectPublicKeyInfo. null when the form is not a plain SPKI/CSR (the
1102
- // build path validates the key separately) or the CSR is unparseable (which fails at the build boundary).
1103
664
  function _extractRequestedSpki(arm, armSpec) {
1104
665
  if (arm === "p10cr") {
1105
666
  if (!guard.bytes.isByteSource(armSpec)) return null;
@@ -1107,41 +668,22 @@ function session(opts) {
1107
668
  catch (_e) { /* allow:swallow-unverified an unparseable p10cr CSR fails closed at the cmp.build boundary; the key-match is simply not applied to a request that never sends */ return null; }
1108
669
  }
1109
670
  var pk = armSpec && armSpec.certTemplate ? armSpec.certTemplate.publicKey : null;
1110
- // SNAPSHOT: this is held in session state across the transport round trip and then compared
1111
- // against the issued certificate's key, so an alias would let the caller rewrite the key the
1112
- // response is checked against after the request went out.
1113
671
  if (guard.bytes.isByteSource(pk)) return guard.bytes.snapshotSource(pk, CmpError, "cmp/bad-input", "the certTemplate publicKey");
1114
672
  return null;
1115
673
  }
1116
674
 
1117
- // Validate the issued leaf's SIGNATURE + chain (pki.schema.x509.parse is structural only, so a corrupted
1118
- // signature would otherwise be confirmed) whenever trust anchors are supplied. A signature session always has
1119
- // them (required); a MAC session validates the issued cert ONLY when the caller opts in with anchors -- the
1120
- // response MAC authenticates the exchange, NOT the embedded X.509 signature, so without anchors an invalid-sig
1121
- // MAC-issued cert would otherwise be confirmed. The pool prioritizes the AUTHENTICATED, leaf-relevant caPubs
1122
- // (the CA's OWN issuer chain for this leaf) as the base -- never evicted by a caller pool that fills the
1123
- // ceiling -- then the caller's intermediates, then the cached protection-signer chain, deduped + ceiling-bounded.
1124
675
  async function _validateLeaf(leaf, caPubs) {
1125
676
  if (_engine == null || opts.trustAnchors == null) return;
1126
677
  var caPubsList = [];
1127
678
  if (Array.isArray(caPubs)) caPubs.forEach(function (c) { if (Buffer.isBuffer(c)) caPubsList.push(c); });
1128
- var added = _asCertList(opts.intermediates); // the caller pool (config-bounded to SESSION_MAX_INTERMEDIATES)
1129
- if (Buffer.isBuffer(cachedSignerCert)) added.push(cachedSignerCert); // the CMP signer MAY be the issued leaf's issuer (a combined CA)
1130
- // cachedChain holds the VALIDATED signer chain from cmp.verify as independent DER copies (or delivered
1131
- // extraCerts DER on the defensive fallback) -- path.build's coerceCert parses each, so add them directly.
679
+ var added = _asCertList(opts.intermediates);
680
+ if (Buffer.isBuffer(cachedSignerCert)) added.push(cachedSignerCert);
1132
681
  cachedChain.forEach(function (c) { if (Buffer.isBuffer(c)) added.push(c); });
1133
- // ONE candidate pool holds everything: the AUTHENTICATED caPubs (the CA's OWN issuer chain for this leaf) as the
1134
- // priority base -- never evicted -- then the whole caller pool + cached signer chain. All fit under the ceiling
1135
- // (caPubs <= CAPUBS_MAX, cached chain <= MAX_EXTRA_CERTS, caller <= SESSION_MAX_INTERMEDIATES = ceiling minus
1136
- // both), so a path assembled from the caPubs AND a late caller intermediate builds in this single attempt.
1137
682
  var pool = _boundedPool(caPubsList, added);
1138
683
  var res;
1139
684
  try { res = await _engine.build(leaf, { trustAnchors: _asCertList(opts.trustAnchors), intermediates: pool, validate: true, time: opts.time != null ? opts.time : new Date() }); }
1140
685
  catch (e) {
1141
686
  if (e.code === "path/bad-input") throw _err("cmp/bad-input", "invalid trust / validation options for issued-certificate validation: " + (e.message || e), e);
1142
- // path.build THROWS path/no-path when the issued leaf cannot chain to a supplied anchor -- a CA that grants a
1143
- // certificate whose issuer it never delivered. Re-type it to the domain error so the transaction fails closed
1144
- // before confirmation, never leaking a path/* code.
1145
687
  throw _err("cmp/bad-cert-response", "the issued certificate could not be validated to a supplied trust anchor: " + (e.message || e), e);
1146
688
  }
1147
689
  if (!res || res.valid !== true) throw _err("cmp/bad-cert-response", "the issued certificate did not validate to a supplied trust anchor (its signature or chain is invalid) (RFC 5280 sec. 6.1)");
@@ -1149,12 +691,6 @@ function session(opts) {
1149
691
 
1150
692
  async function _finish(granted, header) {
1151
693
  var leaf = _leafOf(granted.resp);
1152
- // The returned chain is the issued leaf followed by the authenticated caPubs accumulated across the WHOLE
1153
- // transaction (the issuer certs the CA delivered on any ip/cp/kup leg, RFC 9810 sec. 5.3.4) -- surfaced so a
1154
- // caller whose intermediate arrived only on a `waiting` leg can still assemble the full chain. They are chain
1155
- // material, NOT trust anchors. The CMP parser surfaces each caPubs entry as a raw sequence, so validate every
1156
- // one as X.509 BEFORE the certConf acknowledges the grant -- the returned chain must never carry bytes
1157
- // pki.schema.x509.parse rejects -- and deduplicate a chain re-delivered across the waiting + granting legs.
1158
694
  var chain = [leaf], seenChain = Object.create(null);
1159
695
  caPubsAccum.forEach(function (c) {
1160
696
  if (!Buffer.isBuffer(c)) return;
@@ -1165,16 +701,10 @@ function session(opts) {
1165
701
  if (k != null) seenChain[k] = 1;
1166
702
  chain.push(c);
1167
703
  });
1168
- await _validateLeaf(leaf, caPubsAccum); // verify the leaf's signature + chain BEFORE confirming it
704
+ await _validateLeaf(leaf, caPubsAccum);
1169
705
  var info = { status: PKI_STATUS_NAMES[granted.code] || granted.code, grantedWithMods: granted.code === 1 };
1170
706
  var conf = await _confirm(leaf, header, info);
1171
- // Surface COPIES of the issued certificate + chain, not the internal Buffers (slices of the response bytes),
1172
- // so a caller mutating the returned value cannot reach back into session state -- consistent with the
1173
- // transactionID + transcript defensive copies.
1174
707
  var certOut = Buffer.from(leaf), chainOut = chain.map(function (c) { return Buffer.from(c); });
1175
- // A caller acceptance-policy veto (typically of a grantedWithMods certificate) is a terminal `rejected`
1176
- // outcome that still surfaces the certificate the caller inspected -- the accepting certConf was replaced by
1177
- // a rejecting one (explicit path) so the CA does not treat it as delivered.
1178
708
  if (conf.rejected) {
1179
709
  return _terminal("rejected", { certificate: certOut, chain: chainOut, status: _statusOf(granted.resp), polls: granted.polls || 0 });
1180
710
  }
@@ -1184,23 +714,15 @@ function session(opts) {
1184
714
  });
1185
715
  }
1186
716
 
1187
- // One transaction per session (RFC 9810 sec. 5.1.1: one transactionID per transaction). A second or
1188
- // concurrent call on any verb would reuse this transaction's transactionID / nonce chain / certReqId --
1189
- // a replay or a corrupted interleave -- so it is refused. Every verb checks this BEFORE marking the
1190
- // transaction in flight, so a malformed-request call does not consume the session.
1191
717
  function _assertFresh(what) {
1192
718
  if (completed || inFlight) {
1193
719
  throw _err("cmp/bad-input", "this pki.cmp.session transaction is already " + (completed ? "completed" : "in flight") + "; create a new session per " + what + " (RFC 9810 sec. 5.1.1: one transactionID per transaction)");
1194
720
  }
1195
721
  }
1196
722
 
1197
- // The shared tail of a non-enrollment transaction: send the one request, poll through a delayed
1198
- // delivery, and hand back the terminal classification. Both verbs reach a verdict the same way, so
1199
- // the poll budget, the nonce chaining and the verify-before-read ordering cannot differ between them.
1200
723
  async function _runOneShot(bodySpec, arm, kind) {
1201
724
  txnKind = kind;
1202
725
  expectedRespArm = RESPONSE_ARM[arm];
1203
- // A pollReq raised by this transaction refers to the whole message (RFC 9483 sec. 4.4).
1204
726
  activeCertReqId = WHOLE_MESSAGE_CERT_REQ_ID;
1205
727
  var verdict = await _send(bodySpec, arm);
1206
728
  var t = _classify(verdict);
@@ -1214,69 +736,44 @@ function session(opts) {
1214
736
  return { done: t, polls: polls };
1215
737
  }
1216
738
 
1217
- // The transaction driver: build the initial request arm, send it, then poll / confirm / terminate.
1218
739
  async function enroll(request) {
1219
740
  _assertFresh("enrollment");
1220
741
  guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "enroll(request), a body spec { ir | cr | kur | p10cr },");
1221
- // A key here that is not an arm needs no door of its own: this whole object becomes the message
1222
- // body, and pki.cmp.build counts the body's keys without filtering out the null-valued ones, so
1223
- // a misspelling set to null is already refused there ("must have exactly one arm, got 2"). The
1224
- // info() request is the opposite case and does carry a door, because that object is read for a
1225
- // name and never handed to the builder.
1226
742
  var arms = Object.keys(request).filter(function (k) { return request[k] != null; });
1227
743
  if (arms.length !== 1 || !ENROLL_ARMS[arms[0]]) {
1228
744
  throw _err("cmp/bad-input", "enroll(request) must carry EXACTLY ONE enrollment arm (ir / cr / kur / p10cr)");
1229
745
  }
1230
- // A batched CRMF request ({ messages: [...] }) asks for several certificates in one message; this session
1231
- // drives ONE request (single transactionID, one certReqId, one certConf), so a batch is refused rather than
1232
- // silently confirming only the first CertResponse and leaving the rest unprocessed. Submit one per session.
1233
746
  var armSpec = request[arms[0]];
1234
747
  if (armSpec && typeof armSpec === "object" && !Buffer.isBuffer(armSpec) && armSpec.messages != null) {
1235
748
  throw _err("cmp/bad-input", "a batched CRMF request ({ messages: [...] }) is not supported by a session -- submit one certificate request per pki.cmp.session");
1236
749
  }
1237
- // The session enrolls a CLIENT-submitted key: an ir / cr / kur MUST carry certTemplate.publicKey (a p10cr
1238
- // carries it in the CSR). A keyless request (e.g. raVerified POP with no publicKey, or central key
1239
- // generation) is refused -- else the session would confirm an arbitrary issued key it can neither match
1240
- // to a submitted key nor return a private key for. Checked before the transaction engages the transport.
1241
750
  var reqSpki = _extractRequestedSpki(arms[0], armSpec);
1242
751
  if (arms[0] !== "p10cr" && reqSpki == null) {
1243
752
  throw _err("cmp/bad-input", "an ir / cr / kur enrollment must submit certTemplate.publicKey -- a session enrolls a client-generated key (a raVerified keyless request or central key generation is not supported)");
1244
753
  }
1245
- // The session proves possession by SIGNING the CRMF proof of possession with the requested key (opts.key for
1246
- // a signature session, the arm-local `key` for a MAC session). A raVerified / other non-signature POP override
1247
- // would make crmf.build emit THAT mode and skip the signature entirely -- no requester proof of possession --
1248
- // so refuse it: it would also bypass the MAC key requirement below (a present key is ignored under raVerified).
1249
754
  if (arms[0] !== "p10cr" && armSpec != null && armSpec.pop != null && armSpec.pop.type != null && armSpec.pop.type !== "signature") {
1250
755
  throw _err("cmp/bad-input", "a session ir / cr / kur proves possession by signing the CRMF proof of possession; a non-signature POP mode (" + JSON.stringify(armSpec.pop.type) + ") is not supported (RFC 4211 sec. 4)");
1251
756
  }
1252
- // A signature session signs the CRMF proof of possession with opts.key; a PBMAC1 session has NO signing key,
1253
- // so an ir / cr / kur under MAC protection MUST carry the requested key's private half as the arm-local
1254
- // `key`. Without it crmf.build emits NO proof of possession (RFC 4211 sec. 4) and a POP-enforcing CA rejects
1255
- // the request. crmf.build then verifies the POP signature against certTemplate.publicKey, so a WRONG key
1256
- // fails at build -- here we only require the key is present. (A p10cr proves possession via the CSR signature.)
1257
757
  if (isMac && arms[0] !== "p10cr" && (armSpec == null || armSpec.key == null)) {
1258
758
  throw _err("cmp/bad-input", "a MAC-protected ir / cr / kur must carry the requested key's private half as `key` for the CRMF proof of possession -- a signature session reuses opts.key, but a PBMAC1 session has no signing key, so the request would carry no proof of possession (RFC 4211 sec. 4)");
1259
759
  }
1260
760
  inFlight = true;
1261
761
  try {
1262
762
  txnKind = "enroll";
1263
- expectedRespArm = RESPONSE_ARM[arms[0]]; // ir->ip, cr/p10cr->cp, kur->kup: the arm this request must be answered by
1264
- // The certReqId the session echoes in pollReq / certConf and matches in every CertResponse: the caller's
1265
- // CRMF request id (ir / cr / kur) when supplied, else the arm default -- a p10cr has no CRMF id, so a
1266
- // conforming cp identifies it with the -1 sentinel (RFC 9483), while ir / cr / kur default to 0.
763
+ expectedRespArm = RESPONSE_ARM[arms[0]];
1267
764
  var cid = (typeof armSpec === "object" && !Buffer.isBuffer(armSpec)) ? armSpec.certReqId : undefined;
1268
765
  activeCertReqId = _normalizeCertReqId(cid, arms[0] === "p10cr" ? P10CR_CERT_REQ_ID : DEFAULT_CERT_REQ_ID);
1269
- requestedSpki = reqSpki; // the key the issued cert must carry (RFC 4211 key match)
766
+ requestedSpki = reqSpki;
1270
767
  var initial = await _send(request, arms[0]);
1271
768
  var t = _classify(initial);
1272
- var grantHeader = initial.header; // the header of the response that produced the terminal classification `t`
769
+ var grantHeader = initial.header;
1273
770
  var pollCount = 0;
1274
771
  if (t.state === "waiting") {
1275
772
  var polled = await _pollLoop(t.resp);
1276
773
  pollCount = polled.polls;
1277
774
  if (polled.timeout) return _terminal("poll-timeout", { status: polled.status, polls: pollCount });
1278
775
  t = polled.done;
1279
- grantHeader = polled.header; // the GRANTING (post-poll) response's header -- where an implicitConfirm grant lives
776
+ grantHeader = polled.header;
1280
777
  t.polls = pollCount;
1281
778
  }
1282
779
  if (t.state === "granted") { var r = await _finish({ resp: t.resp, polls: pollCount, code: t.code }, grantHeader); r.polls = pollCount; return r; }
@@ -1284,19 +781,10 @@ function session(opts) {
1284
781
  throw _err("cmp/unexpected-arm", t.reason || "the enrollment transaction reached an unexpected state");
1285
782
  } finally {
1286
783
  inFlight = false;
1287
- // Consume the session only if a request may have reached the transport. A purely LOCAL failure (a build
1288
- // error before the first transfer -- e.g. a missing CRMF template) leaves the session retryable.
1289
784
  completed = started;
1290
785
  }
1291
786
  }
1292
787
 
1293
- // RFC 9483 sec. 4.2: "The revocation request message MUST be signed using the certificate that is to
1294
- // be revoked to prove the authorization to revoke." The signature over this message IS the
1295
- // authorization, so the certificate named in certDetails has to be the one this session protects with.
1296
- // Both request shapes are compared here, through the same encode-then-read path, so the certificate
1297
- // form and the explicit certDetails form cannot diverge on what counts as the same certificate.
1298
- // The issuer comparison is the RFC 5280 sec. 7.1 canonical one, since two spellings of a
1299
- // distinguished name name the same issuer and a byte compare would refuse a caller who wrote it out.
1300
788
  function _assertRevokingOwnCertificate(certTemplateDer) {
1301
789
  var own;
1302
790
  try {
@@ -1311,16 +799,9 @@ function session(opts) {
1311
799
  if (!sameSerial || !sameIssuer) {
1312
800
  throw _err("cmp/bad-input", "a session revokes ITS OWN certificate: the issuer and serialNumber in certDetails must name opts.cert, because the signature over the request is the proof of authorization to revoke (RFC 9483 sec. 4.2). Revocation on behalf of another entity is an RA operation -- drive it with pki.cmp.build + pki.cmp.transfer.");
1313
801
  }
1314
- return own; // the parsed session certificate, so the response's revCerts can be bound to it
802
+ return own;
1315
803
  }
1316
804
 
1317
- // An rp MAY name the certificates it revoked in revCerts (RFC 4210 RevRepContent). This session
1318
- // revokes exactly one -- its own -- so a conforming rp names exactly that one, and a response that
1319
- // names another issuer/serial, or several, is not a verdict about this request. Left unbound, a
1320
- // verified verdict would report an unrelated certificate as revoked. Compared to the session
1321
- // certificate `own` the request was already held to, by serialNumber and the RFC 5280 sec. 7.1
1322
- // canonical issuer rule. A CertId issuer is a GeneralName; a certificate's issuer is a directoryName,
1323
- // so any other arm cannot name the revoked certificate.
1324
805
  function _bindRevCerts(revCerts, own) {
1325
806
  if (revCerts == null) return null;
1326
807
  if (revCerts.length !== 1) {
@@ -1336,21 +817,12 @@ function session(opts) {
1336
817
  return revCerts;
1337
818
  }
1338
819
 
1339
- /**
1340
- * Request revocation of this session's own certificate (RFC 9483 sec. 4.2).
1341
- * `request` is `{ certificate }` or `{ certDetails: { issuer, serialNumber } }`, plus an optional
1342
- * `reason` (a CRLReason name). Returns a terminal verdict: `revoked`, `rejected`, or `poll-timeout`.
1343
- */
820
+
1344
821
  async function revoke(request) {
1345
822
  _assertFresh("revocation");
1346
823
  guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "revoke(request), an object { certificate | certDetails, reason? },");
1347
824
  guard.identifier.assertKnownKeys(request, KNOWN_REVOKE_KEYS, _err, "cmp/bad-input", "unknown revoke request field ");
1348
- // A PBMAC1 session shares a secret with the CA; it holds no certificate, so it has nothing to sign
1349
- // the request with and no way to demonstrate which certificate it is entitled to revoke.
1350
825
  if (isMac) throw _err("cmp/bad-input", "a PBMAC1 session cannot revoke: the request must be signature-protected with the certificate being revoked (RFC 9483 sec. 4.2). Use a signature session ({ key, cert }).");
1351
- // Each field is read ONCE, here, and every use below is of the value that was read. A caller
1352
- // field can be an accessor, so a second read can answer with a different certificate or reason
1353
- // from the one that was checked, and the message would carry what the checks never saw.
1354
826
  var certificateArg = request.certificate, reasonArg = request.reason;
1355
827
  var certDetails = request.certDetails;
1356
828
  if ((certificateArg == null) === (certDetails == null)) {
@@ -1369,8 +841,6 @@ function session(opts) {
1369
841
  }
1370
842
  certDetails = { issuer: target.issuer.bytes, serialNumber: target.serialNumber };
1371
843
  }
1372
- // Encode the CertTemplate HERE rather than letting cmp.build do it, so the identity check below
1373
- // reads the same bytes the message will carry instead of a second interpretation of the spec.
1374
844
  var certTemplateDer;
1375
845
  try {
1376
846
  certTemplateDer = crmfSign.buildCertTemplate(certDetails);
@@ -1379,8 +849,6 @@ function session(opts) {
1379
849
  throw _err("cmp/bad-rev-req", "revoke request.certDetails must be a CertTemplate naming issuer and serialNumber -- " + ((e && e.message) || e), e);
1380
850
  }
1381
851
  var own = _assertRevokingOwnCertificate(certTemplateDer);
1382
- // sec. 4.2: exactly one RevDetails, and crlEntryDetails REQUIRED carrying one reasonCode. The
1383
- // builder emits the reasonCode even at unspecified(0), which the same clause requires.
1384
852
  var body = { rr: [{ certDetails: certTemplateDer, crlEntryDetails: { reason: reasonArg == null ? undefined : reasonArg } }] };
1385
853
  inFlight = true;
1386
854
  try {
@@ -1390,10 +858,7 @@ function session(opts) {
1390
858
  if (t.state === "granted") {
1391
859
  return _terminal("revoked", {
1392
860
  status: t.status, polls: out.polls,
1393
- revokedCerts: _bindRevCerts(t.revCerts, own), // the CertIds the responder confirms it revoked, bound to the certificate this session revoked
1394
- // An rp MAY deliver CRLs, and the schema reads those slots as raw SEQUENCEs for the same
1395
- // reason it does in a genp. They are surfaced on a verdict an operator acts on, so they
1396
- // are held to being CertificateLists here, exactly as the support-message values are.
861
+ revokedCerts: _bindRevCerts(t.revCerts, own),
1397
862
  crls: (t.crls || []).map(_asCrl),
1398
863
  });
1399
864
  }
@@ -1405,22 +870,11 @@ function session(opts) {
1405
870
  }
1406
871
  }
1407
872
 
1408
- /**
1409
- * Ask the PKI management entity for one of the RFC 9483 sec. 4.3 support values.
1410
- * `request` names exactly one operation: `{ caCerts: true }`, `{ rootCaCert: <cert> }`,
1411
- * `{ certReqTemplate: true }`, or `{ crlUpdate: { issuer, dpn?, thisUpdate? } }`.
1412
- * Returns a terminal verdict: `answered` (with `value` and `present`), `rejected`, or `poll-timeout`.
1413
- */
873
+
1414
874
  async function info(request) {
1415
875
  _assertFresh("support message");
1416
876
  guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "info(request), an object naming one support operation (caCerts / rootCaCert / certReqTemplate / crlUpdate),");
1417
- // The name door runs BEFORE the arm count, so a key that is not an operation is refused whatever
1418
- // its value is. Counting non-null keys alone would let a misspelled one set to null pass as
1419
- // absent, and the caller would watch a different operation go out than the one they wrote.
1420
877
  guard.identifier.assertKnownKeys(request, INFO_OPS, _err, "cmp/bad-input", "unknown info request field ");
1421
- // Each field is read ONCE, here, and everything below uses the value that was read. A caller
1422
- // field can be an accessor, so a second read can answer with a different request from the one
1423
- // the arm count was taken over, and what goes on the wire would not be what was checked.
1424
878
  var keys = Object.keys(request), names = [], values = [];
1425
879
  for (var ki = 0; ki < keys.length; ki++) {
1426
880
  var kv = request[keys[ki]];
@@ -1433,36 +887,15 @@ function session(opts) {
1433
887
  }
1434
888
  var op = INFO_OPS[names[0]], asked = values[0];
1435
889
  var itav = { infoType: op.requestOid };
1436
- // sec. 4.3.1 and sec. 4.3.3 both make the request infoValue absent; the other two carry one.
1437
- // For the two that carry none, the only accepted value is `true` -- a caller who supplies a
1438
- // certificate or a template to one of them means something this operation cannot send, and
1439
- // taking it silently would have the request go out without the constraint they wrote.
1440
890
  if (op.value === false) {
1441
891
  if (asked !== true) throw _err("cmp/bad-input", "info request." + op.name + " takes `true`: this operation's genm carries no infoValue (RFC 9483 sec. 4.3.1 and sec. 4.3.3)");
1442
892
  } else if (op.value === "cert") {
1443
- // sec. 4.3.2: the request SHOULD carry the root CA certificate the update is requested for,
1444
- // and the field is a CMPCertificate. It goes through the SAME door revoke() gives a
1445
- // certificate -- DER, PEM, or an already-parsed one -- because it is the same kind of value
1446
- // and a caller should not have to remember which of the two verbs takes which form. Holding
1447
- // it to being a certificate here keeps a mistake a local refusal; sending it unchecked would
1448
- // put a malformed request on the wire and consume this one-shot transaction to learn what the
1449
- // caller could have been told before it started.
1450
893
  var rootCaCertDer = _certificateArgument(asked, "info request rootCaCert");
1451
- // sec. 4.3.2: rootCaCert names the CURRENT root the update chains from, which is a CA. An
1452
- // end-entity certificate here would let a cross-certificate signed by that non-CA key read as a
1453
- // rollover (the response path verifies newWithOld / oldWithNew against this certificate's key), so
1454
- // it is held to being a CA before the one-shot transaction engages rather than after.
1455
894
  _assertCaCapable(x509.parse(rootCaCertDer), "info request rootCaCert", "cmp/bad-input");
1456
895
  itav.infoValue = rootCaCertDer;
1457
896
  } else if (op.value === "crlStatus") {
1458
897
  var built = cmpBuild.buildCrlStatusList(asked);
1459
898
  itav.infoValue = built.der;
1460
- // What the request asked for, kept so the answer can be held to it. sec. 4.3.4 says the
1461
- // responder returns the latest CRL FROM THE REFERENCED SOURCE, and only when it is more
1462
- // recent than a supplied thisUpdate; in every other case the response value is absent.
1463
- // The cutoff comes back FROM the builder, which read it once while encoding. Reading the
1464
- // caller's field a second time here would let an accessor, or a Date moved during the
1465
- // transport round-trip, judge the response against a query the request never carried.
1466
899
  crlQuery = { issuerName: built.issuerName, thisUpdate: built.thisUpdate, dpn: built.dpn };
1467
900
  }
1468
901
  expectedInfoOp = op;
@@ -1472,9 +905,6 @@ function session(opts) {
1472
905
  if (out.timeout) return _terminal("poll-timeout", { operation: op.name, status: out.status, polls: out.polls });
1473
906
  var t = out.done;
1474
907
  if (t.state === "granted") {
1475
- // sec. 4.3: the infoValue is OPTIONAL in every one of the four responses, and its absence is
1476
- // the answer "nothing is available" rather than a malformed message -- so `present` reports
1477
- // which of the two a caller received, and never conflates them into an empty value.
1478
908
  var raw = t.itav.value;
1479
909
  var value = null;
1480
910
  if (raw != null) {
@@ -1496,25 +926,13 @@ function session(opts) {
1496
926
  }
1497
927
  }
1498
928
 
1499
- // The RFC 9483 profile rules that sit ON TOP of the RFC 9480 syntax the decoders enforce. Each is a
1500
- // requirement the ASN.1 cannot express, so a structurally valid response can still be non-conforming.
1501
- // `requestValue` is the infoValue this operation SENT, which the rootCaCert rules compare against:
1502
- // the old root the caller named is what the update has to chain from. It is read here rather than
1503
- // off `request`, so the comparison uses the bytes that went on the wire.
1504
929
  function _checkInfoValue(op, value, requestValue) {
1505
930
  if (op.name === "rootCaCert") {
1506
- // sec. 4.3.2 makes newWithOld REQUIRED of a response ("it is needed for the receiving entity
1507
- // trusting the old root CA certificate to gain trust in the new one"), where RFC 9480's ASN.1
1508
- // marks it OPTIONAL. Without it the update cannot be trusted by the entity that asked for it.
1509
931
  if (value.newWithOld == null) throw _err("cmp/bad-info-value", "a rootCaKeyUpdate response must carry newWithOld -- the certificate that lets an entity trusting the OLD root gain trust in the new one (RFC 9483 sec. 4.3.2)");
1510
932
  return _checkRootCaKeyUpdate(value, requestValue);
1511
933
  }
1512
934
  if (op.name === "certReqTemplate") {
1513
935
  var t = value.certTemplate;
1514
- // sec. 4.3.3: "The publicKey field of type SubjectPublicKeyInfo in the CertTemplate of the
1515
- // CertReqTemplateValue MUST be omitted", and "the serialNumber, signingAlg, issuerUID, and
1516
- // subjectUID fields MUST be omitted". A template stating a key would tell an entity to request
1517
- // a certificate over a key the responder chose, which is what the keySpec field exists to avoid.
1518
936
  ["publicKey", "serialNumber", "signingAlg", "issuerUID", "subjectUID"].forEach(function (f) {
1519
937
  if (t[f] != null) throw _err("cmp/bad-info-value", "a certReqTemplate certTemplate must omit " + f + " (RFC 9483 sec. 4.3.3)");
1520
938
  });
@@ -1522,44 +940,17 @@ function session(opts) {
1522
940
  return value;
1523
941
  }
1524
942
  if (op.name === "caCerts") return value.map(function (d) {
1525
- // sec. 4.3.1 returns CA certificates for chain construction, so each is held to being one: an
1526
- // authenticated responder placing an end-entity certificate here would hand back a list nothing
1527
- // can chain through. The same CA-capability rule the rootCaKeyUpdate rollover certificates get.
1528
943
  var der = _asCertificate(d, "a caCerts entry");
1529
944
  _assertCaCapable(x509.parse(der), "caCerts entry");
1530
945
  return der;
1531
946
  });
1532
947
  if (op.name === "crlUpdate") {
1533
- // sec. 4.3.4: the request carries exactly one CRLStatus (one source), and the response "represents
1534
- // the latest CRL available from the referenced CRL source" -- one CRL. The ASN.1 list is SIZE
1535
- // (1..MAX), so the responder could return several; accepting them all would surface an older CRL
1536
- // beside the latest and let a caller act on a stale value[0]. Exactly one is required, and it is
1537
- // then bound to the query that asked for it.
1538
948
  if (value.length !== 1) throw _err("cmp/bad-info-value", "a crlUpdate response answers a single-source query with exactly one CRL -- the latest from the named source (RFC 9483 sec. 4.3.4); got " + value.length);
1539
949
  return value.map(_bindCrlToQuery);
1540
950
  }
1541
- // Unreachable while INFO_OPS holds exactly the four operations above, each with a branch here.
1542
- // It refuses rather than passing the value through, so adding a fifth operation without its
1543
- // profile rules fails at once instead of surfacing a value nothing checked.
1544
951
  throw _err("cmp/bad-info-value", "no response rules are defined for the " + op.name + " support message");
1545
952
  }
1546
953
 
1547
- // A support-message response is surfaced as an ANSWER an operator acts on, so what it carries is
1548
- // held to being the structure it claims. The CMP schema reads these slots as raw SEQUENCEs on
1549
- // purpose (the parser confers no trust and a CMPCertificate slot is opaque to it), which leaves
1550
- // "is this actually a certificate" to the verb that hands the bytes back -- the same division
1551
- // this module already applies to an authenticated caPubs entry. Without it a responder could
1552
- // answer a chain-construction request with any well-formed SEQUENCE and have it read as a
1553
- // certificate, and a CRL request with a certificate. Returns a COPY, since the decoded value is
1554
- // a slice of the response buffer.
1555
- // A root CA key update is three certificates in NAMED relationships, and the relationships are the
1556
- // whole mechanism (RFC 9483 sec. 4.3.2): newWithOld carries "the new public root CA key signed
1557
- // with the old private root CA key", which is what lets an entity that trusts the old root reach
1558
- // the new one, and oldWithNew carries "the old public root CA key signed with the new private root
1559
- // CA key" for the reverse. Three certificates that merely parse establish neither, so a responder
1560
- // could answer with any three it holds and the update would be reported as usable. Each signature
1561
- // is checked by the path engine, so the EdDSA low-order-point and algorithm-confusion gates apply
1562
- // here as they do to any other untrusted signature.
1563
954
  async function _checkRootCaKeyUpdate(value, oldRootDer) {
1564
955
  var newWithNew = _asCertificate(value.newWithNew, "the rootCaKeyUpdate newWithNew");
1565
956
  var newWithOld = _asCertificate(value.newWithOld, "the rootCaKeyUpdate newWithOld");
@@ -1570,22 +961,8 @@ function session(opts) {
1570
961
  if (!cmpBuild.samePublicKey(pNewOld.subjectPublicKeyInfo.bytes, pNewNew.subjectPublicKeyInfo.bytes)) {
1571
962
  throw _err("cmp/bad-info-value", "the rootCaKeyUpdate newWithOld must certify the NEW root key, the one newWithNew carries (RFC 9483 sec. 4.3.2)");
1572
963
  }
1573
- // A key alone does not make a trust transition. If the old CA has ever signed ANY certificate
1574
- // for the new key -- an ordinary end-entity certificate its holder legitimately obtained --
1575
- // then key equality plus a valid signature is satisfied by that certificate, and its holder
1576
- // could pair it with a self-signed newWithNew of their own choosing and have the result read as
1577
- // the authority's own rollover. What sec. 4.3.2 describes is a certificate FOR the new root
1578
- // issued BY the old one, so both names are bound too, under the RFC 5280 sec. 7.1 comparison.
1579
964
  _assertSameName(pNewOld.issuer, pOldRoot.subject, "newWithOld", "be issued by the OLD root named in the request");
1580
965
  _assertSameName(pNewOld.subject, pNewNew.subject, "newWithOld", "name the same subject as newWithNew, the certificate it vouches for");
1581
- // newWithNew is the replacement certificate, and for a root it is self-issued, so its own key is
1582
- // the only thing that can vouch for the name, validity and extensions the caller would install.
1583
- // sec. 4.3.2's note extends this operation to "trusted non-root certificates, e.g., directly
1584
- // trusted intermediate or issuing CA certificates", and such a certificate is signed by an
1585
- // issuer this message does not carry -- there is nothing here to check it against, and claiming
1586
- // otherwise would refuse a use the profile allows. So the signature is verified exactly when it
1587
- // is verifiable: when the certificate is self-issued. Either way the new KEY is authenticated by
1588
- // newWithOld below, which the old root signed and which must carry this same key.
1589
966
  if (guard.name.dnEqual(pNewNew.issuer.rdns, pNewNew.subject.rdns, _err, "cmp/bad-info-value", "the rootCaKeyUpdate newWithNew name")) {
1590
967
  await _assertSignedBy(pNewNew, pNewNew, "newWithNew", "its own key, which a self-issued root certificate must be");
1591
968
  }
@@ -1602,18 +979,8 @@ function session(opts) {
1602
979
  }
1603
980
  return { newWithNew: newWithNew, newWithOld: newWithOld, oldWithNew: oldWithNew };
1604
981
  }
1605
- // A certificate a caller will build a chain from -- a rootCaKeyUpdate rollover certificate or a
1606
- // caCerts entry -- has to actually be a CA. Names, a key and a signature are satisfied by an
1607
- // ordinary end-entity certificate that certifies nothing, so surfacing one as an issuer reports the
1608
- // answer as usable while handing back a list nothing can chain through. The rule is RFC 5280 sec.
1609
- // 6.1.4's, the one pki.path.validate applies to any intermediate: basicConstraints cA TRUE, marked
1610
- // critical (sec. 4.2.1.9), and keyUsage, where present, allowing keyCertSign. Criticality matters
1611
- // because a relying party that skips extensions it does not recognize would not see the cA bit at
1612
- // all, which is why the path validator refuses a non-critical cA:TRUE; this check holds to the same
1613
- // line so the two never disagree on what a CA certificate is. `which` names the surface for the
1614
- // error. Extensions are read through the shared certificate-extension decoders rather than walked here.
1615
982
  function _assertCaCapable(cert, which, code) {
1616
- code = code || "cmp/bad-info-value"; // a response cert is cmp/bad-info-value; a caller-supplied one (rootCaCert request) is cmp/bad-input
983
+ code = code || "cmp/bad-info-value";
1617
984
  var bc = null, ku = null, bcCritical = false;
1618
985
  cert.extensions.forEach(function (e) {
1619
986
  if (e.oid === OID_BASIC_CONSTRAINTS) { bc = cmpBuild.decodeCertExtension(e.oid, e.value); bcCritical = e.critical === true; }
@@ -1630,9 +997,6 @@ function session(opts) {
1630
997
  }
1631
998
  }
1632
999
 
1633
- // Names are compared through guard-name's RFC 5280 sec. 7.1 canonical rule, the one place a
1634
- // distinguished-name identity is decided here: two spellings of the same name are the same
1635
- // authority, and a byte compare would refuse a re-encoded but equal one.
1636
1000
  function _assertSameName(a, b, which, must) {
1637
1001
  if (!guard.name.dnEqual(a.rdns, b.rdns, _err, "cmp/bad-info-value", "a rootCaKeyUpdate name")) {
1638
1002
  throw _err("cmp/bad-info-value", "the rootCaKeyUpdate " + which + " must " + must + " (RFC 9483 sec. 4.3.2); got " + JSON.stringify(a.dn));
@@ -1642,10 +1006,6 @@ function session(opts) {
1642
1006
  if (!_engine || !_engine.verifyWithSpki) {
1643
1007
  throw _err("cmp/bad-info-value", "the signature engine is unavailable, so a rootCaKeyUpdate cannot be checked; require pki.path to install it");
1644
1008
  }
1645
- // The signature is a BIT STRING and a valid one is octet-aligned (no unused bits). Passing only its
1646
- // octets to the engine would drop the unusedBits metadata, so a signature declaring unused bits would
1647
- // verify on its octets while being a malformed shape the path verifier refuses (path-validate) before
1648
- // the engine. Hold a rollover signature to the same rule.
1649
1009
  if (!guard.crypto.isOctetAligned(cert.signatureValue)) {
1650
1010
  throw _err("cmp/bad-info-value", "the rootCaKeyUpdate " + which + " signature is not octet-aligned (a BIT STRING with unused bits), which no valid signature is (RFC 9483 sec. 4.3.2)");
1651
1011
  }
@@ -1655,11 +1015,6 @@ function session(opts) {
1655
1015
  }
1656
1016
  }
1657
1017
 
1658
- // A certificate this verb has to put BACK on the wire, so it takes the two forms that carry their
1659
- // own bytes: DER, and PEM which decodes to them. An already-parsed certificate is refused rather
1660
- // than re-encoded -- the parsed form retains no source DER, and rebuilding one would emit bytes
1661
- // that are not the ones the issuer signed. revoke() takes the parsed form because it reads an
1662
- // issuer and a serial out of it and never re-emits the certificate itself.
1663
1018
  function _certificateArgument(value, what) {
1664
1019
  var der;
1665
1020
  if (typeof value === "string") {
@@ -1673,9 +1028,6 @@ function session(opts) {
1673
1028
  return _asCertificate(der, what, "cmp/bad-input");
1674
1029
  }
1675
1030
 
1676
- // `code` names the boundary: a response value is malformed protocol (cmp/bad-info-value), while a
1677
- // value the CALLER supplied for a request is an authoring error (cmp/bad-input), and the two must
1678
- // not read alike to whoever catches them.
1679
1031
  function _asCertificate(der, what, code) {
1680
1032
  try { x509.parse(der); }
1681
1033
  catch (e) { throw _err(code || "cmp/bad-info-value", what + " is not a valid X.509 certificate (RFC 9483 sec. 4.3)", e); }
@@ -1686,17 +1038,6 @@ function session(opts) {
1686
1038
  catch (e) { throw _err("cmp/bad-info-value", "a crlUpdate entry is not a valid CertificateList (RFC 9483 sec. 4.3.4)", e); }
1687
1039
  return Buffer.from(der);
1688
1040
  }
1689
- // sec. 4.3.4 does not say "a CRL", it says the latest one FROM THE REFERENCED SOURCE, and only
1690
- // when it is more recent than a supplied thisUpdate; in every other case the value is absent.
1691
- // A CRL that answers some other query is not the answer to this one, and reporting it as one
1692
- // hands the caller a list whose scope they did not ask about.
1693
- //
1694
- // The issuer arm is bound by name. The dpn arm is bound where a CRL says which point it speaks
1695
- // for: RFC 5280 sec. 5.2.5 puts that in the issuingDistributionPoint extension, and sec. 4.3.4's
1696
- // own note points there for where a distribution point name comes from. A CRL carrying no such
1697
- // scope is claiming to be its issuer's complete list, which sec. 5.2.5 permits and which answers
1698
- // a request for any point of that issuer, so there is nothing to refuse -- and nothing to bind,
1699
- // since the request named no issuer. The freshness rule applies to both arms.
1700
1041
  function _bindCrlToQuery(der) {
1701
1042
  var parsed;
1702
1043
  try { parsed = schemaCrl.parse(der); }
@@ -1715,21 +1056,12 @@ function session(opts) {
1715
1056
  idpCritical = crlExts[xi].critical === true;
1716
1057
  break;
1717
1058
  }
1718
- // The comparison is the one the path validator applies to a shard CRL, so a CRL either speaks
1719
- // for the point this request named or it does not, and the answer does not depend on which verb
1720
- // is asking. Only a CRITICAL issuingDistributionPoint establishes that scope (sec. 5.2.5): a
1721
- // relying party may ignore a non-critical one, so a scope it could ignore cannot bind the answer
1722
- // to the named point -- the path validator makes the same fail-closed decision for a shard CRL.
1723
- // A CRL naming no point is a complete list and is left to the freshness rule.
1724
1059
  if (idpDpn !== null && (!idpCritical || !guard.name.dpnCorresponds(crlQuery.dpn, idpDpn, _err, "cmp/bad-info-value", "the crlUpdate distribution point"))) {
1725
1060
  throw _err("cmp/bad-info-value", "the returned CRL is scoped to a distribution point other than the one this request named, or marks that scope non-critical where a relying party may ignore it (RFC 9483 sec. 4.3.4, RFC 5280 sec. 5.2.5)");
1726
1061
  }
1727
1062
  }
1728
1063
  if (crlQuery && crlQuery.thisUpdate != null) {
1729
- var asked = crlQuery.thisUpdate; // already an instant, reduced when the request was built
1730
- // The CRL's own thisUpdate goes through the same door: it arrived from a responder, so it is
1731
- // the side more likely to be missing or unusable, and reading its epoch value directly would
1732
- // compare NaN and quietly answer false.
1064
+ var asked = crlQuery.thisUpdate;
1733
1065
  var got = guard.time.instantOf(parsed.thisUpdate, _err, "cmp/bad-info-value", "the returned CRL's thisUpdate");
1734
1066
  if (got <= asked) {
1735
1067
  throw _err("cmp/bad-info-value", "the returned CRL is no more recent than the thisUpdate this request supplied, so sec. 4.3.4 requires the response to carry no value at all");
@@ -1740,28 +1072,19 @@ function session(opts) {
1740
1072
 
1741
1073
  var OID_REG_CTRL_ALG_ID = oid.byName("algId");
1742
1074
  var OID_REG_CTRL_RSA_KEY_LEN = oid.byName("rsaKeyLen");
1743
- // True for an RSA algorithm, which sec. 4.3.3 excludes from a keySpec algId (RSA uses rsaKeyLen).
1744
- // Membership is decided by OID family: any OID under the PKCS#1 arc, plus the two RSA identifiers
1745
- // that sit off it. A standardized RSA OID the registry has not named (sha1WithRSAEncryption, the
1746
- // md*/sha224 variants) is still under the arc, so it is caught without being listed. An OID that is
1747
- // not an encodable dotted string, or one outside every RSA arc, is NOT RSA here -- it is surfaced.
1748
1075
  function _isRsaAlgorithm(dotted) {
1749
- if (RSA_ARC_EXCLUDE[dotted] === 1) return false; // mgf1 / pSpecified: auxiliary OIDs on the PKCS#1 arc, not RSA algorithms
1750
- var arcs = oid.toArcs(dotted); // dotted is a decoded OID string (readAlgorithmIdentifier), always encodable
1076
+ if (RSA_ARC_EXCLUDE[dotted] === 1) return false;
1077
+ var arcs = oid.toArcs(dotted);
1751
1078
  var underAny = false;
1752
1079
  for (var a = 0; !underAny && a < RSA_ARCS.length; a++) {
1753
1080
  var arc = RSA_ARCS[a];
1754
- if (arcs.length > arc.length) { // the arc node itself is not an algorithm
1081
+ if (arcs.length > arc.length) {
1755
1082
  underAny = true;
1756
1083
  for (var i = 0; underAny && i < arc.length; i++) { underAny = arcs[i] === arc[i]; }
1757
1084
  }
1758
1085
  }
1759
1086
  return underAny || RSA_OFF_ARC[dotted] === 1;
1760
1087
  }
1761
- // sec. 4.3.3: a keySpec element carries "attribute id-regCtrl-algId or id-regCtrl-rsaKeyLen", the
1762
- // first "MUST be of type AlgorithmIdentifier and give an algorithm other than RSA", the second
1763
- // "MUST be a positive integer value". A control outside the pair states a requirement this
1764
- // operation has no meaning for, so it is refused rather than surfaced as an understood one.
1765
1088
  function _checkKeySpec(ctrl) {
1766
1089
  if (ctrl.type === OID_REG_CTRL_RSA_KEY_LEN) {
1767
1090
  var len;
@@ -1772,27 +1095,17 @@ function session(opts) {
1772
1095
  return;
1773
1096
  }
1774
1097
  if (ctrl.type === OID_REG_CTRL_ALG_ID) {
1775
- // Read through the shared AlgorithmIdentifier sub-schema rather than probing the node, so the
1776
- // structure is held to its whole definition: a SEQUENCE of an OID and at most one parameters
1777
- // field. A hand-rolled tag test says only that the first child is an OID, which a SEQUENCE of
1778
- // three fields also satisfies -- and that value would then reach the caller as an algorithm
1779
- // requirement no conforming responder can have meant.
1780
1098
  var alg;
1781
1099
  try { alg = schemaCmp.readAlgorithmIdentifier(ctrl.value); }
1782
1100
  catch (e) {
1783
1101
  if (e && e.isCmpError) throw e;
1784
1102
  throw _err("cmp/bad-info-value", "a keySpec algId must be an AlgorithmIdentifier (RFC 9483 sec. 4.3.3)", e);
1785
1103
  }
1786
- // sec. 4.3.3 / RFC 9480 sec. 2.16 hold the algId to ONE rule: not RSA. A known RSA algorithm is
1787
- // refused (its requirement is stated with rsaKeyLen); every other value -- a known non-RSA
1788
- // algorithm OR an OID the registry does not name -- is surfaced. The responder offers one control
1789
- // per algorithm it supports and the entity picks one, so an unrecognized offer must not fail the
1790
- // whole exchange.
1791
1104
  if (_isRsaAlgorithm(alg.oid)) {
1792
1105
  throw _err("cmp/bad-info-value", "a keySpec algId must give an algorithm other than RSA, whose requirement is stated with rsaKeyLen instead (RFC 9483 sec. 4.3.3); got " + (alg.name || alg.oid));
1793
1106
  }
1794
1107
  ctrl.algorithm = alg.oid;
1795
- ctrl.algorithmName = alg.name; // null for an OID the registry does not name -- surfaced as-is for the caller to weigh
1108
+ ctrl.algorithmName = alg.name;
1796
1109
  ctrl.algorithmParameters = alg.parameters;
1797
1110
  return;
1798
1111
  }
@@ -1803,8 +1116,8 @@ function session(opts) {
1803
1116
  enroll: enroll,
1804
1117
  revoke: revoke,
1805
1118
  info: info,
1806
- get transactionID() { return Buffer.from(transactionID); }, // a copy: a caller mutating it must not desync the transaction
1807
- get transcript() { return _transcriptSnapshot(); }, // a defensive snapshot; never the mutable internal array
1119
+ get transactionID() { return Buffer.from(transactionID); },
1120
+ get transcript() { return _transcriptSnapshot(); },
1808
1121
  };
1809
1122
  }
1810
1123
 
@@ -1814,5 +1127,5 @@ module.exports = {
1814
1127
  wellKnownUrl: cmp.wellKnownUrl,
1815
1128
  verify: cmp.verify,
1816
1129
  session: session,
1817
- setEngine: setEngine, // @internal -- path-validate injects the path build/validate engine (issued-leaf validation)
1130
+ setEngine: setEngine, // @internal
1818
1131
  };