@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
package/lib/scep.js CHANGED
@@ -43,8 +43,8 @@ var cmsSign = require("./cms-sign");
43
43
  var cmsVerify = require("./cms-verify");
44
44
  var cmsEncrypt = require("./cms-encrypt");
45
45
  var cmsDecrypt = require("./cms-decrypt");
46
- require("./path-validate"); // side-effect: injects the certification-path signature engine into csr-verify at load
47
- var csrVerify = require("./csr-verify"); // verify an embedded PKCS#10's proof-of-possession before enveloping it
46
+ require("./path-validate");
47
+ var csrVerify = require("./csr-verify");
48
48
  var schemaCms = require("./schema-cms");
49
49
  var schemaCsr = require("./schema-csr");
50
50
  var schemaX509 = require("./schema-x509");
@@ -52,42 +52,26 @@ var schemaCrl = require("./schema-crl");
52
52
  var pkix = require("./schema-pkix");
53
53
  var guard = require("./guard-all");
54
54
  var C = require("./constants");
55
- var httpTransport = require("./http-transport"); // the injectable https transport for the client verbs (sec. 4)
55
+ var httpTransport = require("./http-transport");
56
56
  var nodeCrypto = require("node:crypto");
57
57
 
58
58
  function _err(code, msg, cause) { return new ScepError(code, msg, cause); }
59
59
 
60
- // The namespace pkix.keyUsageOf reads a certificate's keyUsage extension through.
61
60
  var _KU_NS = pkix.makeNS("scep", ScepError, oid);
62
61
 
63
- // The messageType values are a DATA table (RFC 8894 sec. 3.2.1.2, Table 3), never a switch: name <-> the
64
- // decimal ASCII a PrintableString carries. RFC 8894 renumbered the legacy draft (RenewalReq is 17, and
65
- // there is no GetCertInitial -- it is CertPoll 20).
66
62
  var MESSAGE_TYPE = { CertRep: "3", RenewalReq: "17", PKCSReq: "19", CertPoll: "20", GetCert: "21", GetCRL: "22" };
67
- // pkiStatus (Table 4): SUCCESS 0, FAILURE 2, PENDING 3 -- note there is NO value 1.
68
63
  var PKI_STATUS = { SUCCESS: "0", FAILURE: "2", PENDING: "3" };
69
- // failInfo (Table 5).
70
64
  var FAIL_INFO = { badAlg: "0", badMessageCheck: "1", badRequest: "2", badTime: "3", badCertId: "4" };
71
65
 
72
- // name -> code and code -> name, built once from each table so a lookup is a data read, not a switch.
73
66
  function _invert(table) { var o = Object.create(null); for (var k in table) if (Object.prototype.hasOwnProperty.call(table, k)) o[table[k]] = k; return o; }
74
67
  var MESSAGE_TYPE_BY_CODE = _invert(MESSAGE_TYPE), PKI_STATUS_BY_CODE = _invert(PKI_STATUS), FAIL_INFO_BY_CODE = _invert(FAIL_INFO);
75
68
 
76
- // The message types pki.scep.parse reads: the enrollment request pair a client builds and the CertRep
77
- // response it receives. The CertPoll / GetCert / GetCRL client queries are refused (see parse).
78
69
  var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1 };
79
70
 
80
- // The RFC 8894 sec. 3 EnvelopedData content-encryption profile. AES-128-CBC is the mandatory strong
81
- // cipher; the toolkit's own cms.encrypt refuses the weak legacy ciphers structurally, so the builder
82
- // only names the one it emits.
83
71
  var ENVELOPE_CIPHER = "aes-128-cbc";
84
72
 
85
- // The empty external content a conforming FAILURE / PENDING CertRep is verified against: its
86
- // pkcsPKIEnvelope is omitted, so the SignedData carries no encapsulated content (RFC 8894 sec. 3.3.2).
87
73
  var EMPTY_CONTENT = Buffer.alloc(0);
88
74
 
