@blamejs/pki 0.5.31 → 0.5.32

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 (51) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +2 -2
  3. package/lib/acme.js +347 -90
  4. package/lib/asn1-der.js +93 -33
  5. package/lib/cmc-build.js +19 -8
  6. package/lib/cmc-verify.js +4 -4
  7. package/lib/cmp-build.js +36 -6
  8. package/lib/cmp-session.js +5 -5
  9. package/lib/cmp-verify.js +86 -16
  10. package/lib/cms-compress.js +2 -2
  11. package/lib/cms-decrypt.js +3 -3
  12. package/lib/cms-encrypt.js +1 -1
  13. package/lib/cms-sign.js +7 -7
  14. package/lib/cms-verify.js +9 -4
  15. package/lib/crl-sign.js +4 -4
  16. package/lib/crmf-sign.js +2 -1
  17. package/lib/csr-sign.js +1 -4
  18. package/lib/ct.js +8 -4
  19. package/lib/est.js +234 -34
  20. package/lib/framework-error.js +20 -6
  21. package/lib/guard-bytes.js +24 -3
  22. package/lib/http-digest.js +69 -16
  23. package/lib/http-retry-after.js +68 -24
  24. package/lib/http-transport.js +60 -6
  25. package/lib/inspect.js +13 -2
  26. package/lib/key.js +2 -2
  27. package/lib/lint.js +1 -1
  28. package/lib/mime.js +27 -4
  29. package/lib/ocsp.js +7 -7
  30. package/lib/oid.js +16 -0
  31. package/lib/path-validate.js +76 -18
  32. package/lib/pbes2.js +2 -2
  33. package/lib/pkcs12-build.js +2 -2
  34. package/lib/pki-build.js +6 -6
  35. package/lib/rfc3339.js +75 -17
  36. package/lib/schema-c509.js +36 -9
  37. package/lib/schema-cmc.js +3 -3
  38. package/lib/schema-pkix.js +174 -25
  39. package/lib/schema-tsp.js +10 -1
  40. package/lib/sign-scheme.js +1 -1
  41. package/lib/sigstore.js +52 -9
  42. package/lib/smime.js +86 -6
  43. package/lib/trust.js +114 -14
  44. package/lib/tsp-sign.js +39 -27
  45. package/lib/validator-tpm.js +2 -2
  46. package/lib/webauthn-mds.js +24 -6
  47. package/lib/webauthn.js +3 -3
  48. package/lib/webcrypto.js +1 -1
  49. package/lib/x509-sign.js +1 -1
  50. package/package.json +1 -1
  51. package/sbom.cdx.json +6 -6
@@ -129,7 +129,7 @@ function _parse(input) {
129
129
  return schemaCms.parse(_toDer(input));
130
130
  }
131
131
  function _toDer(input) {
132
- if (Buffer.isBuffer(input) || input instanceof Uint8Array) return guard.bytes.snapshot(input, CmsError, "cms/bad-input", "input");
132
+ if (guard.bytes.isByteSource(input)) return guard.bytes.snapshotSource(input, CmsError, "cms/bad-input", "input");
133
133
  if (typeof input === "string") { try { return schemaCms.pemDecode(input); } catch (e) { throw _err("cms/bad-input", "the CMS PEM could not be decoded", e); } }
134
134
  throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
135
135
  }
@@ -166,7 +166,7 @@ function _riKindForKey(km) {
166
166
  throw _err("cms/bad-input", "key material needs { key, cert }, { password }, { kek }, or { cek }");
167
167
  }
