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.
package/package.json CHANGED
@@ -1,10 +1,26 @@
1
1
  {
2
2
  "name": "lemon-tls",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "TLS 1.3/1.2 DTLS 1.3/1.2 implementation for Node.js - full control over cryptographic keys, record layer, and handshake. Drop-in replacement for node:tls with advanced options impossible in OpenSSL.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "types": "index.d.ts",
8
+ "typesVersions": {
9
+ "*": {
10
+ "wire": [
11
+ "types/wire.d.ts"
12
+ ],
13
+ "crypto": [
14
+ "types/crypto.d.ts"
15
+ ],
16
+ "record": [
17
+ "types/record.d.ts"
18
+ ],
19
+ "session": [
20
+ "types/session.d.ts"
21
+ ]
22
+ }
23
+ },
8
24
  "exports": {
9
25
  ".": {
10
26
  "types": "./index.d.ts",
@@ -12,22 +28,41 @@
12
28
  "require": "./index.cjs",
13
29
  "default": "./index.js"
14
30
  },
15
- "./wire": "./src/wire.js",
16
- "./crypto": "./src/crypto.js",
17
- "./record": "./src/record.js",
18
- "./session": "./src/tls_session.js"
31
+ "./wire": {
32
+ "types": "./types/wire.d.ts",
33
+ "default": "./src/wire.js"
34
+ },
35
+ "./crypto": {
36
+ "types": "./types/crypto.d.ts",
37
+ "default": "./src/crypto.js"
38
+ },
39
+ "./record": {
40
+ "types": "./types/record.d.ts",
41
+ "default": "./src/record.js"
42
+ },
43
+ "./session": {
44
+ "types": "./types/session.d.ts",
45
+ "default": "./src/tls_session.js"
46
+ }
19
47
  },
20
48
  "files": [
21
49
  "index.js",
22
50
  "index.cjs",
23
51
  "index.d.ts",
52
+ "types/",
24
53
  "src/",
25
54
  "README.md",
26
55
  "LICENSE",
27
56
  "lemontls.svg"
28
57
  ],
29
58
  "engines": {
30
- "node": ">=16.0.0"
59
+ "node": ">=24.0.0"
60
+ },
61
+ "dependencies": {
62
+ "cryptographic-formats": "^0.1.0"
63
+ },
64
+ "scripts": {
65
+ "test": "node tests/test_all.js && node tests/test_x509_chain.js && node tests/test_keys_groups.js"
31
66
  },
32
67
  "keywords": [
33
68
  "tls",
@@ -70,6 +105,10 @@
70
105
  "alpn",
71
106
  "ja3",
72
107
  "ja3-fingerprint",
108
+ "ja4",
109
+ "ja4-fingerprint",
110
+ "ja5",
111
+ "ja5-fingerprint",
73
112
  "fingerprint",
74
113
  "keylog",
75
114
  "wireshark",
@@ -77,7 +116,6 @@
77
116
  "http3",
78
117
  "record-layer",
79
118
  "pure-javascript",
80
- "zero-dependencies",
81
119
  "no-openssl",
82
120
  "security",
83
121
  "network",
@@ -0,0 +1,31 @@
1
+ // ============================================================
2
+ // The default trust anchors: Node's CA bundle (Mozilla's)
3
+ // ============================================================
4
+ //
5
+ // One store for every session that is given no `ca` — client or server,
6
+ // TLS over TCP, QUIC (a TLSSession used directly) — and the one
7
+ // tls.setDefaultCACertificates() replaces. It lived in compat.js, and only
8
+ // connect() handed it on: a server (createServer, requestCert) and a session
9
+ // built directly had no anchors at all, and "no anchors" authorized every
10
+ // certificate.
11
+ //
12
+ // Parsing all ~144 certificates costs a few tens of milliseconds, so it is
13
+ // done once, lazily, and only if something actually needs it.
14
+ import nodeTls from 'node:tls';
15
+
16
+ let _rootPems = null;
17
+ function getRootCertificates() {
18
+ if (_rootPems === null) {
19
+ try { _rootPems = nodeTls.rootCertificates.slice(); }
20
+ catch (e) { _rootPems = []; } // a build without the bundle
21
+ }
22
+ return _rootPems;
23
+ }
24
+
25
+ /** tls.setDefaultCACertificates(certs): the store, for every later session. */
26
+ function setRootCertificates(certs) {
27
+ if (!Array.isArray(certs)) throw new TypeError('certs must be an array');
28
+ _rootPems = certs.slice();
29
+ }
30
+
31
+ export { getRootCertificates, setRootCertificates };
package/src/compat.js CHANGED
@@ -6,15 +6,16 @@
6
6
  */
7
7
 
8
8
  import net from 'node:net';
9
- import nodeTls from 'node:tls'; // for the shipped CA bundle only
9
+ import { getRootCertificates, setRootCertificates } from './ca_store.js';
10
10
  import crypto from 'node:crypto';
11
11
  import { EventEmitter } from 'node:events';
12
12
  import { TLS_CIPHER_SUITES } from './crypto.js';
13
13
  import TLSSession from './tls_session.js';
14
14
  import TLSSocket from './tls_socket.js';
15
15
  import createSecureContext from './secure_context.js';
16
- import { parseCiphers, parseEcdhCurve, parseSigalgs } from './node_options.js';
16
+ import { parseCiphers, checkCipherSuites, checkSignatureAlgorithms, parseEcdhCurve, parseSigalgs } from './node_options.js';
17
17
  import { prepare_ech_server } from './session/ech.js';
18
+ import { matches_host, matches_altnames } from './session/x509.js';
18
19
 
19
20
  // ===================== Constants =====================
20
21
 
@@ -49,27 +50,9 @@ function getCiphers() {
49
50
 
50
51
  /* ===================== Default CA store ===================== */
51
52
 
52
- /**
53
- * The trust anchors Node ships (Mozilla's CA bundle). Exposed under the same
54
- * name Node uses so `tls.rootCertificates` works unchanged, and so this
55
- * library has a real default to chain against.
56
- *
57
- * That default matters more than parity. Until now, "no `ca` configured" meant
58
- * "authorize every certificate", while `rejectUnauthorized` defaulted to true
59
- * — the connection reported itself verified without ever verifying anything.
60
- * Node never behaved that way: it falls back to this bundle.
61
- *
62
- * Parsing all ~144 certificates costs a few tens of milliseconds, so it is
63
- * done once, lazily, and only if something actually needs it.
64
- */
65
- let _rootPems = null;
66
- function getRootCertificates() {
67
- if (_rootPems === null) {
68
- try { _rootPems = nodeTls.rootCertificates.slice(); }
69
- catch (e) { _rootPems = []; } // a build without the bundle
70
- }
71
- return _rootPems;
72
- }
53
+ // The trust anchors Node ships (Mozilla's CA bundle), under the name Node
54
+ // uses, so `tls.rootCertificates` works unchanged. The store is ca_store.js:
55
+ // every session given no `ca` chains against it, as Node's does.
73
56
 
74
57
  /** Node-compat: tls.getCACertificates([type]). 'default' and 'bundled' both
75
58
  * resolve to the shipped bundle; 'extra' and 'system' are not sourced here
@@ -83,92 +66,42 @@ function getCACertificates(type) {
83
66
  /** Node-compat: tls.setDefaultCACertificates(certs). Replaces the bundle for
84
67
  * every subsequent connection that does not pass its own `ca`. */
85
68
  function setDefaultCACertificates(certs) {
86
- if (!Array.isArray(certs)) throw new TypeError('certs must be an array');
87
- _rootPems = certs.slice();
69
+ setRootCertificates(certs);
88
70
  }
89
71
 
90
72
  // ===================== tls.checkServerIdentity() =====================
91
73
 
92
74
  /**
93
- * Verifies that the peer certificate matches the hostname per RFC 6125.
94
- * Returns undefined on success, or an Error on mismatch.
75
+ * node:tls's tls.checkServerIdentity(hostname, cert): undefined when the
76
+ * certificate is for the host, an Error (code ERR_TLS_CERT_ALTNAME_INVALID,
77
+ * reason, host, cert) when it is not. The handshake's own check —
78
+ * session/x509.js matches_host, one implementation: the subjectAltName only
79
+ * (no common-name fallback, RFC 9525 §6.3), wildcards a whole leftmost
80
+ * label. Node still falls back to the common name of a certificate without
81
+ * a DNS name; this does not, as browsers do not — and as the handshake does
82
+ * not, which this used to disagree with.
95
83
  *
96
- * This is the default identity check Node uses when `rejectUnauthorized: true`
97
- * (the default for tls.connect). Apps can override via the `checkServerIdentity`
98
- * option in tls.connect to supply their own check.
99
- *
100
- * Matching rules:
101
- * 1. If the cert has Subject Alternative Name (SAN) entries:
102
- * - For DNS SANs: compare against hostname (supports leftmost-label wildcards
103
- * like *.example.com). IP address in hostname only matches IP SAN, not DNS SAN.
104
- * - For IP SANs: compare against hostname as IP.
105
- * - CN is IGNORED (per RFC 6125 and modern browsers).
106
- * 2. If no SAN entries exist, fall back to the cert's CN (common name)
107
- * with the same matching rules — legacy path, deprecated by RFC 6125
108
- * but still accepted by Node and common CAs.
109
- *
110
- * cert: the object returned by tlsSocket.getPeerCertificate() — must have
111
- * `subject` (with `CN`) and `subjectaltname` (OpenSSL-formatted string).
84
+ * cert: what getPeerCertificate() returns — its `raw` DER is what is read.
112
85
  */
113
86
  function checkServerIdentity(hostname, cert) {
114
- if (!cert) return new Error('checkServerIdentity: no certificate');
115
- const host = String(hostname || '').toLowerCase();
116
- if (!host) return new Error('checkServerIdentity: hostname required');
117
-
118
- const isIp = /^(\d{1,3}\.){3}\d{1,3}$|^\[?[0-9a-fA-F:]+\]?$/.test(host);
119
-
120
- // Parse subjectaltname (OpenSSL format: "DNS:a.com, DNS:*.b.com, IP Address:1.2.3.4")
121
- const altnames = [];
122
- if (cert.subjectaltname && typeof cert.subjectaltname === 'string') {
123
- const parts = cert.subjectaltname.split(',');
124
- for (let p of parts) {
125
- p = p.trim();
126
- if (p.startsWith('DNS:')) altnames.push({ type: 'DNS', value: p.slice(4).toLowerCase() });
127
- else if (p.startsWith('IP Address:')) altnames.push({ type: 'IP', value: p.slice(11).trim() });
128
- else if (p.startsWith('IP:')) altnames.push({ type: 'IP', value: p.slice(3).trim() });
129
- // Other SAN types (URI, email, etc.) ignored
130
- }
131
- }
132
-
133
- // DNS wildcard matcher: the wildcard must be the leftmost label only.
134
- // *.example.com matches foo.example.com, NOT foo.bar.example.com, NOT example.com.
135
- function dnsMatches(pattern, name) {
136
- if (pattern === name) return true;
137
- if (!pattern.startsWith('*.')) return false;
138
- const dot = name.indexOf('.');
139
- if (dot < 0) return false;
140
- return pattern.slice(2) === name.slice(dot + 1);
141
- }
142
-
143
- // IP match: string-equal for IPv4; normalize brackets/case for IPv6.
144
- function ipMatches(pattern, name) {
145
- return pattern.replace(/^\[|\]$/g, '').toLowerCase()
146
- === name.replace(/^\[|\]$/g, '').toLowerCase();
147
- }
148
-
149
- // RFC 6125: if SANs are present, do NOT fall back to CN.
150
- if (altnames.length > 0) {
151
- for (const s of altnames) {
152
- if (isIp && s.type === 'IP' && ipMatches(s.value, host)) return undefined;
153
- if (!isIp && s.type === 'DNS' && dnsMatches(s.value, host)) return undefined;
154
- }
155
- const details = altnames.map(s => `${s.type}:${s.value}`).join(', ');
156
- return new Error(`Hostname/IP does not match certificate's altnames: Host: ${hostname}. is not in the cert's altnames: ${details}`);
157
- }
158
-
159
- // Legacy CN fallback (only when no SAN present)
160
- const cn = cert.subject && cert.subject.CN;
161
- if (cn) {
162
- const cnLower = String(cn).toLowerCase();
163
- if (isIp) {
164
- if (ipMatches(cnLower, host)) return undefined;
165
- } else {
166
- if (dnsMatches(cnLower, host)) return undefined;
167
- }
168
- return new Error(`Hostname/IP does not match certificate's CN: Host: ${hostname}. is not cert's CN: ${cn}`);
87
+ const host = String(hostname || '');
88
+ let err = null;
89
+ if (!cert) err = new Error('checkServerIdentity: no certificate to check');
90
+ else if (!host) err = new Error('checkServerIdentity: hostname required');
91
+ else {
92
+ // By the DER when the object has it (getPeerCertificate()'s does), or
93
+ // by its subjectaltname string, as Node reads it.
94
+ let ok = false;
95
+ try { ok = cert.raw ? matches_host(cert.raw, host.toLowerCase()) : matches_altnames(cert.subjectaltname, host); } catch (e) { ok = false; }
96
+ if (ok) return undefined;
97
+ err = new Error("Hostname/IP does not match certificate's altnames: Host: " + host + ". is not in the cert's altnames: " +
98
+ (cert.subjectaltname || '(none)'));
169
99
  }
170
-
171
- return new Error(`Hostname/IP does not match any certificate identity`);
100
+ err.code = 'ERR_TLS_CERT_ALTNAME_INVALID';
101
+ err.reason = err.message;
102
+ err.host = host;
103
+ err.cert = cert;
104
+ return err;
172
105
  }
173
106
 
174
107
  // ===================== tls.connect() =====================
@@ -216,7 +149,11 @@ function connect(/* ...args */) {
216
149
 
217
150
  options = options || {};
218
151
 
219
- let socket = new TLSSocket(null, {
152
+ // Every option goes on to the socket (and from it to the session), with
153
+ // what connect() settles itself over them — the lists that picked options
154
+ // one by one dropped a documented option time and again (pins, ech,
155
+ // customExtensions, recordSizeLimit …).
156
+ let socket = new TLSSocket(null, Object.assign({}, options, {
220
157
  isServer: false,
221
158
  servername: options.servername || host,
222
159
  rejectUnauthorized: options.rejectUnauthorized,
@@ -228,20 +165,18 @@ function connect(/* ...args */) {
228
165
  secureContext: options.secureContext,
229
166
  passphrase: options.passphrase,
230
167
  ca: options.ca,
231
- useDefaultCA: options.useDefaultCA,
232
- defaultCA: getRootCertificates,
233
168
  session: options.session,
234
169
  ALPNProtocols: options.ALPNProtocols,
235
170
  minVersion: options.minVersion,
236
171
  maxVersion: options.maxVersion,
237
172
  // node:tls's own strings (ciphers, ecdhCurve, sigalgs) — taken by
238
173
  // nothing until now — or lemon-tls's code lists, which win.
239
- signatureAlgorithms: options.signatureAlgorithms || parseSigalgs(options.sigalgs),
174
+ signatureAlgorithms: checkSignatureAlgorithms(options.signatureAlgorithms) || parseSigalgs(options.sigalgs),
240
175
  groups: options.groups || parseEcdhCurve(options.ecdhCurve),
241
176
  prioritizeChaCha: options.prioritizeChaCha,
242
177
  // Forwarded on the server side but dropped here, so a client could not
243
178
  // narrow the suites it offers.
244
- allowedCipherSuites: options.allowedCipherSuites || parseCiphers(options.ciphers),
179
+ allowedCipherSuites: checkCipherSuites(options.allowedCipherSuites, 'allowedCipherSuites') || parseCiphers(options.ciphers),
245
180
  maxRecordSize: options.maxRecordSize,
246
181
  sessionTickets: options.sessionTickets,
247
182
  ticketLifetime: options.ticketLifetime,
@@ -260,10 +195,18 @@ function connect(/* ...args */) {
260
195
  // here: neither reached the socket.
261
196
  grease: options.grease,
262
197
  permuteExtensions: options.permuteExtensions,
198
+ // Four more documented options (index.d.ts) this list left behind, so
199
+ // connect() ignored them without a word: certificate pins — a client
200
+ // that set them was not pinned at all — the handshake timeout, raw
201
+ // extensions, and noTickets (the older spelling of sessionTickets: false).
202
+ pins: options.pins,
203
+ handshakeTimeout: options.handshakeTimeout,
204
+ customExtensions: options.customExtensions,
205
+ ...(options.sessionTickets === undefined && options.noTickets === true ? { sessionTickets: false } : {}),
263
206
  // 0-RTT: with `session` whose ticket allows it, what is written before
264
207
  // 'secureConnect' goes in the first flight ('earlyData' says if taken).
265
208
  earlyData: options.earlyData === true,
266
- });
209
+ }));
267
210
 
268
211
  addCompatMethods(socket);
269
212
 
@@ -346,9 +289,9 @@ function Server(options, connectionListener) {
346
289
 
347
290
  // node:tls's strings, read once (node_options.js) — a bad one throws here,
348
291
  // as Node's createServer does, not on the first connection.
349
- let nodeSuites = options.allowedCipherSuites || parseCiphers(options.ciphers);
292
+ let nodeSuites = checkCipherSuites(options.allowedCipherSuites, 'allowedCipherSuites') || parseCiphers(options.ciphers);
350
293
  let nodeGroups = options.groups || parseEcdhCurve(options.ecdhCurve);
351
- let nodeSigalgs = options.signatureAlgorithms || parseSigalgs(options.sigalgs);
294
+ let nodeSigalgs = checkSignatureAlgorithms(options.signatureAlgorithms) || parseSigalgs(options.sigalgs);
352
295
 
353
296
  // sessionTimeout: seconds to cache a TLS 1.2 Session ID (Node default: 300)
354
297
  let sessionTimeoutSec = (typeof options.sessionTimeout === 'number' && options.sessionTimeout > 0)
@@ -401,7 +344,11 @@ function Server(options, connectionListener) {
401
344
  }
402
345
 
403
346
  function buildSocketOpts() {
404
- let socketOpts = {
347
+ // Every server option goes on to each socket, with what the server
348
+ // settles over them. Not the key and certificate themselves: they are in
349
+ // the secure context (defaultCtx) — with several pairs, its alternates.
350
+ let socketOpts = Object.assign({}, options, {
351
+ key: undefined, cert: undefined, pfx: undefined, passphrase: undefined, SNICallback: undefined,
405
352
  isServer: true,
406
353
  ticketKeys: self._ticketKeys,
407
354
  // node:tls's sessionTimeout is how long a session stays good — a
@@ -463,7 +410,7 @@ function Server(options, connectionListener) {
463
410
  // never handed to the socket, so a client sending no server_name got
464
411
  // the callback asked with null instead of this certificate, as in Node.
465
412
  secureContext: defaultCtx || undefined,
466
- };
413
+ });
467
414
 
468
415
  if (options.SNICallback) {
469
416
  // An explicit callback wins outright — it may do things the registry
@@ -528,7 +475,9 @@ function Server(options, connectionListener) {
528
475
  });
529
476
 
530
477
  // 'secureConnection' fires AFTER handshake completes (Node.js semantics)
478
+ let established = false;
531
479
  socket.on('secureConnect', function() {
480
+ established = true;
532
481
  if (connectionListener) connectionListener(socket);
533
482
  self.emit('secureConnection', socket);
534
483
  });
@@ -538,9 +487,12 @@ function Server(options, connectionListener) {
538
487
  self.emit('keylog', line, socket);
539
488
  });
540
489
 
541
- // Surface pre-handshake errors as 'tlsClientError' (Node.js semantics)
490
+ // Surface pre-handshake errors as 'tlsClientError' (Node.js semantics) —
491
+ // those only. It read socket.secureEstablished, which no socket has: every
492
+ // error, after the handshake too (a reset, a bad record), was a
493
+ // tlsClientError as well.
542
494
  socket.on('error', function(err) {
543
- if (!socket.secureEstablished) {
495
+ if (!established) {
544
496
  self.emit('tlsClientError', err, socket);
545
497
  }
546
498
  });
@@ -603,6 +555,12 @@ function Server(options, connectionListener) {
603
555
  return self._tcpServer.getConnections(cb);
604
556
  };
605
557
 
558
+ // node:net's: whether it is listening, and whether the process may exit
559
+ // while it does.
560
+ Object.defineProperty(self, 'listening', { get: function() { return self._tcpServer.listening; }, enumerable: true });
561
+ self.ref = function() { self._tcpServer.ref(); return self; };
562
+ self.unref = function() { self._tcpServer.unref(); return self; };
563
+
606
564
  // Ticket key management
607
565
  self.getTicketKeys = function() {
608
566
  return Buffer.from(self._ticketKeys);
@@ -631,11 +589,13 @@ function Server(options, connectionListener) {
631
589
  self._ech = ech || null;
632
590
  };
633
591
 
634
- // setSecureContext — replace cert/key without restart (Node.js compat)
592
+ // setSecureContext — replace cert/key without restart (Node.js compat).
593
+ // A bad one throws, as Node's does, and the certificate in use stays: it
594
+ // returned quietly when key or cert was missing, and a renewal that read
595
+ // an empty file went unnoticed until the old certificate expired.
635
596
  self.setSecureContext = function(opts) {
636
- if (!opts) return;
637
- if (opts.secureContext) { defaultCtx = opts.secureContext; return; }
638
- if (opts.key && opts.cert) defaultCtx = createSecureContext(opts);
597
+ if (!opts || typeof opts !== 'object') throw new TypeError('setSecureContext requires { key, cert } or { secureContext }');
598
+ defaultCtx = opts.secureContext ? opts.secureContext : createSecureContext(opts);
639
599
  };
640
600
 
641
601
  /**
package/src/crypto.js CHANGED
@@ -128,101 +128,6 @@ const TLS_CIPHER_SUITES = {
128
128
  ivlen: 12,
129
129
  hash: 'sha256'
130
130
  },
131
- 0xCCAA: { // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
132
- name: 'DHE-RSA-CHACHA20-POLY1305', standardName: 'TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
133
- tls: 12,
134
- kex: 'DHE_RSA',
135
- sig: 'RSA',
136
- cipher: 'CHACHA20_POLY1305',
137
- aead: true,
138
- keylen: 32,
139
- fixed_ivlen: 12, // RFC 7905: ChaCha20 uses a full 12-byte IV, no explicit nonce
140
- record_ivlen: 0,
141
- ivlen: 12,
142
- hash: 'sha256'
143
- },
144
- 0x009C: { // TLS_RSA_WITH_AES_128_GCM_SHA256
145
- name: 'AES128-GCM-SHA256', standardName: 'TLS_RSA_WITH_AES_128_GCM_SHA256',
146
- tls: 12,
147
- kex: 'RSA',
148
- sig: 'RSA',
149
- cipher: 'AES_128_GCM',
150
- aead: true,
151
- keylen: 16,
152
- fixed_ivlen: 4,
153
- record_ivlen: 8,
154
- ivlen: 12,
155
- hash: 'sha256'
156
- },
157
- 0x009D: { // TLS_RSA_WITH_AES_256_GCM_SHA384
158
- name: 'AES256-GCM-SHA384', standardName: 'TLS_RSA_WITH_AES_256_GCM_SHA384',
159
- tls: 12,
160
- kex: 'RSA',
161
- sig: 'RSA',
162
- cipher: 'AES_256_GCM',
163
- aead: true,
164
- keylen: 32,
165
- fixed_ivlen: 4,
166
- record_ivlen: 8,
167
- ivlen: 12,
168
- hash: 'sha384'
169
- },
170
-
171
- // ----------------------
172
- // TLS 1.2 CBC (Legacy)
173
- // ----------------------
174
- 0xC013: { // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
175
- name: 'ECDHE-RSA-AES128-SHA', standardName: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
176
- tls: 12,
177
- kex: 'ECDHE_RSA',
178
- sig: 'RSA',
179
- cipher: 'AES_128_CBC',
180
- aead: false,
181
- keylen: 16,
182
- ivlen: 16,
183
- mac: 'sha1',
184
- maclen: 20,
185
- hash: 'sha256'
186
- },
187
- 0xC014: { // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
188
- name: 'ECDHE-RSA-AES256-SHA', standardName: 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
189
- tls: 12,
190
- kex: 'ECDHE_RSA',
191
- sig: 'RSA',
192
- cipher: 'AES_256_CBC',
193
- aead: false,
194
- keylen: 32,
195
- ivlen: 16,
196
- mac: 'sha1',
197
- maclen: 20,
198
- hash: 'sha256'
199
- },
200
- 0x003C: { // TLS_RSA_WITH_AES_128_CBC_SHA256
201
- name: 'AES128-SHA256', standardName: 'TLS_RSA_WITH_AES_128_CBC_SHA256',
202
- tls: 12,
203
- kex: 'RSA',
204
- sig: 'RSA',
205
- cipher: 'AES_128_CBC',
206
- aead: false,
207
- keylen: 16,
208
- ivlen: 16,
209
- mac: 'sha256',
210
- maclen: 32,
211
- hash: 'sha256'
212
- },
213
- 0x003D: { // TLS_RSA_WITH_AES_256_CBC_SHA256
214
- name: 'AES256-SHA256', standardName: 'TLS_RSA_WITH_AES_256_CBC_SHA256',
215
- tls: 12,
216
- kex: 'RSA',
217
- sig: 'RSA',
218
- cipher: 'AES_256_CBC',
219
- aead: false,
220
- keylen: 32,
221
- ivlen: 16,
222
- mac: 'sha256',
223
- maclen: 32,
224
- hash: 'sha256'
225
- }
226
131
  };
227
132
 
228
133
 
@@ -282,6 +187,11 @@ function hkdf_extract(hashName, saltU8, ikmU8) {
282
187
  function hkdf_expand(hashName, prkU8, infoU8, length) {
283
188
  let hashLen = getHashLen(hashName);
284
189
  let N = Math.ceil(length / hashLen);
190
+ // RFC 5869 §2.3: L <= 255*HashLen. The one-byte counter wrapped past it,
191
+ // and the output repeated its own blocks (Wycheproof).
192
+ if (!Number.isInteger(length) || length < 0 || N > 255) {
193
+ throw new RangeError('HKDF-Expand: length ' + length + ' is more than 255 × ' + hashLen);
194
+ }
285
195
  // allocUnsafe is safe here: every byte is overwritten by prev.copy() below.
286
196
  let output = Buffer.allocUnsafe(N * hashLen);
287
197
  let prev = Buffer.alloc(0);
@@ -607,10 +517,13 @@ function tls_derive_from_master_secret_tls12(master_secret, server_random, clien
607
517
  let p = TLS_CIPHER_SUITES[cipher_suite];
608
518
  if (!p || p.tls !== 12) throw new Error('cipher suite not TLS 1.2 or not mapped');
609
519
 
520
+ // AEAD suites only (is_usable_cipher_suite): no MAC keys in the block —
521
+ // client_write_key, server_write_key, then the implicit IVs (RFC 5246 §6.3,
522
+ // RFC 5288 §3, RFC 7905 §2).
523
+ if (!p.aead) throw new Error('cipher suite 0x' + cipher_suite.toString(16) + ' is not an AEAD suite');
610
524
  let hashName = p.hash;
611
- let macLen = p.aead ? 0 : (p.maclen || 0);
612
- let ivFromKbLen = p.aead ? (p.fixed_ivlen || 0) : 0;
613
- let need = (2 * macLen) + (2 * p.keylen) + (2 * ivFromKbLen);
525
+ let ivFromKbLen = p.fixed_ivlen || 0;
526
+ let need = (2 * p.keylen) + (2 * ivFromKbLen);
614
527
 
615
528
  let key_block = tls12_prf(
616
529
  master_secret,
@@ -621,13 +534,6 @@ function tls_derive_from_master_secret_tls12(master_secret, server_random, clien
621
534
  );
622
535
 
623
536
  let off = 0;
624
- let c_mac = null, s_mac = null;
625
-
626
- if (!p.aead && macLen > 0) {
627
- c_mac = key_block.slice(off, off + macLen); off += macLen;
628
- s_mac = key_block.slice(off, off + macLen); off += macLen;
629
- }
630
-
631
537
  let c_key = key_block.slice(off, off + p.keylen); off += p.keylen;
632
538
  let s_key = key_block.slice(off, off + p.keylen); off += p.keylen;
633
539
 
@@ -635,8 +541,6 @@ function tls_derive_from_master_secret_tls12(master_secret, server_random, clien
635
541
  let s_iv_salt = ivFromKbLen ? key_block.slice(off, off + ivFromKbLen) : null; off += ivFromKbLen;
636
542
 
637
543
  return {
638
- client_mac: c_mac,
639
- server_mac: s_mac,
640
544
  client_key: c_key,
641
545
  server_key: s_key,
642
546
  client_iv: c_iv_salt,
@@ -646,7 +550,7 @@ function tls_derive_from_master_secret_tls12(master_secret, server_random, clien
646
550
  prf_hash: hashName,
647
551
  key_len: p.keylen,
648
552
  fixed_ivlen: ivFromKbLen,
649
- record_ivlen: p.aead ? (p.record_ivlen || 0) : 16
553
+ record_ivlen: p.record_ivlen || 0
650
554
  };
651
555
  }
652
556
 
@@ -844,11 +748,9 @@ const DEFAULT_CIPHER_SUITES_TLS12 = [
844
748
  /**
845
749
  * Can the record layer actually protect traffic with this cipher suite?
846
750
  *
847
- * TLS_CIPHER_SUITES describes suites the code can NAME — including CBC ones
848
- * kept for parsing peer offers and for reporting — but record.js implements
849
- * AEAD only. A suite that is merely described is not a capability: selecting
850
- * one produces a handshake that agrees on parameters and then cannot encrypt a
851
- * single record.
751
+ * TLS_CIPHER_SUITES describes the suites the code can name. A suite that is
752
+ * merely described is not a capability: selecting one produces a handshake
753
+ * that agrees on parameters and then cannot go on.
852
754
  *
853
755
  * This mirrors ecdh.js's is_supported_group, and exists for the same reason: a
854
756
  * configured list is not an implementation. Selection asks this, never the
@@ -856,7 +758,12 @@ const DEFAULT_CIPHER_SUITES_TLS12 = [
856
758
  */
857
759
  function is_usable_cipher_suite(id) {
858
760
  let m = TLS_CIPHER_SUITES[id];
859
- return !!(m && m.aead === true);
761
+ // An AEAD suite whose key exchange this library does: TLS 1.3's, or
762
+ // TLS 1.2 ECDHE. (RSA key transport, DHE and CBC suites were listed and
763
+ // never worked — a server that picked one stalled after its Certificate.
764
+ // They are gone: insecure as they are (Marvin, Lucky13), no current
765
+ // client needs them.)
766
+ return !!(m && m.aead === true && (m.tls === 13 || (typeof m.kex === 'string' && m.kex.indexOf('ECDHE_') === 0)));
860
767
  }
861
768
 
862
769
  /**