@blamejs/pki 0.3.25 → 0.3.26

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.
@@ -0,0 +1,705 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the pki.cmp.verify implementation. The operator-facing @module pki.cmp home is
6
+ // 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 -- 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 ONE 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
+
22
+ var asn1 = require("./asn1-der");
23
+ var oid = require("./oid");
24
+ var cmpBuild = require("./cmp-build");
25
+ var cmp = require("./schema-cmp");
26
+ var pkix = require("./schema-pkix");
27
+ var pbes2 = require("./pbes2");
28
+ var x509 = require("./schema-x509");
29
+ var schema = require("./schema-engine");
30
+ var guard = require("./guard-all");
31
+ var constants = require("./constants");
32
+ var ipUtils = require("./ip-utils");
33
+ var frameworkError = require("./framework-error");
34
+
35
+ var CmpError = frameworkError.CmpError;
36
+ var b = asn1.build;
37
+ function _err(code, message, cause) { return new CmpError(code, message, cause); }
38
+
39
+ // The cmp error namespace, reused so PBMAC1-params decode faults surface as cmp/bad-mac-data.
40
+ var NS = pkix.makeNS("cmp", CmpError, oid);
41
+ var PBMAC1_PARAMS = pkix.pbmac1Params(NS);
42
+ // The shared RFC 5280 sec. 4.2.1 extension-value decoders (the acme / inspect home): the signer keyUsage gate
43
+ // reads through the ONE structurally-strict decoder (X.690 sec. 11.2.2 minimal NamedBitList) rather than a
44
+ // hand-rolled bit test, so a malformed KeyUsage fails the gate independently of the path validator also rejecting it.
45
+ var _certExtDecoders = pkix.certExtensionDecoders(NS).byOid;
46
+
47
+ var KNOWN_VERIFY_OPTS = {
48
+ sharedSecret: 1, signerCert: 1, trustAnchors: 1, intermediates: 1, time: 1,
49
+ transactionID: 1, expectRecipNonce: 1, revocationChecker: 1, maxIterations: 1,
50
+ };
51
+
52
+ // PBMAC1 PBKDF2-PRF / messageAuthScheme name -> WebCrypto hash. Only SHA-256/384/512 are supported
53
+ // (RFC 9481 sec. 7 mandates SHA-256; RFC 9579 sec. 7 bars a <= 160-bit digest, so hmacWithSHA1 -- and an
54
+ // omitted-PRF that resolves to it -- is unsupported, never MAC-verified under a weak digest).
55
+ // Keyed by the IMMUTABLE dotted OID, never the display name: pki.oid.register() can rename hmacWithSHA256/384/512,
56
+ // so a name-keyed lookup would reject a valid PBMAC1 message under a renamed registry (the immutable-OID-dispatch rule).
57
+ var PRF_HASH = {};
58
+ PRF_HASH[oid.byName("hmacWithSHA256")] = "SHA-256";
59
+ PRF_HASH[oid.byName("hmacWithSHA384")] = "SHA-384";
60
+ PRF_HASH[oid.byName("hmacWithSHA512")] = "SHA-512";
61
+
62
+ // Attacker-controlled PBKDF2 work-factor bounds (RFC 8018 sec. 4.2; the pkcs12 verifyMac / _capWork model).
63
+ var PBMAC1_MAX_ITER = constants.LIMITS.PBKDF2_MAX_ITERATIONS;
64
+ var PBMAC1_MIN_ITER = 1000; // RFC 8018 sec. 4.2 floor -- matches pki.cmp.build; a trivially small count is refused
65
+ var PBMAC1_MIN_SALT = 8; // octets -- RFC 8018 sec. 4.1 (64-bit) floor; an empty/short salt loses precomputation resistance
66
+ var PBMAC1_MAX_SALT = constants.LIMITS.PBKDF2_MAX_SALT;
67
+ var PBMAC1_KEYLEN_MIN = 20; // RFC 9579 sec. 9 -- a short derived key is refused
68
+ var PBMAC1_KEYLEN_MAX = 1024;
69
+ // PBKDF2 PRF output length (octets) -- the size of one derived block; keyLength beyond it costs extra HMAC rounds.
70
+ var PRF_HLEN = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
71
+
72
+ // The legacy / KEM MAC protection OIDs (RFC 9810 sec. 5.1.3.1/.2/.4) the v1 verifier recognizes and refuses
73
+ // -- build emits only PBMAC1, so the verifier rejects a legacy/KEM MAC OID rather than accept a construction
74
+ // it does not verify (a silent accept of an unverified algorithm would be fail-open).
75
+ var UNSUPPORTED_MAC_OIDS = {};
76
+ ["passwordBasedMac", "dhBasedMac", "kemBasedMac"].forEach(function (n) {
77
+ var o = oid.byName(n);
78
+ if (o) UNSUPPORTED_MAC_OIDS[o] = n;
79
+ });
80
+
81
+ // The signature engine + full path build/validate, injected by path-validate (the crl/ocsp seam pattern) so
82
+ // there is never a second, weaker CMP signature verifier and no require cycle. Null until path-validate loads
83
+ // (index.js always loads pki.path before a pki.cmp.verify call).
84
+ var _engine = null;
85
+ function setEngine(engine) { _engine = engine; }
86
+
87
+ // ---- verdict builders ----
88
+ // One verdict shape (mirrors cms.verify / tsp.verify): `valid` is crypto intactness under the DECLARED
89
+ // protectionAlg; `trusted` is mac=secret-matched / signature=chained-to-a-supplied-anchor; `code`/`reason`
90
+ // are set on a rejected verdict. Only a config-tier / malformed-DER error throws.
91
+ function _verdict(m, type, protectionAlg, valid, trusted, code, reason, signer) {
92
+ var v = {
93
+ valid: valid,
94
+ trusted: trusted,
95
+ protectionType: type,
96
+ protectionAlg: protectionAlg ? { oid: protectionAlg.oid, name: protectionAlg.name || null } : null,
97
+ signer: signer ? { cert: signer.der, spki: signer.spki, subject: signer.subject } : null,
98
+ transactionID: m.header.transactionID || null,
99
+ senderNonce: m.header.senderNonce || null,
100
+ recipNonce: m.header.recipNonce || null,
101
+ header: m.header,
102
+ body: m.body,
103
+ };
104
+ if (code) { v.code = code; v.reason = reason; }
105
+ return v;
106
+ }
107
+ function _ok(m, type, alg, trusted, signer) { return _verdict(m, type, alg, true, trusted, null, null, signer); }
108
+ function _fail(m, type, alg, code, reason, signer) { return _verdict(m, type, alg, false, false, code, reason, signer || null); }
109
+
110
+ // ---- input coercion: parse a DER Buffer / PEM string, or RE-DERIVE an authenticated view of an
111
+ // already-parsed object. A caller-supplied parsed object's DECODED struct (header.transactionID, body, ...)
112
+ // is UNTRUSTED -- middleware that mutated a decoded field after parsing must not have it returned in a
113
+ // valid verdict or slip past the opt-in echo checks while the crypto verifies over the original raw slices.
114
+ // So a parsed object is reassembled from its AUTHENTICATED raw slices (headerBytes / bodyBytes / protection /
115
+ // extraCerts) and re-parsed, so every security-relevant and returned field derives from the authenticated
116
+ // bytes, never the caller's decoded representation.
117
+ function _coerce(message) {
118
+ if (message && typeof message === "object" && !Buffer.isBuffer(message) && !(message instanceof Uint8Array) &&
119
+ message.headerBytes !== undefined && message.bodyBytes !== undefined &&
120
+ message.header !== undefined && message.body !== undefined) {
121
+ // A malformed raw slice (a null protection.bytes, a non-buffer extraCerts entry) makes _reassemble throw a
122
+ // raw Asn1Error; normalize it to the documented typed cmp/bad-input so every malformed input surfaces a
123
+ // cmp/* code (cmp.parse's own faults are already CmpError and propagate unchanged).
124
+ try { return cmp.parse(_reassemble(message)); }
125
+ 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); }
126
+ }
127
+ // A raw Buffer / Uint8Array parses into zero-copy views; a caller that mutates the input after verify would
128
+ // change the returned authenticated fields (transactionID, nonces, header, body) while valid stays true.
129
+ // Parse a PRIVATE copy so every verdict field is bound to the verified snapshot, never the caller's mutable
130
+ // buffer. (A PEM string is immutable and base64-decodes into fresh bytes, so it needs no copy.)
131
+ if (Buffer.isBuffer(message) || message instanceof Uint8Array) return cmp.parse(Buffer.from(message));
132
+ return cmp.parse(message); // a PEM CMP string (throws cmp/* on malformed input)
133
+ }
134
+
135
+ // Rebuild the PKIMessage DER from a parsed object's raw slices: SEQUENCE { header, body, protection [0]
136
+ // EXPLICIT BIT STRING OPTIONAL, extraCerts [1] EXPLICIT SEQUENCE OPTIONAL } -- the exact inverse of the
137
+ // parser's surfaced fields (cmp-build.js assembles the identical shape). Re-parsing this authenticates every
138
+ // field against the raw bytes, discarding any mutated decoded field the caller may have carried.
139
+ function _reassemble(m) {
140
+ var kids = [b.raw(m.headerBytes), b.raw(m.bodyBytes)];
141
+ if (m.protection != null) kids.push(b.explicit(0, b.bitString(m.protection.bytes, m.protection.unusedBits)));
142
+ if (m.extraCerts != null && m.extraCerts.length) {
143
+ kids.push(b.explicit(1, b.sequence(m.extraCerts.map(function (c) { return b.raw(c); }))));
144
+ }
145
+ return b.sequence(kids);
146
+ }
147
+
148
+ // A certificate DER from a Buffer/Uint8Array (detached-view-safe) or a PEM string.
149
+ function _certDer(cert, what) {
150
+ if (Buffer.isBuffer(cert) || cert instanceof Uint8Array) return guard.bytes.view(cert, CmpError, "cmp/bad-input", what);
151
+ if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cmp/bad-input", what + " PEM could not be decoded", e); } }
152
+ throw _err("cmp/bad-input", what + " must be a certificate DER Buffer/Uint8Array or PEM string");
153
+ }
154
+
155
+ // The subjectKeyIdentifier extension value of a parsed certificate, or null when absent/undecodable.
156
+ // x509.parse surfaces extensions as an array of { oid, name, critical, value } (value = raw extnValue DER).
157
+ function _certSki(parsed) {
158
+ var skiOid = oid.byName("subjectKeyIdentifier");
159
+ var exts = parsed.extensions;
160
+ for (var i = 0; i < exts.length; i++) {
161
+ if (exts[i].oid !== skiOid) continue;
162
+ try { return asn1.read.octetString(asn1.decode(exts[i].value)); } catch (_e) { return null; }
163
+ }
164
+ return null;
165
+ }
166
+ function _subjectDn(parsed) { return parsed.subject.dn || null; }
167
+
168
+ // The decoded GeneralName nodes of a parsed certificate's subjectAltName (empty if none / malformed).
169
+ // x509.parse surfaces `value` as the unwrapped extnValue content -- for SAN that is the SEQUENCE OF GeneralName.
170
+ function _sanGeneralNames(parsed) {
171
+ var sanOid = oid.byName("subjectAltName");
172
+ var exts = parsed.extensions;
173
+ for (var i = 0; i < exts.length; i++) {
174
+ if (exts[i].oid !== sanOid) continue;
175
+ // RFC 5280 sec. 4.2.1.6: subjectAltName is a SEQUENCE OF GeneralName. Validate the outer structure AND every
176
+ // entry through the shared generalNames schema before any child is used for identity binding: path validation
177
+ // does not decode a non-critical, otherwise-unused SAN, so a malformed outer tag (e.g. a SET wrapping a valid
178
+ // [2]) or a malformed entry must fail closed to no entries (a sender-mismatch), never bind an unvalidated name.
179
+ try {
180
+ var node = asn1.decode(exts[i].value);
181
+ schema.walk(pkix.generalNames(NS, { code: "cmp/sender-mismatch" }), node, NS);
182
+ return node.children || [];
183
+ } catch (_e) { return []; }
184
+ }
185
+ return [];
186
+ }
187
+
188
+ // RFC 9483 sec. 3.1 sender <-> protection-certificate binding. The sender matches the certificate identity if
189
+ // it equals the POPULATED subject DN (a [4] directoryName under the RFC 5280 sec. 7.1 canonical comparison) OR
190
+ // a subjectAltName GeneralName entry -- checked regardless of whether the subject is empty, so a certificate
191
+ // that carries both a subject and a SAN can be identified by either, and an empty subject (RFC 5280 sec.
192
+ // 4.1.2.6 requires a critical SAN) is bound to its SAN identity rather than accepted with an anonymous sender.
193
+ function _senderBoundToCert(sender, parsed) {
194
+ if (!sender || !sender.bytes) return false;
195
+ var subjectRdns = parsed.subject.rdns;
196
+ if (subjectRdns.length > 0) {
197
+ var isDirName = sender.tagClass === "context" && sender.tagNumber === 4 && sender.value && Array.isArray(sender.value.rdns);
198
+ if (isDirName && guard.name.dnEqual(sender.value.rdns, subjectRdns, NS.E, "cmp/sender-mismatch", "the header sender / signer subject")) return true;
199
+ }
200
+ var san = _sanGeneralNames(parsed);
201
+ for (var i = 0; i < san.length; i++) if (_generalNameMatches(sender, san[i])) return true;
202
+ return false;
203
+ }
204
+
205
+ // GeneralName equality (RFC 5280 sec. 7) for the sender <-> SAN binding, applying the per-type comparison
206
+ // rules rather than a universal byte compare: a dNSName [2] case-insensitively (sec. 4.2.1.6 / 7.2), an
207
+ // rfc822Name [1] with a case-insensitive domain (sec. 7.5), a uniformResourceIdentifier [6] with a
208
+ // case-insensitive scheme + host (sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1), a directoryName [4] under the sec.
209
+ // 7.1 canonical DN comparison, and every other type by exact DER (fail-closed). `sender` is the parsed [n]
210
+ // GeneralName from schema-cmp; `sanNode` is a decoded certificate SAN GeneralName node.
211
+ function _generalNameMatches(sender, sanNode) {
212
+ if (sanNode.tagClass !== sender.tagClass || sanNode.tagNumber !== sender.tagNumber) return false;
213
+ if (sanNode.tagClass === "context") {
214
+ if (sanNode.tagNumber === 2 && sanNode.content) { // dNSName IA5String
215
+ var dnsSan = sanNode.content.toString("latin1"), dnsSender = String(sender.value);
216
+ // Case-fold ONLY a well-formed dNSName (RFC 5280 sec. 4.2.1.6 / RFC 1034, via the shared validator): a
217
+ // malformed name (an empty label like "victim..com", a leading/trailing dot, bad LDH) is compared
218
+ // byte-exact rather than folded, so it cannot bind a byte-distinct identity.
219
+ if (pkix.dnsNameProblem(dnsSan) !== null || pkix.dnsNameProblem(dnsSender) !== null) return dnsSan === dnsSender;
220
+ return dnsSender.toLowerCase() === dnsSan.toLowerCase();
221
+ }
222
+ if (sanNode.tagNumber === 1 && sanNode.content) { // rfc822Name IA5String: case-insensitive domain
223
+ return _rfc822Equal(String(sender.value), sanNode.content.toString("latin1"));
224
+ }
225
+ if (sanNode.tagNumber === 6 && sanNode.content) { // uniformResourceIdentifier IA5String: case-insensitive scheme + host
226
+ return _uriEqual(String(sender.value), sanNode.content.toString("latin1"));
227
+ }
228
+ if (sanNode.tagNumber === 4 && sanNode.children && sanNode.children.length) { // directoryName: canonical DN
229
+ if (!sender.value || !Array.isArray(sender.value.rdns)) return false;
230
+ var sanName;
231
+ try { sanName = schema.embeddedDer(pkix.name(NS), sanNode.children[0].bytes, NS, { code: "cmp/sender-mismatch", what: "SAN directoryName" }).result; }
232
+ catch (_e) { return false; }
233
+ return guard.name.dnEqual(sender.value.rdns, sanName.rdns, NS.E, "cmp/sender-mismatch", "SAN directoryName");
234
+ }
235
+ }
236
+ return sanNode.bytes.equals(sender.bytes);
237
+ }
238
+
239
+ // Index of the "@" separating an RFC 5321 addr-spec Local-part from its Domain, honoring a Quoted-string
240
+ // local-part ("...") that MAY itself contain "@" and backslash escapes. Returns -1 for a malformed mailbox
241
+ // (no separator, an unterminated quote, a quoted local-part not immediately followed by "@", or an unquoted
242
+ // local-part with more than one "@") so the caller compares byte-exact.
243
+ function _mailboxSplit(s) {
244
+ var sep;
245
+ if (s.charAt(0) === "\"") {
246
+ var i = 1;
247
+ while (i < s.length) {
248
+ var c = s.charAt(i);
249
+ if (c === "\\") { i += 2; continue; } // an escaped pair (\c) -- skip both octets
250
+ if (c === "\"") break;
251
+ i++;
252
+ }
253
+ if (i >= s.length || s.charAt(i) !== "\"" || s.charAt(i + 1) !== "@") return -1; // unterminated / not "@"-separated
254
+ sep = i + 1;
255
+ } else {
256
+ sep = s.indexOf("@"); // an unquoted (Dot-string) local-part cannot contain a raw "@" -> exactly one total
257
+ if (sep < 0 || sep !== s.lastIndexOf("@")) return -1;
258
+ }
259
+ // The Domain that follows the separator must be a well-formed FQDN (RFC 1034, via the shared validator) -- an
260
+ // empty / empty-label / extra-"@" domain is malformed. An unquoted Local-part must be a Dot-string: non-empty,
261
+ // no leading/trailing dot, no empty atom ("a..b"). A quoted Local-part is already balanced above. A malformed
262
+ // mailbox returns -1 so _rfc822Equal compares it byte-exact rather than case-folding a malformed domain.
263
+ var local = s.slice(0, sep), domain = s.slice(sep + 1);
264
+ if (pkix.dnsNameProblem(domain) !== null) return -1;
265
+ // An unquoted (Dot-string) Local-part is atext atoms joined by single dots (RFC 5321 sec. 4.1.2): every
266
+ // character must be an atext octet or a "." with no empty atom / leading-trailing dot. A malformed atom
267
+ // character (e.g. a space in "user name") makes the whole address malformed -> byte-exact comparison.
268
+ if (s.charAt(0) !== "\"" && (local.length === 0 || local.charAt(0) === "." || local.charAt(local.length - 1) === "." || local.indexOf("..") !== -1 || !/^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+$/.test(local))) return -1;
269
+ return sep;
270
+ }
271
+
272
+ // RFC 5280 sec. 7.5 rfc822Name comparison: the local-part is case-sensitive, the domain case-insensitive. The
273
+ // mailbox separator is located by _mailboxSplit (quoting-aware) so a valid quoted local-part carrying "@"
274
+ // still binds, while a truly malformed address falls back to byte-exact comparison.
275
+ function _rfc822Equal(a, b) {
276
+ var ai = _mailboxSplit(a), bi = _mailboxSplit(b);
277
+ if (ai < 0 || bi < 0) return a === b;
278
+ return a.slice(0, ai) === b.slice(0, bi) && a.slice(ai + 1).toLowerCase() === b.slice(bi + 1).toLowerCase();
279
+ }
280
+
281
+ // RFC 5280 sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1 URI comparison: the scheme and the authority host are
282
+ // case-insensitive; every other component (userinfo, port, path, query, fragment) is byte-exact. Return
283
+ // null for anything that is not a well-formed absolute URI so the caller falls back to exact-DER (fail-closed
284
+ // -- never normalize an input we cannot confidently parse into a false identity match).
285
+ function _normalizeUri(u) {
286
+ // RFC 3986 syntax: every character must be a URI character and every "%" must introduce a valid pct-encoding
287
+ // (%HH). A URI with an out-of-charset byte or a malformed percent-escape ("%zz") in ANY component (path,
288
+ // query, fragment, userinfo) is not normalized -- return null so it is compared byte-exact.
289
+ if (!/^[A-Za-z0-9._~:/?#@!$&'()*+,;=%[\]-]*$/.test(u) || /%(?![0-9A-Fa-f]{2})/.test(u)) return null;
290
+ var m = /^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/.exec(u); // scheme ":" rest (RFC 3986 sec. 3.1)
291
+ if (!m) return null;
292
+ var scheme = m[1].toLowerCase();
293
+ var rest = m[2];
294
+ // A URI SAN identity is host-based (RFC 5280 sec. 4.2.1.6): an authority-free URI (no "//", e.g. a URN) has
295
+ // no host to anchor the comparison, so compare it byte-exact rather than case-fold its scheme -- the shared
296
+ // GeneralName parser only enforces printable IA5, not a present authority.
297
+ if (rest.slice(0, 2) !== "//") return null;
298
+ var body = rest.slice(2);
299
+ var cut = body.search(/[/?#]/); // authority ends at the first "/", "?" or "#" (RFC 3986 sec. 3.2)
300
+ var split = cut < 0 ? body.length : cut;
301
+ var authority = body.slice(0, split), tail = body.slice(split); // tail: path/query/fragment -- exact
302
+ // A raw "@" is not permitted inside userinfo (RFC 3986 sec. 3.2.1); more than one "@" is a MALFORMED
303
+ // authority -> return null so it is compared byte-exact, never split into a userinfo that folds the host.
304
+ if (authority.indexOf("@") !== authority.lastIndexOf("@")) return null;
305
+ var at = authority.lastIndexOf("@"); // [ userinfo "@" ] host [ ":" port ] -- userinfo stays case-sensitive
306
+ var userinfo = at < 0 ? "" : authority.slice(0, at + 1);
307
+ var hostport = at < 0 ? authority : authority.slice(at + 1);
308
+ // Split the host from an optional ":port": an IPv6 literal "[...]" carries the port after the "]", otherwise
309
+ // the port follows the FIRST ":". Case-fold ONLY the host (RFC 3986 sec. 6.2.2.1); the port stays byte-exact
310
+ // so a malformed non-numeric port (":ADMIN" vs ":admin") is never folded into a false identity match.
311
+ var host, port;
312
+ if (hostport.charAt(0) === "[") {
313
+ var rb = hostport.indexOf("]");
314
+ if (rb < 0) return null; // an unterminated IPv6 literal is malformed -> exact-DER fallback (fail-closed)
315
+ host = hostport.slice(0, rb + 1); port = hostport.slice(rb + 1);
316
+ } else {
317
+ var ci = hostport.indexOf(":");
318
+ host = ci < 0 ? hostport : hostport.slice(0, ci); port = ci < 0 ? "" : hostport.slice(ci);
319
+ }
320
+ // The port must be numeric (RFC 3986 sec. 3.2.3 port = *DIGIT). A non-numeric port is a MALFORMED authority:
321
+ // return null so the caller compares the raw values exactly, rather than case-fold the host of an input the
322
+ // normalizer cannot confidently parse (which would bind two byte-distinct malformed authorities).
323
+ if (port !== "" && !/^:[0-9]*$/.test(port)) return null;
324
+ // The authority host must be a well-formed FQDN or IP literal (RFC 5280 sec. 4.2.1.6 requires a URI SAN
325
+ // authority to carry an FQDN/IP host): an empty, malformed ("Victim..COM"), or bad IPv6-literal host is
326
+ // compared byte-exact rather than case-folded, since the GeneralName parser only enforces printable IA5.
327
+ var hostOk = host.charAt(0) === "["
328
+ ? (host.charAt(host.length - 1) === "]" && ipUtils.expandIpv6Hex(host.slice(1, -1)) !== null)
329
+ : (pkix.dnsNameProblem(host) === null);
330
+ if (!hostOk) return null;
331
+ return scheme + "://" + userinfo + host.toLowerCase() + port + tail;
332
+ }
333
+ function _uriEqual(a, b) {
334
+ var na = _normalizeUri(a), nb = _normalizeUri(b);
335
+ if (na === null || nb === null) return a === b;
336
+ return na === nb;
337
+ }
338
+
339
+ // M11/M14: resolve the signature-protection signer certificate. An explicit opts.signerCert wins (with the
340
+ // senderKID SKI binding when present); else `extra` (the ALREADY dedup+validity+count-bounded extraCerts) by
341
+ // senderKID, else RFC 9483 sec. 3.3 -- extraCerts[0] IS the protection certificate. `extra` is bounded BEFORE
342
+ // this search so an unsigned flood cannot force unbounded X.509 parsing. Returns { der, spki, subject, parsed }
343
+ // or null (never verify against an unauthenticated key).
344
+ function _resolveSignerCert(m, opts, extra) {
345
+ var senderKID = m.header.senderKID; // Buffer or null
346
+ function _signerObj(der, p) { return { der: der, spki: p.subjectPublicKeyInfo.bytes, subject: _subjectDn(p), parsed: p }; }
347
+ function accept(der) { // an UNTRUSTED message candidate: the senderKID (when present) narrows the search to the
348
+ var p; // certificate whose SKI it names; a non-parseable / SKI-mismatched entry is skipped (null).
349
+ try { p = x509.parse(der); } catch (_e) { return null; }
350
+ if (senderKID != null) {
351
+ var ski = _certSki(p);
352
+ if (ski == null || !guard.crypto.constantTimeEqual(ski, senderKID)) return null;
353
+ }
354
+ return _signerObj(der, p);
355
+ }
356
+ if (opts.signerCert != null) {
357
+ // The explicit signer certificate is REQUIRED CONFIG, not untrusted pool material. Snapshot it (_certDer
358
+ // returns a VIEW into a caller Buffer, and der/spki are surfaced in the verdict, so a post-verify mutation
359
+ // would change verdict.signer.cert/.spki). Parse it SEPARATELY: a corrupt / mistyped certificate is a
360
+ // deployment error that THROWS cmp/bad-input, never a routine signer-cert-not-found verdict indistinguishable
361
+ // from a message that simply omitted its signer (the nullable path is reserved for message candidates).
362
+ // The caller EXPLICITLY selected this certificate, so the senderKID (a hint for narrowing a candidate search)
363
+ // is NOT applied here -- the protection signature verification against this cert's key is the real gate; a
364
+ // valid message from a signer certificate that omits an SKI still resolves to its exact opts.signerCert.
365
+ var scDer = Buffer.from(_certDer(opts.signerCert, "opts.signerCert"));
366
+ var scParsed;
367
+ try { scParsed = x509.parse(scDer); }
368
+ catch (e) { throw _err("cmp/bad-input", "opts.signerCert is not a parseable X.509 certificate", e); }
369
+ return _signerObj(scDer, scParsed);
370
+ }
371
+ if (senderKID != null) {
372
+ for (var i = 0; i < extra.length; i++) { var r = accept(extra[i]); if (r) return r; }
373
+ return null;
374
+ }
375
+ if (extra.length) return accept(extra[0]); // RFC 9483 sec. 3.3: extraCerts[0] is the protection cert
376
+ return null;
377
+ }
378
+
379
+ // M20-M22 header consistency (opt-in echoes). Returns { code, reason } on a mismatch, else null. These are
380
+ // public transaction identifiers, not secrets, so a length-checked equality (not a MAC compare) is correct.
381
+ function _bufEq(a, x) {
382
+ if (!Buffer.isBuffer(a)) return false;
383
+ if (!Buffer.isBuffer(x)) { if (x instanceof Uint8Array) x = Buffer.from(x); else return false; }
384
+ return a.length === x.length && a.equals(x);
385
+ }
386
+ function _headerChecks(m, opts) {
387
+ if (opts.transactionID != null && !_bufEq(m.header.transactionID, opts.transactionID)) {
388
+ return { code: "cmp/transaction-id-mismatch", reason: "header.transactionID does not equal the expected value (RFC 9810 sec. 5.1.1)" };
389
+ }
390
+ if (opts.expectRecipNonce != null && !_bufEq(m.header.recipNonce, opts.expectRecipNonce)) {
391
+ return { code: "cmp/bad-recip-nonce", reason: "header.recipNonce does not echo the expected sender nonce" };
392
+ }
393
+ return null;
394
+ }
395
+
396
+ // M13 keyUsage.digitalSignature gate -- a format-local check ON TOP of path.validate: if the signer cert
397
+ // carries a keyUsage extension, digitalSignature MUST be asserted (RFC 9483 sec. 3.2, RFC 9810 sec. 5.1.3.3).
398
+ // Takes the already-parsed signer cert (parsed once in _resolveSignerCert); a malformed keyUsage fails closed.
399
+ function _keyUsageAllowsSigning(parsed) {
400
+ var kuOid = oid.byName("keyUsage");
401
+ var exts = parsed.extensions;
402
+ for (var i = 0; i < exts.length; i++) {
403
+ if (exts[i].oid !== kuOid) continue;
404
+ var ku;
405
+ // Decode through the shared keyUsage decoder -- it enforces the RFC 5280 sec. 4.2.1.3 BIT STRING form + the
406
+ // X.690 sec. 11.2.2 minimal NamedBitList rule (rejecting a non-minimal 03 02 00 80). A malformed KeyUsage
407
+ // fails this gate rather than a hand-rolled bit test authorizing it and leaning on path validation to reject it.
408
+ try { ku = _certExtDecoders[kuOid](exts[i].value); }
409
+ catch (_e) { return false; }
410
+ return ku.digitalSignature === true;
411
+ }
412
+ return true; // no keyUsage extension present -> unconstrained
413
+ }
414
+
415
+ // M15-M19: recompute + constant-time-compare the PBMAC1 protection.
416
+ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
417
+ if (protectionAlg.parameters === null) {
418
+ return _fail(m, "mac", protectionAlg, "cmp/protection-failed", "the PBMAC1 protectionAlg carries no PBMAC1-params (RFC 9579 sec. 4)");
419
+ }
420
+ var params;
421
+ try {
422
+ params = schema.embeddedDer(PBMAC1_PARAMS, protectionAlg.parameters, NS, { code: "cmp/bad-mac-data", what: "PBMAC1-params" }).result;
423
+ } catch (e) {
424
+ // Malformed / keyLength-omitted (RFC 9579 sec. 5) / non-canonical PRF params -> a fail-closed verdict.
425
+ return _fail(m, "mac", protectionAlg, e instanceof CmpError ? e.code : "cmp/protection-failed", "the PBMAC1-params did not decode: " + ((e && e.message) || e));
426
+ }
427
+ var kdf = params.kdf; // { salt, iterationCount, keyLength, prfOid, prfName }
428
+ var prfHash = PRF_HASH[kdf.prfOid]; // dispatch on the immutable OID, not the mutable display name
429
+ var macHash = PRF_HASH[params.schemeOid];
430
+ if (!prfHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 PBKDF2 PRF " + JSON.stringify(kdf.prfName) + " (SHA-256/384/512 only; RFC 9481 sec. 7, RFC 9579 sec. 7)");
431
+ if (!macHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " + JSON.stringify(params.schemeName) + " (SHA-256/384/512 only)");
432
+
433
+ // M18: bound the attacker-controlled work factors BEFORE deriving (CWE-834/400). A config-tier throw.
434
+ _capWork(kdf.iterationCount, kdf.salt, kdf.keyLength, prfHash, opts);
435
+
436
+ // M19: recompute the MAC over the reconstructed ProtectedPart and compare in constant time.
437
+ var secret = typeof opts.sharedSecret === "string" ? Buffer.from(opts.sharedSecret, "utf8") : guard.bytes.view(opts.sharedSecret, CmpError, "cmp/bad-input", "opts.sharedSecret");
438
+ var computed = await pbes2.pbmac1(secret, kdf.salt, kdf.iterationCount, kdf.keyLength, prfHash, macHash, protectedPart);
439
+ if (!guard.crypto.constantTimeEqual(computed, protection.bytes)) {
440
+ return _fail(m, "mac", protectionAlg, "cmp/protection-failed", "the PBMAC1 MAC does not verify (a wrong shared secret or a tampered ProtectedPart)");
441
+ }
442
+ var hc = _headerChecks(m, opts);
443
+ if (hc) return _fail(m, "mac", protectionAlg, hc.code, hc.reason);
444
+ return _ok(m, "mac", protectionAlg, true, null); // mac: valid protection == secret matched == trusted
445
+ }
446
+
447
+ function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
448
+ var cap = PBMAC1_MAX_ITER;
449
+ if (opts.maxIterations != null) {
450
+ if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) {
451
+ throw _err("cmp/bad-input", "opts.maxIterations must be a positive integer");
452
+ }
453
+ cap = Math.min(opts.maxIterations, cap);
454
+ }
455
+ // A trivially small count lets a peer run cheap offline PBMAC1 guesses against a password-like shared secret
456
+ // with parameters pki.cmp.build refuses to produce (RFC 8018 sec. 4.2). Enforce the same floor on verify.
457
+ 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)");
458
+ if (iterationCount > cap) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " exceeds the cap " + cap);
459
+ if (!salt || salt.length < PBMAC1_MIN_SALT || salt.length > 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)");
460
+ 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)");
461
+ // Bound the COMBINED work: PBKDF2 derives ceil(keyLength / hLen) blocks, each costing iterationCount HMACs, so a
462
+ // maximal keyLength multiplies the per-block iteration ceiling (a 1024-octet key over SHA-256 = 32x). Cap the
463
+ // product against the same ceiling so no keyLength/iteration combination exceeds the single-block work (CWE-834/400).
464
+ var blocks = Math.ceil(keyLength / (PRF_HLEN[prfHash] || 32));
465
+ if (iterationCount * blocks > cap) throw _err("cmp/bad-input", "the PBMAC1 combined work (iterationCount " + iterationCount + " x " + blocks + " derived blocks) exceeds the cap " + cap);
466
+ }
467
+
468
+ // M9-M14 + M20-M22: verify signature protection, then (with a trust store) FULL out-of-path cert validation.
469
+ async function _verifySignature(m, protectedPart, protectionAlg, protection, opts) {
470
+ // Bound the UNSIGNED extraCerts ONCE (dedup + drop non-certificates + cap + scan cap) before either the
471
+ // senderKID signer search or path building, so a hostile peer padding extraCerts cannot force unbounded
472
+ // X.509 parsing or a candidate-pool overflow.
473
+ var extra = _boundExtraCerts(m.extraCerts);
474
+ var signer = _resolveSignerCert(m, opts, extra);
475
+ if (!signer) return _fail(m, "signature", protectionAlg, "cmp/signer-cert-not-found", "no signer certificate resolved (opts.signerCert, senderKID, or extraCerts)");
476
+
477
+ // M2/M9: verify over the reconstructed ProtectedPart through the ONE path-validate engine (the EdDSA
478
+ // low-order-point gate + the sig-OID<->key-OID algorithm-confusion gate apply); fail-closed to false.
479
+ var ok = await _engine.verifyWithSpki(protectionAlg, protection.bytes, signer.spki, protectedPart);
480
+ 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);
481
+
482
+ // RFC 9483 sec. 3.1: for signature-based protection the authenticated header sender field MUST match the
483
+ // protection certificate identity (failInfo badMessageCheck). Without this binding a holder of ANY
484
+ // certificate the anchor trusts could set the sender to another party's name and be accepted as that
485
+ // sender -- the signature and path both pass while the claimed identity is forged.
486
+ if (!_senderBoundToCert(m.header.sender, signer.parsed)) {
487
+ 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);
488
+ }
489
+
490
+ // A failed opt-in echo check is a REJECTION verdict (valid:false), consistent with the MAC path -- a caller
491
+ // that requested a transactionID / recipNonce binding must not see valid:true when it does not hold.
492
+ var hc = _headerChecks(m, opts);
493
+ if (hc) return _fail(m, "signature", protectionAlg, hc.code, hc.reason, signer);
494
+
495
+ // M5/M12: with no trust store the verdict is crypto-only (trusted:false) and the signer is surfaced for
496
+ // the caller to anchor. With a trust store the signer cert gets the FULL RFC 5280 sec. 6.1 path gates.
497
+ if (opts.trustAnchors == null) return _ok(m, "signature", protectionAlg, false, signer);
498
+ var trust = await _chainSigner(signer, m, opts, extra);
499
+ return _verdict(m, "signature", protectionAlg, true, trust.trusted, trust.trusted ? null : "cmp/untrusted-signer", trust.reason, signer);
500
+ }
501
+
502
+ // A canonical certificate identity (tbs + signature) for the extraCerts pool dedup: it keys a Buffer /
503
+ // Uint8Array (parse) and an already-parsed candidate object identically (path.build accepts both forms), so
504
+ // an extraCert duplicating a caller intermediate is dropped regardless of which representation the caller used.
505
+ function _certKey(c) {
506
+ var p = c;
507
+ if (Buffer.isBuffer(c) || c instanceof Uint8Array) { try { p = x509.parse(c); } catch (_e) { return null; } }
508
+ if (!p || !p.tbsBytes) return null;
509
+ return p.tbsBytes.toString("base64") + "|" + (p.signatureValue && p.signatureValue.bytes ? p.signatureValue.bytes.toString("base64") : "");
510
+ }
511
+
512
+ async function _chainSigner(signer, m, opts, extra) {
513
+ // M13 first (cheap, format-local): a keyUsage that omits digitalSignature rejects the protection.
514
+ if (!_keyUsageAllowsSigning(signer.parsed)) {
515
+ return { trusted: false, reason: "the signer certificate keyUsage does not assert digitalSignature (RFC 9483 sec. 3.2)" };
516
+ }
517
+ // Validate the signer path at a TRUSTED current time by default -- NOT the message's self-asserted
518
+ // messageTime, which the sender controls: a holder of a now-expired but once-valid signer certificate
519
+ // could otherwise backdate messageTime into the certificate's validity window and be reported trusted.
520
+ // A caller doing historical verification opts into a specific instant via opts.time; a falsy but PRESENT
521
+ // opts.time (0 / false / "") is NOT silently replaced -- it reaches path.build and is rejected as bad-input.
522
+ var time = opts.time != null ? opts.time : new Date();
523
+ var anchors = _certList(opts.trustAnchors);
524
+ // `extra` is the already dedup + validity + count-bounded extraCerts (unsigned pool material), so a flood
525
+ // of duplicate / junk / non-certificate entries cannot exhaust the candidate-search budget or raise a
526
+ // path/bad-input from a malformed pool certificate. Add it only up to the path-builder pool ceiling AFTER
527
+ // the caller's intermediates, so unsigned extraCerts can never push a valid caller configuration over the
528
+ // ceiling (if the caller's intermediates alone exceed it, that is a genuine config error path.build reports).
529
+ var pool = _certList(opts.intermediates);
530
+ var room = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - pool.length;
531
+ if (room > 0) {
532
+ // Drop the signer leaf (already passed to build as the path TARGET) and any extraCert duplicating a caller
533
+ // intermediate BEFORE truncating to the remaining slots, so a tight ceiling spends those slots on genuinely
534
+ // useful embedded issuers rather than a redundant copy of the signer or a duplicate the pool already holds.
535
+ // The identity is the canonical tbs+signature key so a Buffer extraCert dedups against a caller intermediate
536
+ // supplied either as raw bytes OR as an already-parsed certificate object (path.build accepts both).
537
+ var seen = Object.create(null);
538
+ var sk = _certKey(signer.parsed); if (sk) seen[sk] = 1;
539
+ pool.forEach(function (c) { var k = _certKey(c); if (k) seen[k] = 1; });
540
+ var useful = extra.filter(function (c) { var k = _certKey(c); return k == null || !seen[k]; });
541
+ pool = pool.concat(useful.slice(0, room));
542
+ }
543
+ var buildOpts = { trustAnchors: anchors, intermediates: pool, validate: true, time: time };
544
+ if (opts.revocationChecker != null) buildOpts.revocationChecker = opts.revocationChecker;
545
+ try {
546
+ var res = await _engine.build(signer.der, buildOpts);
547
+ if (!res || res.valid !== true) return { trusted: false, reason: "the signer certificate did not chain to a supplied trust anchor" };
548
+ return { trusted: true, reason: null };
549
+ } catch (e) {
550
+ // A config-tier fault from path.build (an invalid opts.time, an empty / malformed trustAnchors or
551
+ // intermediate) is a DEPLOYMENT error, not an untrusted signer -- rethrow it as cmp/bad-input so it is
552
+ // not masked as a routine trust failure (the API contract: a bad required opt throws). A genuine
553
+ // no-assemblable-path / validation failure collapses to an untrusted-signer verdict.
554
+ if (e && e.code === "path/bad-input") {
555
+ throw _err("cmp/bad-input", "invalid trust / validation options for signer-certificate path validation: " + (e.message || e), e);
556
+ }
557
+ return { trusted: false, reason: "signer certificate path validation failed: " + (e.message || e) };
558
+ }
559
+ }
560
+
561
+ // Sanitize the message-provided extraCerts -- they are UNSIGNED pool material a hostile peer can pad without
562
+ // breaking protection: deduplicate by exact DER, DROP any entry that is not a parseable X.509 certificate (a
563
+ // bare non-cert SEQUENCE would otherwise reach path.build and raise a config-tier path/bad-input), and keep at
564
+ // most MAX_EXTRA_CERTS. The whole scan is bounded to MAX_EXTRA_SCAN entries so a flood of junk cannot force
565
+ // unbounded X.509 parsing; the RFC 9483 sec. 3.3 protection cert is extraCerts[0], so it is examined first.
566
+ var MAX_EXTRA_CERTS = 32; // valid, distinct certificates kept for signer resolution + path building
567
+ var MAX_EXTRA_SCAN = 256; // total entries examined -- bounds parse work regardless of the flood size
568
+ function _boundExtraCerts(extra) {
569
+ if (!Array.isArray(extra) || !extra.length) return [];
570
+ var out = [], seen = Object.create(null);
571
+ for (var i = 0; i < extra.length && out.length < MAX_EXTRA_CERTS && i < MAX_EXTRA_SCAN; i++) {
572
+ var c = extra[i];
573
+ if (!Buffer.isBuffer(c) && !(c instanceof Uint8Array)) continue;
574
+ var key = Buffer.from(c).toString("base64");
575
+ if (seen[key]) continue;
576
+ seen[key] = true;
577
+ // drop a non-certificate entry (unsigned, attacker-supplied pool material) before it reaches path.build
578
+ try { x509.parse(c); }
579
+ catch (_e) { continue; }
580
+ out.push(c);
581
+ }
582
+ return out;
583
+ }
584
+
585
+ // A trust-anchor / pool list from a single cert (DER/PEM) or an array of them.
586
+ function _certList(v) {
587
+ if (v == null) return [];
588
+ var arr = Array.isArray(v) ? v : [v];
589
+ return arr.map(function (c) { return (Buffer.isBuffer(c) || c instanceof Uint8Array || typeof c === "string") ? _certDer(c, "a trust anchor / intermediate") : c; });
590
+ }
591
+
592
+ // A usable PBMAC1 shared secret: a non-empty string or a non-empty Buffer/Uint8Array (mirrors cmp-build).
593
+ function _nonEmptySecret(s) {
594
+ if (typeof s === "string") return s.length > 0;
595
+ return (Buffer.isBuffer(s) || s instanceof Uint8Array) && s.length > 0;
596
+ }
597
+
598
+ function verify(message, opts) {
599
+ return Promise.resolve().then(function () { return _verify(message, opts); });
600
+ }
601
+
602
+ async function _verify(message, opts) {
603
+ if (opts == null) opts = {}; // ONLY null / undefined -> the default empty opts (a falsy false/0/"" is a bad config, not a default)
604
+ if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cmp/bad-input", "opts must be an object");
605
+ Object.keys(opts).forEach(function (k) { if (!KNOWN_VERIFY_OPTS[k]) throw _err("cmp/bad-input", "unknown opts field " + JSON.stringify(k)); });
606
+ // The opt-in echo values are byte buffers: a non-buffer (e.g. a string from JSON config) is a DEPLOYMENT
607
+ // error that throws cmp/bad-input, never a routine transaction/nonce mismatch verdict that would misreport
608
+ // a caller's typing mistake as a peer authentication failure.
609
+ ["transactionID", "expectRecipNonce"].forEach(function (k) {
610
+ if (opts[k] != null && !Buffer.isBuffer(opts[k]) && !(opts[k] instanceof Uint8Array)) throw _err("cmp/bad-input", "opts." + k + " must be a Buffer / Uint8Array");
611
+ });
612
+ if (_engine == null) throw _err("cmp/bad-input", "the cmp-verify signature engine is not initialized (require pki before use)");
613
+
614
+ var m = _coerce(message);
615
+ var protectionAlg = m.header.protectionAlg;
616
+ var protection = m.protection;
617
+
618
+ // M3/M5: the parser guarantees protection<->protectionAlg presence agreement; an absent protection is a
619
+ // hard reject -- absence is never "verified".
620
+ if (protection === null || protectionAlg === null) {
621
+ 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");
622
+ }
623
+
624
+ // M2: reconstruct the ProtectedPart from the RAW surfaced slices -- byte-identical to what build signed,
625
+ // never a re-serialization of the decoded header/body structs.
626
+ var protectedPart = b.sequence([b.raw(m.headerBytes), b.raw(m.bodyBytes)]);
627
+
628
+ // M7: a recognized legacy / KEM MAC OID is refused before any dispatch (never a silent accept).
629
+ if (UNSUPPORTED_MAC_OIDS[protectionAlg.oid]) {
630
+ 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)");
631
+ }
632
+
633
+ // M6: flavor from the protectionAlg OID alone, never guessed from the caller's credential.
634
+ var isMac = protectionAlg.oid === oid.byName("pbmac1");
635
+ var hasSigCred = opts.signerCert != null || opts.trustAnchors != null;
636
+ var hasSecret = opts.sharedSecret != null;
637
+
638
+ // M8: flavor<->credential coherence (the wrongIntegrity condition) is a config-tier throw.
639
+ if (isMac) {
640
+ if (hasSigCred) throw _err("cmp/bad-input", "a MAC-protected message takes opts.sharedSecret, not signerCert/trustAnchors");
641
+ // The shared secret must be NON-EMPTY (matching pki.cmp.build): an empty "" / zero-length secret has no
642
+ // entropy, so a peer could forge a matching PBMAC1 with no secret and be reported valid + trusted.
643
+ if (!_nonEmptySecret(opts.sharedSecret)) throw _err("cmp/bad-input", "a PBMAC1-protected message requires a non-empty opts.sharedSecret");
644
+ return _verifyMac(m, protectedPart, protectionAlg, protection, opts);
645
+ }
646
+ if (hasSecret) throw _err("cmp/bad-input", "a signature-protected message takes opts.signerCert/trustAnchors, not sharedSecret");
647
+ return _verifySignature(m, protectedPart, protectionAlg, protection, opts);
648
+ }
649
+
650
+ /**
651
+ * @primitive pki.cmp.verify
652
+ * @signature pki.cmp.verify(message, opts?) -> Promise<verdict>
653
+ * @since 0.3.26
654
+ * @status experimental
655
+ * @spec RFC 9810, RFC 9481, RFC 9579, RFC 9483
656
+ * @related pki.cmp.build, pki.schema.cmp.parse
657
+ * @defends cmp-unverified-protection (CWE-347), cmp-mac-timing (CWE-208)
658
+ *
659
+ * Verify the protection on an incoming RFC 9810 CMP `PKIMessage` -- the verify-inverse of
660
+ * `pki.cmp.build`. `message` is a raw DER `Buffer`, a PEM `CMP` string, or an already-parsed
661
+ * `pki.schema.cmp.parse` result (the protection is always recomputed from the parser-surfaced raw
662
+ * `headerBytes` / `bodyBytes`, so a mutated display field on a parsed object cannot desync the crypto).
663
+ * The protection flavor is read from the header `protectionAlg` alone: `id-PBMAC1` selects the MAC path,
664
+ * a signature `AlgorithmIdentifier` the signature path; an unprotected message, or a recognized legacy /
665
+ * KEM MAC algorithm (`id-PasswordBasedMac` / `id-DHBasedMac` / `id-KemBasedMac`), fails closed. On the
666
+ * signature path the authenticated header `sender` field MUST match the signer certificate subject (RFC 9483
667
+ * sec. 3.1), so a certificate the anchor trusts cannot sign under another party's sender name.
668
+ *
669
+ * Returns a verdict (never a bare boolean): `{ valid, trusted, protectionType, protectionAlg, signer,
670
+ * transactionID, senderNonce, recipNonce, header, body, code?, reason? }`. `valid` is whether the
671
+ * protection is cryptographically intact under the declared algorithm; `trusted` is whether a MAC secret
672
+ * matched or a signature signer certificate chained to a supplied trust anchor. A well-formed but
673
+ * unverifiable message is a `{ valid: false }` verdict carrying a `cmp/*` code, not a throw; only malformed
674
+ * input (a non-PKIMessage, a bad required opt, a flavor/credential mismatch) throws a typed `CmpError`.
675
+ *
676
+ * @opts
677
+ * - `sharedSecret` (string|Buffer) -- the PBMAC1 secret; REQUIRED for a MAC-protected message (UTF-8).
678
+ * - `signerCert` (Buffer|PEM) -- the expected signature signer certificate (else resolved from
679
+ * `extraCerts` by `senderKID` or, per RFC 9483 sec. 3.3, `extraCerts[0]`).
680
+ * - `trustAnchors` (Buffer|Buffer[]|PEM) -- when present the signer certificate is FULLY path-validated
681
+ * (RFC 5280 sec. 6.1 plus the `keyUsage.digitalSignature` gate) to report `trusted`; absent -> the
682
+ * verdict is crypto-only (`trusted: false`) and the signer certificate is surfaced for the caller to
683
+ * anchor. The signature verify never routes through build's self-check (which skips the EdDSA
684
+ * low-order-point gate) -- it uses the same engine `pki.crl.verify` / `pki.ocsp.verify` do.
685
+ * - `intermediates` (Buffer|Buffer[]|PEM) -- extra untrusted pool certificates for path building
686
+ * (`extraCerts` are added automatically, as untrusted pool material).
687
+ * - `time` (Date) -- the validity instant for path validation. Defaults to the current time (the message's
688
+ * self-asserted `messageTime` is NOT trusted for this); pass an explicit instant for historical verification.
689
+ * - `transactionID` (Buffer) -- opt-in: require `header.transactionID` to equal it (response-echo defense).
690
+ * - `expectRecipNonce` (Buffer) -- opt-in: require `header.recipNonce` to echo the sent sender nonce.
691
+ * - `revocationChecker` -- forwarded to `pki.path.validate` when chaining the signer certificate.
692
+ * - `maxIterations` (number) -- downward-only override of the PBKDF2 iteration cap.
693
+ *
694
+ * @example
695
+ * var v = await pki.cmp.verify(cmpDer, { signerCert: signerCertDer, trustAnchors: [certDer] });
696
+ * if (v.valid && v.trusted) console.log("the response protection is authentic and the signer is trusted");
697
+ */
698
+
699
+ module.exports = {
700
+ build: cmpBuild.build,
701
+ transfer: cmpBuild.transfer,
702
+ wellKnownUrl: cmpBuild.wellKnownUrl,
703
+ verify: verify,
704
+ setEngine: setEngine,
705
+ };