@blamejs/pki 0.8.6 → 0.8.9

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 (48) hide show
  1. package/CHANGELOG.md +143 -76
  2. package/README.md +4 -2
  3. package/index.js +11 -0
  4. package/lib/acme.js +15 -78
  5. package/lib/asn1-der.js +58 -6
  6. package/lib/cbor-det.js +11 -1
  7. package/lib/cmc-build.js +1 -1
  8. package/lib/cmc-verify.js +3 -3
  9. package/lib/cmp-session.js +1 -1
  10. package/lib/cms-sign.js +3 -3
  11. package/lib/cms-verify.js +5 -5
  12. package/lib/constants.js +11 -0
  13. package/lib/est.js +1 -1
  14. package/lib/framework-error.js +8 -0
  15. package/lib/guard-async.js +53 -2
  16. package/lib/guard-encoding.js +105 -1
  17. package/lib/guard-identifier.js +74 -1
  18. package/lib/guard-intrinsic.js +2 -0
  19. package/lib/guard-json.js +1 -6
  20. package/lib/guard-limits.js +40 -1
  21. package/lib/guard-list.js +25 -0
  22. package/lib/guard-name.js +134 -22
  23. package/lib/guard-text.js +33 -1
  24. package/lib/http-transport.js +39 -7
  25. package/lib/identity-match.js +561 -0
  26. package/lib/ip-utils.js +52 -1
  27. package/lib/key.js +250 -5
  28. package/lib/ocsp.js +125 -22
  29. package/lib/oid.js +1 -1
  30. package/lib/path-validate.js +199 -61
  31. package/lib/pkcs11-uri.js +646 -0
  32. package/lib/pkcs12-build.js +6 -1
  33. package/lib/revocation-fetch.js +772 -0
  34. package/lib/schema-all.js +124 -0
  35. package/lib/schema-attrcert.js +4 -2
  36. package/lib/schema-crmf.js +4 -2
  37. package/lib/schema-pkcs1.js +335 -0
  38. package/lib/schema-pkcs8.js +15 -0
  39. package/lib/schema-pkix.js +322 -40
  40. package/lib/schema-sec1.js +244 -0
  41. package/lib/schema-tsp.js +4 -2
  42. package/lib/schema-x509.js +4 -2
  43. package/lib/trust.js +2 -2
  44. package/lib/tsp-sign.js +1 -1
  45. package/lib/webauthn.js +4 -4
  46. package/lib/webcrypto.js +17 -5
  47. package/package.json +1 -1
  48. package/sbom.cdx.json +6 -6
@@ -134,7 +134,7 @@ function _pemifyAnchor(a) {
134
134
  return a;
135
135
  }
136
136
  function _pemifyAnchors(anchors) {
137
- var list = Array.isArray(anchors) ? anchors.map(_pemifyAnchor) : [_pemifyAnchor(anchors)];
137
+ var list = Array.isArray(anchors) ? guard.list.copyMap(anchors, _pemifyAnchor) : [_pemifyAnchor(anchors)];
138
138
  return list;
139
139
  }
140
140
 
@@ -366,7 +366,11 @@ function _validateProxy(request, defaults, E, C, url) {
366
366
  var originHost = _stripIpv6Brackets(url.hostname);
367
367
  var originAuthHost = nodeNet.isIP(originHost) === 6 ? "[" + originHost + "]" : originHost;
368
368
  var phost = _stripIpv6Brackets(purl.hostname);
369
- return { secure: secure, host: phost, port: purl.port ? +purl.port : (secure ? 443 : 80), originHost: originHost, originAuthority: originAuthHost + ":" + (url.port || 443), auth: auth, proxyTls: proxyTls };
369
+ /** @internal The CONNECT target is the origin's own authority, so an origin URL that states no
370
+ * port takes the default for the origin's scheme, not the proxy's. Defaulting it to 443 opens
371
+ * the tunnel to a port an `http:` origin does not listen on. */
372
+ var originPort = url.port || (url.protocol === "http:" ? 80 : 443);
373
+ return { secure: secure, host: phost, port: purl.port ? +purl.port : (secure ? 443 : 80), originHost: originHost, originAuthority: originAuthHost + ":" + originPort, auth: auth, proxyTls: proxyTls };
370
374
  }
371
375
 
