lemon-tls 0.10.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/README.md +164 -42
- package/index.d.ts +399 -15
- package/package.json +7 -2
- package/src/ca_store.js +31 -0
- package/src/compat.js +64 -112
- package/src/crypto.js +21 -114
- package/src/dtls_session.js +1238 -412
- package/src/dtls_socket.js +318 -96
- package/src/node_options.js +154 -17
- package/src/record.js +293 -197
- package/src/secure_context.js +86 -42
- package/src/session/ecdh.js +140 -122
- package/src/session/ech.js +92 -32
- package/src/session/hpke.js +12 -44
- package/src/session/message.js +29 -9
- package/src/session/signing.js +80 -22
- package/src/session/ticket.js +5 -3
- package/src/session/x509.js +385 -0
- package/src/tls_session.js +2214 -895
- package/src/tls_socket.js +451 -452
- package/src/wire.js +271 -158
package/src/compat.js
CHANGED
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import net from 'node:net';
|
|
9
|
-
import
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
87
|
-
_rootPems = certs.slice();
|
|
69
|
+
setRootCertificates(certs);
|
|
88
70
|
}
|
|
89
71
|
|
|
90
72
|
// ===================== tls.checkServerIdentity() =====================
|
|
91
73
|
|
|
92
74
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
if (!
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
}
|
|
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)'));
|
|
131
99
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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}`);
|
|
169
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
495
|
+
if (!established) {
|
|
544
496
|
self.emit('tlsClientError', err, socket);
|
|
545
497
|
}
|
|
546
498
|
});
|
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
|
|
612
|
-
let
|
|
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.
|
|
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
|
|
848
|
-
*
|
|
849
|
-
*
|
|
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
|
-
|
|
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
|
/**
|