@blamejs/pki 0.6.14 → 0.6.16

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 +27 -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 +140 -286
  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/cmp-verify.js CHANGED
@@ -1,36 +1,12 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- the pki.cmp.verify implementation. The operator-facing @module pki.cmp home is
4
+ // @internal
6
5
  // lib/cmp-build.js; this file adds the @primitive pki.cmp.verify block and re-exports the cmp producing
7
- // surface (build / transfer / wellKnownUrl) so the whole pki.cmp namespace wires through here (index.js).
8
- //
9
- // pki.cmp.verify -- incoming CMP PKIMessage protection verification (the verify-inverse of the
10
- // pki.cmp.build protection). This module is the cms-verify mirror: it composes the HEAVY verify
11
- // dependency set (the path-validate signature engine + full path building) that pki.cmp.build never
12
- // pulls, and re-exports the cmp-build producing surface so the whole pki.cmp namespace is wired here.
13
- //
14
- // The load-bearing rule: the protection is recomputed over the exact ProtectedPart bytes the builder
15
- // signed, a SEQUENCE { header, body } reconstructed from the parser-surfaced raw headerBytes / bodyBytes
16
- // (schema-cmp), never a re-serialization of the decoded structs. A re-encoder that normalized a malleable
17
- // interior field would run the recompute over different bytes than a strict signer covered: both a false
18
- // reject for a canonical peer and a bypass. The signature path routes through the single path-validate engine
19
- // (with the EdDSA low-order-point gate) injected via setEngine, never build's self-check, which skips it.
20
- // RFC 9810 sec. 5.1.3, algorithms RFC 9481 / RFC 9579, out-of-path signer profile RFC 9483.
21
6
 
22
- // The slot predicates and the array test, snapshotted at load through the guard family's own home
23
- // for that. Both decide whether a caller's value gets COPIED, so reading either at call time hands
24
- // the copy decision to the caller: an `isUint8Array` answering false leaves a live caller buffer
25
- // where the copy belongs and leaves that buffer out of the wipe list, and an `Array.isArray`
26
- // answering false routes a trust list past the list door and back out by reference.
27
7
  var intrinsic = require("./guard-intrinsic");
28
8
  var _sizeOf = intrinsic.sizeOf;
29
9
  var util = { types: intrinsic.types };
30
- // The rest of what this verb reads from the runtime while it is reducing a caller's options, taken
31
- // at load. The option door decides what gets copied and what gets refused, which is the same kind
32
- // of decision a guard makes, so it is held to the same rule: the check over lib/ that refuses a
33
- // live read covers every module importing these, and this one does. See guard-intrinsic.
34
10
  var _isBuffer = intrinsic.isBuffer;
35
11
  var _bufferFrom = intrinsic.bufferFrom;
36
12
  var _fromCharCode = intrinsic.fromCharCode;
@@ -45,22 +21,10 @@ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
45
21
  var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
46
22
  var _strIndexOf = intrinsic.uncurry(String.prototype.indexOf);
47
23
  var _compare = intrinsic.compare;
48
- // The byte-to-text step of the SAN comparison below, and of the certificate identity key. One
49
- // returning a constant makes every sender name equal every SAN, and collapses every certificate in
50
- // the pool to one key.
51
24
  var _toString = intrinsic.uncurry(Buffer.prototype.toString);
52
25
  var _map = intrinsic.map;
53
- // The conversions, captured for the same reason as everything above them: `String` is a writable
54
- // property of globalThis, it renders the sender name that the SAN comparison compares, and it
55
- // decides which own property names count as array indices in the density check below.
56
26
  var _String = intrinsic.String;
57
27
  var _isFinite = intrinsic.isFinite;
58
- // The rest of the string and pattern surface the rfc822Name and URI comparisons below are built
59
- // out of. Those comparisons decide whether a signer is allowed to speak for a sender, and every
60
- // step of them -- where the mailbox separator is, which substring is the domain, whether a
61
- // local-part is well-formed, how a URI splits into scheme and authority -- is one of these calls.
62
- // A replacement anywhere in the chain moves the boundary, and the identity the verb ends up
63
- // comparing is no longer the one the certificate carries.
64
28
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
65
29
  var _strLastIndexOf = intrinsic.uncurry(String.prototype.lastIndexOf);
66
30
  var _isArray = Array.isArray;
@@ -81,12 +45,8 @@ var CmpError = frameworkError.CmpError;
81
45
  var b = asn1.build;
82
46
  function _err(code, message, cause) { return new CmpError(code, message, cause); }
83
47
 
84
- // The cmp error namespace, reused so PBMAC1-params decode faults surface as cmp/bad-mac-data.
85
48
  var NS = pkix.makeNS("cmp", CmpError, oid);
86
49
  var PBMAC1_PARAMS = pkix.pbmac1Params(NS);
87
- // The shared RFC 5280 sec. 4.2.1 extension-value decoders (the acme / inspect home): the signer keyUsage gate
88
- // reads through the single structurally-strict decoder (X.690 sec. 11.2.2 minimal NamedBitList), not a
89
- // hand-rolled bit test, so a malformed KeyUsage fails the gate independently of the path validator also rejecting it.
90
50
  var _certExtDecoders = pkix.certExtensionDecoders(NS).byOid;
91
51
 