372
376
  function _basicProxyHeader(auth) {
@@ -453,13 +457,30 @@ function httpsTransport(defaults) {
453
457
  var url;
454
458
  try { url = new URL(String(request.url)); }
455
459
  catch (e) { throw E(C("bad-url"), "the request URL did not parse: " + String(request.url), e); }
456
- if (url.protocol !== "https:") throw E(C("insecure-url"), "transport requires https (RFC 7030 sec. 3.3), got " + url.protocol);
460
+ /** @internal https is the floor, and `request.allowPlaintextHttp` is the one thing that moves
461
+ * it, per request and never as a default. It exists for the two objects whose integrity does
462
+ * not come from the transport: a CRL and an OCSP response are signed, their signatures are
463
+ * verified after the fetch, and an https-only revocation fetch needs the responder's own
464
+ * certificate validated first, which is the loop the plaintext form exists to break. Nothing
465
+ * else in this toolkit sets it. Every other guard stays: the address checks, the size cap, the
466
+ * timeout, and the refusal to follow a redirect, which is what would otherwise turn one
467
+ * plaintext hop into a downgrade of the next. */
468
+ var plaintextAllowed = request.allowPlaintextHttp === true;
469
+ if (url.protocol !== "https:" && !(plaintextAllowed && url.protocol === "http:")) {
470
+ throw E(C("insecure-url"), "transport requires https (RFC 7030 sec. 3.3), got " + url.protocol);
471
+ }
457
472
 
458
473
  var reqTls = request.tls || {};
459
474
  var anchors = reqTls.anchors !== undefined ? reqTls.anchors : tlsDefaults.anchors;
460
475
  var useSystem = (reqTls.useSystemStore !== undefined ? reqTls.useSystemStore : tlsDefaults.useSystemStore) === true;
461
476
  var hasAnchors = anchors !== undefined && anchors !== null && !(Array.isArray(anchors) && anchors.length === 0);
462
- if (!hasAnchors && !useSystem) throw E(C("no-trust-anchors"), "no explicit trust anchor and useSystemStore not set -- refusing an unpinned server (RFC 7030 sec. 3.6)");
477
+ /** @internal A trust anchor is what makes an https server identifiable, so a request with
478
+ * neither an anchor nor the system store is refused rather than left unpinned. A plaintext hop
479
+ * negotiates no certificate, so there is nothing for an anchor to pin; what protects the object
480
+ * fetched over one is its own signature, checked after the fetch by the verb that asked. */
481
+ if (url.protocol === "https:" && !hasAnchors && !useSystem) {
482
+ throw E(C("no-trust-anchors"), "no explicit trust anchor and useSystemStore not set -- refusing an unpinned server (RFC 7030 sec. 3.6)");
483
+ }
463
484
 
464
485
  var timeout = _budget(request.timeout !== undefined ? request.timeout : defaults.timeout, "timeout", DEFAULT_TIMEOUT, MAX_TIMEOUT, E, C("bad-input"));
465
486
  var maxBytes = _budget(request.maxResponseBytes !== undefined ? request.maxResponseBytes : defaults.maxResponseBytes, "maxResponseBytes", constants.LIMITS.HTTP_MAX_RESPONSE_BYTES, constants.LIMITS.HTTP_MAX_RESPONSE_BYTES, E, C("bad-input"));
@@ -481,10 +502,11 @@ function httpsTransport(defaults) {
481
502
  }
482
503
 
483
504
  var host = _stripIpv6Brackets(url.hostname);
505
+ var plaintext = url.protocol === "http:";
484
506
  var options = {
485
507
  method: request.method || "GET",
486
508
  hostname: host,
487
- port: url.port || 443,
509
+ port: url.port || (plaintext ? 80 : 443),
488
510
  path: url.pathname + url.search,
489
511
  headers: reqHeaders,
490
512
  minVersion: minVersion,
@@ -528,7 +550,7 @@ function httpsTransport(defaults) {
528
550
  options.lookup = _guardedLookup;
529
551
  }
530
552
 
531
- return { options: options, timeout: timeout, maxBytes: maxBytes, body: body, proxy: proxyInfo, blockPrivate: blockPrivate, callerCsi: callerCsi };
553
+ return { options: options, timeout: timeout, maxBytes: maxBytes, body: body, proxy: proxyInfo, blockPrivate: blockPrivate, callerCsi: callerCsi, plaintext: plaintext };
532
554
  }
533
555
 
534
556
  var _transportFn = function transport(request) {
@@ -561,7 +583,10 @@ function httpsTransport(defaults) {
561
583
 
562
584
  function issue(options) {
563
585
  try {
564
- req = nodeHttps.request(options, function (res) {
586
+ /** @internal A plaintext request goes through `node:http`, which has no TLS options to
587
+ * ignore; `prep.plaintext` is set only where the scheme check let `http:` through. */
588
+ var client = prep.plaintext ? nodeHttp : nodeHttps;
589
+ req = client.request(options, function (res) {
565
590
  var tlsInfo = _tlsInfo(res.socket);
566
591
  var declared = parseInt((res.headers || {})["content-length"], 10);
567
592
  if (Number.isFinite(declared) && declared > prep.maxBytes) { fail(C("response-too-large"), "the declared content-length " + declared + " exceeds the " + prep.maxBytes + "-byte cap (RFC 7030 sec. 6)"); return; }
@@ -673,6 +698,13 @@ function httpsTransport(defaults) {
673
698
  creq = null;
674
699
  socket.on("error", function (e) { fail(_classifyError(e, C), "the tunnel socket failed: " + ((e && e.message) || String(e)), e); });
675
700
  var idHost = prep.options.servername || pinfo.originHost;
701
+ /** @internal A plaintext origin speaks HTTP over the tunnel the CONNECT opened, so the
702
+ * connection the request is issued on is that socket. Handing back a TLS wrapper would
703
+ * start a handshake against a server that does not speak TLS. */
704
+ if (prep.plaintext) {
705
+ issue(Object.assign({}, prep.options, { createConnection: function () { return socket; } }));
706
+ return;
707
+ }
676
708
  issue(Object.assign({}, prep.options, {
677
709
  createConnection: function () {
678
710
  return nodeTls.connect({
@@ -0,0 +1,561 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.identity
6
+ * @since 0.8.8
7
+ * @spec RFC 9525
8
+ * @nav Path validation
9
+ * @title Service identity (RFC 9525)
10
+ *
11
+ * @intro
12
+ * Service identity: whether a certificate presents a name the client was trying to reach.
13
+ * RFC 9525 calls the names the client builds reference identities and the names the
14
+ * certificate carries presented identifiers, and the check is whether any of the first
15
+ * matches any of the second.
16
+ *
17
+ * This is a separate answer from path validation, and RFC 9525 sec. 1.2 says so: it does not
18
+ * supersede certificate validation, and an application needs both. `pki.path.validate` takes
19
+ * `opts.identity` to run the two together and report each.
20
+ *
21
+ * @card
22
+ * | Reference form | Reads | Compared against |
23
+ * |---|---|---|
24
+ * | `"www.example.com"` | DNS-ID | `dNSName` entries, label by label |
25
+ * | `"192.0.2.1"`, `"2001:db8::1"`, a 4- or 16-octet BufferSource | IP-ID | `iPAddress` entries, octet for octet |
26
+ * | `{ type: "srv", service: "_imap", value: "example.com" }` | SRV-ID | an `otherName` carrying an RFC 4985 SRVName |
27
+ * | `{ type: "uri", scheme: "https", value: "www.example.com" }` | URI-ID | the scheme and host of a `uniformResourceIdentifier` |
28
+ */
29
+
30
+ var asn1 = require("./asn1-der");
31
+ var oid = require("./oid");
32
+ var constants = require("./constants");
33
+ var guard = require("./guard-all");
34
+ var intrinsic = require("./guard-intrinsic");
35
+ var frameworkError = require("./framework-error");
36
+ var pkix = require("./schema-pkix");
37
+ var ip = require("./ip-utils");
38
+ var x509 = require("./schema-x509");
39
+
40
+ var IdentityError = frameworkError.IdentityError;
41
+ function E(code, message, cause) { return new IdentityError(code, message, cause); }
42
+
43
+ var NS = pkix.makeNS("identity", IdentityError, oid);
44
+ var OID_SAN = oid.byName("subjectAltName");
45
+ var OID_DNS_SRV = oid.byName("dnsSRV");
46
+ var SAN_DECODER = pkix.certExtensionDecoders(NS).byOid[OID_SAN];
47
+
48
+ var _append = guard.list.append;
49
+ var _lowerAscii = guard.name.lowerAscii;
50
+ var _stripTrailingDot = guard.name.stripTrailingDot;
51
+ var _hostLabels = guard.name.hostLabels;
52
+ var _isFqdnHost = guard.name.isFqdnHost;
53
+ var _uriParts = guard.name.uriParts;
54
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
55
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
56
+ var _indexOf = intrinsic.uncurry(String.prototype.indexOf);
57
+ var _isArray = intrinsic.isArray;
58
+
59
+ var MATCH_OPTS = intrinsic.assign(intrinsic.create(null), { wildcards: 1 });
60
+ var REFERENCE_KEYS = intrinsic.assign(intrinsic.create(null), { type: 1, value: 1, service: 1, scheme: 1 });
61
+
62
+ var MAX_LABEL = 63;
63
+ var MAX_HOST = 253;
64
+
65
+ /** @internal Every character of a reference identity is printable ASCII. A byte above 0x7E cannot
66
+ * be compared, because RFC 9525 sec. 6.3 requires a U-label to be converted to an A-label first and
67
+ * this toolkit carries no IDNA implementation; a byte below 0x20 is the CVE-2009-2408 shape the
68
+ * presented side already refuses at decode. The two are separate codes because one is a gap in what
69
+ * the toolkit can do and the other is malformed input. */
70
+ function _assertAsciiReference(s, what) {
71
+ guard.name.assertNoControlBytes(s, E, "identity/bad-reference", what);
72
+ for (var i = 0; i < s.length; i++) {
73
+ var c = _charCodeAt(s, i);
74
+ if (c === 0x7f) throw E("identity/bad-reference", what + " carries a DELETE byte at offset " + i);
75
+ if (c > 0x7e) {
76
+ throw E("identity/unsupported-reference", what + " carries a character outside ASCII at offset " + i +
77
+ ". RFC 9525 sec. 6.3 requires a U-label to be converted to an A-label before it is compared, and this " +
78
+ "toolkit carries no IDNA implementation: pass the name already converted to A-labels");
79
+ }
80
+ }
81
+ }
82
+
83
+ /** @internal A reference identity carries no wildcard. RFC 9525 sec. 6.3 covers a wildcard in a
84
+ * presented identifier only, so one on this side is a caller mistake and not a name. */
85
+ function _assertNoWildcard(s, what) {
86
+ if (_indexOf(s, "*") !== -1) {
87
+ throw E("identity/bad-reference", what + " carries a wildcard character. RFC 9525 sec. 6.3 covers " +
88
+ "wildcards in a presented identifier only, never in a reference identity");
89
+ }
90
+ }
91
+
92
+ /** @internal The domain of a reference identity, normalized the way both sides are normalized: one
93
+ * trailing dot stripped, then held to the label rules so an empty label or a label past 63
94
+ * characters is a caller mistake, not a name that can never match. */
95
+ function _referenceDomain(v, what) {
96
+ if (typeof v !== "string" || v === "") throw E("identity/bad-reference", what + " must be a non-empty string");
97
+ _assertAsciiReference(v, what);
98
+ _assertNoWildcard(v, what);
99
+ var d = _stripTrailingDot(v);
100
+ if (d === "" || d.length > MAX_HOST) {
101
+ throw E("identity/bad-reference", what + " must be a domain name of 1 to " + MAX_HOST + " characters");
102
+ }
103
+ var labels = _hostLabels(d);
104
+ for (var i = 0; i < labels.length; i++) {
105
+ if (labels[i].length === 0 || labels[i].length > MAX_LABEL) {
106
+ throw E("identity/bad-reference", what + " has a label at position " + i + " that is empty or longer than " + MAX_LABEL + " characters");
107
+ }
108
+ }
109
+ return d;
110
+ }
111
+
112
+ /** @internal One reference identity, classified once. RFC 9525 sec. 3 has an IPv4 address read as
113
+ * an IP-ID and not as a name, and sec. 7.4 has the classification carried forward instead of
114
+ * re-derived, so a value that is an address is never also tried as a domain. A record form states
115
+ * its own type, and carries its service type and its domain as ONE value, which is what makes
116
+ * sec. 6.5's prohibition on combining a service from one reference with a domain from another
117
+ * structural instead of a check that could be forgotten. */
118
+ function _reference(r, index) {
119
+ var what = "reference identity [" + index + "]";
120
+ if (guard.bytes.isByteSource(r)) {
121
+ var octets = guard.bytes.snapshotSource(r, IdentityError, "identity/bad-reference", what);
122
+ if (octets.length !== 4 && octets.length !== 16) {
123
+ throw E("identity/bad-reference", what + " is a " + octets.length + "-octet address. RFC 9525 sec. 2 " +
124
+ "has an IP-ID carry exactly 4 octets for IPv4 or 16 for IPv6; an address plus a mask is a name " +
125
+ "constraint rather than a reference identity");
126
+ }
127
+ return { kind: "ip", octets: octets, service: null, text: ip.textFromOctets(octets),
128
+ input: _reported({ type: "ip", value: ip.textFromOctets(octets), octets: octets }) };
129
+ }
130
+ if (typeof r === "string") {
131
+ if (r === "") throw E("identity/bad-reference", what + " must be a non-empty string");
132
+ _assertAsciiReference(r, what);
133
+ if (ip.isIPv4(r) || ip.isIpLiteral(r)) {
134
+ var packed = ip.packIpLiteral(r);
135
+ if (packed === null) throw E("identity/bad-reference", what + " is not a textual IP address this toolkit reads");
136
+ return { kind: "ip", octets: packed, service: null, text: r, input: r };
137
+ }
138
+ return { kind: "dns", domain: _referenceDomain(r, what), service: null, input: r };
139
+ }
140
+ if (!r || typeof r !== "object") {
141
+ throw E("identity/bad-reference", what + " must be a domain name, a textual IP address, a 4- or 16-octet " +
142
+ "BufferSource, or a { type, value } record");
143
+ }
144
+ guard.identifier.assertKnownKeys(r, REFERENCE_KEYS, E, "identity/bad-reference",
145
+ what + " has an unknown key (a reference record is { type, value } plus `service` for srv or `scheme` for uri): ");
146
+ if (r.type === "dns") {
147
+ var dnsDomain = _referenceDomain(r.value, what + " value");
148
+ return { kind: "dns", domain: dnsDomain, service: null, input: _reported({ type: "dns", value: dnsDomain }) };
149
+ }
150
+ if (r.type === "ip") {
151
+ /** @internal The record STATES the type, so the type decides what the value may be.
152
+ * Classifying the value again would let a name inside an `ip` record be compared as a
153
+ * name, which is RFC 9525 sec. 3's consistent classification read backwards: what the
154
+ * caller asked for is an address, so anything else is a caller mistake. */
155
+ var addr = _reference(r.value, index);
156
+ if (addr.kind !== "ip") {
157
+ throw E("identity/bad-reference", what + " of type ip carries " + guard.text.showValue(r.value) +
158
+ ", which is no address. RFC 9525 sec. 3 has an identifier classified once, so a name here is " +
159
+ "a mistake and is never compared as a DNS-ID");
160
+ }
161
+ return { kind: "ip", octets: addr.octets, service: null, text: addr.text,
162
+ input: _reported({ type: "ip", value: addr.text, octets: addr.octets }) };
163
+ }
164
+ if (r.type === "srv") {
165
+ if (typeof r.service !== "string" || r.service === "") throw E("identity/bad-reference", what + " of type srv needs a `service`");
166
+ _assertAsciiReference(r.service, what + " service");
167
+ if (_charCodeAt(r.service, 0) !== 0x5f) {
168
+ throw E("identity/bad-reference", what + " service must open with an underscore. RFC 4985 sec. 2 makes the " +
169
+ "underscore part of the service name rather than a separator, so `_imap` is the value and `imap` is not");
170
+ }
171
+ if (r.service.length < 2) throw E("identity/bad-reference", what + " service must name a service after its underscore");
172
+ var srvDomain = _referenceDomain(r.value, what + " value");
173
+ var srvService = _lowerAscii(r.service);
174
+ return { kind: "srv", domain: srvDomain, service: srvService,
175
+ input: _reported({ type: "srv", service: srvService, value: srvDomain }) };
176
+ }
177
+ if (r.type === "uri") {
178
+ if (typeof r.scheme !== "string" || r.scheme === "") throw E("identity/bad-reference", what + " of type uri needs a `scheme`");
179
+ _assertAsciiReference(r.scheme, what + " scheme");
180
+ if (_indexOf(r.scheme, ":") !== -1) {
181
+ throw E("identity/bad-reference", what + " scheme carries a colon. RFC 3986 sec. 3.1 makes the colon a " +
182
+ "separator rather than part of the scheme");
183
+ }
184
+ var uriDomain = _referenceDomain(r.value, what + " value");
185
+ var uriScheme = _lowerAscii(r.scheme);
186
+ return { kind: "uri", domain: uriDomain, service: uriScheme,
187
+ input: _reported({ type: "uri", scheme: uriScheme, value: uriDomain }) };
188
+ }
189
+ throw E("identity/unsupported-reference", what + " names the type " + guard.text.showValue(r.type) +
190
+ ". The types this verb reads are dns, ip, srv and uri (RFC 9525 sec. 1.3)");
191
+ }
192
+
193
+ /** @internal The caller's reference list, read once and bounded at the door. An empty list is
194
+ * refused, not treated as "match anything", because a check that constrains nothing reads to
195
+ * a caller as a policy in force. */
196
+ /** @internal What the verdict hands back as the matched reference. RFC 9525 sec. 6.6 has the
197
+ * caller use it as the validated identity of the service, so it is built from the values that were
198
+ * checked and frozen, rather than being the caller's own object: a record the caller rewrites
199
+ * afterwards would otherwise name an identity nothing compared. A string is already a value; bytes
200
+ * are the copy taken at the door. */
201
+ function _reported(record) {
202
+ var out = intrinsic.create(null), k;
203
+ for (k in record) {
204
+ if (intrinsic.hasOwn(record, k) && record[k] !== undefined) out[k] = record[k];
205
+ }
206
+ return intrinsic.freeze(out);
207
+ }
208
+
209
+ var _SNAPSHOTS = new WeakSet();
210
+
211
+ /** @internal Whether this list is one `snapshotReferences` produced. The record is kept off the
212
+ * value, so a caller cannot make an arbitrary array pass by writing a field on it. */
213
+ function _isSnapshot(v) {
214
+ return _isArray(v) && _SNAPSHOTS.has(v);
215
+ }
216
+
217
+ /** @internal The reference identities, read and copied in one pass, so a caller that mutates its
218
+ * array or its records afterwards changes nothing the verdict answers for. A verb with awaits
219
+ * between the call and the comparison calls this in its synchronous prologue and passes the
220
+ * result to `match`, which is what makes the policy the verdict answers for the policy the call
221
+ * was made with. */
222
+ function snapshotReferences(references) {
223
+ if (_isSnapshot(references)) return references;
224
+ var parsed = _references(references);
225
+ /** @internal Every part of a settled reference is frozen, the list and each record alike, so
226
+ * what the comparison reads is what the door checked. The octets are held as a frozen list of
227
+ * numbers rather than a Buffer, because a Buffer's elements stay writable however the object
228
+ * around them is sealed, and an address rewritten after the door would be compared against
229
+ * without having been checked. */
230
+ for (var i = 0; i < parsed.length; i++) {
231
+ if (parsed[i].octets) parsed[i].octets = _frozenOctets(parsed[i].octets);
232
+ intrinsic.freeze(parsed[i]);
233
+ }
234
+ intrinsic.freeze(parsed);
235
+ _SNAPSHOTS.add(parsed);
236
+ return parsed;
237
+ }
238
+
239
+ function _frozenOctets(buf) {
240
+ var out = [];
241
+ for (var i = 0; i < buf.length; i++) _append(out, buf[i]);
242
+ return intrinsic.freeze(out);
243
+ }
244
+
245
+ function _references(references) {
246
+ var list = _isArray(references) ? references : [references];
247
+ if (list.length === 0) {
248
+ throw E("identity/bad-input", "the reference identity list is empty. A list that constrains nothing would " +
249
+ "read as an identity check that passed; pass at least one name, address or record");
250
+ }
251
+ if (list.length > constants.LIMITS.IDENTITY_MAX_REFERENCES) {
252
+ throw E("identity/too-many-references", "the reference identity list holds " + list.length +
253
+ " entries and at most " + constants.LIMITS.IDENTITY_MAX_REFERENCES + " are read");
254
+ }
255
+ var out = [];
256
+ for (var i = 0; i < list.length; i++) _append(out, _reference(list[i], i));
257
+ return out;
258
+ }
259
+
260
+ /** @internal Whether a presented `dNSName` is shaped like a name at all. This is deliberately not
261
+ * `guard.name.isFqdnHost` and not the certificate-quality test `lint` applies: RFC 9525 says nothing
262
+ * about an underscore label, and a name this check refuses is a name that can never match rather
263
+ * than a certificate fault. Only the structure the label comparison needs is required. */
264
+ function _presentedNameShape(s) {
265
+ if (s.length === 0 || s.length > MAX_HOST) return false;
266
+ var labels = _hostLabels(s);
267
+ for (var i = 0; i < labels.length; i++) {
268
+ if (labels[i].length === 0 || labels[i].length > MAX_LABEL) return false;
269
+ }
270
+ return true;
271
+ }
272
+
273
+ /** @internal Whether a presented name carries a wildcard RFC 9525 sec. 6.3 admits: exactly one
274
+ * wildcard character, and that character the complete content of the left-most label. A name
275
+ * carrying a wildcard that fails either test is invalid and is IGNORED rather than refused, which
276
+ * sec. 6.3 states and which is why this answers three ways. */
277
+ function _wildcardShape(labels) {
278
+ var total = 0, i, j;
279
+ for (i = 0; i < labels.length; i++) {
280
+ for (j = 0; j < labels[i].length; j++) if (_charCodeAt(labels[i], j) === 0x2a) total += 1;
281
+ }
282
+ if (total === 0) return "none";
283
+ if (total > 1) return "invalid";
284
+ if (labels.length < 2) return "invalid";
285
+ return labels[0] === "*" ? "valid" : "invalid";
286
+ }
287
+
288
+ /** @internal Two names compared label by label (RFC 9525 sec. 6.3): equal label counts, every label
289
+ * equal under an ASCII-only case fold. A wildcard in the left-most presented label matches exactly
290
+ * one reference label, which is where this differs from the suffix relation a name constraint uses. */
291
+ function _dnsMatch(presented, referenceDomain, wildcardsAllowed) {
292
+ var p = _stripTrailingDot(presented);
293
+ if (!_presentedNameShape(p)) return { disposition: "ignored", reason: "the presented name is not shaped like a domain name" };
294
+ var pl = _hostLabels(p), rl = _hostLabels(referenceDomain);
295
+ var shape = _wildcardShape(pl);
296
+ if (shape === "invalid") {
297
+ return { disposition: "ignored", reason: "the presented name carries a wildcard that is not the whole of its left-most label, or carries more than one (RFC 9525 sec. 6.3)" };
298
+ }
299
+ if (shape === "valid") {
300
+ if (!wildcardsAllowed) return { disposition: "ignored", reason: "wildcard matching is off for this call" };
301
+ if (pl.length !== rl.length) return { disposition: "no-match" };
302
+ for (var w = 1; w < pl.length; w++) {
303
+ if (_lowerAscii(pl[w]) !== _lowerAscii(rl[w])) return { disposition: "no-match" };
304
+ }
305
+ return rl[0].length === 0 ? { disposition: "no-match" } : { disposition: "match" };
306
+ }
307
+ if (pl.length !== rl.length) return { disposition: "no-match" };
308
+ for (var i = 0; i < pl.length; i++) {
309
+ if (_lowerAscii(pl[i]) !== _lowerAscii(rl[i])) return { disposition: "no-match" };
310
+ }
311
+ return { disposition: "match" };
312
+ }
313
+
314
+ /** @internal RFC 9525 sec. 6.4 compares an IP-ID octet for octet. A length difference is a
315
+ * non-match, not a truncated comparison, because length is the only thing that tells the two
316
+ * address versions apart (RFC 9110 sec. 4.3.5). */
317
+ function _ipMatch(refOctets, entryOctets) {
318
+ if (refOctets.length !== entryOctets.length) return false;
319
+ var same = 0;
320
+ for (var i = 0; i < refOctets.length; i++) same |= refOctets[i] ^ entryOctets[i];
321
+ return same === 0;
322
+ }
323
+
324
+ /** @internal The `_Service.Name` an RFC 4985 SRVName carries. The value arrives as the inner
325
+ * element's own DER, so it is decoded here with its own universal-tag, primitive and printable
326
+ * checks, not trusted because the OID said what it would be. */
327
+ function _srvName(entryValue) {
328
+ var node;
329
+ try { node = asn1.decode(entryValue.valueBytes); }
330
+ catch (e) { return { reason: "the SRVName value is not valid DER", cause: e }; }
331
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.IA5_STRING) {
332
+ return { reason: "the SRVName value is not an IA5String (RFC 4985 sec. 2)" };
333
+ }
334
+ if (node.constructed) return { reason: "the SRVName value is a constructed IA5String" };
335
+ var content = node.content;
336
+ if (!content || content.length === 0) return { reason: "the SRVName value is empty, and RFC 4985 sec. 2 writes SIZE (1..MAX)" };
337
+ try { guard.name.assertPrintableIa5(content, E, "identity/bad-reference", "the SRVName value"); }
338
+ catch (e) { return { reason: "the SRVName value carries a byte outside printable ASCII", cause: e }; }
339
+ var text = intrinsic.bufToString(content, "latin1");
340
+ if (_charCodeAt(text, 0) !== 0x5f) {
341
+ return { reason: "the SRVName does not open with an underscore, so it names no service (RFC 4985 sec. 2)" };
342
+ }
343
+ var dot = _indexOf(text, ".");
344
+ if (dot < 2 || dot === text.length - 1) {
345
+ return { reason: "the SRVName is not _Service.Name with both parts present (RFC 4985 sec. 2)" };
346
+ }
347
+ return { service: _strSlice(text, 0, dot), domain: _strSlice(text, dot + 1) };
348
+ }
349
+
350
+ /** @internal The scheme and host a URI-ID presents. RFC 9525 sec. 7.2 uses an entry only when it
351
+ * carries both, and requires an entry missing either to be IGNORED with the search continuing.
352
+ * Sec. 6.2 narrows the host to the `reg-name` rule, which excludes an IP literal, so a bracketed
353
+ * address is not a URI-ID. A percent-escape is not decoded: decoding one here would reintroduce the
354
+ * byte the IA5 assertion refused at the certificate's door. */
355
+ function _uriId(text) {
356
+ var parts = _uriParts(text);
357
+ if (parts === null) return { reason: "the presented URI has no scheme (RFC 3986 sec. 3.1)" };
358
+ if (!parts.hasAuthority || parts.host === null) {
359
+ return { reason: "the presented URI carries no host, and RFC 9525 sec. 7.2 requires an entry without both a scheme and a host to be ignored" };
360
+ }
361
+ if (parts.bracketed) {
362
+ return { reason: "the presented URI names an IP literal, and RFC 9525 sec. 6.2 narrows a URI-ID host to the reg-name rule" };
363
+ }
364
+ if (_indexOf(parts.host, "%") !== -1) {
365
+ return { reason: "the presented URI host carries a percent-escape, which is not decoded before comparison" };
366
+ }
367
+ /** @internal A wildcard belongs to a presented identifier (sec. 6.3) and not to one entry
368
+ * form, so a URI-ID host carrying one reaches the same comparison a `dNSName` does. The
369
+ * host is still narrowed to a reg-name that is not an address, which is what sec. 6.2 asks
370
+ * of it, so the wildcard label is set aside for that test and the rest is held to it. */
371
+ var bare = _charCodeAt(parts.host, 0) === 0x2a && _charCodeAt(parts.host, 1) === 0x2e
372
+ ? _strSlice(parts.host, 2) : parts.host;
373
+ if (!_isFqdnHost(bare)) {
374
+ return { reason: "the presented URI host is not a domain name (RFC 9525 sec. 6.2 narrows it to the reg-name rule, which excludes an address)" };
375
+ }
376
+ return { scheme: _lowerAscii(parts.scheme), domain: parts.host };
377
+ }
378
+
379
+ /** @internal The identifiers a certificate presents, read from subjectAltName and from nowhere
380
+ * else. RFC 9525 sec. 2 forbids the subject commonName and every other RDN as a source of identity,
381
+ * so this reads the extension and never the subject. */
382
+ function _presented(cert) {
383
+ var ext = null;
384
+ for (var i = 0; i < cert.extensions.length; i++) {
385
+ if (cert.extensions[i].oid === OID_SAN) { ext = cert.extensions[i]; break; }
386
+ }
387
+ if (ext === null) return [];
388
+ var decoded = SAN_DECODER(ext.value);
389
+ var names = decoded.names || [];
390
+ if (names.length > constants.LIMITS.SAN_MAX_ENTRIES) {
391
+ throw E("identity/too-many-names", "the certificate presents " + names.length +
392
+ " subjectAltName entries and at most " + constants.LIMITS.SAN_MAX_ENTRIES + " are read");
393
+ }
394
+ return names;
395
+ }
396
+
397
+ function _entryLabel(entry) {
398
+ if (entry.tagNumber === 2) return "dNSName " + guard.text.showValue(entry.value);
399
+ if (entry.tagNumber === 6) return "uniformResourceIdentifier " + guard.text.showValue(entry.value);
400
+ if (entry.tagNumber === 7) return "iPAddress " + guard.text.showValue(ip.textFromOctets(entry.value));
401
+ if (entry.tagNumber === 0) return "otherName " + guard.text.showValue(entry.value && entry.value.typeId);
402
+ return "GeneralName [" + entry.tagNumber + "]";
403
+ }
404
+
405
+ /** @internal One presented entry against one reference identity. The dispatch is on the GeneralName
406
+ * tag the decoder already assigned, which is total over the CHOICE, and each arm answers `match`,
407
+ * `no-match`, or `ignored` with the reason it could not be read. A reference that names a service
408
+ * type carries its domain with it, so no arm can pair a service from one reference with a domain
409
+ * from another. */
410
+ function _compare(ref, entry, wildcardsAllowed) {
411
+ var t = entry.tagNumber;
412
+ if (ref.kind === "ip") {
413
+ if (t !== 7) return { disposition: "no-match" };
414
+ return { disposition: _ipMatch(ref.octets, entry.value) ? "match" : "no-match" };
415
+ }
416
+ if (ref.kind === "dns") {
417
+ if (t !== 2) return { disposition: "no-match" };
418
+ return _dnsMatch(entry.value, ref.domain, wildcardsAllowed);
419
+ }
420
+ if (ref.kind === "srv") {
421
+ if (t !== 0) return { disposition: "no-match" };
422
+ if (!entry.value || entry.value.typeId !== OID_DNS_SRV) return { disposition: "no-match" };
423
+ var srv = _srvName(entry.value);
424
+ if (srv.reason) return { disposition: "ignored", reason: srv.reason };
425
+ if (_lowerAscii(srv.service) !== ref.service) return { disposition: "no-match" };
426
+ return _dnsMatch(srv.domain, ref.domain, wildcardsAllowed);
427
+ }
428
+ if (t !== 6) return { disposition: "no-match" };
429
+ var uriId = _uriId(entry.value);
430
+ if (uriId.reason) return { disposition: "ignored", reason: uriId.reason };
431
+ if (uriId.scheme !== ref.service) return { disposition: "no-match" };
432
+ return _dnsMatch(uriId.domain, ref.domain, wildcardsAllowed);
433
+ }
434
+
435
+ /** @internal The search RFC 9525 sec. 6.2 describes: every reference against every presented
436
+ * identifier, stopping at the first match, and failing only once the cross product is exhausted.
437
+ * One counter bounds the pairs, not the two lists independently, because the work is the
438
+ * product. */
439
+ function _search(refs, entries, wildcardsAllowed) {
440
+ var pairs = guard.limits.counter(constants.LIMITS.SAN_MAX_ENTRIES * constants.LIMITS.IDENTITY_MAX_REFERENCES,
441
+ E, "identity/too-many-names", "identity comparison");
442
+ var ignored = [], seenIgnored = intrinsic.create(null);
443
+ for (var r = 0; r < refs.length; r++) {
444
+ for (var e = 0; e < entries.length; e++) {
445
+ pairs.tick();
446
+ var got = _compare(refs[r], entries[e], wildcardsAllowed);
447
+ if (got.disposition === "match") {
448
+ return { matched: true, matchedReference: refs[r].input, matchedEntry: _entryLabel(entries[e]),
449
+ ignored: ignored, reason: null };
450
+ }
451
+ if (got.disposition === "ignored") {
452
+ var key = e + "|" + got.reason;
453
+ if (!intrinsic.hasOwn(seenIgnored, key)) {
454
+ seenIgnored[key] = 1;
455
+ _append(ignored, { entry: _entryLabel(entries[e]), reason: got.reason });
456
+ }
457
+ }
458
+ }
459
+ }
460
+ return { matched: false, matchedReference: null, matchedEntry: null, ignored: ignored,
461
+ reason: entries.length === 0
462
+ ? "the certificate presents no subjectAltName entry, and RFC 9525 sec. 2 forbids reading an identity from the subject"
463
+ : "no presented identifier matched a reference identity" };
464
+ }
465
+
466
+ /**
467
+ * @primitive pki.identity.match
468
+ * @signature pki.identity.match(cert, references, opts?) -> verdict
469
+ * @since 0.8.8
470
+ * @status stable
471
+ * @spec RFC 9525, RFC 4985
472
+ * @related pki.path.validate, pki.schema.x509.parse
473
+ *
474
+ * Check a certificate against the names a client expected to reach, which RFC 9525 calls
475
+ * reference identities. This answers a different question from path validation and stands in
476
+ * for none of it: a certificate that matches here may still chain to nothing, and
477
+ * `pki.path.validate` is what says whether it chains.
478
+ *
479
+ * `references` is one reference identity or an array of them, and the caller builds that list
480
+ * from what it was trying to reach rather than from the certificate (sec. 6.1.1). Four forms
481
+ * are read. A domain name, `"www.example.com"`, is a DNS-ID. A textual IP address,
482
+ * `"192.0.2.1"` or `"2001:db8::1"`, or a 4- or 16-octet `BufferSource`, is an IP-ID. A record
483
+ * `{ type: "srv", service: "_imap", value: "example.com" }` is an SRV-ID, where the leading
484
+ * underscore is part of the service name (RFC 4985 sec. 2). A record
485
+ * `{ type: "uri", scheme: "https", value: "www.example.com" }` is a URI-ID. A record carries
486
+ * its service type and its domain together, so no comparison can pair the service of one
487
+ * reference with the domain of another (sec. 6.5).
488
+ *
489
+ * The subject distinguished name is never a source of identity. The `commonName` is not read
490
+ * under any option or fallback, including when the certificate carries no subjectAltName at
491
+ * all, and neither is any other RDN (sec. 2, RFC 9110 sec. 4.3.4). A certificate presenting
492
+ * nothing this check reads fails with `reason` naming that, never with an absent field.
493
+ *
494
+ * An IP-ID is compared octet for octet against an `iPAddress` entry and never against a
495
+ * `dNSName` carrying the same address as text, and no prefix or mask applies (sec. 6.2,
496
+ * sec. 6.4). `::ffff:192.0.2.1` and `192.0.2.1` pack to sixteen and four octets, so under that
497
+ * comparison they do not match.
498
+ *
499
+ * A name is compared label by label under an ASCII-only case fold, with one trailing dot
500
+ * normalized on both sides. An A-label is compared as the ASCII it is and is not decoded
501
+ * (sec. 7.3), so a reference identity carrying a character outside ASCII is refused with
502
+ * `identity/unsupported-reference`: RFC 9525 sec. 6.3 requires a U-label to be converted first
503
+ * and this toolkit carries no IDNA implementation. Pass a name already in A-labels.
504
+ *
505
+ * Wildcards are ON, and `opts.wildcards: false` turns them off, which sec. 3 requires a
506
+ * specification to state either way. A wildcard matches when it is the complete content of the
507
+ * left-most label and there is exactly one of them, and it reaches exactly one label:
508
+ * `*.example.com` matches `foo.example.com`, and matches neither `example.com` nor
509
+ * `a.b.example.com`. A presented name carrying a wildcard that fails those rules is ignored
510
+ * and the search continues, which is what sec. 6.3 directs; it does not refuse the
511
+ * certificate. A wildcard in a reference identity is a caller mistake and is refused.
512
+ *
513
+ * On a match the verdict's `matchedReference` is what sec. 6.6 has the caller use as the
514
+ * validated identity of the service. A reference given as a string comes back as that string; a
515
+ * record or a `BufferSource` comes back as a frozen `{ type, value, ... }` built from the values
516
+ * that were compared, with an address in its textual form, so what the verdict names is the
517
+ * identity that was checked rather than an object the caller can still write to.
518
+ *
519
+ * Protection against a wildcard that spans an administrative boundary, such as `*.co.uk`, is
520
+ * out of scope here as it is in sec. 7.1: this toolkit ships no public-suffix list.
521
+ *
522
+ * A name constraint and an identity check read the same subjectAltName entries to answer
523
+ * different questions, and neither is evidence for the other. A `dNSName` name constraint on
524
+ * an issuer says nothing about whether an SRV-ID or a URI-ID in a leaf is constrained
525
+ * (sec. 7.6); a client wanting that correspondence layers it on top of both.
526
+ *
527
+ * @opts wildcards Whether a presented identifier may carry a wildcard (RFC 9525 sec. 6.3),
528
+ * default `true`; `false` puts every wildcard entry in `ignored`.
529
+ * @example
530
+ * var pair = await pki.key.generate("Ed25519");
531
+ * var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
532
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
533
+ * extensions: { subjectAltName: [{ dNSName: "www.example.com" }] } },
534
+ * { key: await pki.key.export(pair.privateKey) });
535
+ * var v = pki.identity.match(der, "www.example.com");
536
+ * console.log(v.matched, v.matchedReference);
537
+ */
538
+ function match(cert, references, opts) {
539
+ opts = guard.identifier.optionsObject(opts, E, "identity/bad-input", "match: opts");
540
+ guard.identifier.assertKnownKeys(opts, MATCH_OPTS, E, "identity/bad-input",
541
+ "pki.identity.match has an unknown option (the only option is `wildcards`). The unknown option was: ");
542
+ var snap = guard.identifier.snapshotOptions(opts, MATCH_OPTS);
543
+ if (snap.wildcards !== undefined && typeof snap.wildcards !== "boolean") {
544
+ throw E("identity/bad-input", "match: opts.wildcards must be a boolean");
545
+ }
546
+ var wildcardsAllowed = snap.wildcards === undefined ? true : snap.wildcards;
547
+ var parsed = guard.parsed.acceptDerived(cert, "certificate", x509.parse, E, "identity/bad-input", "a certificate");
548
+ var refs = _isSnapshot(references) ? references : snapshotReferences(references);
549
+ var entries = _presented(parsed);
550
+ var found = _search(refs, entries, wildcardsAllowed);
551
+ return guard.verdict.of({
552
+ matched: found.matched,
553
+ matchedReference: found.matchedReference,
554
+ matchedEntry: found.matchedEntry,
555
+ presented: entries.length,
556
+ ignored: found.ignored,
557
+ reason: found.reason,
558
+ });
559
+ }
560
+
561
+ module.exports = intrinsic.freeze({ match: match, snapshotReferences: snapshotReferences });