lemon-tls 0.9.0 → 0.11.0

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,385 @@
1
+ // ============================================================
2
+ // X.509 for the handshake: certificates read, issuers matched,
3
+ // signatures checked, and the path checks (RFC 5280 §4.2, §6.1)
4
+ // ============================================================
5
+ //
6
+ // The certificate FORMAT is cryptographic-formats': the DER, the extensions, names
7
+ // and their comparison (RFC 5280 §7.1, as OpenSSL canonicalizes them), name
8
+ // constraints, host names. This module holds what a TLS verifier decides on
9
+ // top of it, and the signature check, which is node:crypto's:
10
+ //
11
+ // parse_cert a certificate, read once per DER object
12
+ // issued_by the issuer's name, and its key identifier where the
13
+ // certificate names one (OpenSSL's X509_check_issued,
14
+ // less the keyUsage test — check_path reports that as
15
+ // INVALID_CA, which is OpenSSL's answer too)
16
+ // signed_by the signature, by the issuer's key
17
+ // check_path what each certificate of a built path may do:
18
+ //
19
+ // basicConstraints every issuer a CA (an anchor too: one that is not —
20
+ // an end-entity certificate given as `ca` — vouched for
21
+ // any name); pathLenConstraint; a leaf that is a CA
22
+ // (CA/B Forum BR §7.1.2.7.8: cA MUST be false) refused
23
+ // keyUsage an issuer may sign certificates (keyCertSign); a leaf
24
+ // may sign or agree a key (OpenSSL's TLS purpose rule)
25
+ // extKeyUsage when present, on the leaf and the intermediates, the
26
+ // purpose asked for (serverAuth, clientAuth) or any
27
+ // nameConstraints each CA's, on every certificate below it — the
28
+ // subject, its emailAddress attributes, every
29
+ // subjectAltName, and the leaf's common name when it has
30
+ // no DNS name (OpenSSL's NAME_CONSTRAINTS_check_CN)
31
+ // critical an extension marked critical that is not one this
32
+ // verifier enforces refuses the certificate (§4.2)
33
+ // signature a certificate below the anchor signed with MD2, MD4,
34
+ // MD5 or SHA-1 is refused (CA_MD_TOO_WEAK, OpenSSL's
35
+ // security level 1, node:tls's default)
36
+ //
37
+ // The codes are OpenSSL's names (as node:tls reports them).
38
+ //
39
+ // This module had its own DER reader and its own name-constraint matching.
40
+ // Moving them to cryptographic-formats fixed what they had wrong against OpenSSL: a
41
+ // subject's emailAddress was never checked against email constraints, a
42
+ // leaf's common name never against DNS ones, directory names were compared
43
+ // byte for byte (O=Acme did not match O=ACME), a non-prefix IP mask and a
44
+ // subtree minimum were reported as UNSUPPORTED_CONSTRAINT_SYNTAX where
45
+ // OpenSSL matches the first and says SUBTREE_MINMAX for the second.
46
+
47
+ import crypto from 'node:crypto';
48
+ import * as X509 from 'cryptographic-formats/x509';
49
+
50
+ const ANY_EKU = '2.5.29.37.0', SERVER_AUTH = '1.3.6.1.5.5.7.3.1', CLIENT_AUTH = '1.3.6.1.5.5.7.3.2';
51
+
52
+ // The extensions this verifier enforces, so the only ones that may be marked
53
+ // critical — any other critical: UNHANDLED_CRITICAL_EXTENSION, OpenSSL's
54
+ // error 34. keyUsage, subjectAltName, basicConstraints, nameConstraints and
55
+ // extKeyUsage are checked here; the policy ones restrict nothing this
56
+ // verifier relies on (it requires no policy), and CRL distribution points
57
+ // say where to look, not what to refuse. OpenSSL takes a few more critical
58
+ // (nsCertType, RFC 3779's, proxyCertInfo) because it enforces them by rules
59
+ // of its own; they are not read here, so refused here.
60
+ //
61
+ // The key identifiers, freshest CRL, AIA, SIA, issuer alt name, subject
62
+ // directory attributes and TLS feature are refused critical as well — RFC
63
+ // 5280 has the key identifiers and AIA non-critical ("MUST", §4.2.1.1, .2,
64
+ // §4.2.2.1), and a certificate that marks AIA critical says a verifier that
65
+ // does not fetch from it must not use it (x509-limbo; BoringSSL refuses
66
+ // them too).
67
+ const ENFORCED = X509.enforced_set([
68
+ '2.5.29.15', '2.5.29.17', '2.5.29.19', '2.5.29.30', '2.5.29.37', // KU, SAN, BC, NC, EKU
69
+ '2.5.29.32', '2.5.29.33', '2.5.29.36', '2.5.29.54', // policies, mappings, constraints, inhibitAnyPolicy
70
+ '2.5.29.31', // CRL distribution points
71
+ ]);
72
+
73
+ // ---- certificates ----
74
+
75
+ // Read once per DER object: the anchors are the same objects on every
76
+ // handshake, and the peer's leaf is read by the chain check, the identity
77
+ // check and the handshake signature. Strings (PEM) are not keys here — the
78
+ // caller turns them into DER first.
79
+ const PARSED = new WeakMap();
80
+ // …and once per DER content, a bounded number of them: a client that
81
+ // connects with a `ca` makes a new secure context — new objects for the
82
+ // same CA — every time, and a server presents the same chain to every
83
+ // client. Reading them again cost a quarter of a millisecond a handshake.
84
+ // A certificate is immutable, so its reading is too. Full: cleared, as the
85
+ // anchor cache in tls_session.
86
+ const PARSED_BY_CONTENT = new Map();
87
+ const PARSED_BY_CONTENT_MAX = 1024;
88
+
89
+ function bytes_equal(a, b) {
90
+ if (!a || !b || a.length !== b.length) return false;
91
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
92
+ return true;
93
+ }
94
+
95
+ /**
96
+ * A certificate from its DER (Buffer or Uint8Array), or null when it is not
97
+ * one cryptographic-formats can read. The result:
98
+ * raw Buffer, the DER
99
+ * cf cryptographic-formats' x509.from_der
100
+ * subjectKey, the names as one string each, equal when the names match
101
+ * issuerKey (RFC 5280 §7.1 as OpenSSL compares them) — for an index
102
+ * notBefore, ms
103
+ * notAfter
104
+ * selfIssued subject and issuer the same name
105
+ * bc, ku, eku, nc, ski, aki the extensions' parsed values, or null
106
+ * publicKey() a crypto.KeyObject, made on first use; null when node:crypto
107
+ * cannot use the key
108
+ */
109
+ function parse_cert(der) {
110
+ if (der == null || typeof der !== 'object') return null;
111
+ let hit = PARSED.get(der);
112
+ if (hit !== undefined) return hit;
113
+ let content = Buffer.from(der.buffer, der.byteOffset, der.byteLength).toString('latin1');
114
+ let out = PARSED_BY_CONTENT.get(content);
115
+ if (out === undefined) {
116
+ try { out = make_cert(X509.from_der(der)); } catch (e) { out = null; }
117
+ if (PARSED_BY_CONTENT.size >= PARSED_BY_CONTENT_MAX) PARSED_BY_CONTENT.clear();
118
+ PARSED_BY_CONTENT.set(content, out);
119
+ }
120
+ PARSED.set(der, out);
121
+ return out;
122
+ }
123
+
124
+ function make_cert(cf) {
125
+ let ext = function (name) { let e = X509.extension(cf, name); return e ? e.parsed : null; };
126
+ let subjectKey = X509.name_key(cf.subject), issuerKey = X509.name_key(cf.issuer);
127
+ let key;
128
+ return {
129
+ raw: Buffer.from(cf.der.buffer, cf.der.byteOffset, cf.der.byteLength),
130
+ cf: cf,
131
+ subjectKey: subjectKey,
132
+ issuerKey: issuerKey,
133
+ notBefore: cf.validity.not_before.date.getTime(),
134
+ notAfter: cf.validity.not_after.date.getTime(),
135
+ selfIssued: subjectKey === issuerKey,
136
+ bc: ext('basicConstraints'),
137
+ ku: ext('keyUsage'),
138
+ eku: ext('extKeyUsage'),
139
+ nc: ext('nameConstraints'),
140
+ ski: ext('subjectKeyId'),
141
+ aki: ext('authorityKeyId'),
142
+ publicKey: function () {
143
+ if (key === undefined) {
144
+ try { key = crypto.createPublicKey({ key: Buffer.from(cf.public_key.der), format: 'der', type: 'spki' }); }
145
+ catch (e) { key = null; }
146
+ }
147
+ return key;
148
+ },
149
+ };
150
+ }
151
+
152
+ /** Inside its validity window at `now` (a Date): both ends inclusive (§4.1.2.5). */
153
+ function valid_at(cert, now) {
154
+ let t = now.getTime();
155
+ return t >= cert.notBefore && t <= cert.notAfter;
156
+ }
157
+ /** The node:tls (OpenSSL) code for a certificate outside its window. */
158
+ function validity_error(cert, now) {
159
+ return now.getTime() < cert.notBefore ? 'CERT_NOT_YET_VALID' : 'CERT_HAS_EXPIRED';
160
+ }
161
+
162
+ /**
163
+ * Could `issuer` have issued `cert`? Its subject is the certificate's issuer
164
+ * name, and where the certificate's authorityKeyIdentifier names a key
165
+ * identifier, a serial or an issuer, the candidate is that one — OpenSSL's
166
+ * X509_check_akid. That is what picks the right CA of two with one name.
167
+ * Not the signature (signed_by), and not the issuer's keyUsage (check_path).
168
+ */
169
+ function issued_by(cert, issuer) {
170
+ if (cert.issuerKey !== issuer.subjectKey) return false;
171
+ let aki = cert.aki;
172
+ if (!aki) return true;
173
+ if (aki.key_id && issuer.ski && !bytes_equal(aki.key_id, issuer.ski)) return false;
174
+ if (aki.serial && !bytes_equal(aki.serial, issuer.cf.serial)) return false;
175
+ if (aki.issuer) {
176
+ let dns = aki.issuer.filter(function (g) { return g.type === 'directory'; });
177
+ if (dns.length > 0 && !dns.some(function (g) { return X509.name_key(g.value) === issuer.issuerKey; })) return false;
178
+ }
179
+ return true;
180
+ }
181
+
182
+ /**
183
+ * Is `cert`'s signature `issuer`'s? The algorithm as cryptographic-formats
184
+ * describes it, the check node:crypto's. An algorithm node:crypto cannot
185
+ * check — RSASSA-PSS whose mask hash is not its message hash (node:crypto
186
+ * takes one hash for both), MD2 — is a signature that does not verify.
187
+ */
188
+ function signed_by(cert, issuer) {
189
+ let key = issuer.publicKey();
190
+ if (!key) return false;
191
+ let p = cert.cf.signature_params, tbs = cert.cf.tbs_bytes, sig = cert.cf.signature;
192
+ try {
193
+ switch (p.scheme) {
194
+ case 'pkcs1':
195
+ return crypto.verify(p.hash, tbs, { key: key, padding: crypto.constants.RSA_PKCS1_PADDING }, sig);
196
+ case 'pss':
197
+ if (p.pss.mgf !== 'mgf1' || p.pss.mgf_hash !== p.pss.hash) return false;
198
+ return crypto.verify(p.pss.hash, tbs, { key: key, padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
199
+ saltLength: p.pss.salt_length }, sig);
200
+ case 'ecdsa':
201
+ case 'dsa':
202
+ return crypto.verify(p.hash, tbs, key, sig);
203
+ case 'eddsa':
204
+ case 'ml-dsa':
205
+ case 'slh-dsa':
206
+ return crypto.verify(null, tbs, key, sig);
207
+ default:
208
+ return false;
209
+ }
210
+ } catch (e) {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ /** Issued by itself, and signed by its own key. */
216
+ function self_signed(cert) {
217
+ return issued_by(cert, cert) && signed_by(cert, cert);
218
+ }
219
+
220
+ // ---- name constraints (§4.2.1.10) ----
221
+
222
+ // The cost of name constraints: names × constraints, both chosen by the
223
+ // certificates. Bounded as OpenSSL bounds it — NAME_CHECK_MAX (2^20)
224
+ // comparisons of one certificate's names against one CA's constraints
225
+ // (cryptographic-formats counts the names as OpenSSL does) — and over the whole
226
+ // path by NAME_CHECK_PATH_MAX: five times one pair's. OpenSSL bounds each
227
+ // pair alone, so CAs stacked under one another multiply the cost: four of
228
+ // 1,024 subtrees, a leaf of 1,023 names under each, kept every pair under
229
+ // 2^20 and cost them all. Five pairs' worth takes that path still — four
230
+ // maximal pairs and the CAs' own names under one another — and is the most
231
+ // the constraints of any path can cost (~100 ms); a path past it is refused
232
+ // (x509-limbo nc-dos). Past either bound: X509_V_ERR_UNSPECIFIED, node:tls's
233
+ // 'UNSPECIFIED'.
234
+ const NAME_CHECK_MAX = 1 << 20;
235
+ const NAME_CHECK_PATH_MAX = 5 * NAME_CHECK_MAX;
236
+
237
+ // `cert`'s names within `nc` (a CA's): null, or the error code. `isLeaf`:
238
+ // the common name counts as a DNS name when there is none other (OpenSSL
239
+ // does this for the leaf). `budget` ({ left }): the path's comparisons.
240
+ function name_constraints_error(cert, nc, isLeaf, budget) {
241
+ let r;
242
+ try {
243
+ r = X509.name_constraints_check(cert.cf, nc, {
244
+ common_name: isLeaf, max_checks: Math.min(NAME_CHECK_MAX, budget.left),
245
+ });
246
+ } catch (e) {
247
+ // A subtree with a minimum or maximum: OpenSSL's error 49, whatever
248
+ // the names. Over the bound: UNSPECIFIED, as there.
249
+ return e && e.code === 'subtree-minmax' ? 'SUBTREE_MINMAX' : 'UNSPECIFIED';
250
+ }
251
+ budget.left -= r.checks;
252
+ if (r.ok === false) {
253
+ // Which rule refused it: OpenSSL says EXCLUDED_VIOLATION (48) for a name
254
+ // inside an excluded subtree, PERMITTED_VIOLATION (47) for one outside
255
+ // every permitted subtree of its kind.
256
+ for (const n of r.refused) if (n.reason === 'excluded') return 'EXCLUDED_VIOLATION';
257
+ return 'PERMITTED_VIOLATION';
258
+ }
259
+ // A name of a kind a constraint covers and nothing here can compare — a
260
+ // URI without a host, an otherName: refused, as OpenSSL's error 53.
261
+ if (r.ok === null) return 'UNSUPPORTED_NAME_SYNTAX';
262
+ return null;
263
+ }
264
+
265
+ // ---- the path ----
266
+
267
+ /**
268
+ * The path checks: `path` is [leaf, …, anchor] as parse_cert returns them,
269
+ * `purpose` 'server' (we are the client, checking a server) or 'client'.
270
+ * Returns null, or the error code.
271
+ */
272
+ function check_path(path, purpose) {
273
+ let want = purpose === 'client' ? CLIENT_AUTH : SERVER_AUTH;
274
+ let last = path.length - 1;
275
+ let budget = { left: NAME_CHECK_PATH_MAX }; // the path's name-constraint comparisons
276
+
277
+ for (let i = 0; i <= last; i++) {
278
+ let p = path[i];
279
+ // Once per certificate (it is read once, and kept): the answer is the
280
+ // certificate's.
281
+ if (p.unenforced === undefined) p.unenforced = X509.unenforced_critical(p.cf, ENFORCED).length > 0;
282
+ if (p.unenforced) return 'UNHANDLED_CRITICAL_EXTENSION';
283
+ // Its signature, unless it is the anchor (whose own signature nothing
284
+ // checks — the caller trusts it as it is): not by a weak digest.
285
+ if (i < last && p.cf.signature_params.weak) return 'CA_MD_TOO_WEAK';
286
+ // extendedKeyUsage, where present, on the leaf and the intermediates
287
+ // (the anchor's is the caller's to trust).
288
+ if (i < last && p.eku && !p.eku.some(function (u) { return u.id === want || u.id === ANY_EKU; })) return 'INVALID_PURPOSE';
289
+ if (i === 0) {
290
+ // The leaf: not a CA (a path of one — the leaf is the anchor itself —
291
+ // is the caller's own trust in it, and never reaches here).
292
+ if (p.bc && p.bc.ca) return 'INVALID_PURPOSE';
293
+ if (p.ku && !(p.ku.digitalSignature || p.ku.keyEncipherment || p.ku.keyAgreement)) return 'INVALID_PURPOSE';
294
+ continue;
295
+ }
296
+ // An issuer: a CA (a version 1 self-signed root is one by convention,
297
+ // as OpenSSL takes it). An intermediate that is not one fails OpenSSL's
298
+ // purpose check (a TLS CA) first — INVALID_PURPOSE, as node:tls reports
299
+ // it; an anchor that is not one, INVALID_CA.
300
+ if (!(p.bc && p.bc.ca) && !(p.cf.version === 1 && i === last && p.selfIssued)) {
301
+ return i < last ? 'INVALID_PURPOSE' : 'INVALID_CA';
302
+ }
303
+ // …allowed to sign certificates.
304
+ if (p.ku && !p.ku.keyCertSign) return 'INVALID_CA';
305
+ // pathLenConstraint: the intermediates below it, self-issued ones aside.
306
+ if (p.bc && p.bc.path_len !== null && p.bc.path_len !== undefined) {
307
+ let below = 0;
308
+ for (let j = 1; j < i; j++) if (!path[j].selfIssued) below++;
309
+ if (below > p.bc.path_len) return 'PATH_LENGTH_EXCEEDED';
310
+ }
311
+ // Its name constraints, on every certificate below it — a self-issued
312
+ // intermediate's own names aside (§6.1.3 b).
313
+ if (p.nc) {
314
+ for (let j = 0; j < i; j++) {
315
+ if (j > 0 && path[j].selfIssued) continue;
316
+ let err = name_constraints_error(path[j], p.nc, j === 0, budget);
317
+ if (err) return err;
318
+ }
319
+ }
320
+ }
321
+ return null;
322
+ }
323
+
324
+ // ---- the host ----
325
+
326
+ // An IPv4 or IPv6 address literal (IPv6 with or without brackets) — what
327
+ // SNI may never carry, and what a certificate is checked against by IP SAN.
328
+ function is_ip_literal(host) {
329
+ host = String(host);
330
+ return /^(\d{1,3}\.){3}\d{1,3}$/.test(host) ||
331
+ (host.indexOf(':') >= 0 && /^[0-9a-fA-F:.\[\]]+$/.test(host));
332
+ }
333
+
334
+ /**
335
+ * The certificate for `host`? The one identity check — the handshake's
336
+ * (tls_session) and tls.checkServerIdentity's (compat) both: cryptographic-formats'
337
+ * RFC 9525 rules. A host name against the subjectAltName's DNS names only —
338
+ * the subject's common name is not one (RFC 9525 §6.3: it was, when the
339
+ * certificate had no DNS SAN) — and a wildcard a whole leftmost label
340
+ * (*.example.com, not ba*.example.com, not *.com); an address against its
341
+ * iPAddress names. `cert` is parse_cert's, or DER. True or false: a host
342
+ * that is not ASCII (not in A-labels) matches nothing.
343
+ */
344
+ function matches_host(cert, host) {
345
+ if (cert && !cert.cf) cert = parse_cert(cert);
346
+ if (!cert) return false;
347
+ host = String(host);
348
+ if (is_ip_literal(host)) host = host.replace(/^\[|\]$/g, '');
349
+ try { return X509.hostname_matches(cert.cf, host); } catch (e) { return false; }
350
+ }
351
+
352
+ // The same rules over node:tls's subjectaltname string ('DNS:a.example,
353
+ // IP Address:192.0.2.1'), for a certificate object without its DER — as
354
+ // tls.checkServerIdentity is given by callers that build one themselves.
355
+ // A wildcard only as the whole leftmost label, and not alone over a public
356
+ // suffix of one label ('*.com').
357
+ function matches_altnames(san, host) {
358
+ host = String(host).toLowerCase().replace(/\.$/, '');
359
+ let ip = is_ip_literal(host) ? canonical_ip(host.replace(/^\[|\]$/g, '')) : null;
360
+ let entries = String(san || '').split(/,\s*/);
361
+ for (let i = 0; i < entries.length; i++) {
362
+ let e = entries[i], c = e.indexOf(':');
363
+ if (c < 0) continue;
364
+ let kind = e.slice(0, c), value = e.slice(c + 1).trim().toLowerCase().replace(/\.$/, '');
365
+ if (ip !== null) {
366
+ if (kind === 'IP Address' && canonical_ip(value) === ip) return true;
367
+ continue;
368
+ }
369
+ if (kind !== 'DNS') continue;
370
+ if (value === host) return true;
371
+ if (value.startsWith('*.') && value.indexOf('.', 2) > 0) {
372
+ let dot = host.indexOf('.');
373
+ if (dot > 0 && host.slice(dot + 1) === value.slice(2)) return true;
374
+ }
375
+ }
376
+ return false;
377
+ }
378
+ function canonical_ip(s) {
379
+ try { return new URL('http://' + (s.indexOf(':') >= 0 ? '[' + s + ']' : s)).hostname; } catch (e) { return null; }
380
+ }
381
+
382
+ export {
383
+ parse_cert, issued_by, signed_by, self_signed, valid_at, validity_error,
384
+ check_path, matches_host, matches_altnames, is_ip_literal,
385
+ };