168
168
  function _assertSupported(ri, km) {
169
- if (ri.type === "kari" && ri.keyEncryptionAlgorithm && /mqv/i.test(ri.keyEncryptionAlgorithm.name || "")) throw _err("cms/unsupported-algorithm", "ECMQV kari is not supported");
169
+ if (ri.type === "kari" && ri.keyEncryptionAlgorithm && intrinsic.stringIndexOf(intrinsic.toLowerCase(ri.keyEncryptionAlgorithm.name || ""), "mqv") !== -1) throw _err("cms/unsupported-algorithm", "ECMQV kari is not supported"); // /mqv/i (ASCII name)
170
170
  if (ri.type === "ori") {
171
171
  if (ri.oriType !== O("kem")) throw _err("cms/unsupported-recipient-type", "unsupported OtherRecipientInfo type " + ri.oriType);
172
172
  if (ri.kemri && ri.kemri.kem && ri.kemri.kem.oid === O("id-kem-rsa")) throw _err("cms/unsupported-algorithm", "RSA-KEM is not supported");
@@ -801,7 +801,7 @@ function _releaseKeyDer(k) {
801
801
  if (k && k.owned) guard.secret.zeroize(k.der, CmsError, "cms/bad-input", "the recipient private-key copy");
802
802
  }
803
803
  function _normCertDer(cert) {
804
- if (Buffer.isBuffer(cert) || cert instanceof Uint8Array) return guard.bytes.snapshot(cert, CmsError, "cms/bad-input", "the recipient certificate");
804
+ if (guard.bytes.isByteSource(cert)) return guard.bytes.snapshotSource(cert, CmsError, "cms/bad-input", "the recipient certificate");
805
805
  if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", "the recipient certificate PEM could not be decoded", e); } }
806
806
  throw _err("cms/bad-input", "the recipient certificate must be a DER Buffer or PEM string");
807
807
  }
@@ -41,7 +41,7 @@ function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(nam
41
41
  // A certificate descriptor -> raw DER (the recipient cert is parsed for dispatch + rid; the caller
42
42
  // supplies bytes, not a re-encoded parse).
43
43
  function _normCertDer(cert, what) {
44
- if (Buffer.isBuffer(cert) || cert instanceof Uint8Array) return guard.bytes.snapshot(cert, CmsError, "cms/bad-input", what || "a certificate");
44
+ if (guard.bytes.isByteSource(cert)) return guard.bytes.snapshotSource(cert, CmsError, "cms/bad-input", what || "a certificate");
45
45
  if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
46
46
  throw _err("cms/bad-input", (what || "a certificate") + " must be a DER Buffer, Uint8Array, or PEM string");
47
47
  }
package/lib/cms-sign.js CHANGED
@@ -134,7 +134,7 @@ function _resolveAttrPairs(list, what) {
134
134
  return (list || []).map(function (a) {
135
135
  var vals = (a.values || []).map(function (v) { return _toBuf(v, what); });
136
136
  if (!vals.length) throw _err("cms/bad-input", "a signed attribute must carry at least one value (RFC 5652 -- Attribute values is SET SIZE (1..MAX))");
137
- return { type: /^\d+(\.\d+)+$/.test(a.type) ? a.type : O(a.type), values: vals };
137
+ return { type: oid.isDottedDecimal(a.type) ? a.type : O(a.type), values: vals };
138
138
  });
139
139
  }
140
140
 
@@ -351,17 +351,17 @@ function _keyOnlyKeyId(so) {
351
351
 
352
352
  function _normCertDer(c) {
353
353
  if (c == null) throw _err("cms/bad-input", "each signer requires a certificate (cert)");
354
- if (c instanceof Uint8Array || Buffer.isBuffer(c)) {
355
- c = guard.bytes.snapshot(c, CmsError, "cms/bad-input", "a signer certificate");
354
+ if (guard.bytes.isByteSource(c)) {
355
+ c = guard.bytes.snapshotSource(c, CmsError, "cms/bad-input", "a signer certificate");
356
356
  return c[0] === 0x30 ? c : _pemToDer(c.toString("latin1")); // DER as-is, else PEM
357
357
  }
358
358
  if (typeof c === "string") return _pemToDer(c);
359
359
  throw _err("cms/bad-input", "a signer certificate must be a DER Buffer or a PEM string");
360
360
  }
361
361
  function _pemToDer(text) {
362
- var m = text.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/);
363
- if (!m) throw _err("cms/bad-input", "a signer certificate PEM is not a CERTIFICATE block");
364
- return Buffer.from(m[1].replace(/[^A-Za-z0-9+/=]/g, ""), "base64");
362
+ var der = pkix.pemDecodeLenient(text, "CERTIFICATE");
363
+ if (der === null) throw _err("cms/bad-input", "a signer certificate PEM is not a CERTIFICATE block");
364
+ return der;
365
365
  }
366
366
 
367
367
  // pki.cms.sign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
@@ -460,7 +460,7 @@ function _dedupe(ders) {
460
460
  }
461
461
 
462
462
  function _toBuf(v, what) {
463
- if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, CmsError, "cms/bad-input", what);
463
+ if (guard.bytes.isByteSource(v)) return guard.bytes.snapshotSource(v, CmsError, "cms/bad-input", what);
464
464
  throw _err("cms/bad-input", what + " must be a Buffer");
465
465
  }
466
466
 
package/lib/cms-verify.js CHANGED
@@ -123,7 +123,7 @@ EC_CURVE[oid.byName("secp384r1")] = { curve: "P-384", coordLen: 48 };
123
123
  EC_CURVE[oid.byName("secp521r1")] = { curve: "P-521", coordLen: 66 };
124
124
 
125
125
  function _toBuf(v, what) {
126
- if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, CmsError, "cms/bad-input", what);
126
+ if (guard.bytes.isByteSource(v)) return guard.bytes.snapshotSource(v, CmsError, "cms/bad-input", what);
127
127
  throw _err("cms/bad-input", what + " must be a Buffer");
128
128
  }
129
129
 
@@ -679,7 +679,7 @@ function _snapshotIfBytes(input, label) {
679
679
  // Every byte form the parser accepts -- an ArrayBuffer or a DataView reaches the
680
680
  // decoder as readily as a Buffer, and leaving those aliased would reopen the
681
681
  // window for exactly the inputs that came in by the wider door.
682
- if (ArrayBuffer.isView(input) || input instanceof ArrayBuffer) {
682
+ if (guard.bytes.isByteSource(input)) {
683
683
  return guard.bytes.snapshotSource(input, CmsError, "cms/bad-input", label);
684
684
  }
685
685
  return input;
@@ -804,7 +804,7 @@ function setEngine(engine) { _engine = engine; }
804
804
  // rewritten. `requiredEku` is copied for the same reason as the array.
805
805
  var _ANCHOR_CLONE_DEPTH = 8;
806
806
  function _cloneAnchorValue(v, depth) {
807
- if (Buffer.isBuffer(v) || ArrayBuffer.isView(v) || v instanceof ArrayBuffer) {
807
+ if (guard.bytes.isByteSource(v)) {
808
808
  return _snapshotIfBytes(v, "opts.trustAnchors[]");
809
809
  }
810
810
  if (v === null || typeof v !== "object") return v;
@@ -969,7 +969,12 @@ function _applyTrust(res, parsedCerts, cfg) {
969
969
  // A CONFIG fault -- unusable anchors, an invalid time -- is the caller's mistake and must
970
970
  // not be absorbed into "untrusted", which would read as a verdict about the message. A
971
971
  // chain that simply does not reach an anchor is not that: it is the answer.
972
- if (e && e.code && /bad-input|bad-anchor|bad-time/.test(String(e.code))) throw e;
972
+ if (e && e.code) {
973
+ var code = String(e.code);
974
+ if (intrinsic.stringIndexOf(code, "bad-input") !== -1 ||
975
+ intrinsic.stringIndexOf(code, "bad-anchor") !== -1 ||
976
+ intrinsic.stringIndexOf(code, "bad-time") !== -1) throw e;
977
+ }
973
978
  s.trusted = false;
974
979
  });
975
980
  });
package/lib/crl-sign.js CHANGED
@@ -180,7 +180,7 @@ function _resolveReason(reason, isDelta) {
180
180
  // The AKI keyIdentifier from the issuer: an explicit Buffer, or true -> the issuer cert's subjectKeyIdentifier,
181
181
  // else the SHA-1 of the issuer SPKI (RFC 5280 sec. 5.2.1 key-identifier method).
182
182
  function _akiKeyId(val, ctx) {
183
- if (Buffer.isBuffer(val)) return guard.bytes.snapshot(val, CrlError, "crl/bad-input", "the authorityKeyIdentifier keyIdentifier");
183
+ if (guard.bytes.isByteSource(val)) return guard.bytes.snapshotSource(val, CrlError, "crl/bad-input", "the authorityKeyIdentifier keyIdentifier");
184
184
  if (val === true) {
185
185
  if (ctx.issuerCert) {
186
186
  var ski = (ctx.issuerCert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
@@ -597,7 +597,7 @@ function _coerceCrl(crl) {
597
597
  // names, are separate questions that only a certificate can answer.
598
598
  function _resolveIssuer(issuer) {
599
599
  if (issuer == null) throw _err("crl/bad-input", "an issuer is required to verify a CRL");
600
- if (Buffer.isBuffer(issuer)) { _assertValidSpki(issuer, "issuer SPKI"); return { spki: issuer, cert: null }; }
600
+ if (guard.bytes.isByteSource(issuer)) { var _isp = guard.bytes.source(issuer, CrlError, "crl/bad-input", "issuer SPKI"); _assertValidSpki(_isp, "issuer SPKI"); return { spki: _isp, cert: null }; }
601
601
  if (issuer.cert != null) { var ic = _parseIssuerCert(issuer.cert); return { spki: ic.subjectPublicKeyInfo.bytes, cert: ic }; }
602
602
  if (issuer.publicKey != null) { var spki = _reqDer(issuer.publicKey, "issuer.publicKey"); _assertValidSpki(spki, "issuer.publicKey"); return { spki: spki, cert: null }; }
603
603
  // A parsed certificate passed directly. It reaches the same identity and cRLSign checks the
@@ -685,8 +685,8 @@ function _serialHexOf(serial) {
685
685
  if (typeof serial === "bigint") v = serial;
686
686
  else if (typeof serial === "number") { if (!Number.isSafeInteger(serial)) throw _err("crl/bad-input", "serialNumber number must be a safe integer (pass a BigInt, hex string, or Buffer)"); v = BigInt(serial); }
687
687
  else if (typeof serial === "string") { try { v = BigInt(serial); } catch (e) { throw _err("crl/bad-input", "serialNumber string must be a decimal or 0x-hex integer", e); } }
688
- else if (Buffer.isBuffer(serial)) { v = serial.length ? BigInt("0x" + serial.toString("hex")) : 0n; }
689
- else throw _err("crl/bad-input", "serialNumber must be a BigInt, integer, hex string, or Buffer");
688
+ else if (guard.bytes.isByteSource(serial)) { var _sb = guard.bytes.source(serial, CrlError, "crl/bad-input", "serialNumber"); v = _sb.length ? BigInt("0x" + _sb.toString("hex")) : 0n; }
689
+ else throw _err("crl/bad-input", "serialNumber must be a BigInt, integer, hex string, or BufferSource");
690
690
  if (v <= 0n) throw _err("crl/bad-input", "serialNumber must be a positive integer");
691
691
  // Match schema-crl's serialNumberHex: the DER INTEGER content octets (preserving sign padding).
692
692
  return asn1.decode(b.integer(v)).content.toString("hex");
package/lib/crmf-sign.js CHANGED
@@ -328,7 +328,8 @@ function _buildEncryptedKeyPop(pop, outerTag, template) {
328
328
  // reject. CBC is the default here for that reason, not as a preference between the two.
329
329
  var cea = pop.contentEncryptionAlgorithm || "aes-256-cbc";
330
330
  if (typeof cea !== "string") throw _err("crmf/bad-input", "pop.contentEncryptionAlgorithm must be an algorithm name");
331
- if (/gcm|ccm|chacha/i.test(cea)) {
331
+ var _ceaLc = intrinsic.toLowerCase(cea); // /gcm|ccm|chacha/i (ASCII algorithm name)
332
+ if (intrinsic.stringIndexOf(_ceaLc, "gcm") !== -1 || intrinsic.stringIndexOf(_ceaLc, "ccm") !== -1 || intrinsic.stringIndexOf(_ceaLc, "chacha") !== -1) {
332
333
  throw _err("crmf/bad-popo", "encryptedKey is typed EnvelopedData, and the AEAD algorithm " + cea +
333
334
  " produces an AuthEnvelopedData instead (RFC 5083) -- choose a CBC content-encryption algorithm");
334
335
  }
package/lib/csr-sign.js CHANGED
@@ -47,9 +47,6 @@ var _b = pkiBuild.makeBuilder({
47
47
  NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
48
48
  });
49
49
 
50
- // challengePassword DirectoryString charset (PrintableString set); anything else uses UTF8String.
51
- var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
52
-
53
50
  // attributes ::= [0] IMPLICIT SET OF Attribute (RFC 2986). The IMPLICIT tag replaces the universal SET,
54
51
  // so it is a context-constructed [0] whose DER-sorted members ARE the SET-OF; empty -> A0 00.
55
52
  function _implicitSetOf0(members) {
@@ -64,7 +61,7 @@ function _buildRequestedExtensions(extSpec, subjectSpki) { return _b.requestedEx
64
61
  // value is in that charset, else UTF8String; never a Teletex/BMP/Universal string for a new value.
65
62
  function _challengePassword(pw) {
66
63
  if (typeof pw !== "string" || pw.length < 1 || pw.length > 255) throw _err("csr/bad-input", "challengePassword must be a 1..255 character string (RFC 2985 sec. 5.4.1)");
67
- return PRINTABLE_RE.test(pw) ? b.printable(pw) : b.utf8(pw);
64
+ return asn1.isPrintableString(pw) ? b.printable(pw) : b.utf8(pw);
68
65
  }
69
66
 
70
67
  /**
package/lib/ct.js CHANGED
@@ -49,6 +49,10 @@ var intrinsic = require("./guard-intrinsic.js");
49
49
  // replaceable properties to answer one thing, and here it decides whether a log state is one this
50
50
  // toolkit trusts -- answering true for a name the table never carried trusts an untrusted log.
51
51
  var _hasOwn = intrinsic.hasOwn;
52
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
53
+ var _JSON_SUFFIX = ".json";
54
+ // A pathname ending with the literal ".json" suffix (matching /\.json$/), by suffix compare (no regex).
55
+ function _endsWithJson(s) { return s.length >= _JSON_SUFFIX.length && _strSlice(s, s.length - _JSON_SUFFIX.length) === _JSON_SUFFIX; }
52
56
  var ByteReader = require("./byte-reader.js");
53
57
  var ByteWriter = require("./byte-writer.js");
54
58
  var oid = require("./oid.js");
@@ -973,9 +977,9 @@ function _parseHttpsUrl(u, label) {
973
977
  // yield an unambiguous sig URL, so the caller must pass opts.sigUrl explicitly (guards never guess a value).
974
978
  // The derived URL inherits the json URL's https origin.
975
979
  function _sigUrlFor(parsed) {
976
- if (!/\.json$/.test(parsed.pathname)) throw _ctErr("ct/bad-input", "cannot derive the detached-signature URL from a non-.json path (" + JSON.stringify(parsed.pathname) + "); pass opts.sigUrl explicitly");
980
+ if (!_endsWithJson(parsed.pathname)) throw _ctErr("ct/bad-input", "cannot derive the detached-signature URL from a non-.json path (" + JSON.stringify(parsed.pathname) + "); pass opts.sigUrl explicitly");
977
981
  var sig = new URL(parsed.href);
978
- sig.pathname = parsed.pathname.replace(/\.json$/, ".sig");
982
+ sig.pathname = _strSlice(parsed.pathname, 0, parsed.pathname.length - _JSON_SUFFIX.length) + ".sig";
979
983
  return sig.href;
980
984
  }
981
985
 
@@ -994,8 +998,8 @@ function _fetchBody(transport, url, req, label) {
994
998
  res = res || {};
995
999
  var h = {};
996
1000
  Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
997
- var body = (Buffer.isBuffer(res.body) || res.body instanceof Uint8Array)
998
- ? guard.bytes.view(res.body, CtError, "ct/bad-input", label)
1001
+ var body = guard.bytes.isByteSource(res.body)
1002
+ ? guard.bytes.source(res.body, CtError, "ct/bad-input", label)
999
1003
  : Buffer.from(String(res.body == null ? "" : res.body), "utf8");
1000
1004
  if (body.length > req.maxResponseBytes) throw _ctErr("ct/response-too-large", "the " + label + " (" + body.length + " bytes) exceeds the " + req.maxResponseBytes + "-byte cap");
1001
1005
  if (res.status !== 200) throw _ctErr("ct/http-error", "the CT server returned HTTP " + JSON.stringify(res.status) + " for the " + label);
package/lib/est.js CHANGED
@@ -73,6 +73,10 @@ var crmfFmt = require("./schema-crmf");
73
73
  var frameworkError = require("./framework-error");
74
74
  var guard = require("./guard-all");
75
75
  var intrinsic = require("./guard-intrinsic");
76
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
77
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
78
+ var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
79
+ var pkix = require("./schema-pkix"); // shared base64-whitespace strip + JS-\s predicate
76
80
  // Own-key membership through an operation taken at load: written out it reads two replaceable
77
81
  // properties to answer one question, and here it decides which fields of a response are copied.
78
82
  var _hasOwn = intrinsic.hasOwn;
@@ -85,6 +89,14 @@ function E(code, message, cause) { return new EstError(code, message, cause); }
85
89
  var ID_SIGNED_DATA = oid.byName("signedData");
86
90
  var OID_CHALLENGE_PASSWORD = oid.byName("challengePassword");
87
91
  var OID_DECRYPT_KEY_ID = oid.byName("decryptKeyID");
92
+
93
+ // Charset tables built once from the shared pkix.charTable / pkix.allCharsIn char-scan home.
94
+ var _DIGITS_TABLE = pkix.charTable("0123456789"); // /^[0-9]+$/
95
+ var _LABEL_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-"); // /^[A-Za-z0-9._~-]+$/
96
+
97
+ // Trailing-slash strip (/\/+$/ -> ""): the length after dropping a run of trailing "/" (0x2f); the
98
+ // caller slices to it, so the removed regex read is traded for the slice that replaces it.
99
+ function _stripTrailingSlashEnd(s) { var end = s.length; while (end > 0 && _charCodeAt(s, end - 1) === 0x2f) end -= 1; return end; }
88
100
  var OID_ASYMM_DECRYPT_KEY_ID = oid.byName("asymmDecryptKeyID");
89
101
  var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
90
102
  var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
@@ -175,7 +187,7 @@ function transferDecode(body) {
175
187
  var s = guard.text.decode(body, cap, EstError, {
176
188
  charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the EST payload",
177
189
  });
178
- var stripped = s.replace(/[\r\n \t]/g, "");
190
+ var stripped = pkix.stripBase64Whitespace(s);
179
191
  // Strict canonical RFC 4648 base64 (RFC 8951 sec. 3.1) via the shared encoding
180
192
  // guard: the alphabet + whole-4-group + canonical round-trip fail a malformed
181
193
  // body closed instead of silently truncating to a shorter, different DER.
@@ -208,11 +220,31 @@ function transferEncode(der) {
208
220
 
209
221
  // ---- the multipart/mixed splitter (/serverkeygen) -----------------------
210
222
 
223
+ // Case-insensitive ASCII prefix test: does `s` begin with the uppercase literal `litUpper`?
224
+ function _ciStartsWith(s, litUpper) {
225
+ if (s.length < litUpper.length) return false;
226
+ for (var k = 0; k < litUpper.length; k++) {
227
+ var c = _charCodeAt(s, k);
228
+ if (c >= 97 && c <= 122) c -= 32; // fold lowercase -> uppercase
229
+ if (c !== _charCodeAt(litUpper, k)) return false;
230
+ }
231
+ return true;
232
+ }
233
+ // /^multipart\/mixed\s*(;|$)/i by character scan: the media type "multipart/mixed" (case-insensitive),
234
+ // then JS \s* whitespace, then a `;` parameter separator or end of the header value.
235
+ var _MULTIPART_MIXED = "MULTIPART/MIXED";
236
+ function _isMultipartMixedPrefix(ct) {
237
+ if (!_ciStartsWith(ct, _MULTIPART_MIXED)) return false;
238
+ var p = _MULTIPART_MIXED.length;
239
+ while (p < ct.length && pkix.isJsWhitespace(_charCodeAt(ct, p))) p++;
240
+ return p === ct.length || _charCodeAt(ct, p) === 0x3b;
241
+ }
242
+
211
243
  // Extract the boundary from a `multipart/mixed; boundary=...` content-type,
212
244
  // tolerating whitespace before the semicolon (erratum 5779 REJECTED the ban).
213
245
  function _multipartBoundary(contentType) {
214
246
  var ct = String(contentType || "");
215
- if (!/^multipart\/mixed\s*(;|$)/i.test(ct)) return null;
247
+ if (!_isMultipartMixedPrefix(ct)) return null;
216
248
  // Two boundaries name two different splits of the same body; there is no reading
217
249
  // of that header, so it is refused rather than resolved by taking the first.
218
250
  var bp = _ctParam(ct, "boundary");
@@ -223,6 +255,94 @@ function _multipartBoundary(contentType) {
223
255
  return bp.value;
224
256
  }
225
257
 
258
+ // A boundary delimiter at a candidate line start `ls`: `--boundary`, an optional `--` close marker, optional
259
+ // SP/HTAB transport-padding, then a CRLF/LF or end-of-text. Returns { end, close } (end = the index past the
260
+ // trailing line terminator) or null. `(--)?` is taken greedily without backtracking loss: a `-` can be neither
261
+ // padding nor a terminator, so if the close marker is present it is the only possible parse.
262
+ function _matchDelimAt(text, ls, boundary) {
263
+ var n = text.length, q = ls, blen = boundary.length, k;
264
+ if (q + 2 > n || _charCodeAt(text, q) !== 0x2d || _charCodeAt(text, q + 1) !== 0x2d) return null;
265
+ q += 2;
266
+ if (q + blen > n) return null;
267
+ for (k = 0; k < blen; k++) { if (_charCodeAt(text, q + k) !== _charCodeAt(boundary, k)) return null; }
268
+ q += blen;
269
+ var close = false;
270
+ if (q + 2 <= n && _charCodeAt(text, q) === 0x2d && _charCodeAt(text, q + 1) === 0x2d) { close = true; q += 2; }
271
+ while (q < n && (_charCodeAt(text, q) === 0x20 || _charCodeAt(text, q) === 0x09)) q += 1;
272
+ if (q === n) return { end: q, close: close };
273
+ if (_charCodeAt(text, q) === 0x0a) return { end: q + 1, close: close };
274
+ if (_charCodeAt(text, q) === 0x0d && q + 1 < n && _charCodeAt(text, q + 1) === 0x0a) return { end: q + 2, close: close };
275
+ return null;
276
+ }
277
+ // Every delimiter in `text`, replacing the global /(?:^|\r?\n)--boundary(--)?[ \t]*(?:\r?\n|$)/g scan: a
278
+ // delimiter begins the text or follows a CRLF/LF (which the match CONSUMES), so `at` is the index of that
279
+ // leading terminator (0 at the start) and `bodyStart` is the index past the trailing one. The leftmost match
280
+ // at or after the previous `bodyStart` is taken, so two adjacent delimiters share one terminator and only the
281
+ // first is found -- the same non-overlapping behavior the regex has.
282
+ function _findMultipartMarks(text, boundary) {
283
+ var marks = [], n = text.length, searchStart = 0, p;
284
+ while (searchStart <= n) {
285
+ var found = null, foundAt = -1;
286
+ for (p = searchStart; p <= n; p++) {
287
+ if (p === 0) { var r0 = _matchDelimAt(text, 0, boundary); if (r0) { found = r0; foundAt = 0; break; } }
288
+ var lsN = -1;
289
+ if (_charCodeAt(text, p) === 0x0a) lsN = p + 1;
290
+ else if (_charCodeAt(text, p) === 0x0d && p + 1 < n && _charCodeAt(text, p + 1) === 0x0a) lsN = p + 2;
291
+ if (lsN !== -1) { var r1 = _matchDelimAt(text, lsN, boundary); if (r1) { found = r1; foundAt = p; break; } }
292
+ }
293
+ if (!found) break;
294
+ marks[marks.length] = { at: foundAt, bodyStart: found.end, close: found.close };
295
+ searchStart = found.end === foundAt ? found.end + 1 : found.end;
296
+ }
297
+ return marks;
298
+ }
299
+ // Strip the part-body framing the regexes /^(\r?\n){2}/ and /\r?\n$/ removed: the two leading line terminators
300
+ // (all-or-nothing) that follow the header/body separator, and one trailing terminator.
301
+ function _stripTwoLeadingEol(seg, from) {
302
+ var q = from;
303
+ for (var t = 0; t < 2; t++) {
304
+ if (_charCodeAt(seg, q) === 0x0d && _charCodeAt(seg, q + 1) === 0x0a) q += 2;
305
+ else if (_charCodeAt(seg, q) === 0x0a) q += 1;
306
+ else return from;
307
+ }
308
+ return q;
309
+ }
310
+ function _stripOneTrailingEol(seg, end) {
311
+ if (end >= 1 && _charCodeAt(seg, end - 1) === 0x0a) {
312
+ if (end >= 2 && _charCodeAt(seg, end - 2) === 0x0d) return end - 2;
313
+ return end - 1;
314
+ }
315
+ return end;
316
+ }
317
+ // Unfold folded MIME header lines (/\r?\n(?=[ \t])/g -> ""): a CRLF/LF immediately followed by SP/HTAB is a
318
+ // FOLD, not a header break, so the terminator is dropped while the whitespace stays.
319
+ function _unfoldHeaders(raw) {
320
+ var out = "", last = 0, i = 0, n = raw.length;
321
+ while (i < n) {
322
+ var nlLen = 0;
323
+ if (_charCodeAt(raw, i) === 0x0d && i + 1 < n && _charCodeAt(raw, i + 1) === 0x0a) nlLen = 2;
324
+ else if (_charCodeAt(raw, i) === 0x0a) nlLen = 1;
325
+ if (nlLen > 0) {
326
+ var after = i + nlLen;
327
+ if (after < n && (_charCodeAt(raw, after) === 0x20 || _charCodeAt(raw, after) === 0x09)) { out += _strSlice(raw, last, i); last = after; i = after; continue; }
328
+ }
329
+ i += 1;
330
+ }
331
+ return out + _strSlice(raw, last, n);
332
+ }
333
+ // Split on CRLF/LF (.split(/\r?\n/)) by character scan, preserving JS split semantics (leading/trailing "").
334
+ function _splitCrlf(s) {
335
+ var out = [], last = 0, i = 0, n = s.length;
336
+ while (i < n) {
337
+ var nlLen = 0;
338
+ if (_charCodeAt(s, i) === 0x0d && i + 1 < n && _charCodeAt(s, i + 1) === 0x0a) nlLen = 2;
339
+ else if (_charCodeAt(s, i) === 0x0a) nlLen = 1;
340
+ if (nlLen > 0) { out[out.length] = _strSlice(s, last, i); i += nlLen; last = i; } else i += 1;
341
+ }
342
+ out[out.length] = _strSlice(s, last, n);
343
+ return out;
344
+ }
345
+
226
346
  // Split a multipart/mixed body into its parts, each { headers, contentType,
227
347
  // body }. A boundary delimiter is `--boundary` only at the START OF A LINE (body
228
348
  // start or after CRLF), optional transport-padding, then CRLF (a part) or `--`
@@ -234,34 +354,30 @@ function splitMultipartMixed(body, contentType) {
234
354
  var text = guard.text.decode(body, constants.LIMITS.DER_MAX_BYTES * 2, EstError, {
235
355
  charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the multipart body",
236
356
  });
237
- var esc = boundary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
238
- var delimRe = new RegExp("(?:^|\\r?\\n)--" + esc + "(--)?[ \\t]*(?:\\r?\\n|$)", "g");
239
- var marks = [], m;
240
- while ((m = delimRe.exec(text)) !== null) {
241
- marks.push({ at: m.index, bodyStart: delimRe.lastIndex, close: m[1] === "--" });
242
- if (delimRe.lastIndex === m.index) delimRe.lastIndex += 1; // guard a zero-length match
243
- }
357
+ var marks = _findMultipartMarks(text, boundary);
244
358
  var closeAt = -1;
245
359
  for (var c = 0; c < marks.length; c++) { if (marks[c].close) { closeAt = c; break; } }
246
360
  if (closeAt === -1) throw E("est/bad-multipart", "the multipart body is missing its terminal boundary (RFC 2046)");
247
361
  var parts = [];
248
362
  for (var i = 0; i < closeAt; i++) {
249
- var seg = text.slice(marks[i].bodyStart, marks[i + 1].at); // between this delimiter and the next
363
+ var seg = _strSlice(text, marks[i].bodyStart, marks[i + 1].at); // between this delimiter and the next
250
364
  var sep = seg.indexOf("\r\n\r\n");
251
365
  if (sep === -1) sep = seg.indexOf("\n\n");
252
366
  if (sep === -1) throw E("est/bad-multipart", "a multipart part is missing its header/body separator");
253
- var rawHeaders = seg.slice(0, sep);
254
- var partBody = seg.slice(sep).replace(/^(\r?\n){2}/, "").replace(/\r?\n$/, "");
367
+ var rawHeaders = _strSlice(seg, 0, sep);
368
+ var partBody = _strSlice(seg, _stripTwoLeadingEol(seg, sep), _stripOneTrailingEol(seg, seg.length));
255
369
  var headers = {};
256
370
  // Unfold MIME part headers (RFC 5322 sec. 2.2.3 / RFC 2046): a CRLF immediately followed by whitespace is a
257
371
  // FOLD, not a header break, so it is removed before the split -- a Content-Type whose smime-type parameter
258
372
  // continues on the next line must be read whole, not truncated.
259
- rawHeaders.replace(/\r?\n(?=[ \t])/g, "").split(/\r?\n/).forEach(function (line) {
373
+ var lines = _splitCrlf(_unfoldHeaders(rawHeaders));
374
+ for (var li = 0; li < lines.length; li++) {
375
+ var line = lines[li];
260
376
  var col = line.indexOf(":");
261
377
  if (col > 0) headers[line.slice(0, col).trim().toLowerCase()] = line.slice(col + 1).trim();
262
- });
378
+ }
263
379
  var partCt = headers["content-type"] || "";
264
- if (/^multipart\//i.test(partCt)) throw E("est/bad-multipart", "a nested multipart part is not permitted");
380
+ if (_ciStartsWith(partCt, "MULTIPART/")) throw E("est/bad-multipart", "a nested multipart part is not permitted");
265
381
  parts.push({ headers: headers, contentType: partCt, body: partBody });
266
382
  }
267
383
  return parts;
@@ -376,7 +492,7 @@ function _recipientMatches(recipientInfos, opts) {
376
492
  var sn = ias.serialNumber, want;
377
493
  if (typeof sn === "bigint" && sn >= 0n) want = sn;
378
494
  else if (typeof sn === "number" && Number.isSafeInteger(sn) && sn >= 0) want = BigInt(sn);
379
- else if (typeof sn === "string" && /^[0-9]+$/.test(sn)) want = BigInt(sn);
495
+ else if (typeof sn === "string" && pkix.allCharsIn(sn, _DIGITS_TABLE)) want = BigInt(sn);
380
496
  else throw E("est/bad-input", "expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
381
497
  // An issuerAndSerialNumber names an ASYMMETRIC recipient -- a certificate identity the server derived from
382
498
  // an AsymmetricDecryptKeyIdentifier (RFC 7030 sec. 4.4.2); a symmetric KEK recipient has none. So a SYMMETRIC
@@ -442,12 +558,31 @@ function _ctParam(contentType, name) {
442
558
  return { value: value, duplicated: count > 1 };
443
559
  }
444
560
 
561
+ // A media type `type/subtype` by character scan (RFC 2045 sec. 5.1 restricted token): the first character
562
+ // of each part is alphanumeric, the rest add !#$&^_.+- (case-insensitive), exactly one `/` separates them.
563
+ var _MT_FIRST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
564
+ var _MT_REST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$&^_.+-");
565
+ function _isMediaToken(s, from, to) {
566
+ if (to <= from || !_MT_FIRST[_charCodeAt(s, from)]) return false;
567
+ for (var i = from + 1; i < to; i++) { if (!_MT_REST[_charCodeAt(s, i)]) return false; }
568
+ return true;
569
+ }
570
+ // /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i -> the lowercased media type, or null:
571
+ // trim JS whitespace, require a single `/` splitting two restricted tokens.
572
+ function _mediaType(seg) {
573
+ var a = 0, b = seg.length, slash = -1, i;
574
+ while (a < b && pkix.isJsWhitespace(_charCodeAt(seg, a))) a += 1;
575
+ while (b > a && pkix.isJsWhitespace(_charCodeAt(seg, b - 1))) b -= 1;
576
+ for (i = a; i < b; i++) { if (_charCodeAt(seg, i) === 0x2f) { if (slash !== -1) return null; slash = i; } }
577
+ if (slash === -1 || !_isMediaToken(seg, a, slash) || !_isMediaToken(seg, slash + 1, b)) return null;
578
+ return _toLowerCase(_strSlice(seg, a, b));
579
+ }
580
+
445
581
  function _partMediaType(contentType) {
446
582
  var segs = _splitContentTypeParams(String(contentType || ""));
447
- var mediaMatch = /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i.exec(segs[0]);
448
583
  var st = _ctParam(contentType, "smime-type");
449
584
  return {
450
- media: mediaMatch ? mediaMatch[1].toLowerCase() : null,
585
+ media: _mediaType(segs[0]),
451
586
  smimeType: st.value === null ? null : st.value.toLowerCase(),
452
587
  ambiguous: st.duplicated,
453
588
  };
@@ -679,14 +814,15 @@ function classifyResponse(status, headers, body, opts) {
679
814
  function paths(baseUrl, opts) {
680
815
  opts = opts || {};
681
816
  _knownOpts(opts, PATHS_OPTS, "paths");
682
- var prefix = String(baseUrl).replace(/\/+$/, "") + "/.well-known/est";
817
+ var base = String(baseUrl);
818
+ var prefix = base.slice(0, _stripTrailingSlashEnd(base)) + "/.well-known/est";
683
819
  if (opts.label != null) {
684
820
  var label = String(opts.label);
685
821
  // A label is ONE URL path segment: unreserved characters only (RFC 3986),
686
822
  // never a dot-segment or an operation name. Rejecting rather than
687
823
  // percent-encoding keeps a reserved char (`/` `?` `#` `%`) or `..` from
688
824
  // silently retargeting the request to a different resource.
689
- if (label === "" || label === "." || label === ".." || !/^[A-Za-z0-9._~-]+$/.test(label) || OPERATIONS.indexOf(label) !== -1) {
825
+ if (label === "" || label === "." || label === ".." || !pkix.allCharsIn(label, _LABEL_TABLE) || OPERATIONS.indexOf(label) !== -1) {
690
826
  throw E("est/bad-label", "an EST CA label must be a single path segment of unreserved characters, not '.' / '..' or an operation name (RFC 7030 sec. 3.2.2)");
691
827
  }
692
828
  prefix += "/" + label;
@@ -707,8 +843,10 @@ function challengePasswordFromTlsUnique(channelBinding) {
707
843
  // The count comes from the guard, so an own `length` on the caller's Buffer cannot report a
708
844
  // full one as empty. `length` is shadowable on any instance, and `Object.defineProperty(buf,
709
845
  // "length", { get: function () { return 0; } })` had this refuse bytes that were there.
710
- if (!Buffer.isBuffer(channelBinding) || guard.bytes.lengthOf(channelBinding) === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
711
- var b64 = guard.bytes.view(channelBinding, EstError, "est/bad-input", "the channel-binding bytes").toString("base64");
846
+ if (!guard.bytes.isByteSource(channelBinding)) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
847
+ var _cb = guard.bytes.source(channelBinding, EstError, "est/bad-input", "the channel-binding bytes");
848
+ if (_cb.length === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
849
+ var b64 = _cb.toString("base64");
712
850
  if (b64.length > 255) throw E("est/tls-unique-too-long", "the base64 tls-unique value exceeds 255 octets (RFC 7030 sec. 3.5)");
713
851
  return _attr(OID_CHALLENGE_PASSWORD, [asn1.build.printable(b64)]);
714
852
  }
@@ -834,9 +972,9 @@ var MAX_TIMEOUT = constants.TIME.seconds(600);
834
972
  // The DER of a caller-supplied CSR: a DER Buffer as-is, or a PEM "CERTIFICATE REQUEST"
835
973
  // decoded. Any other input is a config-time est/bad-input.
836
974
  function _csrDer(input) {
837
- if (Buffer.isBuffer(input)) return guard.bytes.snapshot(input, EstError, "est/bad-input", "a CSR");
975
+ if (guard.bytes.isByteSource(input)) return guard.bytes.snapshotSource(input, EstError, "est/bad-input", "a CSR");
838
976
  if (typeof input === "string") return csr.pemDecode(input);
839
- throw E("est/bad-input", "a CSR must be a DER Buffer or a PEM CERTIFICATE REQUEST string");
977
+ throw E("est/bad-input", "a CSR must be a DER BufferSource or a PEM CERTIFICATE REQUEST string");
840
978
  }
841
979
 
842
980
  // Parse + validate the request URL BEFORE any transport is called: a URL that will not
@@ -880,9 +1018,47 @@ function _redirectTarget(current, location, method, opts) {
880
1018
  // which must NOT be read as a Basic challenge), then the scheme is matched at the start or after a
881
1019
  // comma-separated challenge, followed by whitespace / a comma / end -- never a substring, so `Digest
882
1020
  // realm="basic"` is not taken as Basic. `scheme` is always our own literal ("Basic" / "Digest").
1021
+ // Blank the CONTENTS of every complete quoted-string (/"(?:[^"\\]|\\.)*"/g -> `""`) by character scan: a
1022
+ // `\` escapes the next character, so `\"` does not close; an unterminated final quote is left as written
1023
+ // (the regex only matches a closed quoted-string).
1024
+ function _blankQuotedStrings(s) {
1025
+ var out = "", last = 0, i = 0, n = s.length;
1026
+ while (i < n) {
1027
+ if (_charCodeAt(s, i) === 0x22) {
1028
+ var j = i + 1, closed = -1;
1029
+ while (j < n) { var c = _charCodeAt(s, j); if (c === 0x5c) { j += 2; continue; } if (c === 0x22) { closed = j; break; } j += 1; }
1030
+ if (closed === -1) break;
1031
+ out += _strSlice(s, last, i) + "\"\"";
1032
+ last = closed + 1; i = closed + 1;
1033
+ } else i += 1;
1034
+ }
1035
+ return out + _strSlice(s, last, n);
1036
+ }
1037
+ // Is `scheme` (our own literal, case-insensitive) offered as a challenge TOKEN in the blanked header?
1038
+ // (?:^|,)\s*SCHEME(?=[\s,]|$): at the start or after a comma, optional whitespace, the scheme, then a
1039
+ // whitespace / comma / end boundary -- never a substring, so `Digest realm="basic"` is not read as Basic.
1040
+ function _schemeOffered(s, scheme) {
1041
+ var n = s.length, slen = scheme.length, p, k;
1042
+ for (p = 0; p <= n; p++) {
1043
+ if (p !== 0 && _charCodeAt(s, p - 1) !== 0x2c) continue;
1044
+ var q = p;
1045
+ while (q < n && pkix.isJsWhitespace(_charCodeAt(s, q))) q += 1;
1046
+ if (q + slen > n) continue;
1047
+ var ok = true;
1048
+ for (k = 0; k < slen; k++) {
1049
+ var x = _charCodeAt(s, q + k), y = _charCodeAt(scheme, k);
1050
+ if (x >= 65 && x <= 90) x += 32;
1051
+ if (y >= 65 && y <= 90) y += 32;
1052
+ if (x !== y) { ok = false; break; }
1053
+ }
1054
+ if (!ok) continue;
1055
+ var after = q + slen;
1056
+ if (after === n || pkix.isJsWhitespace(_charCodeAt(s, after)) || _charCodeAt(s, after) === 0x2c) return true;
1057
+ }
1058
+ return false;
1059
+ }
883
1060
  function _offersScheme(www, scheme) {
884
- var s = String(www || "").replace(/"(?:[^"\\]|\\.)*"/g, "\"\"");
885
- return new RegExp("(?:^|,)\\s*" + scheme + "(?=[\\s,]|$)", "i").test(s);
1061
+ return _schemeOffered(_blankQuotedStrings(String(www || "")), scheme);
886
1062
  }
887
1063
  // The chosen HTTP auth scheme: an explicit opts.auth.scheme wins; a legacy opts.username / opts.password
888
1064
  // WITHOUT opts.auth means Basic (backwards compatible); otherwise none. There is NO scheme:"auto" -- a
@@ -1573,8 +1749,8 @@ function _collectNestedPaths(list, prefix, out, depth) {
1573
1749
  function _assertAgrees(name, supplied, carried) {
1574
1750
  if (supplied == null) return;
1575
1751
  var same;
1576
- if (Buffer.isBuffer(supplied) || supplied instanceof Uint8Array) {
1577
- same = Buffer.isBuffer(carried) && Buffer.from(supplied).equals(carried);
1752
+ if (guard.bytes.isByteSource(supplied)) {
1753
+ same = Buffer.isBuffer(carried) && guard.bytes.source(supplied, EstError, "est/bad-input", "opts.senderNonce").equals(carried);
1578
1754
  } else {
1579
1755
  // Through the shared authoring guard, not a bare BigInt(): a `number` above
1580
1756
  // Number.MAX_SAFE_INTEGER has already lost the digits that distinguish it from
@@ -1624,8 +1800,8 @@ function _shallowCopy(o) {
1624
1800
  function _cmcRequestDer(request) {
1625
1801
  // COPIED, not aliased: these bytes are parsed now (for the requested keys) and
1626
1802
  // transmitted later, so sharing the caller's buffer would let the two disagree.
1627
- if (Buffer.isBuffer(request) || request instanceof Uint8Array) {
1628
- return guard.bytes.snapshot(request, EstError, "est/bad-input", "the Full PKI Request");
1803
+ if (guard.bytes.isByteSource(request)) {
1804
+ return guard.bytes.snapshotSource(request, EstError, "est/bad-input", "the Full PKI Request");
1629
1805
  }
1630
1806
  if (typeof request === "string") return cms.pemDecode(request);
1631
1807
  throw E("est/bad-input", "pki.est.fullcmc requires the Full PKI Request as DER bytes or a PEM CMS block");
@@ -1991,13 +2167,37 @@ function _serverkeygenEncryptionFromCsr(csrDer) {
1991
2167
  // The delivered private key rides TLS confidentiality alone: a NULL / anonymous / EXPORT suite MUST NOT be
1992
2168
  // used to carry it (RFC 7030 sec. 4.4, sec. 6). Assert (never reconfigure) the negotiated cipher when the
1993
2169
  // transport surfaces it; an injected transport that reports no cipher is trusted (the test/loopback channel).
2170
+ // The weak-cipher token scan (RFC 3986/2119-free ASCII, by character code): a "word" character is
2171
+ // [A-Za-z0-9_] (regex \w), and a boundary (\b) sits where a word character meets a non-word one or a
2172
+ // string edge. `_subAt`/`_containsSub` test a literal token at / anywhere in an already-uppercased name.
2173
+ function _isWordChar(c) { return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95; }
2174
+ function _subAt(s, sub, i) { var m = sub.length; if (i + m > s.length) return false; for (var k = 0; k < m; k++) { if (_charCodeAt(s, i + k) !== _charCodeAt(sub, k)) return false; } return true; }
2175
+ function _containsSub(s, sub) { for (var i = 0; i + sub.length <= s.length; i++) { if (_subAt(s, sub, i)) return true; } return false; }
2176
+ function _wbBefore(s, i) { return i === 0 || !_isWordChar(_charCodeAt(s, i - 1)); }
2177
+ function _wbAfter(s, j) { return j === s.length || !_isWordChar(_charCodeAt(s, j)); }
2178
+ // /NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/ over the uppercased name: the three plain substrings, then
2179
+ // EXP at a word boundary followed by a separator OR a digit run (EXP-RC4-MD5, EXP1024-RC4-SHA -- a bare
2180
+ // \bEXP\b misses EXP1024), then ADH / AECDH each bounded on both sides.
2181
+ function _hasWeakCipherToken(name) {
2182
+ if (_containsSub(name, "NULL") || _containsSub(name, "ANON") || _containsSub(name, "EXPORT")) return true;
2183
+ var i, n = name.length;
2184
+ for (i = 0; i + 3 <= n; i++) {
2185
+ if (_subAt(name, "EXP", i) && _wbBefore(name, i)) {
2186
+ var c = _charCodeAt(name, i + 3);
2187
+ if (c === 45 || c === 95 || (c >= 48 && c <= 57)) return true;
2188
+ }
2189
+ }
2190
+ for (i = 0; i < n; i++) {
2191
+ if (_subAt(name, "ADH", i) && _wbBefore(name, i) && _wbAfter(name, i + 3)) return true;
2192
+ if (_subAt(name, "AECDH", i) && _wbBefore(name, i) && _wbAfter(name, i + 5)) return true;
2193
+ }
2194
+ return false;
2195
+ }
1994
2196
  function _assertConfidentialCipher(res) {
1995
2197
  if (!res || !res.tls || !res.tls.cipher) return;
1996
2198
  var c = res.tls.cipher;
1997
2199
  var name = (String(c.name || "") + " " + String(c.standardName || "")).toUpperCase();
1998
- // EXPORT covers the RFC/IANA name; the OpenSSL short names use an EXP prefix followed by a separator OR a
1999
- // digit run (EXP-RC4-MD5, EXP1024-RC4-SHA), so match EXP before either -- a bare `\bEXP\b` misses EXP1024.
2000
- if (/NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/.test(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
2200
+ if (_hasWeakCipherToken(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
2001
2201
  }
2002
2202
 
2003
2203
  // A case-insensitive header lookup: the injectable transport seam only promises { status, headers, body }, so a
@@ -2130,7 +2330,7 @@ function serverkeygen(baseUrl, csrInput, opts) {
2130
2330
  var eis = opts.expectedRecipientIssuerSerial;
2131
2331
  if (typeof eis !== "object" || Buffer.isBuffer(eis) || !Buffer.isBuffer(eis.issuer)) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial must be { issuer: Buffer, serialNumber }");
2132
2332
  var s = eis.serialNumber;
2133
- if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && /^[0-9]+$/.test(s)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
2333
+ if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && pkix.allCharsIn(s, _DIGITS_TABLE)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
2134
2334
  }
2135
2335
  // A recipient expectation implies the key part MUST be encrypted to that recipient; if the CSR advertised no
2136
2336
  // key-encryption attribute the coherence check below never runs, so a compromised CA could deliver the key