89
- // A 16-byte nonce (RFC 8894 sec. 3.2.1.5): a caller value copied at the door and validated to width, or
90
- // a fresh random one. The copy makes a caller value read after an await immune to concurrent mutation.
91
75
  function _nonce(v, label) {
92
76
  if (v == null) return nodeCrypto.randomBytes(16);
93
77
  var buf = guard.bytes.snapshotSource(v, ScepError, "scep/bad-nonce", label);
@@ -95,19 +79,12 @@ function _nonce(v, label) {
95
79
  return buf;
96
80
  }
97
81
 
98
- // A caller PrintableString for a transaction id: bounded (the RFC sets no length, so cap it against a DoS)
99
- // and restricted to the PrintableString character set (asn1.build.printable enforces the set).
100
82
  function _transactionId(v) {
101
83
  if (typeof v !== "string" || v.length === 0) throw _err("scep/bad-input", "transactionId must be a non-empty string (RFC 8894 sec. 3.2.1.1)");
102
84
  if (v.length > C.LIMITS.SCEP_TRANSACTION_ID_MAX) throw _err("scep/bad-input", "transactionId exceeds the " + C.LIMITS.SCEP_TRANSACTION_ID_MAX + "-character cap");
103
85
  return v;
104
86
  }
105
87
 
106
- // Assert some bytes are a CMS ContentInfo wrapping a well-formed EnvelopedData whose encrypted content
107
- // type is id-data -- the pkcsPKIEnvelope profile (RFC 8894 sec. 3.2, sec. 3.2.2). Delegated to the
108
- // strict CMS parser, and checked structurally without decryption, so an absent, malformed, or
109
- // off-profile envelope is refused for every envelope-carrying message whether or not a recipient key
110
- // was supplied.
111
88
  function _assertValidEnvelope(bytes, messageType) {
112
89
  if (bytes == null) throw _err("scep/missing-envelope", "the pkcsPKIEnvelope is mandatory for a " + messageType + " message but the signed content is absent (RFC 8894 sec. 3.2)");
113
90
  var parsed;
@@ -115,17 +92,10 @@ function _assertValidEnvelope(bytes, messageType) {
115
92
  catch (e) { throw _err("scep/missing-envelope", "the pkcsPKIEnvelope of a " + messageType + " message is not a decodable CMS EnvelopedData (RFC 8894 sec. 3.2)", e); }
116
93
  if (parsed.contentTypeName !== "envelopedData") throw _err("scep/missing-envelope", "the pkcsPKIEnvelope of a " + messageType + " message must be an EnvelopedData (RFC 8894 sec. 3.2), got " + parsed.contentTypeName);
117
94
  if (parsed.encryptedContentInfo.contentType !== oid.byName("data")) throw _err("scep/bad-envelope-content-type", "the pkcsPKIEnvelope's encrypted content type must be id-data (RFC 8894 sec. 3.2.2), got " + parsed.encryptedContentInfo.contentType);
118
- // The EnvelopedData must actually carry ciphertext: an inner envelope whose encryptedContent is absent
119
- // or a zero-length OCTET STRING has no messageData to recover (SCEP provides none out of band), so it
120
- // is refused (RFC 8894 sec. 3.2).
121
95
  var ct = parsed.encryptedContentInfo.encryptedContent;
122
96
  if (ct == null || ct.length === 0) throw _err("scep/missing-envelope", "the pkcsPKIEnvelope of a " + messageType + " message carries no encrypted content (RFC 8894 sec. 3.2)");
123
97
  }
124
98
 
125
- // Validate a SUCCESS CertRep's messageData as a degenerate certificates-only CMS SignedData (RFC 8894
126
- // sec. 3.4) and surface its certificates and CRLs raw, for the caller to path-validate. The issued
127
- // certificate(s) answer an enrollment; a CRL answers a GetCRL (sec. 3.3.4), so certificates and CRLs are
128
- // accepted independently and a payload carrying neither is refused.
129
99
  function _readIssuancePayload(der) {
130
100
  var r;
131
101
  try { r = schemaCms.parse(der); }
@@ -134,9 +104,6 @@ function _readIssuancePayload(der) {
134
104
  r.encapContentInfo.eContent !== null || r.signerInfos.length !== 0) {
135
105
  throw _err("scep/not-certs-only", "a SUCCESS CertRep messageData must be a degenerate certificates-only CMS SignedData (RFC 8894 sec. 3.4)");
136
106
  }
137
- // Validate each entry as a plain X.509 certificate / CRL: a universal-SEQUENCE CertificateChoice or
138
- // RevocationInfoChoice must be a well-formed certificate / CRL, and a tagged alternative (attribute
139
- // cert, other-message, otherRevInfo) is not a plain issuance object and is refused.
140
107
  var certs = [], crls = [], i;
141
108
  var rc = r.certificates || [], rl = r.crls || [];
142
109
  for (i = 0; i < rc.length; i++) {
@@ -157,14 +124,8 @@ function _readIssuancePayload(der) {
157
124
  return { certificates: certs, crls: crls };
158
125
  }
159
126
 
160
- // Build one SCEP transaction attribute { type, values:[valueDER] } for cms.sign's additionalSignedAttributes.
161
127
  function _attr(name, valueDer) { return { type: oid.byName(name), values: [valueDer] }; }
162
128
 
163
- // Find the single surfaced signed attribute of a given OID name (the values are raw AttributeValue
164
- // DER). The CMS parser rejects a repeated attribute type (cms/duplicate-signed-attr) before these
165
- // attributes are surfaced, so a SCEP transaction attribute appears at most once. It is also
166
- // single-valued: a value SET that does not hold exactly one value is an ambiguous transaction state
167
- // and is refused rather than resolved to the first value (RFC 8894 sec. 3.2.1).
168
129
  function _find(attrs, name) {
169
130
  var want = oid.byName(name);
170
131
  for (var i = 0; i < attrs.length; i++) {
@@ -176,9 +137,6 @@ function _find(attrs, name) {
176
137
  }
177
138
  return null;
178
139
  }
179
- // Read a string transaction attribute to its JS string, requiring the exact ASN.1 string type RFC 8894
180
- // sec. 3.2 pins for it (a generic string reader would accept an off-profile UTF8String or IA5String
181
- // where a PrintableString is required). Returns null when the attribute is absent.
182
140
  function _readStr(attrs, name, expectedTag) {
183
141
  var a = _find(attrs, name);
184
142
  if (a == null) return null;
@@ -192,7 +150,6 @@ function _readStr(attrs, name, expectedTag) {
192
150
  catch (e) { throw _err("scep/bad-attribute", "the " + name + " transaction attribute is not a readable string", e); }
193
151
  }
194
152
  var _T_PRINTABLE = asn1.TAGS.PRINTABLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
195
- // Read a 16-byte OCTET STRING nonce attribute, or null when absent.
196
153
  function _readNonce(attrs, name) {
197
154
  var a = _find(attrs, name);
198
155
  if (a == null) return null;
@@ -203,9 +160,6 @@ function _readNonce(attrs, name) {
203
160
  return n;
204
161
  }
205
162
 
206
- // Whether two certificates carry the same subject public key -- the SCEP signer-authentication test
207
- // (a CertRep must be signed by the CA the client already holds, RFC 8894 sec. 3.1). Compared by the
208
- // SubjectPublicKeyInfo so a re-encoded certificate of the same key still matches.
209
163
  function _sameKey(certA, certB) {
210
164
  var a, bb;
211
165
  try { a = schemaX509.parse(certA).subjectPublicKeyInfo.bytes; bb = schemaX509.parse(certB).subjectPublicKeyInfo.bytes; }
@@ -213,9 +167,6 @@ function _sameKey(certA, certB) {
213
167
  return guard.crypto.constantTimeEqual(a, bb);
214
168
  }
215
169
 
216
- // The pkcsPKIEnvelope uses RSAES-OAEP key transport, so the recipient MUST be an RSA key-transport
217
- // certificate (RFC 8894 sec. 3). An EC / X25519 / ML-KEM recipient would make cms.encrypt emit a
218
- // key-agreement or KEM recipient off the SCEP profile, so it is refused before the CMS dispatch.
219
170
  function _assertRsaRecipient(recipientDer) {
220
171
  var spki;
221
172
  try { spki = schemaX509.parse(recipientDer).subjectPublicKeyInfo; }
@@ -225,10 +176,6 @@ function _assertRsaRecipient(recipientDer) {
225
176
  }
226
177
  }
227
178
 
228
- // A SCEP signer's certificate MUST be authorized to sign: its keyUsage extension, when present, must
229
- // assert digitalSignature (RFC 8894 sec. 2.3 requires that bit specifically). A certificate whose
230
- // keyUsage omits it, permitting only encryption or non-repudiation, is refused, so a signature it makes
231
- // is not read as authenticated. A certificate with no keyUsage extension is unrestricted and passes.
232
179
  function _assertSigningKeyUsage(certDer, label) {
233
180
  var cert;
234
181
  try { cert = schemaX509.parse(certDer); }
@@ -239,9 +186,6 @@ function _assertSigningKeyUsage(certDer, label) {
239
186
  }
240
187
  }
241
188
 
242
- // Snapshot the recipient key material at the parse door: the certificate byte source is copied so a
243
- // concurrent mutation cannot redirect the decryption, and the private / content key is held by
244
- // reference (copying a secret is the worse defect). null passes through unchanged.
245
189
  function _captureRecipientKey(rk) {
246
190
  if (rk == null) return null;
247
191
  rk = guard.identifier.optionsObject(rk, _err, "scep/bad-input", "opts.recipientKey");
@@ -301,14 +245,6 @@ async function build(spec) {
301
245
  if (spec.signer == null || typeof spec.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
302
246
  var senderNonce = _nonce(spec.senderNonce, "senderNonce");
303
247
  var txnId = _transactionId(spec.transactionId);
304
- // The messageData is a PKCS#10 CertificationRequest whose self-signature is its proof-of-possession
305
- // (RFC 8894 sec. 3.3.1, over RFC 2986). Verify the structure and that proof before enveloping it, so
306
- // arbitrary bytes or a request whose signature does not verify under its own subject public key are
307
- // refused here rather than encrypted into a message a CA would reject.
308
- // Capture every input read after an await at this synchronous door, so a caller that mutates or reuses
309
- // the spec while verification or encryption is pending cannot desync what was verified from what is
310
- // emitted, or substitute the recipient a request is encrypted to. The certificate byte sources are
311
- // copied; the signer private key is held by reference (copying a secret is the worse defect).
312
248
  var messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
313
249
  var recipient = guard.bytes.snapshotSource(spec.recipient, ScepError, "scep/bad-input", "recipient");
314
250
  _assertRsaRecipient(recipient);
@@ -321,7 +257,6 @@ async function build(spec) {
321
257
  if (!(await csrVerify.verifyCsrSignature(parsedCsr))) {
322
258
  throw _err("scep/bad-popo", "the PKCS#10 messageData failed its proof-of-possession: its self-signature does not verify under its own subject public key, so a CA would reject it");
323
259
  }
324
- // Inner EnvelopedData: encrypt the PKCS#10 to the recipient CA, AES-128-CBC (RFC 8894 sec. 3).
325
260
  var pkcsPKIEnvelope;
326
261
  try {
327
262
  pkcsPKIEnvelope = await cmsEncrypt.encrypt(messageData, [{ cert: recipient }], { contentEncryptionAlgorithm: ENVELOPE_CIPHER });
@@ -329,7 +264,6 @@ async function build(spec) {
329
264
  if (e && e.isPkiError) throw _err("scep/bad-input", "the pkcsPKIEnvelope could not be built (recipient must be a keyEncipherment CA certificate and messageData a PKCS#10): " + e.message, e);
330
265
  throw e;
331
266
  }
332
- // Outer SignedData over the envelope, carrying the transaction attributes as authenticated attributes.
333
267
  var attrs = [
334
268
  _attr("scepMessageType", b.printable(mtCode)),
335
269
  _attr("scepTransactionId", b.printable(txnId)),
@@ -386,24 +320,13 @@ var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, tra
386
320
  async function parse(bytes, opts) {
387
321
  opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.parse options");
388
322
  guard.identifier.assertKnownKeys(opts, _PARSE_KEYS, _err, "scep/bad-input", "pki.scep.parse has an unknown option: ");
389
- // Snapshot the message bytes once at this synchronous door: parse verifies them twice (a detached
390
- // FAILURE / PENDING is re-verified against an empty payload), so a caller that swaps a mutable Buffer
391
- // between the two attempts must not be able to return one message's attributes for another.
392
323
  var msgBytes = guard.bytes.snapshotSource(bytes, ScepError, "scep/bad-der", "pkiMessage");
393
- // Capture every option read after the awaited verification at this synchronous door, so a caller that
394
- // mutates or reuses opts while verification is pending cannot swap the authenticated signer, the echo
395
- // nonce, or the decryption recipient after the checks that depend on them have started. Certificate
396
- // byte sources are copied; the recipient private key is held by reference.
397
324
  var expectedSignerCert = opts.signerCert != null ? guard.bytes.snapshotSource(opts.signerCert, ScepError, "scep/bad-input", "opts.signerCert") : null;
398
325
  var expectedSenderNonce = opts.expectedSenderNonce != null ? _nonce(opts.expectedSenderNonce, "expectedSenderNonce") : null;
399
326
  var recipientKey = _captureRecipientKey(opts.recipientKey);
400
327
  var v;
401
328
  try { v = await cmsVerify.verify(msgBytes); }
402
329
  catch (e) {
403
- // A conforming FAILURE or PENDING CertRep omits the pkcsPKIEnvelope, so the SignedData has no
404
- // encapsulated content and cms.verify reports it as detached. Re-verify against an empty payload,
405
- // which the signature accepts only when the message-digest was taken over no content; a message
406
- // signed over any other bytes still fails (RFC 8894 sec. 3.3.2).
407
330
  if (e && e.code === "cms/detached-content-required") {
408
331
  try { v = await cmsVerify.verify(msgBytes, { content: EMPTY_CONTENT }); }
409
332
  catch (e2) {
@@ -415,29 +338,12 @@ async function parse(bytes, opts) {
415
338
  } else { throw e; }
416
339
  }
417
340
  if (v.signers.length !== 1) throw _err("scep/bad-signer", "a SCEP pkiMessage MUST carry exactly one SignerInfo (RFC 8894 sec. 3.1), got " + v.signers.length);
418
- // Fail closed: the transaction attributes are only trustworthy once the outer signature verifies.
419
- // cms.verify returns a per-signer verdict rather than throwing, so a bad signature MUST be refused
420
- // here before any attribute is read -- never surfaced as a false verdict alongside the attributes.
421
341
  if (v.valid !== true) throw _err("scep/bad-signature", "the pkiMessage outer signature did not verify (RFC 8894 sec. 3.1)");
422
- // The SignedData eContentType MUST be id-data (RFC 8894 sec. 3.2): a message that verifies as CMS but
423
- // names another encapsulated content type is not a valid SCEP pkiMessage.
424
342
  if (v.eContentType !== oid.byName("data")) throw _err("scep/bad-content-type", "the pkiMessage eContentType must be id-data (RFC 8894 sec. 3.2)");
425
343
  var signer = v.signers[0];
426
- // A SCEP signer's certificate must be authorized to sign (RFC 8894 sec. 2.3): a signature from a
427
- // certificate whose keyUsage permits only encryption is not read as an authenticated pkiMessage.
428
344
  _assertSigningKeyUsage(signer.cert, "pkiMessage signer certificate");
429
- // A valid signature proves only that the message is self-consistent with the certificate it embeds,
430
- // NOT that the signer is the expected party: anyone can mint a certificate and sign a forged CertRep
431
- // that echoes an observed nonce. A SCEP client MUST authenticate a CA response against the CA
432
- // certificate it already holds (RFC 8894 sec. 3.1). When opts.signerCert names the expected signer,
433
- // require the message signer's public key to match it; a caller that omits it gets a crypto-only
434
- // verdict (signerAuthenticated: false) and MUST authenticate the surfaced signerCert itself before
435
- // acting on the transaction state.
436
345
  var signerAuthenticated = false;
437
346
  if (expectedSignerCert != null) {
438
- // The certificate the caller pins is authenticated only if it is itself authorized to sign: a
439
- // same-key certificate embedded in the message cannot lend its digitalSignature bit to a pinned
440
- // certificate that permits only encryption (RFC 8894 sec. 2.3).
441
347
  _assertSigningKeyUsage(expectedSignerCert, "opts.signerCert");
442
348
  if (!_sameKey(signer.cert, expectedSignerCert)) {
443
349
  throw _err("scep/untrusted-signer", "the pkiMessage signer's public key does not match opts.signerCert (RFC 8894 sec. 3.1)");
@@ -445,7 +351,6 @@ async function parse(bytes, opts) {
445
351
  signerAuthenticated = true;
446
352
  }
447
353
  var attrs = signer.signedAttributes;
448
- // The mandatory attributes (RFC 8894 sec. 3.2.1): transactionID, messageType, senderNonce.
449
354
  var messageTypeCode = _readStr(attrs, "scepMessageType", _T_PRINTABLE);
450
355
  var transactionId = _readStr(attrs, "scepTransactionId", _T_PRINTABLE);
451
356
  var senderNonce = _readNonce(attrs, "scepSenderNonce");
@@ -454,10 +359,6 @@ async function parse(bytes, opts) {
454
359
  if (senderNonce == null) throw _err("scep/missing-attribute", "the pkiMessage is missing its senderNonce attribute (RFC 8894 sec. 3.2.1)");
455
360
  var messageType = MESSAGE_TYPE_BY_CODE[messageTypeCode];
456
361
  if (messageType == null) throw _err("scep/bad-message-type", "unknown messageType " + JSON.stringify(messageTypeCode) + " (RFC 8894 sec. 3.2.1.2)");
457
- // This release reads the enrollment messages a SCEP client builds and receives: PKCSReq, RenewalReq,
458
- // and the CertRep response. The CertPoll / GetCert / GetCRL client queries a CA processes carry
459
- // IssuerAndSubject / IssuerAndSerialNumber messageData this codec does not model, so they are refused
460
- // rather than accepted with an unvalidated payload (RFC 8894 sec. 3.3.3, sec. 3.3.4).
461
362
  if (_PARSE_SUPPORTED[messageType] !== 1) {
462
363
  throw _err("scep/unsupported-message-type", "pki.scep.parse reads PKCSReq, RenewalReq, and CertRep; " + messageType + " is not read in this release");
463
364
  }
@@ -467,16 +368,12 @@ async function parse(bytes, opts) {
467
368
  recipientNonce: null, pkiStatus: null, failInfo: null, failInfoText: null, messageData: null,
468
369
  certificates: null, crls: null,
469
370
  };
470
- // A CertRep carries pkiStatus (+ failInfo/failInfoText on FAILURE) and echoes the request's senderNonce.
471
371
  if (messageType === "CertRep") {
472
372
  var statusCode = _readStr(attrs, "scepPkiStatus", _T_PRINTABLE);
473
373
  if (statusCode == null) throw _err("scep/missing-attribute", "a CertRep is missing its pkiStatus attribute (RFC 8894 sec. 3.2.1.3)");
474
374
  out.pkiStatus = PKI_STATUS_BY_CODE[statusCode];
475
375
  if (out.pkiStatus == null) throw _err("scep/bad-pki-status", "unknown pkiStatus " + JSON.stringify(statusCode) + " (RFC 8894 sec. 3.2.1.3)");
476
376
  out.recipientNonce = _readNonce(attrs, "scepRecipientNonce");
477
- // A CertRep MUST echo the request's senderNonce in a recipientNonce (RFC 8894 sec. 3.1), so a
478
- // response missing it cannot be correlated to its request and is refused regardless of whether the
479
- // caller supplied expectedSenderNonce.
480
377
  if (out.recipientNonce == null) throw _err("scep/missing-attribute", "a CertRep is missing its recipientNonce attribute (RFC 8894 sec. 3.2.1.5)");
481
378
  if (out.pkiStatus === "FAILURE") {
482
379
  var failCode = _readStr(attrs, "scepFailInfo", _T_PRINTABLE);
@@ -485,43 +382,23 @@ async function parse(bytes, opts) {
485
382
  if (out.failInfo == null) throw _err("scep/bad-fail-info", "unknown failInfo " + JSON.stringify(failCode) + " (RFC 8894 sec. 3.2.1.4)");
486
383
  out.failInfoText = _readStr(attrs, "scepFailInfoText", _T_UTF8);
487
384
  } else if (_find(attrs, "scepFailInfo") != null || _find(attrs, "scepFailInfoText") != null) {
488
- // failInfo and failInfoText are defined for a FAILURE response (RFC 8894 sec. 3.2.1.4): a SUCCESS
489
- // or PENDING CertRep carrying them asserts a contradictory authenticated status and is refused.
490
385
  throw _err("scep/unexpected-attribute", "a " + out.pkiStatus + " CertRep must not carry a failInfo or failInfoText attribute (RFC 8894 sec. 3.2.1.4)");
491
386
  }
492
387
  } else {
493
- // A request (PKCSReq / RenewalReq) is an initial message: the response-only attributes pkiStatus,
494
- // failInfo, failInfoText, and recipientNonce are defined for a CertRep and MUST NOT appear here
495
- // (RFC 8894 sec. 3.2.1). A request carrying any of them is off-profile and refused.
496
388
  if (_find(attrs, "scepPkiStatus") != null || _find(attrs, "scepFailInfo") != null ||
497
389
  _find(attrs, "scepFailInfoText") != null || _find(attrs, "scepRecipientNonce") != null) {
498
390
  throw _err("scep/unexpected-attribute", "a " + messageType + " request must not carry a CertRep response attribute, pkiStatus / failInfo / failInfoText / recipientNonce (RFC 8894 sec. 3.2.1)");
499
391
  }
500
392
  }
501
- // A caller-stated expected senderNonce echo (RFC 8894 sec. 3.2.1.5): a message whose recipientNonce
502
- // does not match the nonce we sent MUST be rejected.
503
393
  if (expectedSenderNonce != null) {
504
394
  if (out.recipientNonce == null || !guard.crypto.constantTimeEqual(out.recipientNonce, expectedSenderNonce)) {
505
395
  throw _err("scep/nonce-mismatch", "the pkiMessage recipientNonce does not echo the sent senderNonce (RFC 8894 sec. 3.2.1.5)");
506
396
  }
507
397
  }
508
- // The pkcsPKIEnvelope is carried by a request and a SUCCESS CertRep, and is OMITTED by a FAILURE or
509
- // PENDING CertRep, whose payload is the status itself (RFC 8894 sec. 3.3.2.1). Decrypt only when the
510
- // message is one that carries the envelope, so a non-SUCCESS CertRep's authenticated status is
511
- // returned rather than turned into a decrypt failure over content that is not an EnvelopedData. The
512
- // eContent is bound to the verified signature (cms.verify).
513
398
  var carriesEnvelope = !(messageType === "CertRep" && (out.pkiStatus === "FAILURE" || out.pkiStatus === "PENDING"));
514
399
  if (carriesEnvelope) {
515
- // For a message that carries messageData, the pkcsPKIEnvelope is mandatory, MUST be an EnvelopedData,
516
- // and MUST encrypt id-data (RFC 8894 sec. 3.2, sec. 3.2.2). This is checked structurally, before any
517
- // recipient-key decryption, so an absent or off-profile envelope is never accepted as a valid
518
- // message with a null messageData, whether or not a recipient key was supplied.
519
400
  _assertValidEnvelope(v.eContent, messageType);
520
401
  } else {
521
- // A FAILURE or PENDING CertRep MUST omit the pkcsPKIEnvelope (RFC 8894 sec. 3.3.2): its payload is
522
- // the status itself, so the SignedData carries no encapsulated content (cms.verify surfaces it as a
523
- // null eContent). Any present eContent -- an attached OCTET STRING even a zero-length one -- is
524
- // off-profile and refused, so attacker-supplied bytes are never accepted under a non-SUCCESS verdict.
525
402
  if (v.eContent != null) {
526
403
  throw _err("scep/unexpected-envelope", "a " + out.pkiStatus + " CertRep must omit the pkcsPKIEnvelope (RFC 8894 sec. 3.3.2), but it carries encapsulated content");
527
404
  }
@@ -534,18 +411,11 @@ async function parse(bytes, opts) {
534
411
  throw e;
535
412
  }
536
413
  out.messageData = dec.content;
537
- // A SUCCESS CertRep's messageData MUST be a degenerate certs-only CMS SignedData carrying the
538
- // issued certificate(s) (RFC 8894 sec. 3.3.2, sec. 3.4). Validate that structure through the strict
539
- // CMS parser and surface the certificates, so a decrypted payload that is not a valid issuance
540
- // cannot read as a successful enrollment. The certificates are raw, for the caller to path-validate.
541
414
  if (messageType === "CertRep" && out.pkiStatus === "SUCCESS") {
542
415
  var payload = _readIssuancePayload(dec.content);
543
416
  out.certificates = payload.certificates;
544
417
  out.crls = payload.crls;
545
418
  } else if (messageType === "PKCSReq" || messageType === "RenewalReq") {
546
- // A request's messageData MUST be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1). Validate
547
- // the structure so a decrypted payload that is not one cannot read as a valid request; the raw
548
- // bytes stay in messageData for the caller to verify the proof-of-possession.
549
419
  try { schemaCsr.parse(dec.content); }
550
420
  catch (e) { throw _err("scep/bad-request-payload", "a " + messageType + " messageData must be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1)", e); }
551
421
  }
@@ -554,35 +424,20 @@ async function parse(bytes, opts) {
554
424
  }
555
425
  var _PARSE_KEYS = { recipientKey: 1, expectedSenderNonce: 1, signerCert: 1 };
556
426
 
557
- // ---- HTTP client (RFC 8894 sec. 4) -------------------------------------------
558
- //
559
- // The client verbs compose the shipped pkiMessage codec (build / parse) over an injectable transport
560
- // (pki.transport.https by default; tests inject a fake). SCEP's message security is the CMS layer, not
561
- // the transport (RFC 8894 sec. 2.8), so https is not forced at the URL level; a caller with a plain-HTTP
562
- // CA injects its own transport. This release ships the mandatory-to-implement set (GetCACaps, GetCACert,
563
- // PKCSReq via HTTP POST, AES-128-CBC + SHA-256, sec. 2.9) plus RenewalReq. Deferred, each re-opening
564
- // under its named condition: GetNextCACert (the rollover-certificate authentication model, sec. 4.7);
565
- // auto-poll / GetCert / GetCRL (their CertPoll / IssuerAndSerialNumber messageData builders are not in
566
- // this codec); GET-carried PKIOperation (POST is mandatory, sec. 4.3); and HTTP auth (a CA behind an
567
- // authenticating proxy).
568
427
  var DEFAULT_TIMEOUT = C.TIME.seconds(30);
569
428
  var MAX_TIMEOUT = C.TIME.seconds(600);
570
429
 
571
- // The SCEP media types (RFC 8894 sec. 4.2 / 4.6). A 200 response MUST carry the operation's media type;
572
- // a proxy error page or a wrong type is refused (fail-closed).
573
- var CT_CA_CERT = "application/x-x509-ca-cert"; // GetCACert, a single DER certificate
574
- var CT_CA_RA_CERT = "application/x-x509-ca-ra-cert"; // GetCACert, a certs-only CMS chain
575
- var CT_PKI_MESSAGE = "application/x-pki-message"; // PKIOperation, a pkiMessage
576
- var CT_NEXT_CA_CERT = "application/x-x509-next-ca-cert"; // GetNextCACert, the rollover certificate(s), sec. 4.7.1
577
- var CT_PLAIN = "text/plain"; // GetCACaps, a newline-separated capability list
430
+ var CT_CA_CERT = "application/x-x509-ca-cert";
431
+ var CT_CA_RA_CERT = "application/x-x509-ca-ra-cert";
432
+ var CT_PKI_MESSAGE = "application/x-pki-message";
433
+ var CT_NEXT_CA_CERT = "application/x-x509-next-ca-cert";
434
+ var CT_PLAIN = "text/plain";
578
435
 
579
436
  function _isWs(c) { return c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d || c === 0x0b || c === 0x0c; }
580
437
  function _foldLower(s) { var o = "", i, n = s.length; for (i = 0; i < n; i++) { var c = s.charCodeAt(i); o += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c); } return o; }
581
438
  function _foldUpper(s) { var o = "", i, n = s.length; for (i = 0; i < n; i++) { var c = s.charCodeAt(i); o += String.fromCharCode(c >= 97 && c <= 122 ? c - 32 : c); } return o; }
582
439
  function _trim(s) { var a = 0, e = s.length; while (a < e && _isWs(s.charCodeAt(a))) a += 1; while (e > a && _isWs(s.charCodeAt(e - 1))) e -= 1; return s.slice(a, e); }
583
440
 
584
- // A case-insensitive single-header read. A duplicated header is refused: an injected transport promises
585
- // only { status, headers, body }, and a smuggled duplicate must never be silently coalesced.
586
441
  function _header(headers, name) {
587
442
  if (!headers || typeof headers !== "object") return null;
588
443
  var found = null, seen = 0, k;
@@ -593,7 +448,6 @@ function _header(headers, name) {
593
448
  if (seen > 1) throw _err("scep/bad-response", "the response carried more than one " + name + " header");
594
449
  return found == null ? null : String(found);
595
450
  }
596
- // The bare media type of a Content-Type (strip parameters, lower-case), by character scan (no regex).
597
451
  function _mediaType(ct) {
598
452
  if (ct == null) return "";
599
453
  var s = String(ct), semi = s.indexOf(";");
@@ -603,73 +457,45 @@ function _bodyBuf(body) {
603
457
  if (Buffer.isBuffer(body)) return body;
604
458
  if (body == null) return Buffer.alloc(0);
605
459
  if (typeof body === "string") return Buffer.from(body, "utf8");
606
- // A BufferSource (Uint8Array, ArrayBuffer, DataView) from an injected transport: normalize its raw bytes
607
- // rather than String()-coercing it, which would turn a Uint8Array into its "48,1,..." toString.
608
460
  return guard.bytes.snapshotSource(body, ScepError, "scep/bad-response", "the response body");
609
461
  }
610
- // The byte length of a transport response body, read without a full copy so the size cap is enforced before
611
- // the body is materialized (cap-before-copy): a string is measured as UTF-8, a Buffer or other BufferSource
612
- // through the byte guard, which reads the length via a captured intrinsic rather than a shadowable accessor.
613
462
  function _responseLen(body) {
614
463
  if (body == null) return 0;
615
464
  if (typeof body === "string") return Buffer.byteLength(body, "utf8");
616
465
  if (Buffer.isBuffer(body)) return guard.bytes.lengthOf(body);
617
466
  return guard.bytes.lengthOf(guard.bytes.source(body, ScepError, "scep/bad-response", "the response body"));
618
467
  }
619
- // Map opts.tls to the transport request.tls shape. rejectUnauthorized is NOT set -- the transport forces
620
- // it on for an https connection.
621
468
  function _tlsForRequest(opts) {
622
469
  var t = opts.tls || {};
623
470
  return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
624
471
  }
625
- // Resolve a redirect target: refuse an https->http downgrade (a stripping MITM) and a cross-origin
626
- // redirect on a non-idempotent method without opt-in; an unparseable Location is scep/bad-url.
627
472
  function _redirectTarget(current, location, method, allowCrossOrigin) {
628
473
  if (location == null || _trim(String(location)) === "") throw _err("scep/bad-response", "a redirect response carried no Location header");
629
474
  var resolved;
630
475
  try { resolved = new URL(String(location), current.href); }
631
476
  catch (e) { throw _err("scep/bad-url", "a redirect Location did not parse: " + location, e); }
632
- // SCEP is an HTTP operation (RFC 8894 sec. 4): a redirect target on any other scheme (ftp:, file:, ...)
633
- // is refused before it reaches the transport, so a CA cannot steer an injected transport off HTTP.
634
477
  if (resolved.protocol !== "http:" && resolved.protocol !== "https:") throw _err("scep/bad-url", "a redirect to a non-HTTP scheme is refused: " + resolved.protocol);
635
478
  if (current.protocol === "https:" && resolved.protocol !== "https:") throw _err("scep/insecure-redirect", "a redirect from https to " + resolved.protocol + " is refused");
636
479
  var safe = method === "GET" || method === "HEAD";
637
480
  if (resolved.origin !== current.origin && !safe && !allowCrossOrigin) throw _err("scep/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect");
638
481
  return resolved;
639
482
  }
640
- // Build a SCEP operation URL: the caller's base URL with ?operation=<op> appended (RFC 8894 sec. 4.1;
641
- // SCEPPATH is whatever the base carries, the CA typically ignores it).
642
483
  function _scepUrl(baseUrl, operation) {
643
484
  var url;
644
485
  try { url = new URL(String(baseUrl)); }
645
486
  catch (e) { throw _err("scep/bad-url", "the SCEP base URL did not parse: " + String(baseUrl), e); }
646
- // SCEP is an HTTP operation (RFC 8894 sec. 4): any other scheme (ftp:, file:, ...) is refused before the
647
- // URL reaches the transport, so an injected transport cannot be steered off HTTP.
648
487
  if (url.protocol !== "http:" && url.protocol !== "https:") throw _err("scep/bad-url", "the SCEP base URL must be http or https, got " + url.protocol);
649
488
  if (url.hash) throw _err("scep/bad-url", "the SCEP base URL must not carry a fragment");
650
489
  url.searchParams.set("operation", operation);
651
490
  return url;
652
491
  }
653
- // The transport request / redirect loop. Returns the terminal transport response ({ status, headers,
654
- // body }). Digest / Basic auth is not answered (deferred); a 401 is returned to the verb, which throws.
655
492
  function _drive(method, url, body, headers, opts, transport, budgets) {
656
493
  var redirects = 0, curMethod = method, curBody = body, initialOrigin = url.origin;
657
- // Snapshot the caller's TLS fields and redirect policy once, at the start, before the first transport call:
658
- // the redirect loop reruns per hop after each transport promise settles, so a hop must use the identity and
659
- // policy the initial hop did even if opts (or opts.tls) is replaced while a request is in flight.
660
494
  var tlsBase = _tlsForRequest(opts);
661
495
  var allowCrossOrigin = !!opts.allowCrossOriginRedirect;
662
- // The mTLS client identity (cert / key / pinned SNI) is sent ONLY to the origin the caller intended; a
663
- // cross-origin redirect gets a copy with those stripped, so a CA that redirects to another origin cannot
664
- // make the client authenticate to it with the caller's client credential. Derived fresh each hop, so a
665
- // redirect back to the original origin restores it.
666
496
  function _hopTls(current) {
667
497
  var t = tlsBase;
668
498
  if (current.origin !== initialOrigin) {
669
- // Every cross-origin hop overrides the client identity with an explicit null, unconditionally: an
670
- // omitted field falls back to the transport's own configured defaults (http-transport resolves
671
- // reqTls.cert !== undefined ? reqTls.cert : tlsDefaults.cert), so the override must fire even when the
672
- // request-level tls carries no credential, or a transport default would still reach the new origin.
673
499
  t = Object.assign({}, t);
674
500
  t.cert = null; t.key = null; t.servername = null;
675
501
  }
@@ -704,7 +530,6 @@ function _client(method, url, body, headers, opts) {
704
530
  };
705
531
  return _drive(method, url, body, Object.assign({}, headers), opts, transport, budgets);
706
532
  }
707
- // Require a 200 with one of the expected media types; otherwise a typed error. Returns the body Buffer.
708
533
  function _requireOk(res, expected, what) {
709
534
  var status = res.status;
710
535
  if (status !== 200) {
@@ -718,8 +543,6 @@ function _requireOk(res, expected, what) {
718
543
  return _bodyBuf(res.body);
719
544
  }
720
545
 
721
- // The GetCACaps keyword set (RFC 8894 sec. 3.5.2, Table 7). Comparison is case-folded; the canonical
722
- // spelling is stored. An unknown keyword is ignored (sec. 3.5.2). SCEPStandard implies the sec. 2.9 set.
723
546
  var CAP_CANON = { AES: "AES", DES3: "DES3", GETNEXTCACERT: "GetNextCACert", POSTPKIOPERATION: "POSTPKIOperation",
724
547
  RENEWAL: "Renewal", "SHA-1": "SHA-1", "SHA-256": "SHA-256", "SHA-512": "SHA-512", SCEPSTANDARD: "SCEPStandard" };
725
548
  function _splitLines(s) {
@@ -790,29 +613,17 @@ function _assertStrongProfile(caps, profile) {
790
613
  async function getCACaps(baseUrl, opts) {
791
614
  opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getCACaps options");
792
615
  guard.identifier.assertKnownKeys(opts, _CACAPS_KEYS, _err, "scep/bad-input", "pki.scep.getCACaps has an unknown option: ");
793
- // Capture the strong-profile policy at the door: the downgrade check runs only after the GET await, so a
794
- // caller must not be able to relax its own guard by mutating opts while the request is in flight.
795
616
  var profile = { expectSCEPStandard: !!opts.expectSCEPStandard, requireStrongProfile: !!opts.requireStrongProfile };
796
617
  var res = await _client("GET", _scepUrl(baseUrl, "GetCACaps"), null, {}, opts);
797
- // An HTTP error or empty body means "none supported" (RFC 8894 sec. 3.5.1); a non-200 that is not a
798
- // clean error is still surfaced so a caller is not silently told "no capabilities" on a broken CA.
799
618
  if (res.status === 200) {
800
- // A conforming GetCACaps response carries content-type text/plain (RFC 8894 sec. 3.5.1). Enforcing it
801
- // stops a proxy's 200 HTML error page from being read as an empty (no capabilities) capability set.
802
619
  var caps = parseCapabilities(_requireOk(res, [CT_PLAIN], "GetCACaps"));
803
620
  _assertStrongProfile(caps, profile);
804
621
  return caps;
805
622
  }
806
- // A CA that does not implement GetCACaps signals it with an HTTP error, which RFC 8894 sec. 3.5.1 reads
807
- // as "no capabilities advertised": the codes that mean the operation is unsupported or unrecognized are
808
- // 400 (an old CA that does not parse the query), 404 (no such operation), 405 (GET not allowed on it), and
809
- // 501 Not Implemented. A transient server error (500, 502, 503) is surfaced instead of masked.
810
623
  if (res.status === 204 || res.status === 400 || res.status === 404 || res.status === 405 || res.status === 501) { _assertStrongProfile({}, profile); return {}; }
811
624
  throw _err("scep/http-error", "the SCEP GetCACaps returned HTTP " + res.status);
812
625
  }
813
626
 
814
- // The exact SPKI byte-match that identifies an issued certificate (RFC 5272 sec. 4.1); positional
815
- // guessing is forbidden. `certs` is an array of raw cert Buffers, `wantSpki` the SPKI Buffer to match.
816
627
  function findIssuedCert(certs, wantSpki) {
817
628
  if (!Buffer.isBuffer(wantSpki)) return null;
818
629
  for (var i = 0; i < certs.length; i++) {
@@ -823,12 +634,8 @@ function findIssuedCert(certs, wantSpki) {
823
634
  }
824
635
  return null;
825
636
  }
826
- // Normalize an expected fingerprint (hex string, optionally colon/space separated, or a Buffer) to
827
- // { algorithm, value }. Character-scan the hex (Hard rule #11).
828
637
  function _normalizeFingerprint(fp, algName) {
829
638
  var algorithm = _foldLower(String(algName == null ? "sha256" : algName));
830
- // Validate the digest name at the entry point so a misspelled or build-unavailable fingerprintAlgorithm
831
- // is the typed scep/bad-input the client API documents, not a native createHash exception downstream.
832
639
  try { nodeCrypto.createHash(algorithm); }
833
640
  catch (_e) { throw _err("scep/bad-input", "fingerprintAlgorithm " + JSON.stringify(algorithm) + " is not an available hash"); }
834
641
  var value;
@@ -878,9 +685,6 @@ function _normalizeFingerprint(fp, algName) {
878
685
  async function getCACert(baseUrl, opts) {
879
686
  opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getCACert options");
880
687
  guard.identifier.assertKnownKeys(opts, _CACERT_KEYS, _err, "scep/bad-input", "pki.scep.getCACert has an unknown option: ");
881
- // Normalize the expected fingerprint at the synchronous entry point: fail fast on a bad hash before the
882
- // network call, and pin against the value the caller supplied at the call rather than one mutated while
883
- // the GET is in flight.
884
688
  var fp = opts.expectedFingerprint != null ? _normalizeFingerprint(opts.expectedFingerprint, opts.fingerprintAlgorithm) : null;
885
689
  var res = await _client("GET", _scepUrl(baseUrl, "GetCACert"), null, {}, opts);
886
690
  var body = _requireOk(res, [CT_CA_CERT, CT_CA_RA_CERT], "GetCACert");
@@ -890,7 +694,6 @@ async function getCACert(baseUrl, opts) {
890
694
  certs = [Buffer.from(body)];
891
695
  single = true;
892
696
  } else {
893
- // parseCertsOnly fails closed with scep/no-certificates on an empty CertificateSet, so certs is non-empty here.
894
697
  certs = schemaCms.parseCertsOnly(body, _err, "scep").certificates;
895
698
  }
896
699
  var caCertificate = null;
@@ -903,9 +706,6 @@ async function getCACert(baseUrl, opts) {
903
706
  if (!matched) throw _err("scep/fingerprint-mismatch", "no GetCACert certificate matched the expected " + fp.algorithm + " fingerprint (RFC 8894 sec. 2.2)");
904
707
  caCertificate = matched;
905
708
  } else if (single) {
906
- // A single-certificate response (application/x-x509-ca-cert) is unambiguously the CA certificate. A
907
- // ca-ra chain carries the CA, any intermediates, and RA certificate(s) in a CMS CertificateSet whose
908
- // DER order does not identify the CA role, so caCertificate stays null there unless a fingerprint pins it.
909
709
  caCertificate = certs[0];
910
710
  }
911
711
  return { caCertificate: caCertificate, certificates: certs };
@@ -940,22 +740,12 @@ async function getCACert(baseUrl, opts) {
940
740
  async function getNextCACert(baseUrl, opts) {
941
741
  opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getNextCACert options");
942
742
  guard.identifier.assertKnownKeys(opts, _NEXTCACERT_KEYS, _err, "scep/bad-input", "pki.scep.getNextCACert has an unknown option: ");
943
- // The current CA certificate authenticates the response (RFC 8894 sec. 4.7.1). Require it and snapshot
944
- // its bytes at this synchronous door, before the GET await, so a caller cannot switch the authenticating
945
- // certificate out from under the exchange, and so an unauthenticated rollover certificate -- which would
946
- // become a future trust anchor -- is never returned.
947
743
  if (opts.caCertificate == null) throw _err("scep/bad-input", "a caCertificate (the current SCEP CA certificate DER) is required to authenticate the GetNextCACert response (RFC 8894 sec. 4.7.1)");
948
744
  var currentCa = guard.bytes.snapshotSource(opts.caCertificate, ScepError, "scep/bad-input", "the caCertificate");
949
- // The current CA must itself be authorized to sign, or its signature over the rollover certificate
950
- // authenticates nothing (RFC 8894 sec. 2.3); fail fast at the door before the network call.
951
745
  _assertSigningKeyUsage(currentCa, "the caCertificate");
952
746
  var url = _scepUrl(baseUrl, "GetNextCACert");
953
747
  var res = await _client("GET", url, null, {}, opts);
954
748
  var body = _requireOk(res, [CT_NEXT_CA_CERT], "GetNextCACert");
955
- // Verify the outer SignedData, passing the current CA as a candidate so the response need not embed the
956
- // signer. cms.verify reports a per-signer verdict rather than throwing on a bad signature, so an invalid
957
- // signature is refused here before any content is read. A detached outer (no content) has no rollover
958
- // certificate to carry and is refused.
959
749
  var v;
960
750
  try { v = await cmsVerify.verify(body, { certs: [currentCa] }); }
961
751
  catch (e) {
@@ -963,25 +753,13 @@ async function getNextCACert(baseUrl, opts) {
963
753
  if (e && e.isPkiError) throw _err("scep/bad-response", "the GetNextCACert response is not a verifiable CMS SignedData (RFC 8894 sec. 4.7.1): " + e.message, e);
964
754
  throw e;
965
755
  }
966
- // A GetNextCACert response is signed by the current CA and only the current CA (RFC 8894 sec. 4.7.1);
967
- // exactly one SignerInfo is expected, as with any authenticated SCEP message (sec. 3.1). Requiring one
968
- // signer binds the signature-verified check and the current-CA key pin to the SAME signer, so a second
969
- // signer's valid signature cannot stand in for a current-CA identity whose own signature does not verify.
970
756
  if (v.signers.length !== 1) throw _err("scep/bad-signer", "a GetNextCACert response must carry exactly one SignerInfo (RFC 8894 sec. 4.7.1), got " + v.signers.length);
971
757
  if (v.valid !== true) throw _err("scep/bad-signature", "the GetNextCACert outer signature did not verify (RFC 8894 sec. 4.7.1)");
972
- // Pin the one verified signer to the current CA key: only the current CA holds the current CA key, so the
973
- // verified signer carrying that key IS the current CA (RFC 8894 sec. 4.7.1).
974
758
  if (!_sameKey(v.signers[0].cert, currentCa)) throw _err("scep/untrusted-signer", "the GetNextCACert response was not signed by the current CA key (RFC 8894 sec. 4.7.1)");
975
- // The verified content is a degenerate certs-only SignedData carrying the next CA certificate(s);
976
- // parseCertsOnly fails closed on an empty CertificateSet (scep/no-certificates).
977
759
  var certs = schemaCms.parseCertsOnly(v.eContent, _err, "scep").certificates;
978
760
  return { certificates: certs };
979
761
  }
980
762
 
981
- // The default transaction ID: 16 random bytes, hex-encoded. RFC 8894 sec. 3.2.1.1 requires the ID to be
982
- // unique per operation, so a per-key value would collide across separate enrollments of the same key (a
983
- // renewal could not be told from the original request). A caller wanting a stable per-device ID passes an
984
- // explicit transactionId.
985
763
  function _defaultTransactionId() {
986
764
  return nodeCrypto.randomBytes(16).toString("hex");
987
765
  }
@@ -992,22 +770,10 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
992
770
  if (opts.caCert == null) throw _err("scep/bad-input", "a caCert (the SCEP CA certificate DER) is required");
993
771
  if (opts.signer == null || typeof opts.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
994
772
  if (opts.recipientKey == null || typeof opts.recipientKey !== "object") throw _err("scep/bad-input", "a recipientKey { cert, key } is required to decrypt the CA response");
995
- // Snapshot the response-authentication inputs at the synchronous entry point: the response is
996
- // authenticated (signerCert) and decrypted (recipientKey) only after the build and POST awaits, so a
997
- // caller mutating opts mid-flight cannot switch the trusted signer, or the CA the request was encrypted
998
- // to, out from under the exchange. Certificate bytes are copied; the recipient private key and the
999
- // request signer are held by reference.
1000
773
  var caCert = guard.bytes.snapshotSource(opts.caCert, ScepError, "scep/bad-input", "the caCert");
1001
774
  var signerCert = opts.responderCert != null ? guard.bytes.snapshotSource(opts.responderCert, ScepError, "scep/bad-input", "the responderCert") : caCert;
1002
- // Capture the recipient-key FIELDS at the door (snapshotSource copies the cert bytes; the private key is
1003
- // held by reference), not the opts.recipientKey object: a caller replacing opts.recipientKey.cert or .key
1004
- // while the POST is in flight must not switch the credential the response is decrypted with.
1005
775
  var recipientKey = _captureRecipientKey(opts.recipientKey);
1006
776
  var signer = opts.signer;
1007
- // The transport options are read by _client only after the build await, so snapshot them at the door too:
1008
- // a caller replacing opts.transport (rerouting the completed request) or opts.tls.cert/.key (presenting the
1009
- // next operation's mTLS identity) while the POST is in flight must not take effect. tls is shallow-copied
1010
- // so a replaced field does not reach the transport; the transport function and budgets are held by value.
1011
777
  var clientOpts = {
1012
778
  transport: opts.transport,
1013
779
  tls: opts.tls != null ? Object.assign({}, opts.tls) : opts.tls,
@@ -1016,8 +782,6 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
1016
782
  timeout: opts.timeout,
1017
783
  maxResponseBytes: opts.maxResponseBytes,
1018
784
  };
1019
- // Resolve the request URL at the door: _scepUrl reads String(baseUrl) only when the POST is sent (after the
1020
- // build await), so a baseUrl whose toString() the caller changes mid-flight could otherwise redirect it.
1021
785
  var postUrl = _scepUrl(baseUrl, "PKIOperation");
1022
786
  var csrView = guard.bytes.snapshotSource(opts.csr, ScepError, "scep/bad-input", "the csr");
1023
787
  var spki;
@@ -1030,13 +794,8 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
1030
794
  var body = _requireOk(res, [CT_PKI_MESSAGE], "PKIOperation");
1031
795
  var rep = await parse(body, { signerCert: signerCert, expectedSenderNonce: senderNonce, recipientKey: recipientKey });
1032
796
  if (rep.messageType !== "CertRep") throw _err("scep/unexpected-message-type", "the SCEP response is a " + rep.messageType + ", not a CertRep");
1033
- // Every message in one SCEP transaction carries the same transactionID (RFC 8894 sec. 3.2.1.1); a
1034
- // response whose transactionID does not echo the request is for a different exchange and is refused,
1035
- // even if its nonce and signer check out.
1036
797
  if (rep.transactionId !== transactionId) throw _err("scep/transaction-id-mismatch", "the CertRep transactionID does not match the request (RFC 8894 sec. 3.2.1.1)");
1037
798
  if (rep.pkiStatus === "FAILURE") {
1038
- // Surface the CA's reason as structured fields, not only interpolated text, so a caller can branch on
1039
- // the failInfo enumerant (RFC 8894 sec. 3.3.2.1) without parsing the human-readable message.
1040
799
  var failErr = _err("scep/enrollment-failed", "the CA refused the request with failInfo " + JSON.stringify(rep.failInfo) + (rep.failInfoText ? " (" + rep.failInfoText + ")" : ""));
1041
800
  failErr.failInfo = rep.failInfo;
1042
801
  failErr.failInfoText = rep.failInfoText;
@@ -1046,9 +805,6 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
1046
805
  var repCerts = rep.certificates || [];
1047
806
  var issued = findIssuedCert(repCerts, spki.bytes);
1048
807
  if (issued == null) throw _err("scep/no-issued-cert", "the SUCCESS CertRep did not carry a certificate matching the request public key");
1049
- // A CMS CertificateSet states no issuance order, so a second certificate carrying the request public key
1050
- // (a same-key renewal returning old and new, or a duplicate) leaves the issued certificate ambiguous; it
1051
- // is refused rather than resolved by position.
1052
808
  if (findIssuedCert(repCerts.filter(function (c) { return c !== issued; }), spki.bytes) != null) {
1053
809
  throw _err("scep/ambiguous-issued-cert", "the SUCCESS CertRep carried more than one certificate matching the request public key; the issued certificate is ambiguous (RFC 8894 sec. 3.3)");
1054
810
  }