92
52
  var KNOWN_VERIFY_OPTS = {
@@ -94,48 +54,28 @@ var KNOWN_VERIFY_OPTS = {
94
54
  transactionID: 1, expectRecipNonce: 1, revocationChecker: 1, maxIterations: 1,
95
55
  };
96
56
 
97
- // PBMAC1 PBKDF2-PRF / messageAuthScheme name -> WebCrypto hash. Only SHA-256/384/512 are supported
98
- // (RFC 9481 sec. 7 mandates SHA-256; RFC 9579 sec. 7 bars a <= 160-bit digest, so hmacWithSHA1, and an
99
- // omitted-PRF that resolves to it, is unsupported and never MAC-verified under a weak digest).
100
- // Keyed by the immutable dotted OID, never the display name: pki.oid.register() can rename hmacWithSHA256/384/512,
101
- // so a name-keyed lookup would reject a valid PBMAC1 message under a renamed registry (the immutable-OID-dispatch rule).
102
- // Null-prototype, because this table is read as an allowlist: on a `{}` a planted
103
- // `Object.prototype["1.2.3"]` names a hash for a PRF OID the table never carried, and an
104
- // unsupported PRF is then admitted and dispatched.
105
57
  var PRF_HASH = _create(null);
106
58
  PRF_HASH[oid.byName("hmacWithSHA256")] = "SHA-256";
107
59
  PRF_HASH[oid.byName("hmacWithSHA384")] = "SHA-384";
108
60
  PRF_HASH[oid.byName("hmacWithSHA512")] = "SHA-512";
109
61
 
110
- // Attacker-controlled PBKDF2 work-factor bounds (RFC 8018 sec. 4.2; the pkcs12 verifyMac / _capWork model).
111
62
  var PBMAC1_MAX_ITER = constants.LIMITS.PBKDF2_MAX_ITERATIONS;
112
- var PBMAC1_MIN_ITER = 1000; // RFC 8018 sec. 4.2 floor -- matches pki.cmp.build; a trivially small count is refused
113
- var PBMAC1_MIN_SALT = 8; // octets -- RFC 8018 sec. 4.1 (64-bit) floor; an empty/short salt loses precomputation resistance
63
+ var PBMAC1_MIN_ITER = 1000;
64
+ var PBMAC1_MIN_SALT = 8;
114
65
  var PBMAC1_MAX_SALT = constants.LIMITS.PBKDF2_MAX_SALT;
115
- var PBMAC1_KEYLEN_MIN = 20; // RFC 9579 sec. 9 -- a short derived key is refused
66
+ var PBMAC1_KEYLEN_MIN = 20;
116
67
  var PBMAC1_KEYLEN_MAX = 1024;
117
- // PBKDF2 PRF output length (octets) -- the size of one derived block; keyLength beyond it costs extra HMAC rounds.
118
68
  var PRF_HLEN = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
119
69
 
120
- // The legacy / KEM MAC protection OIDs (RFC 9810 sec. 5.1.3.1/.2/.4) the v1 verifier recognizes and refuses.
121
- // Build emits only PBMAC1, so the verifier rejects a legacy/KEM MAC OID and never accepts a construction
122
- // it does not verify (a silent accept of an unverified algorithm would be fail-open).
123
70
  var UNSUPPORTED_MAC_OIDS = _create(null);
124
71
  ["passwordBasedMac", "dhBasedMac", "kemBasedMac"].forEach(function (n) {
125
72
  var o = oid.byName(n);
126
73
  if (o) UNSUPPORTED_MAC_OIDS[o] = n;
127
74
  });
128
75
 
129
- // The signature engine + full path build/validate, injected by path-validate (the crl/ocsp seam pattern) so
130
- // there is never a second, weaker CMP signature verifier and no require cycle. Null until path-validate loads
131
- // (index.js always loads pki.path before a pki.cmp.verify call).
132
76
  var _engine = null;
133
77
  function setEngine(engine) { _engine = engine; }
134
78
 
135
- // ---- verdict builders ----
136
- // One verdict shape (mirrors cms.verify / tsp.verify): `valid` is crypto intactness under the DECLARED
137
- // protectionAlg; `trusted` is mac=secret-matched / signature=chained-to-a-supplied-anchor; `code`/`reason`
138
- // are set on a rejected verdict. Only a config-tier / malformed-DER error throws.
139
79
  function _verdict(m, type, protectionAlg, valid, trusted, code, reason, signer) {
140
80
  var v = {
141
81
  valid: valid,
@@ -155,80 +95,48 @@ function _verdict(m, type, protectionAlg, valid, trusted, code, reason, signer)
155
95
  function _ok(m, type, alg, trusted, signer) { return _verdict(m, type, alg, true, trusted, null, null, signer); }
156
96
  function _fail(m, type, alg, code, reason, signer) { return _verdict(m, type, alg, false, false, code, reason, signer || null); }
157
97
 
158
- // ---- input coercion: parse a DER Buffer / PEM string, or RE-DERIVE an authenticated view of an
159
- // already-parsed object. A caller-supplied parsed object's DECODED struct (header.transactionID, body, ...)
160
- // is UNTRUSTED -- middleware that mutated a decoded field after parsing must not have it returned in a
161
- // valid verdict or slip past the opt-in echo checks while the crypto verifies over the original raw slices.
162
- // So a parsed object is reassembled from its AUTHENTICATED raw slices (headerBytes / bodyBytes / protection /
163
- // extraCerts) and re-parsed, so every security-relevant and returned field derives from the authenticated
164
- // bytes, never the caller's decoded representation.
165
98
  function _coerce(message) {
166
99
  if (message && typeof message === "object" && !_isBuffer(message) && !util.types.isUint8Array(message) &&
167
100
  message.headerBytes !== undefined && message.bodyBytes !== undefined &&
168
101
  message.header !== undefined && message.body !== undefined) {
169
- // A malformed raw slice (a null protection.bytes, a non-buffer extraCerts entry) makes _reassemble throw a
170
- // raw Asn1Error; normalize it to the documented typed cmp/bad-input so every malformed input surfaces a
171
- // cmp/* code (cmp.parse's own faults are already CmpError and propagate unchanged).
172
102
  try { return cmp.parse(_reassemble(message)); }
173
103
  catch (e) { throw e instanceof CmpError ? e : _err("cmp/bad-input", "the parsed PKIMessage object carries a malformed raw slice (headerBytes / bodyBytes / protection / extraCerts): " + ((e && e.message) || e), e); }
174
104
  }
175
- // A raw Buffer / Uint8Array parses into zero-copy views; a caller that mutates the input after verify would
176
- // change the returned authenticated fields (transactionID, nonces, header, body) while valid stays true.
177
- // Parse a PRIVATE copy so every verdict field is bound to the verified snapshot, never the caller's mutable
178
- // buffer. (A PEM string is immutable and base64-decodes into fresh bytes, so it needs no copy.)
179
- // The slot, not the prototype: this decides whether the message is privately copied before it is
180
- // parsed, and `instanceof` consults `Uint8Array[Symbol.hasInstance]` at call time.
181
105
  if (guard.bytes.isByteSource(message)) return cmp.parse(guard.bytes.snapshotSource(message, CmpError, "cmp/bad-input", "the CMP message"));
182
- return cmp.parse(message); // a PEM CMP string (immutable; base64-decodes into fresh bytes, so it needs no copy; throws cmp/* on malformed input)
106
+ return cmp.parse(message);
183
107
  }
184
108
 
185
- // Rebuild the PKIMessage DER from a parsed object's raw slices: SEQUENCE { header, body, protection [0]
186
- // EXPLICIT BIT STRING OPTIONAL, extraCerts [1] EXPLICIT SEQUENCE OPTIONAL }, the exact inverse of the
187
- // parser's surfaced fields (cmp-build.js assembles the identical shape). Re-parsing this authenticates every
188
- // field against the raw bytes, discarding any mutated decoded field the caller may have carried.
189
109
  function _reassemble(m) {
190
110
  var kids = [b.raw(m.headerBytes), b.raw(m.bodyBytes)];
191
111
  if (m.protection != null) _append(kids, b.explicit(0, b.bitString(m.protection.bytes, m.protection.unusedBits)));
192
112
  if (m.extraCerts != null && m.extraCerts.length) {
193
- // Through the captured `map`: this rebuilds the exact bytes the protection is checked against,
194
- // so a replacement returning a different array decides what the signature is verified over.
195
113
  _append(kids, b.explicit(1, b.sequence(_map(m.extraCerts, function (c) { return b.raw(c); }))));
196
114
  }
197
115
  return b.sequence(kids);
198
116
  }
199
117
 
200
- // A certificate DER from any BufferSource (Buffer / typed array / DataView / ArrayBuffer,
201
- // detached-view-safe) or a PEM string.
202
118
  function _certDer(cert, what) {
203
119
  if (guard.bytes.isByteSource(cert)) return guard.bytes.source(cert, CmpError, "cmp/bad-input", what);
204
120
  if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cmp/bad-input", what + " PEM could not be decoded", e); } }
205
121
  throw _err("cmp/bad-input", what + " must be a certificate DER BufferSource or PEM string");
206
122
  }
207
123
 
208
- // The subjectKeyIdentifier extension value of a parsed certificate, or null when absent/undecodable.
209
- // x509.parse surfaces extensions as an array of { oid, name, critical, value } (value = raw extnValue DER).
210
124
  function _certSki(parsed) {
211
125
  var skiOid = oid.byName("subjectKeyIdentifier");
212
126
  var exts = parsed.extensions;
213
127
  for (var i = 0; i < exts.length; i++) {
214
128
  if (exts[i].oid !== skiOid) continue;
215
- try { return asn1.read.octetString(asn1.decode(exts[i].value)); } catch (_e) { return null; }
129
+ try { return asn1.read.octetString(asn1.decode(exts[i].value)); } catch (_e) { return null; } // allow:swallow-unverified -- fail-closed: a malformed subjectKeyIdentifier yields null, so no SKI is used to match
216
130
  }
217
131
  return null;
218
132
  }
219
133
  function _subjectDn(parsed) { return parsed.subject.dn || null; }
220
134
 
221
- // The decoded GeneralName nodes of a parsed certificate's subjectAltName (empty if none / malformed).
222
- // x509.parse surfaces `value` as the unwrapped extnValue content; for SAN that is the SEQUENCE OF GeneralName.
223
135
  function _sanGeneralNames(parsed) {
224
136
  var sanOid = oid.byName("subjectAltName");
225
137
  var exts = parsed.extensions;
226
138
  for (var i = 0; i < exts.length; i++) {
227
139
  if (exts[i].oid !== sanOid) continue;
228
- // RFC 5280 sec. 4.2.1.6: subjectAltName is a SEQUENCE OF GeneralName. Validate the outer structure AND every
229
- // entry through the shared generalNames schema before any child is used for identity binding: path validation
230
- // does not decode a non-critical, otherwise-unused SAN, so a malformed outer tag (e.g. a SET wrapping a valid
231
- // [2]) or a malformed entry must fail closed to no entries (a sender-mismatch), never bind an unvalidated name.
232
140
  try {
233
141
  var node = asn1.decode(exts[i].value);
234
142
  schema.walk(pkix.generalNames(NS, { code: "cmp/sender-mismatch" }), node, NS);
@@ -238,11 +146,6 @@ function _sanGeneralNames(parsed) {
238
146
  return [];
239
147
  }
240
148
 
241
- // RFC 9483 sec. 3.1 sender <-> protection-certificate binding. The sender matches the certificate identity if
242
- // it equals the POPULATED subject DN (a [4] directoryName under the RFC 5280 sec. 7.1 canonical comparison) OR
243
- // a subjectAltName GeneralName entry -- checked regardless of whether the subject is empty, so a certificate
244
- // that carries both a subject and a SAN can be identified by either, and an empty subject (RFC 5280 sec.
245
- // 4.1.2.6 requires a critical SAN) is bound to its SAN identity, never accepted with an anonymous sender.
246
149
  function _senderBoundToCert(sender, parsed) {
247
150
  if (!sender || !sender.bytes) return false;
248
151
  var subjectRdns = parsed.subject.rdns;
@@ -255,30 +158,21 @@ function _senderBoundToCert(sender, parsed) {
255
158
  return false;
256
159
  }
257
160
 
258
- // GeneralName equality (RFC 5280 sec. 7) for the sender <-> SAN binding, applying the per-type comparison
259
- // rules in place of a universal byte compare: a dNSName [2] case-insensitively (sec. 4.2.1.6 / 7.2), an
260
- // rfc822Name [1] with a case-insensitive domain (sec. 7.5), a uniformResourceIdentifier [6] with a
261
- // case-insensitive scheme + host (sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1), a directoryName [4] under the sec.
262
- // 7.1 canonical DN comparison, and every other type by exact DER (fail-closed). `sender` is the parsed [n]
263
- // GeneralName from schema-cmp; `sanNode` is a decoded certificate SAN GeneralName node.
264
161
  function _generalNameMatches(sender, sanNode) {
265
162
  if (sanNode.tagClass !== sender.tagClass || sanNode.tagNumber !== sender.tagNumber) return false;
266
163
  if (sanNode.tagClass === "context") {
267
- if (sanNode.tagNumber === 2 && sanNode.content) { // dNSName IA5String
164
+ if (sanNode.tagNumber === 2 && sanNode.content) {
268
165
  var dnsSan = _toString(sanNode.content, "latin1"), dnsSender = _String(sender.value);
269
- // Case-fold only a well-formed dNSName (RFC 5280 sec. 4.2.1.6 / RFC 1034, via the shared validator): a
270
- // malformed name (an empty label like "victim..com", a leading/trailing dot, bad LDH) is compared
271
- // byte-exact and never folded, so it cannot bind a byte-distinct identity.
272
166
  if (pkix.dnsNameProblem(dnsSan) !== null || pkix.dnsNameProblem(dnsSender) !== null) return dnsSan === dnsSender;
273
167
  return _toLowerCase(dnsSender) === _toLowerCase(dnsSan);
274
168
  }
275
- if (sanNode.tagNumber === 1 && sanNode.content) { // rfc822Name IA5String: case-insensitive domain
169
+ if (sanNode.tagNumber === 1 && sanNode.content) {
276
170
  return _rfc822Equal(_String(sender.value), _toString(sanNode.content, "latin1"));
277
171
  }
278
- if (sanNode.tagNumber === 6 && sanNode.content) { // uniformResourceIdentifier IA5String: case-insensitive scheme + host
172
+ if (sanNode.tagNumber === 6 && sanNode.content) {
279
173
  return _uriEqual(_String(sender.value), _toString(sanNode.content, "latin1"));
280
174
  }
281
- if (sanNode.tagNumber === 4 && sanNode.children && sanNode.children.length) { // directoryName: canonical DN
175
+ if (sanNode.tagNumber === 4 && sanNode.children && sanNode.children.length) {
282
176
  if (!sender.value || !_isArray(sender.value.rdns)) return false;
283
177
  var sanName;
284
178
  try { sanName = schema.embeddedDer(pkix.name(NS), sanNode.children[0].bytes, NS, { code: "cmp/sender-mismatch", what: "SAN directoryName" }).result; }
@@ -289,42 +183,28 @@ function _generalNameMatches(sender, sanNode) {
289
183
  return _compare(sanNode.bytes, sender.bytes) === 0;
290
184
  }
291
185
 
292
- // Index of the "@" separating an RFC 5321 addr-spec Local-part from its Domain, honoring a Quoted-string
293
- // local-part ("...") that MAY itself contain "@" and backslash escapes. Returns -1 for a malformed mailbox
294
- // (no separator, an unterminated quote, a quoted local-part not immediately followed by "@", or an unquoted
295
- // local-part with more than one "@") so the caller compares byte-exact.
296
186
  function _mailboxSplit(s) {
297
187
  var sep;
298
188
  if (_charAt(s,0) === "\"") {
299
189
  var i = 1;
300
190
  while (i < s.length) {
301
191
  var c = _charAt(s,i);
302
- if (c === "\\") { i += 2; continue; } // an escaped pair (\c) -- skip both octets
192
+ if (c === "\\") { i += 2; continue; }
303
193
  if (c === "\"") break;
304
194
  i++;
305
195
  }
306
- if (i >= s.length || _charAt(s,i) !== "\"" || _charAt(s,i + 1) !== "@") return -1; // unterminated / not "@"-separated
196
+ if (i >= s.length || _charAt(s,i) !== "\"" || _charAt(s,i + 1) !== "@") return -1;
307
197
  sep = i + 1;
308
198
  } else {
309
- sep = _strIndexOf(s, "@"); // an unquoted (Dot-string) local-part cannot contain a raw "@" -> exactly one total
199
+ sep = _strIndexOf(s, "@");
310
200
  if (sep < 0 || sep !== _strLastIndexOf(s, "@")) return -1;
311
201
  }
312
- // The Domain that follows the separator must be a well-formed FQDN (RFC 1034, via the shared validator): an
313
- // empty / empty-label / extra-"@" domain is malformed. An unquoted Local-part must be a Dot-string: non-empty,
314
- // no leading/trailing dot, no empty atom ("a..b"). A quoted Local-part is already balanced above. A malformed
315
- // mailbox returns -1 so _rfc822Equal compares it byte-exact and never case-folds a malformed domain.
316
202
  var local = _strSlice(s, 0, sep), domain = _strSlice(s, sep + 1);
317
203
  if (pkix.dnsNameProblem(domain) !== null) return -1;
318
- // An unquoted (Dot-string) Local-part is atext atoms joined by single dots (RFC 5321 sec. 4.1.2): every
319
- // character must be an atext octet or a "." with no empty atom / leading-trailing dot. A malformed atom
320
- // character (e.g. a space in "user name") makes the whole address malformed -> byte-exact comparison.
321
204
  if (_charAt(s, 0) !== "\"" && (local.length === 0 || _charAt(local, 0) === "." || _charAt(local, local.length - 1) === "." || _strIndexOf(local, "..") !== -1 || !_isEmailLocalPart(local))) return -1;
322
205
  return sep;
323
206
  }
324
207
 
325
- // RFC 5280 sec. 7.5 rfc822Name comparison: the local-part is case-sensitive, the domain case-insensitive. The
326
- // mailbox separator is located by _mailboxSplit (quoting-aware) so a valid quoted local-part carrying "@"
327
- // still binds, while a truly malformed address falls back to byte-exact comparison.
328
208
  function _rfc822Equal(a, b) {
329
209
  var ai = _mailboxSplit(a), bi = _mailboxSplit(b);
330
210
  if (ai < 0 || bi < 0) return a === b;
@@ -332,25 +212,13 @@ function _rfc822Equal(a, b) {
332
212
  _lowerAsciiDomain(_strSlice(a, ai + 1)) === _lowerAsciiDomain(_strSlice(b, bi + 1));
333
213
  }
334
214
 
335
- // sec. 7.5 authorizes a case-insensitive ASCII comparison of the host-part, and nothing
336
- // wider. String.toLowerCase() is Unicode-aware and folds well beyond A-Z: U+212A KELVIN
337
- // SIGN lowercases to "k", so a host carrying it would compare EQUAL to the ASCII spelling
338
- // and two distinct domains would read as one identity. Fold only A-Z and leave every other
339
- // octet alone, so a non-ASCII host can differ from an ASCII one but never collide with it.
340
- // Through the toolkit's one definition of that fold, so no two name comparisons can disagree
341
- // about which characters case-fold.
342
215
  var _lowerAsciiDomain = guard.name.lowerAscii;
343
216
 
344
- // ---- URI / email grammar predicates by character scan (no regex) ----------------------------------
345
- // The non-alphanumeric members of the RFC 5321 atext set and the RFC 3986 URI character set (the
346
- // A-Za-z0-9 range is checked directly), so charset membership is an indexOf rather than a live regex
347
- // match protocol.
348
217
  var _EMAIL_LOCAL_SPECIALS = "!#$%&'*+/=?^_`{|}~.-";
349
218
  var _URI_SPECIALS = "._~:/?#@!$&'()*+,;=%[]-";
350
219
  function _isAsciiAlnum(ch) { return (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9"); }
351
220
  function _isHexDigit(ch) { return (ch >= "0" && ch <= "9") || (ch >= "A" && ch <= "F") || (ch >= "a" && ch <= "f"); }
352
221
 
353
- // A non-empty RFC 5321 Dot-string atext run (matching /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+$/).
354
222
  function _isEmailLocalPart(s) {
355
223
  if (s.length === 0) return false;
356
224
  for (var i = 0; i < s.length; i++) {
@@ -360,7 +228,6 @@ function _isEmailLocalPart(s) {
360
228
  return true;
361
229
  }
362
230
 
363
- // Every character an RFC 3986 URI character (matching /^[A-Za-z0-9._~:/?#@!$&'()*+,;=%[\]-]*$/; empty is valid).
364
231
  function _isAllUriChars(s) {
365
232
  for (var i = 0; i < s.length; i++) {
366
233
  var ch = _charAt(s, i);
@@ -369,7 +236,6 @@ function _isAllUriChars(s) {
369
236
  return true;
370
237
  }
371
238
 
372
- // A "%" not introducing a valid %HH pct-encoding (matching /%(?![0-9A-Fa-f]{2})/): true when malformed.
373
239
  function _hasInvalidPercent(u) {
374
240
  for (var i = 0; i < u.length; i++) {
375
241
  if (_charAt(u, i) === "%" && !(_isHexDigit(_charAt(u, i + 1)) && _isHexDigit(_charAt(u, i + 2)))) return true;
@@ -377,9 +243,6 @@ function _hasInvalidPercent(u) {
377
243
  return false;
378
244
  }
379
245
 
380
- // A scheme + rest split (matching /^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/): a leading letter, then
381
- // [a-zA-Z0-9+.-]*, then ":"; returns { scheme, rest } or null. The URI charset gate runs first, so `u`
382
- // carries no CR/LF here and the [^\n] semantics of "." never apply.
383
246
  function _splitScheme(u) {
384
247
  if (u.length === 0) return null;
385
248
  var first = _charAt(u, 0);
@@ -394,7 +257,6 @@ function _splitScheme(u) {
394
257
  return { scheme: _strSlice(u, 0, p), rest: _strSlice(u, p + 1) };
395
258
  }
396
259
 
397
- // The index of the first "/", "?" or "#" (the authority terminator), or -1 (matching search(/[/?#]/)).
398
260
  function _firstAuthorityDelim(s) {
399
261
  for (var i = 0; i < s.length; i++) {
400
262
  var ch = _charAt(s, i);
@@ -403,7 +265,6 @@ function _firstAuthorityDelim(s) {
403
265
  return -1;
404
266
  }
405
267
 
406
- // A ":" followed by zero-or-more digits, whole string (matching /^:[0-9]*$/).
407
268
  function _isColonPort(s) {
408
269
  if (s.length === 0 || _charAt(s, 0) !== ":") return false;
409
270
  for (var i = 1; i < s.length; i++) {
@@ -413,52 +274,31 @@ function _isColonPort(s) {
413
274
  return true;
414
275
  }
415
276
 
416
- // RFC 5280 sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1 URI comparison: the scheme and the authority host are
417
- // case-insensitive; every other component (userinfo, port, path, query, fragment) is byte-exact. Return
418
- // null for anything that is not a well-formed absolute URI so the caller falls back to exact-DER (fail-closed
419
- // -- never normalize an input we cannot confidently parse into a false identity match).
420
277
  function _normalizeUri(u) {
421
- // RFC 3986 syntax: every character must be a URI character and every "%" must introduce a valid pct-encoding
422
- // (%HH). A URI with an out-of-charset byte or a malformed percent-escape ("%zz") in any component (path,
423
- // query, fragment, userinfo) is not normalized; return null so it is compared byte-exact.
424
278
  if (!_isAllUriChars(u) || _hasInvalidPercent(u)) return null;
425
- var m = _splitScheme(u); // scheme ":" rest (RFC 3986 sec. 3.1)
279
+ var m = _splitScheme(u);
426
280
  if (!m) return null;
427
281
  var scheme = _toLowerCase(m.scheme);
428
282
  var rest = m.rest;
429
- // A URI SAN identity is host-based (RFC 5280 sec. 4.2.1.6): an authority-free URI (no "//", e.g. a URN) has
430
- // no host to anchor the comparison, so compare it byte-exact and never case-fold its scheme; the shared
431
- // GeneralName parser only enforces printable IA5, not a present authority.
432
283
  if (_strSlice(rest, 0, 2) !== "//") return null;
433
284
  var body = _strSlice(rest, 2);
434
- var cut = _firstAuthorityDelim(body); // authority ends at the first "/", "?" or "#" (RFC 3986 sec. 3.2)
285
+ var cut = _firstAuthorityDelim(body);
435
286
  var split = cut < 0 ? body.length : cut;
436
- var authority = _strSlice(body, 0, split), tail = _strSlice(body, split); // tail: path/query/fragment -- exact
437
- // A raw "@" is not permitted inside userinfo (RFC 3986 sec. 3.2.1); more than one "@" is a MALFORMED
438
- // authority -> return null so it is compared byte-exact, never split into a userinfo that folds the host.
287
+ var authority = _strSlice(body, 0, split), tail = _strSlice(body, split);
439
288
  if (_strIndexOf(authority, "@") !== _strLastIndexOf(authority, "@")) return null;
440
- var at = _strLastIndexOf(authority, "@"); // [ userinfo "@" ] host [ ":" port ] -- userinfo stays case-sensitive
289
+ var at = _strLastIndexOf(authority, "@");
441
290
  var userinfo = at < 0 ? "" : _strSlice(authority, 0, at + 1);
442
291
  var hostport = at < 0 ? authority : _strSlice(authority, at + 1);
443
- // Split the host from an optional ":port": an IPv6 literal "[...]" carries the port after the "]", otherwise
444
- // the port follows the first ":". Case-fold only the host (RFC 3986 sec. 6.2.2.1); the port stays byte-exact
445
- // so a malformed non-numeric port (":ADMIN" vs ":admin") is never folded into a false identity match.
446
292
  var host, port;
447
293
  if (_charAt(hostport, 0) === "[") {
448
294
  var rb = _strIndexOf(hostport, "]");
449
- if (rb < 0) return null; // an unterminated IPv6 literal is malformed -> exact-DER fallback (fail-closed)
295
+ if (rb < 0) return null;
450
296
  host = _strSlice(hostport, 0, rb + 1); port = _strSlice(hostport, rb + 1);
451
297
  } else {
452
298
  var ci = _strIndexOf(hostport, ":");
453
299
  host = ci < 0 ? hostport : _strSlice(hostport, 0, ci); port = ci < 0 ? "" : _strSlice(hostport, ci);
454
300
  }
455
- // The port must be numeric (RFC 3986 sec. 3.2.3 port = *DIGIT). A non-numeric port is a malformed authority:
456
- // return null so the caller compares the raw values exactly and never case-folds the host of an input the
457
- // normalizer cannot confidently parse (which would bind two byte-distinct malformed authorities).
458
301
  if (port !== "" && !_isColonPort(port)) return null;
459
- // The authority host must be a well-formed FQDN or IP literal (RFC 5280 sec. 4.2.1.6 requires a URI SAN
460
- // authority to carry an FQDN/IP host): an empty, malformed ("Victim..COM"), or bad IPv6-literal host is
461
- // compared byte-exact and never case-folded, since the GeneralName parser only enforces printable IA5.
462
302
  var hostOk = _charAt(host, 0) === "["
463
303
  ? (_charAt(host, host.length - 1) === "]" && ipUtils.expandIpv6Hex(_strSlice(host, 1, -1)) !== null)
464
304
  : (pkix.dnsNameProblem(host) === null);
@@ -471,16 +311,11 @@ function _uriEqual(a, b) {
471
311
  return na === nb;
472
312
  }
473
313
 
474
- // M11/M14: resolve the signature-protection signer certificate. An explicit opts.signerCert wins (with the
475
- // senderKID SKI binding when present); else `extra` (the already dedup+validity+count-bounded extraCerts) by
476
- // senderKID, else RFC 9483 sec. 3.3, where extraCerts[0] is the protection certificate. `extra` is bounded before
477
- // this search so an unsigned flood cannot force unbounded X.509 parsing. Returns { der, spki, subject, parsed }
478
- // or null (never verify against an unauthenticated key).
479
314
  function _resolveSignerCert(m, opts, extra) {
480
- var senderKID = m.header.senderKID; // Buffer or null
315
+ var senderKID = m.header.senderKID;
481
316
  function _signerObj(der, p) { return { der: der, spki: p.subjectPublicKeyInfo.bytes, subject: _subjectDn(p), parsed: p }; }
482
- function accept(der) { // an UNTRUSTED message candidate: the senderKID (when present) narrows the search to the
483
- var p; // certificate whose SKI it names; a non-parseable / SKI-mismatched entry is skipped (null).
317
+ function accept(der) {
318
+ var p;
484
319
  try { p = x509.parse(der); } catch (_e) { return null; }
485
320
  if (senderKID != null) {
486
321
  var ski = _certSki(p);
@@ -489,14 +324,6 @@ function _resolveSignerCert(m, opts, extra) {
489
324
  return _signerObj(der, p);
490
325
  }
491
326
  if (opts.signerCert != null) {
492
- // The explicit signer certificate is REQUIRED CONFIG, not untrusted pool material. Snapshot it (_certDer
493
- // returns a VIEW into a caller Buffer, and der/spki are surfaced in the verdict, so a post-verify mutation
494
- // would change verdict.signer.cert/.spki). Parse it SEPARATELY: a corrupt / mistyped certificate is a
495
- // deployment error that THROWS cmp/bad-input, never a routine signer-cert-not-found verdict indistinguishable
496
- // from a message that simply omitted its signer (the nullable path is reserved for message candidates).
497
- // The caller explicitly selected this certificate, so the senderKID (a hint for narrowing a candidate search)
498
- // is not applied here: the protection signature verification against this cert's key is the real gate; a
499
- // valid message from a signer certificate that omits an SKI still resolves to its exact opts.signerCert.
500
327
  var scDer = _bufferFrom(_certDer(opts.signerCert, "opts.signerCert"));
501
328
  var scParsed;
502
329
  try { scParsed = x509.parse(scDer); }
@@ -507,32 +334,22 @@ function _resolveSignerCert(m, opts, extra) {
507
334
  for (var i = 0; i < extra.length; i++) { var r = accept(extra[i]); if (r) return r; }
508
335
  return null;
509
336
  }
510
- if (extra.length) return accept(extra[0]); // RFC 9483 sec. 3.3: extraCerts[0] is the protection cert
337
+ if (extra.length) return accept(extra[0]);
511
338
  return null;
512
339
  }
513
340
 
514
- // M20-M22 header consistency (opt-in echoes). Returns { code, reason } on a mismatch, else null. These are
515
- // public transaction identifiers, not secrets, so a length-checked equality (not a MAC compare) is correct.
516
- // The comparison runs through the captured `Buffer.compare`, which decides length and content
517
- // together from the values' slots. `a.equals(x)` reads `equals` off the prototype at call time, so
518
- // one returning true matches any expected identifier against any received one -- and it sat behind
519
- // a length gate reading the same replaceable accessor, which cannot refuse what it is told.
520
341
  function _bufEq(a, x) {
521
342
  if (!_isBuffer(a)) return false;
522
343
  if (!_isBuffer(x)) { if (util.types.isUint8Array(x)) x = _bufferFrom(x); else return false; }
523
344
  return _compare(a, x) === 0;
524
345
  }
525
346
  function _headerChecks(m, opts) {
526
- // RFC 9483 sec. 3.5: the pvno MUST be cmp2000(2) or cmp2021(3) (failInfo unsupportedVersion). The parser
527
- // accepts cmp1999(1) as valid RFC 9810 sec. 5.1.1 syntax; the lightweight profile refuses it on receipt.
528
347
  if (m.header.pvno !== 2 && m.header.pvno !== 3) {
529
348
  return { code: "cmp/unsupported-version", reason: "the header pvno is " + m.header.pvno + "; a received PKIMessage MUST carry cmp2000(2) or cmp2021(3) (RFC 9483 sec. 3.5)" };
530
349
  }
531
- // RFC 9483 sec. 3.5: the transactionID MUST be present (failInfo badDataFormat).
532
350
  if (!guard.bytes.isByteSource(m.header.transactionID)) {
533
351
  return { code: "cmp/bad-transaction-id", reason: "the header transactionID is absent; a received PKIMessage MUST carry one (RFC 9483 sec. 3.5)" };
534
352
  }
535
- // RFC 9483 sec. 3.5: the senderNonce MUST be present and MUST contain at least 128 bits (failInfo badSenderNonce).
536
353
  if (!guard.bytes.isByteSource(m.header.senderNonce) || guard.bytes.lengthOf(m.header.senderNonce) < 16) {
537
354
  return { code: "cmp/bad-sender-nonce", reason: "the header senderNonce is absent or shorter than 128 bits (RFC 9483 sec. 3.5)" };
538
355
  }
@@ -545,26 +362,19 @@ function _headerChecks(m, opts) {
545
362
  return null;
546
363
  }
547
364
 
548
- // M13 keyUsage.digitalSignature gate -- a format-local check ON TOP of path.validate: if the signer cert
549
- // carries a keyUsage extension, digitalSignature MUST be asserted (RFC 9483 sec. 3.2, RFC 9810 sec. 5.1.3.3).
550
- // Takes the already-parsed signer cert (parsed once in _resolveSignerCert); a malformed keyUsage fails closed.
551
365
  function _keyUsageAllowsSigning(parsed) {
552
366
  var kuOid = oid.byName("keyUsage");
553
367
  var exts = parsed.extensions;
554
368
  for (var i = 0; i < exts.length; i++) {
555
369
  if (exts[i].oid !== kuOid) continue;
556
370
  var ku;
557
- // Decode through the shared keyUsage decoder: it enforces the RFC 5280 sec. 4.2.1.3 BIT STRING form + the
558
- // X.690 sec. 11.2.2 minimal NamedBitList rule (rejecting a non-minimal 03 02 00 80). A malformed KeyUsage
559
- // fails this gate, so no hand-rolled bit test authorizes it and leans on path validation to reject it.
560
371
  try { ku = _certExtDecoders[kuOid](exts[i].value); }
561
372
  catch (_e) { return false; }
562
373
  return ku.digitalSignature === true;
563
374
  }
564
- return true; // no keyUsage extension present -> unconstrained
375
+ return true;
565
376
  }
566
377
 
567
- // M15-M19: recompute + constant-time-compare the PBMAC1 protection.
568
378
  async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
569
379
  if (protectionAlg.parameters === null) {
570
380
  return _fail(m, "mac", protectionAlg, "cmp/protection-failed", "the PBMAC1 protectionAlg carries no PBMAC1-params (RFC 9579 sec. 4)");
@@ -573,19 +383,16 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
573
383
  try {
574
384
  params = schema.embeddedDer(PBMAC1_PARAMS, protectionAlg.parameters, NS, { code: "cmp/bad-mac-data", what: "PBMAC1-params" }).result;
575
385
  } catch (e) {
576
- // Malformed / keyLength-omitted (RFC 9579 sec. 5) / non-canonical PRF params -> a fail-closed verdict.
577
386
  return _fail(m, "mac", protectionAlg, e instanceof CmpError ? e.code : "cmp/protection-failed", "the PBMAC1-params did not decode: " + ((e && e.message) || e));
578
387
  }
579
- var kdf = params.kdf; // { salt, iterationCount, keyLength, prfOid, prfName }
580
- var prfHash = PRF_HASH[kdf.prfOid]; // dispatch on the immutable OID, not the mutable display name
388
+ var kdf = params.kdf;
389
+ var prfHash = PRF_HASH[kdf.prfOid];
581
390
  var macHash = PRF_HASH[params.schemeOid];
582
391
  if (!prfHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 PBKDF2 PRF " + _stringify(kdf.prfName) + " (SHA-256/384/512 only; RFC 9481 sec. 7, RFC 9579 sec. 7)");
583
392
  if (!macHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " + _stringify(params.schemeName) + " (SHA-256/384/512 only)");
584
393
 
585
- // M18: bound the attacker-controlled work factors before deriving (CWE-834/400). A config-tier throw.
586
394
  _capWork(kdf.iterationCount, kdf.salt, kdf.keyLength, prfHash, opts);
587
395
 
588
- // M19: recompute the MAC over the reconstructed ProtectedPart and compare in constant time.
589
396
  var secret = typeof opts.sharedSecret === "string" ? _bufferFrom(opts.sharedSecret, "utf8") : guard.bytes.view(opts.sharedSecret, CmpError, "cmp/bad-input", "opts.sharedSecret");
590
397
  var computed = await pbes2.pbmac1(secret, kdf.salt, kdf.iterationCount, kdf.keyLength, prfHash, macHash, protectedPart);
591
398
  if (!guard.crypto.constantTimeEqual(computed, protection.bytes)) {
@@ -593,7 +400,7 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
593
400
  }
594
401
  var hc = _headerChecks(m, opts);
595
402
  if (hc) return _fail(m, "mac", protectionAlg, hc.code, hc.reason);
596
- return _ok(m, "mac", protectionAlg, true, null); // mac: valid protection == secret matched == trusted
403
+ return _ok(m, "mac", protectionAlg, true, null);
597
404
  }
598
405
 
599
406
  function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
@@ -604,115 +411,60 @@ function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
604
411
  }
605
412
  cap = _min(opts.maxIterations, cap);
606
413
  }
607
- // A trivially small count lets a peer run cheap offline PBMAC1 guesses against a password-like shared secret
608
- // with parameters pki.cmp.build refuses to produce (RFC 8018 sec. 4.2). Enforce the same floor on verify.
609
414
  if (iterationCount < PBMAC1_MIN_ITER) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " is below the floor " + PBMAC1_MIN_ITER + " (RFC 8018 sec. 4.2)");
610
415
  if (iterationCount > cap) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " exceeds the cap " + cap);
611
416
  var saltLen = salt ? _sizeOf(salt) : 0;
612
417
  if (!salt || saltLen < PBMAC1_MIN_SALT || saltLen > PBMAC1_MAX_SALT) throw _err("cmp/bad-input", "the PBMAC1 salt length must be in [" + PBMAC1_MIN_SALT + ", " + PBMAC1_MAX_SALT + "] octets (RFC 8018 sec. 4.1)");
613
418
  if (keyLength < PBMAC1_KEYLEN_MIN || keyLength > PBMAC1_KEYLEN_MAX) throw _err("cmp/bad-input", "the PBMAC1 keyLength must be in [" + PBMAC1_KEYLEN_MIN + ", " + PBMAC1_KEYLEN_MAX + "] (RFC 9579 sec. 9)");
614
- // Bound the COMBINED work: PBKDF2 derives ceil(keyLength / hLen) blocks, each costing iterationCount HMACs, so a
615
- // maximal keyLength multiplies the per-block iteration ceiling (a 1024-octet key over SHA-256 = 32x). Cap the
616
- // product against the same ceiling so no keyLength/iteration combination exceeds the single-block work (CWE-834/400).
617
419
  var blocks = _ceil(keyLength / (PRF_HLEN[prfHash] || 32));
618
420
  if (iterationCount * blocks > cap) throw _err("cmp/bad-input", "the PBMAC1 combined work (iterationCount " + iterationCount + " x " + blocks + " derived blocks) exceeds the cap " + cap);
619
421
  }
620
422
 
621
- // M9-M14 + M20-M22: verify signature protection, then (with a trust store) full out-of-path cert validation.
622
423
  async function _verifySignature(m, protectedPart, protectionAlg, protection, opts) {
623
- // Bound the unsigned extraCerts once (dedup + drop non-certificates + cap + scan cap) before either the
624
- // senderKID signer search or path building, so a hostile peer padding extraCerts cannot force unbounded
625
- // X.509 parsing or a candidate-pool overflow.
626
424
  var extra = _boundExtraCerts(m.extraCerts);
627
425
  var signer = _resolveSignerCert(m, opts, extra);
628
426
  if (!signer) return _fail(m, "signature", protectionAlg, "cmp/signer-cert-not-found", "no signer certificate resolved (opts.signerCert, senderKID, or extraCerts)");
629
427
 
630
- // M2/M9: verify over the reconstructed ProtectedPart through the single path-validate engine (the EdDSA
631
- // low-order-point gate + the sig-OID<->key-OID algorithm-confusion gate apply); fail-closed to false.
632
428
  var ok = await _engine.verifyWithSpki(protectionAlg, protection.bytes, signer.spki, protectedPart);
633
429
  if (ok !== true) return _fail(m, "signature", protectionAlg, "cmp/protection-failed", "the protection signature does not verify over the ProtectedPart under the declared protectionAlg", signer);
634
430
 
635
- // RFC 9483 sec. 3.1: for signature-based protection the authenticated header sender field MUST match the
636
- // protection certificate identity (failInfo badMessageCheck). Without this binding a holder of ANY
637
- // certificate the anchor trusts could set the sender to another party's name and be accepted as that
638
- // sender -- the signature and path both pass while the claimed identity is forged.
639
431
  if (!_senderBoundToCert(m.header.sender, signer.parsed)) {
640
432
  return _fail(m, "signature", protectionAlg, "cmp/sender-mismatch", "the header sender field does not match the signer certificate subject, or (for an empty subject) a subjectAltName entry (RFC 9483 sec. 3.1, RFC 5280 sec. 7.1)", signer);
641
433
  }
642
434
 
643
- // A failed opt-in echo check is a rejection verdict (valid:false), consistent with the MAC path: a caller
644
- // that requested a transactionID / recipNonce binding must not see valid:true when it does not hold.
645
435
  var hc = _headerChecks(m, opts);
646
436
  if (hc) return _fail(m, "signature", protectionAlg, hc.code, hc.reason, signer);
647
437
 
648
- // M5/M12: with no trust store the verdict is crypto-only (trusted:false) and the signer is surfaced for
649
- // the caller to anchor. With a trust store the signer cert gets the full RFC 5280 sec. 6.1 path gates.
650
438
  if (opts.trustAnchors == null) return _ok(m, "signature", protectionAlg, false, signer);
651
439
  var trust = await _chainSigner(signer, m, opts, extra);
652
- // Surface the chain that actually validated (signer + the intermediates path.build used) on a trusted verdict,
653
- // so a caller can cache exactly the certificates that established trust, not the unsigned extraCerts a peer
654
- // can pad: an appended-but-unused certificate never enters this chain.
655
440
  if (trust.chain) signer.chain = trust.chain;
656
441
  return _verdict(m, "signature", protectionAlg, true, trust.trusted, trust.trusted ? null : "cmp/untrusted-signer", trust.reason, signer);
657
442
  }
658
443
 
659
- // A canonical certificate identity (tbs + signature) for the extraCerts pool dedup: it keys a Buffer /
660
- // Uint8Array (parse) and an already-parsed candidate object identically (path.build accepts both forms), so
661
- // an extraCert duplicating a caller intermediate is dropped regardless of which representation the caller used.
662
- // The key is derived from the bytes the parser recorded, through the same door every other
663
- // certificate boundary uses -- never from the fields of the object handed in. Deriving it from a
664
- // caller-shaped object would let two different certificates collapse onto one key, which is the one
665
- // outcome this must not have. Failure returns null (the caller's contract here): an underivable
666
- // identity leaves a redundant pool slot, which is safe, while a wrong merge is not.
667
444
  function _certKey(c) {
668
445
  try {
669
446
  var p = guard.parsed.acceptDerived(c, "certificate", x509.parse, _err, "cmp/bad-input", "a pool certificate");
670
447
  if (!guard.parsed.isCert(p)) return null;
671
448
  return _toString(p.tbsBytes, "base64") + "|" + _toString(p.signatureValue.bytes, "base64");
672
449
  } catch (_e) {
673
- return null; // underivable: kept as its own slot, never merged onto another certificate's
450
+ return null;
674
451
  }
675
452
  }
676
453
 
677
454
  async function _chainSigner(signer, m, opts, extra) {
678
- // M13 first (cheap, format-local): a keyUsage that omits digitalSignature rejects the protection.
679
455
  if (!_keyUsageAllowsSigning(signer.parsed)) {
680
456
  return { trusted: false, reason: "the signer certificate keyUsage does not assert digitalSignature (RFC 9483 sec. 3.2)" };
681
457
  }
682
- // Validate the signer path at a trusted current time by default, never the message's self-asserted
683
- // messageTime, which the sender controls: a holder of a now-expired but once-valid signer certificate
684
- // could otherwise backdate messageTime into the certificate's validity window and be reported trusted.
685
- // A caller doing historical verification opts into a specific instant via opts.time; a falsy but present
686
- // opts.time (0 / false / "") is never silently replaced: it reaches path.build and is rejected as bad-input.
687
- // The default validation time, from the captured constructor. A replacement chooses the instant
688
- // every not-before / not-after and every revocation window is judged against, so an expired or
689
- // revoked certificate can be validated at a moment when it was neither.
690
458
  var time = opts.time != null ? opts.time : new _Date();
691
459
  var anchors = _certList(opts.trustAnchors);
692
- // `extra` is the already dedup + validity + count-bounded extraCerts (unsigned pool material), so a flood
693
- // of duplicate / junk / non-certificate entries cannot exhaust the candidate-search budget or raise a
694
- // path/bad-input from a malformed pool certificate. Add it only up to the path-builder pool ceiling, after
695
- // the caller's intermediates, so unsigned extraCerts can never push a valid caller configuration over the
696
- // ceiling (if the caller's intermediates alone exceed it, that is a genuine config error path.build reports).
697
460
  var pool = _certList(opts.intermediates);
698
461
  var room = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - pool.length;
699
462
  if (room > 0) {
700
- // Drop the signer leaf (already passed to build as the path TARGET) and any extraCert duplicating a caller
701
- // intermediate before truncating to the remaining slots, so a tight ceiling spends those slots on genuinely
702
- // useful embedded issuers, not on a redundant copy of the signer or a duplicate the pool already holds.
703
- // The identity is the canonical tbs+signature key so a Buffer extraCert dedups against a caller intermediate
704
- // supplied either as raw bytes or as an already-parsed certificate object (path.build accepts both).
705
- // Explicit loops, never forEach / filter / concat / slice. Everything from here to the verdict
706
- // runs after verification suspended, and every one of those is a replaceable global, so a
707
- // caller who swaps one during the window would have the pool assembled by their code.
708
463
  var seen = _create(null);
709
464
  var sk = _certKey(signer.parsed); if (sk) seen[sk] = 1;
710
465
  var pi, pk;
711
466
  for (pi = 0; pi < pool.length; pi++) { pk = _certKey(pool[pi]); if (pk) seen[pk] = 1; }
712
467
  var merged = [];
713
- // Indexed assignment, never push: push is a replaceable global too, and everything here runs
714
- // after verification suspended, so a swapped one could put a certificate into the pool that the
715
- // caller never supplied. Writing the slot dispatches nothing.
716
468
  for (pi = 0; pi < pool.length; pi++) _append(merged, pool[pi]);
717
469
  for (pi = 0; pi < extra.length && merged.length - pool.length < room; pi++) {
718
470
  pk = _certKey(extra[pi]);
@@ -725,12 +477,6 @@ async function _chainSigner(signer, m, opts, extra) {
725
477
  try {
726
478
  var res = await _engine.build(signer.der, buildOpts);
727
479
  if (!res || res.valid !== true) return { trusted: false, reason: "the signer certificate did not chain to a supplied trust anchor" };
728
- // res.path is the ordered certificates path.build assembled (signer + the intermediates it used), as PARSED
729
- // objects whose byte fields are subarrays of the (possibly multi-MB) response allocation. Map each back to its
730
- // source DER (the signer, or a DER pool entry) and return an INDEPENDENT copy, so a caller caching the chain
731
- // holds standalone buffers, never slices pinning the whole response. Only certs on the trusted path enter it,
732
- // so unsigned extraCerts padding is excluded. (A path cert sourced from an already-parsed intermediate carries
733
- // no DER to copy and is omitted; for a Buffer/DER pool, the cmp.session case, the chain is complete.)
734
480
  var byKey = _create(null);
735
481
  var skey = _certKey(signer.parsed); if (skey) byKey[skey] = signer.der;
736
482
  var ci, ck;
@@ -747,10 +493,6 @@ async function _chainSigner(signer, m, opts, extra) {
747
493
  }
748
494
  return { trusted: true, reason: null, chain: chain };
749
495
  } catch (e) {
750
- // A config-tier fault from path.build (an invalid opts.time, an empty / malformed trustAnchors or
751
- // intermediate) is a deployment error, not an untrusted signer, so rethrow it as cmp/bad-input and it is
752
- // not masked as a routine trust failure (the API contract: a bad required opt throws). A genuine
753
- // no-assemblable-path / validation failure collapses to an untrusted-signer verdict.
754
496
  if (e && e.code === "path/bad-input") {
755
497
  throw _err("cmp/bad-input", "invalid trust / validation options for signer-certificate path validation: " + (e.message || e), e);
756
498
  }
@@ -758,26 +500,17 @@ async function _chainSigner(signer, m, opts, extra) {
758
500
  }
759
501
  }
760
502
 
761
- // Sanitize the message-provided extraCerts -- they are UNSIGNED pool material a hostile peer can pad without
762
- // breaking protection: deduplicate by exact DER, DROP any entry that is not a parseable X.509 certificate (a
763
- // bare non-cert SEQUENCE would otherwise reach path.build and raise a config-tier path/bad-input), and keep at
764
- // most MAX_EXTRA_CERTS. The whole scan is bounded to MAX_EXTRA_SCAN entries so a flood of junk cannot force
765
- // unbounded X.509 parsing; the RFC 9483 sec. 3.3 protection cert is extraCerts[0], so it is examined first.
766
- var MAX_EXTRA_CERTS = 32; // valid, distinct certificates kept for signer resolution + path building
767
- var MAX_EXTRA_SCAN = 256; // total entries examined -- bounds parse work regardless of the flood size
503
+ var MAX_EXTRA_CERTS = 32;
504
+ var MAX_EXTRA_SCAN = 256;
768
505
  function _boundExtraCerts(extra) {
769
506
  if (!_isArray(extra) || !extra.length) return [];
770
507
  var out = [], seen = _create(null);
771
508
  for (var i = 0; i < extra.length && out.length < MAX_EXTRA_CERTS && i < MAX_EXTRA_SCAN; i++) {
772
509
  var c = extra[i];
773
- if (!_isBuffer(c) && !util.types.isUint8Array(c)) continue; // the slot, not the prototype
774
- // The pool's other identity key, through the same capture as _certKey's: one `toString`
775
- // returning a constant collapses every peer-supplied certificate onto one key, so the first
776
- // entry scanned suppresses all the rest.
510
+ if (!_isBuffer(c) && !util.types.isUint8Array(c)) continue;
777
511
  var key = _toString(_bufferFrom(c), "base64");
778
512
  if (seen[key]) continue;
779
513
  seen[key] = true;
780
- // drop a non-certificate entry (unsigned, attacker-supplied pool material) before it reaches path.build
781
514
  try { x509.parse(c); }
782
515
  catch (_e) { continue; }
783
516
  _append(out, c);
@@ -785,40 +518,12 @@ function _boundExtraCerts(extra) {
785
518
  return out;
786
519
  }
787
520
 
788
- // Appending to a list this module owns, without dispatching anywhere a caller can reach.
789
- //
790
- // Everything that assembles a trust set runs AFTER verification has suspended, which is a window a
791
- // caller can act in. `arr.push(v)` goes through Array.prototype.push, a replaceable global; and
792
- // `arr[arr.length] = v` is a [[Set]], which walks the prototype chain and finds an
793
- // `Array.prototype[0]` setter when the fresh array has no own slot there yet. Both hand control to
794
- // caller code at the moment the anchor set is being decided.
795
- //
796
- // defineProperty is [[DefineOwnProperty]]: it creates the own slot outright, consulting no setter.
797
- // The reference is captured at module load, before any caller code has run, so replacing
798
- // `Object.defineProperty` afterwards cannot reach this either.
799
521
  var _defineOwn = Object.defineProperty;
800
- // The Date constructor, captured for the same reason and at the same moment. `global.Date` is
801
- // replaceable, and a replacement asked for a fresh instant could hand back the caller's own object
802
- // instead -- leaving the "copy" of opts.time as the very Date the caller can still call setTime on.
803
522
  var _Date = Date;
804
- // And Buffer.from, for the string shared secret. `Buffer.from` is a writable own property of the
805
- // Buffer global, and the FIRST thing the reduction does is read opts.sharedSecret -- so that very
806
- // accessor can install a replacement and then return the string, and an unguarded conversion would
807
- // hand the plaintext to caller code and take back whatever it returned as the PBMAC1 key. The
808
- // reference itself is the one taken at the top of this file, through guard-intrinsic.
809
523
  function _append(arr, v) {
810
524
  _defineOwn(arr, arr.length, { value: v, writable: true, enumerable: true, configurable: true });
811
525
  }
812
526
 
813
- // A trust-anchor / pool list from a single cert (DER/PEM) or an array of them.
814
- //
815
- // Built with an explicit loop and INDEXED ASSIGNMENT -- never `arr.map`, and never `out.push`. This
816
- // runs AFTER verification has suspended, and every Array.prototype method is a replaceable global,
817
- // so a caller who swaps one during that window would otherwise have this call dispatch into their
818
- // code -- and an implementation that recognized the unrelated-anchor list and answered with the real
819
- // CA would make a verification started with only the unrelated anchor settle `trusted: true`.
820
- // Copying the caller's array at the door does not close that on its own: the fresh array is still an
821
- // Array, and the replacement is on the prototype. Writing `out[out.length]` dispatches nothing.
822
527
  function _certList(v) {
823
528
  if (v == null) return [];
824
529
  var arr = _isArray(v) ? v : [v];
@@ -831,12 +536,6 @@ function _certList(v) {
831
536
  return out;
832
537
  }
833
538
 
834
- // A usable PBMAC1 shared secret: a non-empty string or a non-empty Buffer/Uint8Array (mirrors cmp-build).
835
- // The size comes from the slot rather than from `s.length`. This gate stands in front of PBMAC1, and
836
- // a typed array's `length` is a replaceable accessor: one reporting a positive value for an empty
837
- // buffer lets `sharedSecret: ""` past, and a peer-generated message authenticated under an empty key
838
- // then verifies. A string secret has already been converted to bytes at the option door, so the
839
- // value measured here is a view whenever it is not refused outright.
840
539
  function _nonEmptySecret(s) {
841
540
  if (typeof s === "string") return _sizeOf(s) > 0;
842
541
  if (!_isBuffer(s) && !util.types.isUint8Array(s)) return false;
@@ -844,29 +543,6 @@ function _nonEmptySecret(s) {
844
543
  }
845
544
 
846
545
  function verify(message, opts) {
847
- // The reduced options are built here rather than inside _verify so the copy of a PBMAC1 shared
848
- // secret has an owner for its whole lifetime. That copy is this module's, holding the plaintext
849
- // secret, and pbes2.pbmac1 clears only the MAC key it derives -- so without this the secret would
850
- // sit in the heap until the allocator happened to reuse it. Cleared on every path out: resolved,
851
- // rejected, or never run.
852
- // The list is owned HERE and handed down, so a copy is clearable from the moment it exists rather
853
- // than from the moment the whole reduction succeeds. Reducing the options can fault partway -- a
854
- // subsequent getter throws, a certificate list turns out not to be dense -- and the secret's
855
- // copy has already been made by then. Binding it to the returned object would leave that copy with
856
- // no owner on exactly the paths where the caller learns something went wrong.
857
- // The cleanup is a language-level try/finally inside the async body, never a `.finally(...)` on
858
- // the returned promise. `Promise.prototype.finally` is a replaceable global, and reducing the
859
- // options runs caller getters, so one of those could install a replacement that this very line
860
- // would then dispatch into -- handing back a promise of its own choosing while the genuine
861
- // rejection went unobserved and the copied secret went unwiped. `try/finally` is syntax: nothing
862
- // is looked up, so nothing can be swapped.
863
- //
864
- // The `await` below is a different matter, and this comment previously claimed otherwise. `await v`
865
- // performs PromiseResolve, which reads `v.constructor`; when that no longer names %Promise% the
866
- // fast path is skipped and `v.then` is called instead -- both properties live on Promise.prototype
867
- // and both are writable. So a caller CAN interpose on the awaited value, here and at every other
868
- // await this module makes. That is a toolkit-wide exposure rather than one this function can close,
869
- // and it is not closed here: see the tracked work on capturing the promise plumbing.
870
546
  var made = [];
871
547
  return guard.async.deferred(async function () {
872
548
  try {
@@ -877,67 +553,23 @@ function verify(message, opts) {
877
553
  });
878
554
  }
879
555
 
880
- // Every caller-owned option, reduced HERE to a value the caller no longer reaches.
881
- //
882
- // Verification suspends this call: PBMAC1 derivation on the MAC path, the signature engine on the
883
- // other. Options read after it resumes answer for whatever the caller last wrote, so a caller could
884
- // pass a transactionID that does not match the message, overwrite the buffer while the MAC derived,
885
- // and be told the message was valid. `transactionID`, `expectRecipNonce`, `trustAnchors`,
886
- // `intermediates` and `time` are all read past that suspension.
887
- //
888
- // The rule is two exhaustive outcomes rather than a list of which fields matter: anything carrying
889
- // bytes is COPIED, and anything else is passed through by reference. Passing through is what a
890
- // parsed certificate needs -- guard.parsed records provenance against the object's identity, so a
891
- // copy is a different object and stops being recognized as parser output -- and it is also right for
892
- // a caller's revocationChecker, which is a handle rather than data. Only the array CONTAINER of a
893
- // certificate list is copied, so appending to the caller's array cannot widen the anchor set while
894
- // the chain is being built.
895
- //
896
- // Two things therefore stay caller-owned, and both are safe for their own reason. A parsed anchor or
897
- // intermediate is held by reference, and path validation re-derives it from the bytes it recorded,
898
- // so a field edited on the caller's object never reaches the decision (pinned by vector 23l). A
899
- // revocationChecker is the caller's callback, and what it answers is theirs to decide by definition.
900
- // Copied through guard.bytes.snapshot, never a bare Buffer.from. The guard is this toolkit's door
901
- // for a caller-supplied byte source, and it refuses the ones the rest of the code is entitled to
902
- // assume it never sees -- a detached view, or one backed by a SharedArrayBuffer whose contents
903
- // another thread can still rewrite. Copying with Buffer.from would launder such a source into an
904
- // ordinary Buffer that every later check accepts without ever having applied the door's rules.
905
- // The kind test asks the internal SLOT, never the prototype. `Buffer.isBuffer` is a writable
906
- // property of the Buffer global and `v instanceof Uint8Array` consults Uint8Array[Symbol.hasInstance],
907
- // and both are reachable from an option getter that has already run by the time this is evaluated.
908
- // A caller who makes this answer `false` for their own bytes gets the copy skipped, which puts a
909
- // live caller buffer where the copy should be -- and one that the wipe would then destroy, memory
910
- // the toolkit does not own. util.types.isUint8Array is a fact about the value.
911
556
  function _fixByteish(v, label) {
912
557
  if (!util.types.isUint8Array(v)) return v;
913
558
  return guard.bytes.snapshot(v, CmpError, "cmp/bad-input", "opts." + label);
914
559
  }
915
- // An opt-in echo value is a byte buffer or it is absent. A non-buffer (a string from JSON config,
916
- // say) is a DEPLOYMENT error that throws cmp/bad-input, never a routine transaction/nonce mismatch
917
- // verdict that would misreport a caller's typing mistake as a peer authentication failure.
918
560
  function _assertEchoBytes(v, name) {
919
561
  if (v == null) return;
920
- if (util.types.isUint8Array(v)) return; // the slot, not the prototype -- see _fixByteish
562
+ if (util.types.isUint8Array(v)) return;
921
563
  throw _err("cmp/bad-input", "opts." + name + " must be a Buffer / Uint8Array");
922
564
  }
923
565
 
924
- // The shared secret, copied and recorded in one step so the copy is never unowned.
925
566
  function _fixSecret(v, made) {
926
- // A string secret is converted to bytes HERE, once, and recorded -- not deeper inside the MAC
927
- // path. The conversion is the thing that creates a wipeable plaintext copy, so leaving it
928
- // downstream left that copy with no owner and the wipe never reached it. The immutable string
929
- // itself still cannot be cleared, which is the reason a caller who needs the secret gone from
930
- // memory hands over bytes; what changes here is that the toolkit stops adding a second copy it
931
- // then abandons.
932
567
  if (typeof v === "string") {
933
568
  var fromString = _bufferFrom(v, "utf8");
934
569
  _append(made, fromString);
935
570
  return fromString;
936
571
  }
937
572
  var copy = _fixByteish(v, "sharedSecret");
938
- // Slot test again, and for the sharper reason: this decides whether the copy is ENTERED in the
939
- // wipe list. A caller who makes it answer false leaves a genuine toolkit-allocated plaintext copy
940
- // with no owner, so the wipe never reaches it.
941
573
  if (util.types.isUint8Array(copy)) _append(made, copy);
942
574
  return copy;
943
575
  }
@@ -945,48 +577,6 @@ function _fixSecret(v, made) {
945
577
  function _fixCertList(v, label) {
946
578
  if (v == null) return v;
947
579
  if (!_isArray(v)) return _fixByteish(v, label);
948
- // Copied through a plain loop rather than v.map(). `map` is a property of the CALLER's array, and
949
- // an own property or a subclass can define it to return the receiver -- which would leave this
950
- // "copy" aliasing the very array the caller can still append a trusted CA to while verification
951
- // is suspended, defeating the copy entirely. `length` and each index are read once, into a fresh
952
- // array this module owns.
953
- //
954
- // The elements that EXIST are enumerated, rather than walking 0..length. An array's `length` is
955
- // independent of how many elements it holds, so a sparse list carrying one certificate at index
956
- // 100000000 costs nothing to build and would cost a dense hundred-million-entry allocation to
957
- // walk. Own index keys come back in ascending numeric order, so a dense list copies exactly as it
958
- // did and a sparse one collapses to the certificates it actually carries. No ceiling is imposed
959
- // here: a trust store is the caller's own configuration and path.build sets no limit on how many
960
- // anchors it may hold, so capping the list would refuse a large store that verifies today.
961
- // A certificate list is a PLAIN DENSE array or it is refused. Two outcomes, no third.
962
- //
963
- // Faithfully reproducing what an ordinary array operation would consume means handling holes,
964
- // elements inherited from a subclass or a custom prototype, non-enumerable elements, and indices
965
- // at the uint32 boundary -- and each of those pulls against the next: reading `length` positions
966
- // covers inheritance but scans every hole of a sparse list, while enumerating present keys is
967
- // bounded but silently drops an inherited anchor. Silently dropping one turns a trusted
968
- // verification into an untrusted one, and that is not a trade to make quietly inside a trust
969
- // decision.
970
- //
971
- // So the shape is refused instead of emulated. The own index properties below `length` are counted
972
- // -- bounded by how many properties the object actually has, never by `length` -- and the list is
973
- // accepted only when that count IS the length, which is exactly a dense array with nothing
974
- // inherited. A caller holding an exotic array normalizes it themselves, and the failure is a typed
975
- // error naming the problem rather than an anchor that quietly went missing.
976
- // The door comes first, ahead of every property read below it. `Array.isArray` pierces a Proxy
977
- // without reaching a trap, so at this point nothing caller-written has run; the `length` read and
978
- // the descriptor walk are what would hand control over. A Proxy whose target is an array answers
979
- // both from traps, and its `ownKeys` and `get` traps need not agree with each other or with the
980
- // answer an element read gives afterwards, so a count through them describes nothing. guard.identifier
981
- // refuses a Proxy anywhere in the chain by identity, and it has to do that while the list is still
982
- // untouched for the refusal to mean anything.
983
- //
984
- // The elements are then held to the same rule as the option names, through the same primitive: an
985
- // accessor under an index is caller code exactly as an accessor under a name is, and a list is
986
- // where it pays -- an element can answer as one certificate to the check and as another to the
987
- // read, or simply run and rewrite a predicate the reduction has not reached yet. guard-bytes
988
- // refuses index accessors on the arguments it copies for this reason; a certificate list is the
989
- // same shape and gets the same treatment.
990
580
  var indices = guard.identifier.readableIndices(v, _err, "cmp/bad-input", "opts." + label);
991
581
  guard.identifier.refuseAccessorFields(v, indices, _err, "cmp/bad-input", "opts." + label);
992
582
  var n = v.length;
@@ -1004,45 +594,14 @@ function _fixCertList(v, label) {
1004
594
  for (var i = 0; i < n; i++) _append(out, _fixByteish(v[i], label + "[" + i + "]"));
1005
595
  return out;
1006
596
  }
1007
- // The evaluation instant, taken from ONE read of the caller's value. A real Date is re-made at the
1008
- // same instant, so setTime on the caller's object cannot move it. Anything else passes through
1009
- // untouched, including a falsy-but-present value, so the rejection it already earns downstream is
1010
- // the one it still gets.
1011
597
  function _fixTime(v) {
1012
598
  return guard.time.isDate(v) ? new _Date(guard.time.instantOf(v)) : v;
1013
599
  }
1014
600
  function _fixVerifyOptions(opts, made) {
1015
- if (opts == null) opts = {}; // ONLY null / undefined -> the default empty opts (a falsy false/0/"" is a bad config, not a default)
1016
- // Through the toolkit's own options door rather than a pair of local field checks. The door
1017
- // REFUSES an options bag whose fields are accessors, and refuses one that changes which fields it
1018
- // carries while they are read; a local check on each field it needs can do neither.
1019
- //
1020
- // That is what closes this whole class rather than one member of it. An accessor is caller code
1021
- // running inside the call, before the verb has done anything, and from there it can rewrite the
1022
- // very predicates and constructors the verb is about to use -- Buffer.from, Array.isArray,
1023
- // util.types.isUint8Array, Promise.prototype.then. Each one hardened in turn is another entry on
1024
- // a list with no end; refusing the accessor removes the window they all need. What is asked of a
1025
- // caller is only that the options themselves be values.
601
+ if (opts == null) opts = {};
1026
602
  opts = guard.identifier.optionsObject(opts, _err, "cmp/bad-input", "pki.cmp.verify options");
1027
603
  guard.identifier.assertKnownKeys(opts, KNOWN_VERIFY_OPTS, _err, "cmp/bad-input", "unknown opts field ");
1028
- // Each field is read from the caller's object exactly once AND reduced in the same step, before
1029
- // the next field is read. Both halves matter, and each closes a way the caller could otherwise
1030
- // still be holding the value the verdict turns on:
1031
- //
1032
- // reading once -- a property can be an accessor, and an accessor is free to answer a different
1033
- // value each time it is asked, so a second read would let the value that passed the check
1034
- // differ from the value that was used;
1035
- //
1036
- // reducing immediately -- an accessor invoked for a LATER field can reach back and mutate the
1037
- // buffer an EARLIER field handed over, so reading everything first and copying afterwards would
1038
- // copy whatever the last getter left behind.
1039
- //
1040
- // An object literal evaluates its properties in source order, so each line below is one read
1041
- // followed at once by its copy.
1042
604
  var f = {
1043
- // Recorded in `made` the instant it is copied. Only the byte form: a string secret is a
1044
- // JavaScript string, immutable and interned wherever the caller created it, so there is nothing
1045
- // here to wipe -- a caller who needs the secret gone from memory passes it as bytes.
1046
605
  sharedSecret: _fixSecret(opts.sharedSecret, made),
1047
606
  signerCert: _fixByteish(opts.signerCert, "signerCert"),
1048
607
  trustAnchors: _fixCertList(opts.trustAnchors, "trustAnchors"),
@@ -1054,12 +613,6 @@ function _fixVerifyOptions(opts, made) {
1054
613
  maxIterations: opts.maxIterations,
1055
614
  };
1056
615
 
1057
- // Checked on the COPY, which is what the comparison later runs against.
1058
- //
1059
- // Written out rather than looped with forEach. The reduction above has already run the caller's
1060
- // getters, so one of them could have replaced Array.prototype.forEach with a no-op -- and this
1061
- // check would then simply not happen, turning a configuration error the caller must see into a
1062
- // routine mismatch verdict that blames the peer. Two named checks dispatch nothing.
1063
616
  _assertEchoBytes(f.transactionID, "transactionID");
1064
617
  _assertEchoBytes(f.expectRecipNonce, "expectRecipNonce");
1065
618
  return f;
@@ -1072,31 +625,22 @@ async function _verify(message, opts) {
1072
625
  var protectionAlg = m.header.protectionAlg;
1073
626
  var protection = m.protection;
1074
627
 
1075
- // M3/M5: the parser guarantees protection<->protectionAlg presence agreement; an absent protection is a
1076
- // hard reject -- absence is never "verified".
1077
628
  if (protection === null || protectionAlg === null) {
1078
629
  return _fail(m, null, protectionAlg, "cmp/no-protection", "the PKIMessage carries no protection (RFC 9810 sec. 5.1.3); an unprotected message is never verified");
1079
630
  }
1080
631
 
1081
- // M2: reconstruct the ProtectedPart from the raw surfaced slices, byte-identical to what build signed,
1082
- // never a re-serialization of the decoded header/body structs.
1083
632
  var protectedPart = b.sequence([b.raw(m.headerBytes), b.raw(m.bodyBytes)]);
1084
633
 
1085
- // M7: a recognized legacy / KEM MAC OID is refused before any dispatch (never a silent accept).
1086
634
  if (UNSUPPORTED_MAC_OIDS[protectionAlg.oid]) {
1087
635
  return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "the " + UNSUPPORTED_MAC_OIDS[protectionAlg.oid] + " protection algorithm is not supported (v1 verifies PBMAC1 and signature protection; RFC 9481 sec. 6.1.1)");
1088
636
  }
1089
637
 
1090
- // M6: flavor from the protectionAlg OID alone, never guessed from the caller's credential.
1091
638
  var isMac = protectionAlg.oid === oid.byName("pbmac1");
1092
639
  var hasSigCred = opts.signerCert != null || opts.trustAnchors != null;
1093
640
  var hasSecret = opts.sharedSecret != null;
1094
641
 
1095
- // M8: flavor<->credential coherence (the wrongIntegrity condition) is a config-tier throw.
1096
642
  if (isMac) {
1097
643
  if (hasSigCred) throw _err("cmp/bad-input", "a MAC-protected message takes opts.sharedSecret, not signerCert/trustAnchors");
1098
- // The shared secret must be NON-EMPTY (matching pki.cmp.build): an empty "" / zero-length secret has no
1099
- // entropy, so a peer could forge a matching PBMAC1 with no secret and be reported valid + trusted.
1100
644
  if (!_nonEmptySecret(opts.sharedSecret)) throw _err("cmp/bad-input", "a PBMAC1-protected message requires a non-empty opts.sharedSecret");
1101
645
  return _verifyMac(m, protectedPart, protectionAlg, protection, opts);
1102
646
  }