@blamejs/pki 0.8.7 → 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.
- package/CHANGELOG.md +124 -76
- package/README.md +3 -2
- package/index.js +5 -0
- package/lib/acme.js +15 -78
- package/lib/cmc-build.js +1 -1
- package/lib/cmc-verify.js +3 -3
- package/lib/cmp-session.js +1 -1
- package/lib/cms-sign.js +3 -3
- package/lib/cms-verify.js +5 -5
- package/lib/constants.js +11 -0
- package/lib/est.js +1 -1
- package/lib/framework-error.js +6 -0
- package/lib/guard-async.js +53 -2
- package/lib/guard-encoding.js +74 -0
- package/lib/guard-identifier.js +74 -1
- package/lib/guard-intrinsic.js +2 -0
- package/lib/guard-limits.js +40 -1
- package/lib/guard-list.js +25 -0
- package/lib/guard-name.js +133 -16
- package/lib/guard-text.js +33 -1
- package/lib/http-transport.js +39 -7
- package/lib/identity-match.js +561 -0
- package/lib/ip-utils.js +52 -1
- package/lib/key.js +250 -5
- package/lib/ocsp.js +125 -22
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +199 -61
- package/lib/revocation-fetch.js +772 -0
- package/lib/schema-all.js +124 -0
- package/lib/schema-attrcert.js +4 -2
- package/lib/schema-crmf.js +4 -2
- package/lib/schema-pkcs1.js +335 -0
- package/lib/schema-pkcs8.js +15 -0
- package/lib/schema-pkix.js +322 -40
- package/lib/schema-sec1.js +244 -0
- package/lib/schema-tsp.js +4 -2
- package/lib/schema-x509.js +4 -2
- package/lib/trust.js +2 -2
- package/lib/tsp-sign.js +1 -1
- package/lib/webauthn.js +4 -4
- package/lib/webcrypto.js +17 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -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 });
|
package/lib/ip-utils.js
CHANGED
|
@@ -102,4 +102,55 @@ function packIpLiteral(s) {
|
|
|
102
102
|
return hex === null ? null : _bufferFrom(hex, "hex");
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
/** @internal The RFC 5952 sec. 4 textual form of an IPv6 address given as eight group values: each
|
|
106
|
+
* group written in lower-case hexadecimal with no leading zeros, and the longest run of two or more
|
|
107
|
+
* zero groups replaced by `::`. A run of one is written out, which sec. 4.2.2 requires. */
|
|
108
|
+
function _ipv6Text(nums) {
|
|
109
|
+
var hex = [], i;
|
|
110
|
+
for (i = 0; i < 8; i += 1) _push(hex, _numToString(nums[i], 16));
|
|
111
|
+
var bestStart = -1, bestLen = 0, curStart = -1, curLen = 0;
|
|
112
|
+
for (i = 0; i < 8; i += 1) {
|
|
113
|
+
if (nums[i] === 0) {
|
|
114
|
+
if (curStart === -1) curStart = i;
|
|
115
|
+
curLen += 1;
|
|
116
|
+
if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; }
|
|
117
|
+
} else { curStart = -1; curLen = 0; }
|
|
118
|
+
}
|
|
119
|
+
if (bestLen < 2) return _joinOn(hex, 0, 8, ":");
|
|
120
|
+
return _joinOn(hex, 0, bestStart, ":") + "::" + _joinOn(hex, bestStart + bestLen, 8, ":");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _joinOn(list, from, to, sep) {
|
|
124
|
+
var out = "";
|
|
125
|
+
for (var i = from; i < to; i += 1) out += (i > from ? sep : "") + list[i];
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @internal The textual address an `iPAddress` entry's octets carry: dotted-quad for four octets
|
|
130
|
+
* and the RFC 5952 form for sixteen. `null` for any other length, because no IP version is defined
|
|
131
|
+
* for one (RFC 9110 sec. 4.3.5). The inverse of `packIpLiteral` for every value it produces. */
|
|
132
|
+
function textFromOctets(buf) {
|
|
133
|
+
if (!buf || typeof buf.length !== "number") return null;
|
|
134
|
+
if (buf.length === 4) return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3];
|
|
135
|
+
if (buf.length !== 16) return null;
|
|
136
|
+
var nums = [];
|
|
137
|
+
for (var i = 0; i < 16; i += 2) _push(nums, (buf[i] << 8) | buf[i + 1]);
|
|
138
|
+
return _ipv6Text(nums);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @internal The RFC 5952 canonical form of an IPv6 address written as text, or `null` when the
|
|
142
|
+
* text is not eight groups. A caller comparing its input against the result learns whether the
|
|
143
|
+
* input was already canonical. */
|
|
144
|
+
function canonicalizeIpv6(value) {
|
|
145
|
+
if (typeof value !== "string") return null;
|
|
146
|
+
var hex = expandIpv6Hex(value);
|
|
147
|
+
if (hex === null) return null;
|
|
148
|
+
var nums = [];
|
|
149
|
+
for (var i = 0; i < 8; i += 1) _push(nums, _Number("0x" + _slice(hex, i * 4, i * 4 + 4)));
|
|
150
|
+
return _ipv6Text(nums);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
isIPv4: isIPv4, expandIpv6Hex: expandIpv6Hex, isIpLiteral: isIpLiteral, packIpLiteral: packIpLiteral,
|
|
155
|
+
textFromOctets: textFromOctets, canonicalizeIpv6: canonicalizeIpv6,
|
|
156
|
+
};
|