lemon-tls 0.7.0 → 0.9.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 +253 -20
- package/index.d.ts +94 -6
- package/index.js +126 -0
- package/package.json +1 -1
- package/src/compat.js +316 -45
- package/src/crypto.js +19 -0
- package/src/dtls_session.js +62 -6
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +72 -5
- package/src/secure_context.js +49 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +39 -4
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +38 -25
- package/src/tls_session.js +2825 -195
- package/src/tls_socket.js +839 -148
- package/src/utils.js +68 -1
- package/src/wire.js +148 -9
package/src/tls_socket.js
CHANGED
|
@@ -4,10 +4,25 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import { Duplex } from 'node:stream';
|
|
5
5
|
|
|
6
6
|
import { TLS_CIPHER_SUITES, default_cipher_suites } from './crypto.js';
|
|
7
|
-
import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT } from './wire.js';
|
|
8
|
-
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS } from './session/ecdh.js';
|
|
7
|
+
import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT, HANDSHAKE_MESSAGE_MAX } from './wire.js';
|
|
8
|
+
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS, SERVER_DEFAULT_GROUPS, group_name as ecdh_group_name } from './session/ecdh.js';
|
|
9
9
|
import { default_signature_schemes } from './session/signing.js';
|
|
10
|
-
import {
|
|
10
|
+
import { certificateObject } from './utils.js';
|
|
11
|
+
|
|
12
|
+
// The suite list a socket offers or accepts, from the defaults: ChaCha20
|
|
13
|
+
// suites moved to the front when asked (prioritizeChaCha), then narrowed to an
|
|
14
|
+
// allow-list. One function for the client and server paths, which each kept a
|
|
15
|
+
// copy — and both copies knew ChaCha20 by two codes (0x1303, 0xCCA8), so
|
|
16
|
+
// ECDHE_ECDSA_WITH_CHACHA20_POLY1305 (0xCCA9) was never moved forward. The
|
|
17
|
+
// suite table says which suites are ChaCha20.
|
|
18
|
+
function chooseCipherSuites(ciphers, prioritizeChaCha, allowed) {
|
|
19
|
+
if (prioritizeChaCha) {
|
|
20
|
+
let isChaCha = function (c) { return !!TLS_CIPHER_SUITES[c] && TLS_CIPHER_SUITES[c].cipher === 'CHACHA20_POLY1305'; };
|
|
21
|
+
ciphers = ciphers.filter(isChaCha).concat(ciphers.filter(function (c) { return !isChaCha(c); }));
|
|
22
|
+
}
|
|
23
|
+
if (allowed) ciphers = ciphers.filter(function (c) { return allowed.includes(c); });
|
|
24
|
+
return ciphers;
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
import {
|
|
13
28
|
getAeadAlgo,
|
|
@@ -19,13 +34,21 @@ import {
|
|
|
19
34
|
decryptRecord as decrypt_tls_record,
|
|
20
35
|
decryptRecordWithAadView as decrypt_tls_record_with_aad, // zero-alloc AAD path
|
|
21
36
|
parseInnerPlaintext as parse_tls_inner_plaintext,
|
|
22
|
-
encrypt12 as
|
|
37
|
+
encrypt12 as encrypt_tls12_fragment,
|
|
23
38
|
encryptCompleteRecord12, // fused encrypt + header for TLS 1.2 app-data hot path
|
|
24
|
-
decrypt12 as
|
|
39
|
+
decrypt12 as decrypt_tls12_fragment,
|
|
25
40
|
deriveKeys12,
|
|
26
41
|
writeRecord as writeRawRecord,
|
|
27
42
|
} from './record.js';
|
|
28
43
|
|
|
44
|
+
// What a ticket of ours lets a client send early over TCP, by default: room
|
|
45
|
+
// for a request (an HTTP/2 preface, SETTINGS and HEADERS; an HTTP/1.1 GET),
|
|
46
|
+
// not for an upload — what does not fit goes after the handshake.
|
|
47
|
+
const EARLY_DATA_MAX_TCP = 16384;
|
|
48
|
+
|
|
49
|
+
// Handshake messages: refused on sight past HANDSHAKE_MESSAGE_MAX (wire.js) —
|
|
50
|
+
// before their bytes are kept.
|
|
51
|
+
|
|
29
52
|
// legacy_record_version (TLS 1.3 uses 0x0303 in record header)
|
|
30
53
|
const REC_VERSION = 0x0303;
|
|
31
54
|
|
|
@@ -75,11 +98,19 @@ function parseVersion(v) {
|
|
|
75
98
|
|
|
76
99
|
|
|
77
100
|
// ==== TLSSocket ====
|
|
101
|
+
|
|
78
102
|
function TLSSocket(duplex, options){
|
|
79
103
|
if (!(this instanceof TLSSocket)) return new TLSSocket(duplex, options);
|
|
80
104
|
options = options || {};
|
|
81
105
|
|
|
82
|
-
//
|
|
106
|
+
// allowHalfOpen follows Node: false unless the caller asks otherwise, so
|
|
107
|
+
// that when the peer stops writing our writable side ends too. This was
|
|
108
|
+
// hardcoded true, which is the OPPOSITE of the documented default — code
|
|
109
|
+
// written against Node's semantics waits for a close that never arrives and
|
|
110
|
+
// the connection just sits there, looking like a leak with no clue as to
|
|
111
|
+
// why. Node also notes the option has no effect when `socket` is supplied,
|
|
112
|
+
// since the underlying stream's own setting governs.
|
|
113
|
+
//
|
|
83
114
|
// highWaterMark: raised to 256KB from the Node default of 16KB. TLS records max
|
|
84
115
|
// at 16KB of plaintext, so the default meant "pause after one record" — terrible
|
|
85
116
|
// for bulk transfers where we want many records in flight. 256KB keeps ~16 records
|
|
@@ -87,15 +118,41 @@ function TLSSocket(duplex, options){
|
|
|
87
118
|
// small bursts interrupted by drain events. Most impactful on the Download path
|
|
88
119
|
// where the sender is aggressive (tight-loop sock.write).
|
|
89
120
|
Duplex.call(this, {
|
|
90
|
-
allowHalfOpen: true,
|
|
121
|
+
allowHalfOpen: options.allowHalfOpen === true,
|
|
122
|
+
// 'close' is emitted by this socket itself, when its transport closes —
|
|
123
|
+
// as it always has. Left on, autoDestroy would add a second 'close' of its
|
|
124
|
+
// own once the readable side can end (it now does, see endReadable).
|
|
125
|
+
autoDestroy: false,
|
|
91
126
|
readableObjectMode: false,
|
|
92
127
|
writableObjectMode: false,
|
|
93
|
-
highWaterMark: 256 * 1024,
|
|
128
|
+
highWaterMark: options.highWaterMark || 256 * 1024,
|
|
94
129
|
});
|
|
95
130
|
const self = this;
|
|
96
131
|
|
|
97
132
|
let _ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
|
|
98
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Read a credential out of a caller-supplied `secureContext` (Node parity).
|
|
136
|
+
*
|
|
137
|
+
* tls.connect() accepts a context built once by createSecureContext() and
|
|
138
|
+
* reused across connections, so the same PEM is not re-parsed per socket.
|
|
139
|
+
* Ours returns { certificateChain, privateKey, ca }, so map those onto the
|
|
140
|
+
* cert/key/ca the session expects. Direct options always win, matching
|
|
141
|
+
* Node's precedence.
|
|
142
|
+
*/
|
|
143
|
+
let _sc = function(what){
|
|
144
|
+
let sc = options.secureContext;
|
|
145
|
+
if (!sc) return null;
|
|
146
|
+
if (what === 'ca') return sc.ca && sc.ca.length ? sc.ca : null;
|
|
147
|
+
if (what === 'key') return sc.privateKey || null;
|
|
148
|
+
if (what === 'cert') {
|
|
149
|
+
if (!sc.certificateChain || sc.certificateChain.length === 0) return null;
|
|
150
|
+
// The session takes a single cert/PEM here; hand it the leaf.
|
|
151
|
+
return sc.certificateChain[0].cert || sc.certificateChain[0];
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
};
|
|
155
|
+
|
|
99
156
|
let context = {
|
|
100
157
|
|
|
101
158
|
options: options,
|
|
@@ -110,17 +167,57 @@ function TLSSocket(duplex, options){
|
|
|
110
167
|
ALPNProtocols: options.ALPNProtocols || null,
|
|
111
168
|
SNICallback: options.SNICallback || null,
|
|
112
169
|
ticketKeys: _ticketKeys,
|
|
170
|
+
// A session ID only where something stores the session ('newSession').
|
|
171
|
+
sessionIdStore: function() { return self.listenerCount('newSession') > 0; },
|
|
113
172
|
ticketLifetime: options.ticketLifetime,
|
|
173
|
+
ticketData: options.ticketData || null,
|
|
174
|
+
certificateCompression: options.certificateCompression,
|
|
175
|
+
honorCipherOrder: options.honorCipherOrder === true,
|
|
176
|
+
// Encrypted Client Hello: server — the keys, { keys: [{ config,
|
|
177
|
+
// privateKey, retry }] }; client — the ECHConfigList to encrypt the
|
|
178
|
+
// real ClientHello to, and echGrease (GREASE ECH without one).
|
|
179
|
+
ech: options.ech,
|
|
180
|
+
echGrease: options.echGrease === true,
|
|
181
|
+
// OCSP stapling (RFC 6066): server — (certificate, issuer, cb), what
|
|
182
|
+
// tls.Server's 'OCSPRequest' event is wired to; client — ask for it.
|
|
183
|
+
ocspRequest: typeof options.ocspRequest === 'function' ? options.ocspRequest : null,
|
|
184
|
+
requestOCSP: options.requestOCSP === true,
|
|
114
185
|
session: options.session || null,
|
|
115
186
|
psk: options.psk || null,
|
|
116
187
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
117
|
-
|
|
188
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
189
|
+
ALPNCallback: options.ALPNCallback || null,
|
|
190
|
+
ca: options.ca || _sc('ca') || null,
|
|
191
|
+
// The platform trust store, for when no `ca` is given. compat.js
|
|
192
|
+
// hands both to this socket (defaultCA: Node's root certificates);
|
|
193
|
+
// they were never passed on, so the session had no trust anchors at
|
|
194
|
+
// all, and with none it checks only the name in the certificate. A
|
|
195
|
+
// self-signed certificate for the right name — anyone can make one —
|
|
196
|
+
// was accepted as verified: every connection this library opened
|
|
197
|
+
// (quico.request, WebSocket over wss://) was open to a
|
|
198
|
+
// man-in-the-middle, with rejectUnauthorized true.
|
|
199
|
+
useDefaultCA: options.useDefaultCA,
|
|
200
|
+
defaultCA: options.defaultCA,
|
|
118
201
|
sessionTickets: options.sessionTickets,
|
|
119
202
|
maxHandshakeSize: options.maxHandshakeSize || 0,
|
|
120
203
|
customExtensions: options.customExtensions || [],
|
|
204
|
+
permuteExtensions: !!options.permuteExtensions,
|
|
205
|
+
grease: !!options.grease,
|
|
121
206
|
requestCert: !!options.requestCert,
|
|
122
|
-
cert: options.cert || null,
|
|
123
|
-
key: options.key || null,
|
|
207
|
+
cert: options.cert || _sc('cert') || null,
|
|
208
|
+
key: options.key || _sc('key') || null,
|
|
209
|
+
passphrase: options.passphrase,
|
|
210
|
+
// 0-RTT over TCP (RFC 8446 §4.2.10, §4.5). Server: tickets that allow
|
|
211
|
+
// it (maxEarlyData bytes, 16 KB by default — a request, not an
|
|
212
|
+
// upload) and taking it, under the session's checks. Client: with a
|
|
213
|
+
// session whose ticket allows it, what is written before the
|
|
214
|
+
// handshake ends goes in the first flight (see "Early data" below).
|
|
215
|
+
earlyData: options.earlyData === true && (!!options.isServer || !!options.session),
|
|
216
|
+
earlyDataMaxAge: options.earlyDataMaxAge,
|
|
217
|
+
earlyDataReplay: options.earlyDataReplay,
|
|
218
|
+
earlyDataContext: options.earlyDataContext,
|
|
219
|
+
maxEarlyData: typeof options.maxEarlyData === 'number' ? options.maxEarlyData : EARLY_DATA_MAX_TCP,
|
|
220
|
+
endOfEarlyData: true,
|
|
124
221
|
}),
|
|
125
222
|
|
|
126
223
|
// Handshake write
|
|
@@ -212,6 +309,15 @@ function TLSSocket(duplex, options){
|
|
|
212
309
|
hsBuf: null,
|
|
213
310
|
|
|
214
311
|
appWriteQueue: [],
|
|
312
|
+
|
|
313
|
+
// 0-RTT (see "Early data"). Client: the early write key, what it may
|
|
314
|
+
// still send early (max − sent), and copies of what went, to send again
|
|
315
|
+
// if the server does not take it. Server: the early read key, what came
|
|
316
|
+
// early (recv), what was skipped of early data not taken, and whether
|
|
317
|
+
// it may answer before the client's Finished (srvWrite: 0.5-RTT).
|
|
318
|
+
early: { status: null, write: null, read: null, max: 0, sent: 0, copies: [], helloSent: false,
|
|
319
|
+
recv: 0, skipped: 0, trialDone: false, eoed: false, srvWrite: false },
|
|
320
|
+
parsing: false,
|
|
215
321
|
pendingHandshake: [],
|
|
216
322
|
|
|
217
323
|
// General state
|
|
@@ -237,6 +343,22 @@ function TLSSocket(duplex, options){
|
|
|
237
343
|
|
|
238
344
|
let session = context.session;
|
|
239
345
|
|
|
346
|
+
// A server's own key/cert given up front. They went into the session as
|
|
347
|
+
// cert/key — the CLIENT credentials it sends when a server asks for one —
|
|
348
|
+
// and never became the certificate this server presents. createServer()
|
|
349
|
+
// hid that, since every certificate there comes through SNICallback; a
|
|
350
|
+
// server socket built directly (`new TLSSocket(socket, { isServer, key,
|
|
351
|
+
// cert })`, which is what every STARTTLS upgrade does) presented nothing
|
|
352
|
+
// and failed the handshake. Direct options win over secureContext, as in
|
|
353
|
+
// Node. SNICallback, when it answers, still replaces this per name.
|
|
354
|
+
if (options.isServer) {
|
|
355
|
+
if (options.key && options.cert) {
|
|
356
|
+
session.setKeyCert({ key: options.key, cert: options.cert, passphrase: options.passphrase });
|
|
357
|
+
} else if (options.secureContext) {
|
|
358
|
+
session.setKeyCert(options.secureContext);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
240
362
|
|
|
241
363
|
// === Record Layer ===
|
|
242
364
|
// writeRecord returns the transport's backpressure signal (true = ready for more,
|
|
@@ -244,10 +366,32 @@ function TLSSocket(duplex, options){
|
|
|
244
366
|
function writeRecord(type, payload){
|
|
245
367
|
if (!context.transport) throw new Error('No transport attached to TLSSocket');
|
|
246
368
|
if (context.destroyed || context.transport.destroyed || context.transport.writableEnded) return false;
|
|
369
|
+
coalesceFlight();
|
|
247
370
|
try { return writeRawRecord(context.transport, type, payload, context.rec_version); }
|
|
248
371
|
catch(e){ self.emit('error', e); return false; }
|
|
249
372
|
}
|
|
250
373
|
|
|
374
|
+
// One flight, one send. A handshake flight is several records written one
|
|
375
|
+
// after another in the same tick — ServerHello, ChangeCipherSpec,
|
|
376
|
+
// EncryptedExtensions, Certificate, CertificateVerify, Finished. Written
|
|
377
|
+
// one by one, each is a send of its own; on a socket where Nagle is on
|
|
378
|
+
// (net.Socket's default, and tls.createServer's), everything after the
|
|
379
|
+
// first then waits for the peer's ACK: a full round trip, and TLS 1.3's
|
|
380
|
+
// one-round-trip handshake takes two. node:tls writes a flight at once.
|
|
381
|
+
// Corked until the end of the tick, the records leave as one writev.
|
|
382
|
+
let corkedTransport = null;
|
|
383
|
+
function coalesceFlight(){
|
|
384
|
+
const t = context.transport;
|
|
385
|
+
if (!t || corkedTransport === t || t.destroyed) return;
|
|
386
|
+
if (typeof t.cork !== 'function' || typeof t.uncork !== 'function') return;
|
|
387
|
+
corkedTransport = t;
|
|
388
|
+
t.cork();
|
|
389
|
+
process.nextTick(function(){
|
|
390
|
+
if (corkedTransport === t) corkedTransport = null;
|
|
391
|
+
try { t.uncork(); } catch(e) {}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
251
395
|
const MAX_RECORD_PLAINTEXT = 16384; // TLS max record size (2^14)
|
|
252
396
|
|
|
253
397
|
// writeAppData / writeAppDataSingle return the transport's backpressure signal:
|
|
@@ -347,7 +491,10 @@ function TLSSocket(duplex, options){
|
|
|
347
491
|
}
|
|
348
492
|
}
|
|
349
493
|
|
|
350
|
-
|
|
494
|
+
// trial: early data not taken may come before the client's handshake
|
|
495
|
+
// flight (RFC 8446 §4.2.10): a record that the handshake key does not
|
|
496
|
+
// open is that, and is skipped (false) rather than fatal.
|
|
497
|
+
function processCiphertext(body, header, trial){
|
|
351
498
|
// Hot path — same optimizations as writeAppDataSingle:
|
|
352
499
|
// - cached context.isTls13 instead of per-record session.getVersion()
|
|
353
500
|
// - cached context.aeadAlgo instead of per-record getAeadAlgo()
|
|
@@ -390,7 +537,7 @@ function TLSSocket(duplex, options){
|
|
|
390
537
|
}
|
|
391
538
|
|
|
392
539
|
const recordType = context.using_app_keys ? 0x17 : 0x16;
|
|
393
|
-
out =
|
|
540
|
+
out = decrypt_tls12_fragment(body, context.app_read_key, context.app_read_iv, context.tls12_read_seq, recordType);
|
|
394
541
|
|
|
395
542
|
} else if (isTls13 && context.using_app_keys === true) {
|
|
396
543
|
// TLS 1.3: decrypt app data with app traffic secret
|
|
@@ -435,6 +582,7 @@ function TLSSocket(duplex, options){
|
|
|
435
582
|
context.handshake_read_seq++;
|
|
436
583
|
}
|
|
437
584
|
} catch (e) {
|
|
585
|
+
if (trial) return false;
|
|
438
586
|
// Record failed authentication → fatal bad_record_mac (RFC 8446 §5.2).
|
|
439
587
|
try { session.sendAlert ? session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC) : null; } catch (_) {}
|
|
440
588
|
self.emit('error', new Error('TLS record decryption failed (bad_record_mac)'));
|
|
@@ -461,23 +609,172 @@ function TLSSocket(duplex, options){
|
|
|
461
609
|
|
|
462
610
|
if (content_type === CT.APPLICATION_DATA) {
|
|
463
611
|
// zero-copy view into the decrypted plaintext
|
|
464
|
-
|
|
612
|
+
pushAppData(out.subarray(0, j));
|
|
465
613
|
} else if (content_type === CT.HANDSHAKE) {
|
|
466
614
|
feedHandshakeBytes(out.subarray(0, j));
|
|
467
615
|
} else if (content_type === CT.ALERT) {
|
|
468
|
-
//
|
|
616
|
+
// TLS 1.3 alerts after ServerHello are encrypted — a client
|
|
617
|
+
// refusing our certificate always sends its alert this way.
|
|
618
|
+
// They were decrypted and dropped here.
|
|
619
|
+
handleAlert(out.subarray(0, j));
|
|
469
620
|
}
|
|
470
621
|
} else {
|
|
471
622
|
// TLS 1.2: no inner plaintext wrapping — `out` is the Buffer from decrypt12
|
|
472
623
|
if (context.using_app_keys === true) {
|
|
473
|
-
|
|
624
|
+
pushAppData(out);
|
|
474
625
|
} else {
|
|
475
626
|
feedHandshakeBytes(out);
|
|
476
627
|
}
|
|
477
628
|
}
|
|
478
629
|
}
|
|
630
|
+
if (trial && out !== null) context.early.trialDone = true;
|
|
631
|
+
return out !== null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
// ============================================================================
|
|
636
|
+
// Early data (0-RTT over TCP, RFC 8446 §4.2.10, §4.5)
|
|
637
|
+
// ============================================================================
|
|
638
|
+
//
|
|
639
|
+
// The session decides, and hands out the key ('earlySecret'); the records
|
|
640
|
+
// are this layer's. A client sends, after its ClientHello and the
|
|
641
|
+
// compatibility ChangeCipherSpec (§D.4: right after the first hello when
|
|
642
|
+
// offering early data), what was written before the handshake ends — up
|
|
643
|
+
// to the ticket's max_early_data_size — under the early key; then, the
|
|
644
|
+
// server having taken it, EndOfEarlyData under the same key, first in its
|
|
645
|
+
// second flight. Not taken, the same bytes go again after the handshake,
|
|
646
|
+
// as if written then: the server never processed them.
|
|
647
|
+
//
|
|
648
|
+
// A server reads the records after the ClientHello as follows: before
|
|
649
|
+
// the decision (an asynchronous replay check), they wait in the buffer;
|
|
650
|
+
// taken, they are opened with the early key and given to the reader as
|
|
651
|
+
// they come, before 'secure' (isEarlyData true), until EndOfEarlyData;
|
|
652
|
+
// not taken, each record that the handshake key does not open is early
|
|
653
|
+
// data and is skipped; after a HelloRetryRequest, every application_data
|
|
654
|
+
// record until the second ClientHello is. Skipped or taken, no more than
|
|
655
|
+
// maxEarlyData bytes (unexpected_message past it). Taken, the server may
|
|
656
|
+
// answer as soon as its Finished is out (0.5-RTT), not only once the
|
|
657
|
+
// client's has come.
|
|
658
|
+
|
|
659
|
+
function serverEarlyMode() {
|
|
660
|
+
const sc = session.context;
|
|
661
|
+
if (!sc || !sc.early || !sc.early.clientOffered || context.early.eoed || context.early.trialDone) return 'none';
|
|
662
|
+
if (sc.helloRetried) return sc.client_hellos_received < 2 ? 'skip' : 'none';
|
|
663
|
+
if (sc.early.status === 'accepted') return 'accepted';
|
|
664
|
+
if (sc.early.offered && sc.early.status === null) return 'pending';
|
|
665
|
+
return 'trial';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function earlyLimit() {
|
|
669
|
+
return typeof options.maxEarlyData === 'number' ? options.maxEarlyData : EARLY_DATA_MAX_TCP;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function earlyTooMuch() {
|
|
673
|
+
try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
|
|
674
|
+
self.emit('error', new Error('More early data than max_early_data_size'));
|
|
675
|
+
markTransportDead();
|
|
676
|
+
closeAfterAlert();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// A record of early data not taken: skipped, counted (its plaintext at
|
|
680
|
+
// most: the record less the tag and the content type).
|
|
681
|
+
function skipEarly(len) {
|
|
682
|
+
context.early.skipped += Math.max(0, len - 17);
|
|
683
|
+
if (context.early.skipped > earlyLimit()) { earlyTooMuch(); return false; }
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function processEarlyRecord(body, header) {
|
|
688
|
+
const r = context.early.read;
|
|
689
|
+
let out;
|
|
690
|
+
try {
|
|
691
|
+
out = decrypt_tls_record_with_aad(header, body, r.key, get_nonce_into(context._nonceDecScratch, r.iv, r.seq), r.algo);
|
|
692
|
+
} catch (e) {
|
|
693
|
+
try { session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC); } catch (_) {}
|
|
694
|
+
self.emit('error', new Error('TLS early data record decryption failed (bad_record_mac)'));
|
|
695
|
+
self.destroy();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
r.seq++;
|
|
699
|
+
let j = out.length - 1;
|
|
700
|
+
while (j >= 0 && out[j] === 0) j--;
|
|
701
|
+
if (j < 0) { self.emit('error', new Error('Malformed TLSInnerPlaintext')); return; }
|
|
702
|
+
const ct = out[j];
|
|
703
|
+
if (ct === CT.APPLICATION_DATA) {
|
|
704
|
+
context.early.recv += j;
|
|
705
|
+
if (context.early.recv > earlyLimit()) { earlyTooMuch(); return; }
|
|
706
|
+
if (j > 0) pushAppData(out.subarray(0, j));
|
|
707
|
+
} else if (ct === CT.HANDSHAKE) {
|
|
708
|
+
feedHandshakeBytes(out.subarray(0, j)); // EndOfEarlyData, and nothing else (the session checks)
|
|
709
|
+
} else if (ct === CT.ALERT) {
|
|
710
|
+
handleAlert(out.subarray(0, j));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Client: what is queued goes early, as much as the ticket allows.
|
|
715
|
+
function flushEarly() {
|
|
716
|
+
const e = context.early;
|
|
717
|
+
if (!e.write || e.status !== 'offered' || !e.helloSent) return;
|
|
718
|
+
coalesceFlight();
|
|
719
|
+
while (context.appWriteQueue.length > 0 && e.sent < e.max) {
|
|
720
|
+
let buf = context.appWriteQueue.shift();
|
|
721
|
+
const room = e.max - e.sent;
|
|
722
|
+
if (buf.length > room) { context.appWriteQueue.unshift(buf.subarray(room)); buf = buf.subarray(0, room); }
|
|
723
|
+
for (let off = 0; off < buf.length; off += MAX_RECORD_PLAINTEXT) writeEarlyRecord(CT.APPLICATION_DATA, buf.subarray(off, Math.min(buf.length, off + MAX_RECORD_PLAINTEXT)));
|
|
724
|
+
e.copies.push(buf);
|
|
725
|
+
e.sent += buf.length;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function writeEarlyRecord(innerType, plain) {
|
|
730
|
+
const w = context.early.write;
|
|
731
|
+
if (!w || !canWrite()) return;
|
|
732
|
+
try {
|
|
733
|
+
const rec = encryptCompleteRecord13(innerType, plain, w.key, get_nonce_into(context._nonceEncScratch, w.iv, w.seq), w.algo, context.rec_version);
|
|
734
|
+
w.seq++;
|
|
735
|
+
context.transport.write(rec);
|
|
736
|
+
} catch (err) { self.emit('error', err); }
|
|
479
737
|
}
|
|
480
738
|
|
|
739
|
+
session.on('earlySecret', function(secret, info){
|
|
740
|
+
if (session.isServer) {
|
|
741
|
+
const cs = session.getCipher();
|
|
742
|
+
const d = tls_derive_from_tls_secrets(secret, cs);
|
|
743
|
+
context.early.read = { key: d.key, iv: d.iv, seq: 0, algo: getAeadAlgo(cs) };
|
|
744
|
+
} else {
|
|
745
|
+
const cs = info && info.cipher;
|
|
746
|
+
if (!cs || !TLS_CIPHER_SUITES[cs]) return;
|
|
747
|
+
const d = tls_derive_from_tls_secrets(secret, cs);
|
|
748
|
+
context.early.write = { key: d.key, iv: d.iv, seq: 0, algo: getAeadAlgo(cs) };
|
|
749
|
+
context.early.max = Math.min((info.maxEarlyData >>> 0) || 0, 0x7fffffff);
|
|
750
|
+
context.early.status = 'offered';
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
session.on('earlyData', function(accepted){
|
|
755
|
+
const e = context.early;
|
|
756
|
+
e.status = accepted ? 'accepted' : 'rejected';
|
|
757
|
+
if (session.isServer) {
|
|
758
|
+
if (!accepted) e.read = null;
|
|
759
|
+
// Records held for the decision: read now (the decision may come
|
|
760
|
+
// from an asynchronous replay check, outside any read).
|
|
761
|
+
if (!context.parsing) process.nextTick(function(){ if (!context.destroyed) parseRecordsAndDispatch(); });
|
|
762
|
+
} else if (!accepted) {
|
|
763
|
+
// Not taken: written again after the handshake, ahead of what
|
|
764
|
+
// waited behind it.
|
|
765
|
+
e.write = null;
|
|
766
|
+
if (e.copies.length) context.appWriteQueue = e.copies.concat(context.appWriteQueue);
|
|
767
|
+
e.copies = [];
|
|
768
|
+
} else {
|
|
769
|
+
e.copies = [];
|
|
770
|
+
}
|
|
771
|
+
self.emit('earlyData', accepted);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
session.on('endOfEarlyData', function(){
|
|
775
|
+
context.early.eoed = true;
|
|
776
|
+
context.early.read = null;
|
|
777
|
+
});
|
|
481
778
|
|
|
482
779
|
// ============================================================================
|
|
483
780
|
// Read buffer management
|
|
@@ -558,6 +855,15 @@ function TLSSocket(duplex, options){
|
|
|
558
855
|
|
|
559
856
|
while (context.hsBuf.length >= 4) {
|
|
560
857
|
const len = (context.hsBuf[1] << 16) | (context.hsBuf[2] << 8) | context.hsBuf[3];
|
|
858
|
+
const maxMsg = options.maxHandshakeSize > 0 ? Math.min(options.maxHandshakeSize, HANDSHAKE_MESSAGE_MAX) : HANDSHAKE_MESSAGE_MAX;
|
|
859
|
+
if (len > maxMsg) {
|
|
860
|
+
// OpenSSL's answer to an excessive message: illegal_parameter.
|
|
861
|
+
context.hsBuf = null;
|
|
862
|
+
try { session.sendAlert(2, TLS_ALERT.ILLEGAL_PARAMETER); } catch (_) {}
|
|
863
|
+
self.emit('error', new Error('TLS handshake message of ' + len + ' bytes, more than ' + maxMsg));
|
|
864
|
+
self.destroy();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
561
867
|
if (context.hsBuf.length < 4 + len) break; // partial message — wait for more records
|
|
562
868
|
|
|
563
869
|
const msg = context.hsBuf.subarray(0, 4 + len);
|
|
@@ -572,6 +878,11 @@ function TLSSocket(duplex, options){
|
|
|
572
878
|
}
|
|
573
879
|
|
|
574
880
|
function parseRecordsAndDispatch(){
|
|
881
|
+
if (context.parsing) return;
|
|
882
|
+
context.parsing = true;
|
|
883
|
+
try { parseRecords(); } finally { context.parsing = false; }
|
|
884
|
+
}
|
|
885
|
+
function parseRecords(){
|
|
575
886
|
// Hot path — zero-copy parsing:
|
|
576
887
|
// - Work directly on readBuffer with moving readStart offset (no slicing)
|
|
577
888
|
// - Body is a subarray view (no copy) passed down to crypto
|
|
@@ -602,9 +913,39 @@ function TLSSocket(duplex, options){
|
|
|
602
913
|
}
|
|
603
914
|
|
|
604
915
|
const type = rb[off];
|
|
605
|
-
// Version at rb[off+1..off+3] — currently unused (legacy 0x0303 always)
|
|
606
916
|
const len = (rb[off + 3] << 8) | rb[off + 4];
|
|
607
917
|
|
|
918
|
+
// ── Record header, checked as soon as its 5 bytes are here ──
|
|
919
|
+
// Not once the whole record has arrived: a header that is not TLS
|
|
920
|
+
// claims a length from whatever bytes sit there — "GET /" asks for
|
|
921
|
+
// 8 239 — and the socket waited for a body that never comes. Plain
|
|
922
|
+
// HTTP sent to the TLS port, or a scanner, held a connection open
|
|
923
|
+
// in silence; OpenSSL rejects on the header ("wrong version
|
|
924
|
+
// number"), and so does this now, with node:tls's codes.
|
|
925
|
+
// RFC 8446 §5.1: legacy_record_version is 0x03xx, ContentType
|
|
926
|
+
// one of the four; §5.2: no record beyond 2^14 + 256 bytes of
|
|
927
|
+
// ciphertext (2^14 + 2048 in TLS 1.2, RFC 5246 §6.2.3).
|
|
928
|
+
{
|
|
929
|
+
const recVer = (rb[off + 1] << 8) | rb[off + 2];
|
|
930
|
+
let bad = null, alert = 0;
|
|
931
|
+
if ((recVer >>> 8) !== 0x03 || type < CT.CHANGE_CIPHER_SPEC || type > CT.APPLICATION_DATA) {
|
|
932
|
+
bad = new Error('wrong version number: not a TLS record (type 0x' + type.toString(16) + ', version 0x' + recVer.toString(16) + ')');
|
|
933
|
+
bad.code = 'ERR_SSL_WRONG_VERSION_NUMBER';
|
|
934
|
+
alert = (recVer >>> 8) !== 0x03 ? TLS_ALERT.PROTOCOL_VERSION : TLS_ALERT.UNEXPECTED_MESSAGE;
|
|
935
|
+
} else if (len > 16384 + 2048) {
|
|
936
|
+
bad = new Error('record overflow: ' + len + ' bytes');
|
|
937
|
+
bad.code = 'ERR_SSL_RECORD_OVERFLOW';
|
|
938
|
+
alert = TLS_ALERT.RECORD_OVERFLOW;
|
|
939
|
+
}
|
|
940
|
+
if (bad) {
|
|
941
|
+
try { session.sendAlert(2, alert); } catch (e) {}
|
|
942
|
+
self.emit('error', bad);
|
|
943
|
+
markTransportDead();
|
|
944
|
+
closeAfterAlert();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
608
949
|
if (end - off < 5 + len) break; // record not fully received
|
|
609
950
|
|
|
610
951
|
// Zero-copy views: header (5 bytes) + body
|
|
@@ -612,20 +953,6 @@ function TLSSocket(duplex, options){
|
|
|
612
953
|
const body = rb.subarray(off + 5, off + 5 + len);
|
|
613
954
|
off += 5 + len;
|
|
614
955
|
|
|
615
|
-
// ── Record-layer sanity, applied to every record ──
|
|
616
|
-
// RFC 8446 §5.1: the legacy_record_version is 0x0303 for every
|
|
617
|
-
// record after the first ClientHello, and 0x03xx before it. A
|
|
618
|
-
// record claiming anything else is malformed framing, not a
|
|
619
|
-
// version negotiation signal.
|
|
620
|
-
const recVer = (rb[off - len - 5 + 1] << 8) | rb[off - len - 5 + 2];
|
|
621
|
-
if ((recVer >>> 8) !== 0x03) {
|
|
622
|
-
try { session.sendAlert(2, TLS_ALERT.PROTOCOL_VERSION); } catch (e) {}
|
|
623
|
-
self.emit('error', new Error('Record with illegal legacy_record_version 0x' + recVer.toString(16)));
|
|
624
|
-
markTransportDead();
|
|
625
|
-
if (context.transport && !context.transport.destroyed) context.transport.destroy();
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
956
|
// A peer may legally send an empty record, and a TLS 1.3 peer may
|
|
630
957
|
// legally send a compat CCS — but only finitely many. Unbounded
|
|
631
958
|
// streams of either make no progress and exist only to burn our
|
|
@@ -635,7 +962,7 @@ function TLSSocket(duplex, options){
|
|
|
635
962
|
try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
|
|
636
963
|
self.emit('error', new Error('Too many consecutive empty records'));
|
|
637
964
|
markTransportDead();
|
|
638
|
-
|
|
965
|
+
closeAfterAlert();
|
|
639
966
|
return;
|
|
640
967
|
}
|
|
641
968
|
} else {
|
|
@@ -647,13 +974,20 @@ function TLSSocket(duplex, options){
|
|
|
647
974
|
try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
|
|
648
975
|
self.emit('error', new Error('Too many consecutive ChangeCipherSpec records'));
|
|
649
976
|
markTransportDead();
|
|
650
|
-
|
|
977
|
+
closeAfterAlert();
|
|
651
978
|
return;
|
|
652
979
|
}
|
|
653
980
|
} else {
|
|
654
981
|
context.consecutive_ccs_records = 0;
|
|
655
982
|
}
|
|
656
983
|
|
|
984
|
+
if (type === CT.APPLICATION_DATA && session.isServer && !context.using_app_keys && context.isTls13 !== false) {
|
|
985
|
+
const em = serverEarlyMode();
|
|
986
|
+
if (em === 'pending') { off -= 5 + len; break; } // the decision first; this record waits
|
|
987
|
+
if (em === 'skip') { if (!skipEarly(len)) return; continue; }
|
|
988
|
+
if (em === 'accepted') { processEarlyRecord(body, header); if (context.destroyed) return; continue; }
|
|
989
|
+
if (em === 'trial') { if (!processCiphertext(body, header, true) && !skipEarly(len)) return; continue; }
|
|
990
|
+
}
|
|
657
991
|
if (type === CT.APPLICATION_DATA) {
|
|
658
992
|
processCiphertext(body, header);
|
|
659
993
|
context.tls12_read_seq++;
|
|
@@ -703,23 +1037,7 @@ function TLSSocket(duplex, options){
|
|
|
703
1037
|
}
|
|
704
1038
|
// TLS 1.3, or version not yet negotiated: swallow silently.
|
|
705
1039
|
} else if (type === CT.ALERT) {
|
|
706
|
-
|
|
707
|
-
if (body.length >= 2) {
|
|
708
|
-
const level = body[0];
|
|
709
|
-
const desc = body[1];
|
|
710
|
-
self.emit('alert', { level: level, description: desc });
|
|
711
|
-
if (desc === TLS_ALERT.CLOSE_NOTIFY) {
|
|
712
|
-
session.close();
|
|
713
|
-
if (context.transport && typeof context.transport.end === 'function') {
|
|
714
|
-
context.transport.end();
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
if (level === TLS_ALERT_LEVEL.FATAL) {
|
|
718
|
-
if (context.transport && typeof context.transport.destroy === 'function') {
|
|
719
|
-
context.transport.destroy();
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
1040
|
+
handleAlert(body);
|
|
723
1041
|
}
|
|
724
1042
|
}
|
|
725
1043
|
|
|
@@ -784,7 +1102,77 @@ function TLSSocket(duplex, options){
|
|
|
784
1102
|
return true;
|
|
785
1103
|
}
|
|
786
1104
|
|
|
1105
|
+
// An alert from the peer, cleartext or decrypted: 2 bytes, level and
|
|
1106
|
+
// description. close_notify ends our readable side; a fatal alert ends
|
|
1107
|
+
// the connection and is an 'error', as node:tls reports one ("alert bad
|
|
1108
|
+
// certificate") — without it the socket just closed and nothing learned
|
|
1109
|
+
// why: a server could not see that clients were refusing its certificate.
|
|
1110
|
+
function handleAlert(body) {
|
|
1111
|
+
if (!body || body.length < 2) return;
|
|
1112
|
+
const level = body[0];
|
|
1113
|
+
const desc = body[1];
|
|
1114
|
+
self.emit('alert', { level: level, description: desc });
|
|
1115
|
+
if (desc === TLS_ALERT.CLOSE_NOTIFY) {
|
|
1116
|
+
// The peer is done sending: end our readable side, so 'end' is
|
|
1117
|
+
// emitted once what came before it is read.
|
|
1118
|
+
endReadable();
|
|
1119
|
+
// Ours: TLS 1.2 answers at once (RFC 5246 §7.2.1: "respond with
|
|
1120
|
+
// a close_notify alert of its own and close down the connection
|
|
1121
|
+
// immediately"). TLS 1.3 does not — a close_notify closes the
|
|
1122
|
+
// sender's side only, "This does not have any effect on its read
|
|
1123
|
+
// side" (RFC 8446 §6.1, a change it calls out from earlier
|
|
1124
|
+
// versions): with allowHalfOpen we go on writing, and send ours
|
|
1125
|
+
// when we end(). Without it, the Duplex ends us now anyway — its
|
|
1126
|
+
// automatic end() after 'end' — and _final sends it. Answering
|
|
1127
|
+
// at once in TLS 1.3 cut off every response a server was still
|
|
1128
|
+
// writing to a client that had merely finished sending.
|
|
1129
|
+
if (session.getVersion() !== 0x0304 || !self.allowHalfOpen) sendCloseNotify();
|
|
1130
|
+
}
|
|
1131
|
+
if (level === TLS_ALERT_LEVEL.FATAL) {
|
|
1132
|
+
// Both layers inert first (markTransportDead: the session goes
|
|
1133
|
+
// terminal, so the reactive loop produces nothing more), then the
|
|
1134
|
+
// report — the order of session.on('error') below: a listener
|
|
1135
|
+
// reacting to it finds the session already over.
|
|
1136
|
+
markTransportDead();
|
|
1137
|
+
if (desc !== TLS_ALERT.CLOSE_NOTIFY) {
|
|
1138
|
+
let name = null;
|
|
1139
|
+
for (const k in TLS_ALERT) if (TLS_ALERT[k] === desc) { name = k.toLowerCase(); break; }
|
|
1140
|
+
const err = new Error('peer sent fatal alert: ' + (name || 'alert') + ' (' + desc + ')');
|
|
1141
|
+
err.code = 'ERR_TLS_ALERT_RECEIVED';
|
|
1142
|
+
err.alert = desc;
|
|
1143
|
+
self.emit('error', err);
|
|
1144
|
+
}
|
|
1145
|
+
// Nothing of ours to flush: the peer has ended the connection.
|
|
1146
|
+
if (context.transport && typeof context.transport.destroy === 'function') context.transport.destroy();
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Close the transport after we have sent a fatal alert: end() — the
|
|
1151
|
+
// alert, written just before, leaves first, then FIN — not destroy(),
|
|
1152
|
+
// which drops whatever is still buffered. It dropped the alert: neither
|
|
1153
|
+
// side of a refused handshake learned why (a server could not see that
|
|
1154
|
+
// clients were refusing its certificate). A peer that does not close its
|
|
1155
|
+
// side in time is cut off.
|
|
1156
|
+
const ALERT_LINGER_MS = 1000;
|
|
1157
|
+
function closeAfterAlert() {
|
|
1158
|
+
const t = context.transport;
|
|
1159
|
+
if (!t || t.destroyed) return;
|
|
1160
|
+
if (typeof t.end !== 'function') { if (typeof t.destroy === 'function') t.destroy(); return; }
|
|
1161
|
+
try { t.end(); } catch (e) { try { t.destroy(); } catch (e2) {} return; }
|
|
1162
|
+
const timer = setTimeout(function () { if (!t.destroyed) t.destroy(); }, ALERT_LINGER_MS);
|
|
1163
|
+
if (timer.unref) timer.unref();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// The handshake's deadline, gone with the handshake: cleared at 'secure'
|
|
1167
|
+
// and here, when the socket goes. It stayed set on a handshake that
|
|
1168
|
+
// failed — 120 s for every refused or abandoned one, holding the socket's
|
|
1169
|
+
// state in memory and keeping the process from exiting.
|
|
1170
|
+
function clearHandshakeTimer() {
|
|
1171
|
+
if (context.handshakeTimer) { clearTimeout(context.handshakeTimer); context.handshakeTimer = null; }
|
|
1172
|
+
}
|
|
1173
|
+
|
|
787
1174
|
function markTransportDead() {
|
|
1175
|
+
clearHandshakeTimer();
|
|
788
1176
|
if (context.destroyed) return;
|
|
789
1177
|
context.destroyed = true;
|
|
790
1178
|
try { session.close(); } catch (e) {}
|
|
@@ -792,16 +1180,67 @@ function TLSSocket(duplex, options){
|
|
|
792
1180
|
|
|
793
1181
|
function bindTransport(){
|
|
794
1182
|
if (!context.transport) return;
|
|
1183
|
+
// Take over the stream's reads, as node:tls does: once a socket is
|
|
1184
|
+
// wrapped, what arrives on it is TLS records and belongs to us alone.
|
|
1185
|
+
// A 'data' listener the caller had on it before — the plaintext
|
|
1186
|
+
// protocol reader of a STARTTLS upgrade — kept receiving every
|
|
1187
|
+
// encrypted record alongside us and fed it into that protocol's
|
|
1188
|
+
// parser. Under node:tls that listener simply goes quiet; code
|
|
1189
|
+
// written for it never removes it, and here it hung the connection.
|
|
1190
|
+
context.transport.removeAllListeners('data');
|
|
795
1191
|
context.transport.on('data', function(chunk){
|
|
796
1192
|
_appendChunk(chunk);
|
|
797
1193
|
parseRecordsAndDispatch();
|
|
798
1194
|
});
|
|
799
1195
|
context.transport.on('error', function(err){ markTransportDead(); self.emit('error', err); });
|
|
800
|
-
|
|
1196
|
+
// The transport's end of data: the readable side ends too — as Node's
|
|
1197
|
+
// TLSSocket does on EOF, close_notify or not. Before the handshake is
|
|
1198
|
+
// done it is the handshake's end: a peer that has sent its FIN can
|
|
1199
|
+
// send nothing more, so it cannot finish. It was left waiting — the
|
|
1200
|
+
// socket half-open (allowHalfOpen) until handshakeTimeout, 120 s by
|
|
1201
|
+
// default, for every client that connected, sent part of a hello and
|
|
1202
|
+
// closed. Reported as the close before the handshake is (below), and
|
|
1203
|
+
// the socket let go.
|
|
1204
|
+
context.transport.on('end', function(){
|
|
1205
|
+
if (!context.secureEstablished && !errorEmitted && !context.destroyed) {
|
|
1206
|
+
const e = new Error('Client network socket disconnected before secure TLS connection was established');
|
|
1207
|
+
e.code = 'ECONNRESET';
|
|
1208
|
+
self.destroy(e);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
endReadable();
|
|
1212
|
+
});
|
|
1213
|
+
context.transport.on('close', function(){
|
|
1214
|
+
const destroyedByUs = context.destroyed; // destroy() before the transport went
|
|
1215
|
+
markTransportDead();
|
|
1216
|
+
// Closed under us before the handshake finished, and nothing said
|
|
1217
|
+
// why (no alert, no transport error): node:tls reports it as an
|
|
1218
|
+
// error — ECONNRESET, "Client network socket disconnected before
|
|
1219
|
+
// secure TLS connection was established". Reporting only 'close',
|
|
1220
|
+
// a caller waiting for the handshake (to send its request) was
|
|
1221
|
+
// never told it would not come, and waited forever: an HTTP/2
|
|
1222
|
+
// attempt refused by a server without h2 hung the request.
|
|
1223
|
+
if (!context.secureEstablished && !errorEmitted && !destroyedByUs) {
|
|
1224
|
+
const e = new Error('Client network socket disconnected before secure TLS connection was established');
|
|
1225
|
+
e.code = 'ECONNRESET';
|
|
1226
|
+
self.emit('error', e);
|
|
1227
|
+
}
|
|
1228
|
+
self.emit('close');
|
|
1229
|
+
});
|
|
801
1230
|
}
|
|
802
1231
|
|
|
1232
|
+
// Whether an 'error' has been emitted — every path that emits one goes
|
|
1233
|
+
// through emit, the stream's own destroy(err) included.
|
|
1234
|
+
let errorEmitted = false;
|
|
1235
|
+
const emitOriginal = self.emit;
|
|
1236
|
+
self.emit = function (name) {
|
|
1237
|
+
if (name === 'error') errorEmitted = true;
|
|
1238
|
+
return emitOriginal.apply(this, arguments);
|
|
1239
|
+
};
|
|
1240
|
+
|
|
803
1241
|
session.on('message', function(epoch, seq, type, data){
|
|
804
1242
|
const buf = toBuf(data || []);
|
|
1243
|
+
coalesceFlight();
|
|
805
1244
|
|
|
806
1245
|
// Resolve TLS version: use cached isTls13 if set (post-secureConnect); otherwise query.
|
|
807
1246
|
// During handshake we may not yet have context.isTls13 set — only after secureConnect.
|
|
@@ -921,7 +1360,7 @@ function TLSSocket(duplex, options){
|
|
|
921
1360
|
return;
|
|
922
1361
|
}
|
|
923
1362
|
|
|
924
|
-
const fragment =
|
|
1363
|
+
const fragment = encrypt_tls12_fragment(
|
|
925
1364
|
buf, context.app_write_key, context.app_write_iv,
|
|
926
1365
|
context.app_write_seq, CT.ALERT
|
|
927
1366
|
);
|
|
@@ -933,45 +1372,24 @@ function TLSSocket(duplex, options){
|
|
|
933
1372
|
|
|
934
1373
|
if (epoch === 0) {
|
|
935
1374
|
// Cleartext handshake (ClientHello/ServerHello)
|
|
1375
|
+
// Before the transport is there (a host name still being
|
|
1376
|
+
// looked up): queued, and written by the same function once it
|
|
1377
|
+
// is (setSocket) — the hello's early data included. Queued as
|
|
1378
|
+
// bare records, the early data never went: the hello was written
|
|
1379
|
+
// later without it, and every 0-RTT connection made by host name
|
|
1380
|
+
// sent its request after the handshake.
|
|
936
1381
|
if (!context.transport) {
|
|
937
|
-
context.pendingHandshake.push({
|
|
1382
|
+
context.pendingHandshake.push({ hsType: type, data: buf });
|
|
938
1383
|
return;
|
|
939
1384
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
//
|
|
944
|
-
// server — "immediately AFTER its first handshake message",
|
|
945
|
-
// i.e. after ServerHello or HelloRetryRequest.
|
|
946
|
-
// client — "immediately BEFORE its second flight", i.e. BEFORE
|
|
947
|
-
// the second ClientHello.
|
|
948
|
-
//
|
|
949
|
-
// Getting the client side backwards is not cosmetic: a peer that
|
|
950
|
-
// requires the compat record reads the next thing on the wire and
|
|
951
|
-
// finds a handshake record where the CCS should be, and reports
|
|
952
|
-
// exactly that ("invalid TLS 1.3 ChangeCipherSpec: 160303...").
|
|
953
|
-
// So the client's CCS is emitted BEFORE writeRecord below, and the
|
|
954
|
-
// server's after.
|
|
955
|
-
//
|
|
956
|
-
// We cannot rely on context.isTls13 yet (it is set at
|
|
957
|
-
// secureConnect), so both gate on the session's negotiated
|
|
958
|
-
// version. For the client that is only known once a
|
|
959
|
-
// HelloRetryRequest has told us — which is precisely the CH2 case
|
|
960
|
-
// this branch exists for. In the no-HRR flow the version is still
|
|
961
|
-
// unknown at CH1 time, so the flag stays false and the CCS goes out
|
|
962
|
-
// just before the first encrypted record instead (epoch-1 path
|
|
963
|
-
// below); that is the same "before its second flight" placement.
|
|
964
|
-
if (!session.isServer && type === 'hello' && session.getVersion() === 0x0304) {
|
|
965
|
-
sendCompatCCS();
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
writeRecord(CT.HANDSHAKE, buf);
|
|
1385
|
+
writeCleartextHandshake(type, buf);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
969
1388
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
1389
|
+
if (epoch === 3) {
|
|
1390
|
+
// EndOfEarlyData: under the early key, and the last thing it seals.
|
|
1391
|
+
writeEarlyRecord(CT.HANDSHAKE, buf);
|
|
1392
|
+
context.early.write = null;
|
|
975
1393
|
return;
|
|
976
1394
|
}
|
|
977
1395
|
|
|
@@ -1015,6 +1433,10 @@ function TLSSocket(duplex, options){
|
|
|
1015
1433
|
if (canWrite()) {
|
|
1016
1434
|
context.transport.write(rec);
|
|
1017
1435
|
}
|
|
1436
|
+
if (session.isServer && type === 'finished' && context.early.status === 'accepted' && !context.early.srvWrite) {
|
|
1437
|
+
context.early.srvWrite = true;
|
|
1438
|
+
process.nextTick(flushServerEarlyWrites);
|
|
1439
|
+
}
|
|
1018
1440
|
} catch(e) {
|
|
1019
1441
|
self.emit('error', e);
|
|
1020
1442
|
}
|
|
@@ -1110,19 +1532,11 @@ function TLSSocket(duplex, options){
|
|
|
1110
1532
|
|| default_signature_schemes();
|
|
1111
1533
|
|
|
1112
1534
|
// Groups: override or default
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (options.prioritizeChaCha) {
|
|
1117
|
-
let chacha = ciphers.filter(c => c === 0x1303 || c === 0xCCA8);
|
|
1118
|
-
let rest = ciphers.filter(c => c !== 0x1303 && c !== 0xCCA8);
|
|
1119
|
-
ciphers = [...chacha, ...rest];
|
|
1120
|
-
}
|
|
1535
|
+
// A server's default: post-quantum hybrid first (ecdh.js). A client that
|
|
1536
|
+
// offers it gets it; any other gets the classical group it asked for.
|
|
1537
|
+
let groups = options.groups || SERVER_DEFAULT_GROUPS.slice();
|
|
1121
1538
|
|
|
1122
|
-
|
|
1123
|
-
if (context.allowedCipherSuites) {
|
|
1124
|
-
ciphers = ciphers.filter(c => context.allowedCipherSuites.includes(c));
|
|
1125
|
-
}
|
|
1539
|
+
ciphers = chooseCipherSuites(ciphers, options.prioritizeChaCha, context.allowedCipherSuites);
|
|
1126
1540
|
|
|
1127
1541
|
// ALPN from options
|
|
1128
1542
|
let alpns = Array.isArray(options.ALPNProtocols) ? options.ALPNProtocols : [];
|
|
@@ -1215,7 +1629,21 @@ function TLSSocket(duplex, options){
|
|
|
1215
1629
|
if (corkable) t.uncork();
|
|
1216
1630
|
}
|
|
1217
1631
|
}
|
|
1632
|
+
// end() was called before the handshake completed: its close was held
|
|
1633
|
+
// back until the writes queued ahead of it were out (see _final).
|
|
1634
|
+
if (context.closeAfterFlush) {
|
|
1635
|
+
context.closeAfterFlush = false;
|
|
1636
|
+
sendCloseNotify();
|
|
1637
|
+
}
|
|
1218
1638
|
|
|
1639
|
+
// 'secure' fires on BOTH sides and on sockets built with
|
|
1640
|
+
// `new TLSSocket()`, which is precisely where 'secureConnect' does
|
|
1641
|
+
// NOT fire (Node documents that exclusion). It is therefore the only
|
|
1642
|
+
// completion signal a STARTTLS caller — who wraps an existing socket
|
|
1643
|
+
// rather than going through connect() — can rely on. Emitted first so
|
|
1644
|
+
// a listener on both sees the handshake-complete event before the
|
|
1645
|
+
// client-only one.
|
|
1646
|
+
self.emit('secure');
|
|
1219
1647
|
self.emit('secureConnect');
|
|
1220
1648
|
});
|
|
1221
1649
|
|
|
@@ -1238,8 +1666,7 @@ function TLSSocket(duplex, options){
|
|
|
1238
1666
|
session.on('error', function(err) {
|
|
1239
1667
|
markTransportDead();
|
|
1240
1668
|
self.emit('error', err);
|
|
1241
|
-
|
|
1242
|
-
if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
|
|
1669
|
+
closeAfterAlert(); // the session sent its alert just before reporting
|
|
1243
1670
|
});
|
|
1244
1671
|
|
|
1245
1672
|
// Forward handshakeMessage event (for debugging/inspection)
|
|
@@ -1248,11 +1675,17 @@ function TLSSocket(duplex, options){
|
|
|
1248
1675
|
});
|
|
1249
1676
|
|
|
1250
1677
|
// Forward keylog event (Node.js compat — NSS SSLKEYLOGFILE format for Wireshark)
|
|
1678
|
+
// node:tls: emitted on a client that set requestOCSP, with the server's
|
|
1679
|
+
// stapled OCSP response (DER), before 'secureConnect'.
|
|
1680
|
+
session.on('OCSPResponse', function(response) {
|
|
1681
|
+
self.emit('OCSPResponse', Buffer.from(response));
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1251
1684
|
session.on('keylog', function(line) {
|
|
1252
1685
|
self.emit('keylog', line);
|
|
1253
1686
|
});
|
|
1254
1687
|
|
|
1255
|
-
// Forward raw clienthello event (server-side, for
|
|
1688
|
+
// Forward raw clienthello event (server-side, for fingerprints/inspection)
|
|
1256
1689
|
session.on('clienthello', function(raw, parsed) {
|
|
1257
1690
|
self.emit('clienthello', raw, parsed);
|
|
1258
1691
|
});
|
|
@@ -1312,24 +1745,16 @@ function TLSSocket(duplex, options){
|
|
|
1312
1745
|
self.emit('certificateRequest', msg);
|
|
1313
1746
|
});
|
|
1314
1747
|
|
|
1315
|
-
// Server:
|
|
1316
|
-
//
|
|
1748
|
+
// Server: a 'psk' listener on the socket may decide a TLS 1.3 PSK identity
|
|
1749
|
+
// itself; payload { identity, obfuscatedAge }, answered synchronously with
|
|
1750
|
+
// { psk, cipher, data? } or null. Without one, nothing is answered here and
|
|
1751
|
+
// the session opens its own ticket — it issued it, with the same
|
|
1752
|
+
// ticketKeys. (This used to decrypt the ticket right here, which left a
|
|
1753
|
+
// TLSSession used without a socket — QUIC — unable to accept its own
|
|
1754
|
+
// tickets, and skipped the lifetime check the session now makes.)
|
|
1317
1755
|
if (options.isServer) {
|
|
1318
1756
|
session.on('psk', function(info, callback) {
|
|
1319
|
-
|
|
1320
|
-
if (self.listenerCount('psk') > 0) {
|
|
1321
|
-
// Let user handle it — pass through the same payload
|
|
1322
|
-
self.emit('psk', info, callback);
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// Auto-decrypt ticket using ticketKeys (unified format)
|
|
1327
|
-
let state = decrypt_session_blob(info.identity, context.ticketKeys);
|
|
1328
|
-
if (state && state.v === 13 && state.psk && state.cipher) {
|
|
1329
|
-
callback({ psk: state.psk, cipher: state.cipher });
|
|
1330
|
-
} else {
|
|
1331
|
-
callback(null); // Decryption failed / not our ticket → full handshake
|
|
1332
|
-
}
|
|
1757
|
+
if (self.listenerCount('psk') > 0) self.emit('psk', info, callback);
|
|
1333
1758
|
});
|
|
1334
1759
|
}
|
|
1335
1760
|
|
|
@@ -1354,6 +1779,65 @@ function TLSSocket(duplex, options){
|
|
|
1354
1779
|
}
|
|
1355
1780
|
});
|
|
1356
1781
|
|
|
1782
|
+
/**
|
|
1783
|
+
* Push the client's negotiable preferences into the session before its
|
|
1784
|
+
* ClientHello is built.
|
|
1785
|
+
*
|
|
1786
|
+
* The server does this from the 'hello' event, because a server only
|
|
1787
|
+
* learns what to offer once it has seen the client's hello. A client has
|
|
1788
|
+
* no such event to wait for — its hello IS the first message — so the
|
|
1789
|
+
* equivalent has to happen at construction. TLSSession builds the
|
|
1790
|
+
* ClientHello from a setTimeout, so a synchronous set_context here lands
|
|
1791
|
+
* first.
|
|
1792
|
+
*
|
|
1793
|
+
* ALPNProtocols is the one that matters most: RFC 7301 negotiation cannot
|
|
1794
|
+
* start unless the client offers something, and HTTP/2 over TLS does not
|
|
1795
|
+
* merely benefit from ALPN but REQUIRES it (RFC 9113 §3.2). A client that
|
|
1796
|
+
* silently offers nothing can never speak h2 to any server.
|
|
1797
|
+
*
|
|
1798
|
+
* The other four are applied for the same reason — each is documented as a
|
|
1799
|
+
* per-connection option, and each was being dropped on the client side.
|
|
1800
|
+
*/
|
|
1801
|
+
function applyClientContext() {
|
|
1802
|
+
if (session.isServer) return;
|
|
1803
|
+
|
|
1804
|
+
let update = {};
|
|
1805
|
+
|
|
1806
|
+
if (Array.isArray(options.ALPNProtocols) && options.ALPNProtocols.length > 0) {
|
|
1807
|
+
update.local_supported_alpns = options.ALPNProtocols.map(function (p) {
|
|
1808
|
+
return Buffer.isBuffer(p) ? p.toString('latin1') : String(p);
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
let maxVer = parseVersion(options.maxVersion);
|
|
1813
|
+
let minVer = parseVersion(options.minVersion);
|
|
1814
|
+
if (maxVer || minVer) {
|
|
1815
|
+
maxVer = maxVer || 0x0304;
|
|
1816
|
+
minVer = minVer || 0x0303;
|
|
1817
|
+
let versions = [];
|
|
1818
|
+
if (maxVer >= 0x0304 && minVer <= 0x0304) versions.push(0x0304);
|
|
1819
|
+
if (maxVer >= 0x0303 && minVer <= 0x0303) versions.push(0x0303);
|
|
1820
|
+
if (versions.length === 0) versions.push(0x0303);
|
|
1821
|
+
update.local_supported_versions = versions;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
if (options.groups) update.local_supported_groups = options.groups;
|
|
1825
|
+
if (options.signatureAlgorithms) {
|
|
1826
|
+
update.local_supported_signature_algorithms = options.signatureAlgorithms;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
if (options.allowedCipherSuites || options.prioritizeChaCha) {
|
|
1830
|
+
let vers = update.local_supported_versions || [0x0304, 0x0303];
|
|
1831
|
+
let ciphers = default_cipher_suites(vers.indexOf(0x0304) >= 0,
|
|
1832
|
+
vers.indexOf(0x0303) >= 0);
|
|
1833
|
+
update.local_supported_cipher_suites = chooseCipherSuites(ciphers, options.prioritizeChaCha, options.allowedCipherSuites);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (Object.keys(update).length > 0) session.set_context(update);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
applyClientContext();
|
|
1840
|
+
|
|
1357
1841
|
// If duplex was passed in constructor, start reading
|
|
1358
1842
|
if (context.transport) {
|
|
1359
1843
|
bindTransport();
|
|
@@ -1400,9 +1884,11 @@ function TLSSocket(duplex, options){
|
|
|
1400
1884
|
if (context.destroyed) { callback(new Error('Socket destroyed')); return; }
|
|
1401
1885
|
const buf = toBuf(chunk);
|
|
1402
1886
|
|
|
1403
|
-
if (!context.using_app_keys) {
|
|
1404
|
-
// Pre-handshake: queue for flush at secureConnect
|
|
1887
|
+
if (!context.using_app_keys && !context.early.srvWrite) {
|
|
1888
|
+
// Pre-handshake: queue for flush at secureConnect — or, a client
|
|
1889
|
+
// offering early data, sent early from the queue now.
|
|
1405
1890
|
context.appWriteQueue.push(buf);
|
|
1891
|
+
if (context.early.write) flushEarly();
|
|
1406
1892
|
process.nextTick(callback);
|
|
1407
1893
|
return;
|
|
1408
1894
|
}
|
|
@@ -1448,8 +1934,9 @@ function TLSSocket(duplex, options){
|
|
|
1448
1934
|
}
|
|
1449
1935
|
}
|
|
1450
1936
|
|
|
1451
|
-
if (!context.using_app_keys) {
|
|
1937
|
+
if (!context.using_app_keys && !context.early.srvWrite) {
|
|
1452
1938
|
context.appWriteQueue.push(combined);
|
|
1939
|
+
if (context.early.write) flushEarly();
|
|
1453
1940
|
process.nextTick(callback);
|
|
1454
1941
|
return;
|
|
1455
1942
|
}
|
|
@@ -1462,8 +1949,103 @@ function TLSSocket(duplex, options){
|
|
|
1462
1949
|
}
|
|
1463
1950
|
};
|
|
1464
1951
|
|
|
1465
|
-
/**
|
|
1466
|
-
|
|
1952
|
+
/**
|
|
1953
|
+
* Read backpressure, as net.Socket has it. Decrypted data is pushed as
|
|
1954
|
+
* records arrive; when the reader stops taking it (push() returns false —
|
|
1955
|
+
* a paused stream, a consumer that has fallen behind) the transport is
|
|
1956
|
+
* paused, so the kernel's buffer fills and TCP slows the peer down. The
|
|
1957
|
+
* stream asks for more (_read) once its buffer has room, and the
|
|
1958
|
+
* transport resumes. Without it pause() on this socket stopped only the
|
|
1959
|
+
* 'data' events: the transport read on, and everything the peer sent
|
|
1960
|
+
* piled up decrypted in this stream's buffer, without bound.
|
|
1961
|
+
*/
|
|
1962
|
+
let transportPausedForRead = false;
|
|
1963
|
+
function pushAppData(buf) {
|
|
1964
|
+
if (self.push(buf)) return;
|
|
1965
|
+
const t = context.transport;
|
|
1966
|
+
if (!transportPausedForRead && t && typeof t.pause === 'function') {
|
|
1967
|
+
transportPausedForRead = true;
|
|
1968
|
+
t.pause();
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
self._read = function() {
|
|
1972
|
+
if (!transportPausedForRead) return;
|
|
1973
|
+
transportPausedForRead = false;
|
|
1974
|
+
const t = context.transport;
|
|
1975
|
+
if (t && typeof t.resume === 'function' && !t.destroyed) t.resume();
|
|
1976
|
+
};
|
|
1977
|
+
|
|
1978
|
+
// The server, early data taken and its Finished out: what was written
|
|
1979
|
+
// meanwhile goes now, in order, under the application key.
|
|
1980
|
+
function flushServerEarlyWrites() {
|
|
1981
|
+
if (context.destroyed || context.appWriteQueue.length === 0) return;
|
|
1982
|
+
const q = context.appWriteQueue;
|
|
1983
|
+
context.appWriteQueue = [];
|
|
1984
|
+
for (let i = 0; i < q.length; i++) writeAppData(q[i]);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// 0-RTT. Server: whether what is read now came as early data (taken,
|
|
1988
|
+
// its EndOfEarlyData not yet here) — it could be a replay; client:
|
|
1989
|
+
// whether it is sending early. earlyDataBytes: what came (server) or
|
|
1990
|
+
// went (client) early. earlyDataStatus: null (none), 'offered',
|
|
1991
|
+
// 'accepted', 'rejected'. And 'earlyData' (accepted) when it is decided.
|
|
1992
|
+
Object.defineProperty(self, 'isEarlyData', { configurable: true, get: function () {
|
|
1993
|
+
return session.isServer ? (context.early.status === 'accepted' && !context.early.eoed)
|
|
1994
|
+
: (context.early.status === 'offered' && !!context.early.write);
|
|
1995
|
+
} });
|
|
1996
|
+
Object.defineProperty(self, 'earlyDataBytes', { configurable: true, get: function () {
|
|
1997
|
+
return session.isServer ? context.early.recv : context.early.sent;
|
|
1998
|
+
} });
|
|
1999
|
+
Object.defineProperty(self, 'earlyDataStatus', { configurable: true, get: function () { return context.early.status; } });
|
|
2000
|
+
|
|
2001
|
+
// A cleartext handshake message (ClientHello, ServerHello, HRR) onto
|
|
2002
|
+
// the transport, with what goes around it: the compatibility CCS
|
|
2003
|
+
// (RFC 8446 §D.4), and a client's early data after its first hello.
|
|
2004
|
+
function writeCleartextHandshake(type, buf) {
|
|
2005
|
+
// ── Middlebox-compat CCS (RFC 8446 §D.4) ──
|
|
2006
|
+
// The two roles place it on OPPOSITE sides of their hello, and the
|
|
2007
|
+
// spec is explicit about both:
|
|
2008
|
+
//
|
|
2009
|
+
// server — "immediately AFTER its first handshake message",
|
|
2010
|
+
// i.e. after ServerHello or HelloRetryRequest.
|
|
2011
|
+
// client — "immediately BEFORE its second flight", i.e. BEFORE
|
|
2012
|
+
// the second ClientHello.
|
|
2013
|
+
//
|
|
2014
|
+
// Getting the client side backwards is not cosmetic: a peer that
|
|
2015
|
+
// requires the compat record reads the next thing on the wire and
|
|
2016
|
+
// finds a handshake record where the CCS should be, and reports
|
|
2017
|
+
// exactly that ("invalid TLS 1.3 ChangeCipherSpec: 160303...").
|
|
2018
|
+
// So the client's CCS is emitted BEFORE writeRecord below, and the
|
|
2019
|
+
// server's after.
|
|
2020
|
+
//
|
|
2021
|
+
// We cannot rely on context.isTls13 yet (it is set at
|
|
2022
|
+
// secureConnect), so both gate on the session's negotiated
|
|
2023
|
+
// version. For the client that is only known once a
|
|
2024
|
+
// HelloRetryRequest has told us — which is precisely the CH2 case
|
|
2025
|
+
// this branch exists for. In the no-HRR flow the version is still
|
|
2026
|
+
// unknown at CH1 time, so the flag stays false and the CCS goes out
|
|
2027
|
+
// just before the first encrypted record instead (epoch-1 path
|
|
2028
|
+
// below); that is the same "before its second flight" placement.
|
|
2029
|
+
if (!session.isServer && type === 'hello' && session.getVersion() === 0x0304) {
|
|
2030
|
+
sendCompatCCS();
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
writeRecord(CT.HANDSHAKE, buf);
|
|
2034
|
+
|
|
2035
|
+
// Offering early data: the compatibility CCS right after this
|
|
2036
|
+
// first hello (§D.4), then the early data.
|
|
2037
|
+
if (!session.isServer && type === 'hello' && context.early.write && context.early.status === 'offered' && !context.early.helloSent) {
|
|
2038
|
+
context.early.helloSent = true;
|
|
2039
|
+
sendCompatCCS();
|
|
2040
|
+
flushEarly();
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
if (session.isServer &&
|
|
2044
|
+
(type === 'hello' || type === 'hello_retry_request') &&
|
|
2045
|
+
session.getVersion() === 0x0304) {
|
|
2046
|
+
sendCompatCCS();
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
1467
2049
|
|
|
1468
2050
|
// === Node-compatible API ===
|
|
1469
2051
|
|
|
@@ -1475,22 +2057,64 @@ function TLSSocket(duplex, options){
|
|
|
1475
2057
|
// Flush any pending handshake messages (e.g. ClientHello queued before transport was ready)
|
|
1476
2058
|
while (context.pendingHandshake.length > 0) {
|
|
1477
2059
|
let msg = context.pendingHandshake.shift();
|
|
1478
|
-
|
|
2060
|
+
writeCleartextHandshake(msg.hsType, Buffer.from(msg.data));
|
|
1479
2061
|
}
|
|
1480
2062
|
};
|
|
1481
2063
|
|
|
1482
|
-
/**
|
|
2064
|
+
/**
|
|
2065
|
+
* end(): the Writable's own. It finishes the writes still queued, THEN
|
|
2066
|
+
* calls _final below, which sends close_notify and closes the transport.
|
|
2067
|
+
*
|
|
2068
|
+
* This used to send close_notify and end the transport first, and only
|
|
2069
|
+
* then hand over to the Writable — so everything still queued was written
|
|
2070
|
+
* into a closed connection and lost. And something always is queued:
|
|
2071
|
+
* _write defers its callback a tick so that writes in the same tick
|
|
2072
|
+
* coalesce into one record. So `write(head); write(body); end()` sent the
|
|
2073
|
+
* head and dropped the body, and end(data) dropped data outright. Seen as
|
|
2074
|
+
* every HTTP/1.1 response to a request with Connection: close arriving
|
|
2075
|
+
* with its headers and without its body.
|
|
2076
|
+
*/
|
|
1483
2077
|
self.end = (function(originalEnd) {
|
|
1484
2078
|
return function(data, encoding, callback) {
|
|
1485
2079
|
if (context.destroyed) return this;
|
|
1486
|
-
session.close(); // sends close_notify alert
|
|
1487
|
-
try { context.transport && context.transport.end && context.transport.end(); } catch(e){}
|
|
1488
2080
|
return originalEnd.call(this, data, encoding, callback);
|
|
1489
2081
|
};
|
|
1490
2082
|
})(self.end);
|
|
1491
2083
|
|
|
2084
|
+
// The Writable calls this once every write before end() has been handed
|
|
2085
|
+
// down. Before the handshake completes, those writes are only queued
|
|
2086
|
+
// (appWriteQueue, flushed at secureConnect) — closing here would drop
|
|
2087
|
+
// them, so the close waits for that flush.
|
|
2088
|
+
// Once: our end(), and the automatic end() that follows the readable
|
|
2089
|
+
// side ending (allowHalfOpen off), would otherwise each send one.
|
|
2090
|
+
function sendCloseNotify() {
|
|
2091
|
+
if (context.destroyed || context.closeNotifySent) return;
|
|
2092
|
+
context.closeNotifySent = true;
|
|
2093
|
+
session.close(); // sends close_notify alert
|
|
2094
|
+
try { context.transport && context.transport.end && context.transport.end(); } catch(e){}
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* The peer will send nothing more (close_notify, or EOF on the transport):
|
|
2098
|
+
* end the readable side, so 'end' is emitted after what is already
|
|
2099
|
+
* buffered — the event a reader waits on to know it has everything. It
|
|
2100
|
+
* was never emitted; only 'close' was, so a response delimited by the
|
|
2101
|
+
* connection closing could not be told apart from one cut short.
|
|
2102
|
+
*/
|
|
2103
|
+
function endReadable() {
|
|
2104
|
+
if (context.readEnded || self.destroyed) return;
|
|
2105
|
+
context.readEnded = true;
|
|
2106
|
+
self.push(null);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
self._final = function(callback) {
|
|
2110
|
+
if (!context.using_app_keys) context.closeAfterFlush = true;
|
|
2111
|
+
else sendCloseNotify();
|
|
2112
|
+
callback();
|
|
2113
|
+
};
|
|
2114
|
+
|
|
1492
2115
|
self.destroy = (function(originalDestroy) {
|
|
1493
2116
|
return function(err) {
|
|
2117
|
+
clearHandshakeTimer();
|
|
1494
2118
|
if (context.destroyed) return this;
|
|
1495
2119
|
context.destroyed = true;
|
|
1496
2120
|
try { context.transport && context.transport.destroy && context.transport.destroy(); } catch(e){}
|
|
@@ -1534,6 +2158,9 @@ function TLSSocket(duplex, options){
|
|
|
1534
2158
|
};
|
|
1535
2159
|
|
|
1536
2160
|
/** Returns negotiated ALPN protocol string, or false. */
|
|
2161
|
+
// Encrypted Client Hello (server): null (not offered) | 'accepted' | 'rejected'.
|
|
2162
|
+
Object.defineProperty(self, 'echStatus', { get: function(){ return session.getEchStatus(); } });
|
|
2163
|
+
Object.defineProperty(self, 'echAccepted', { get: function(){ return session.getEchStatus() === 'accepted'; } });
|
|
1537
2164
|
Object.defineProperty(self, 'alpnProtocol', {
|
|
1538
2165
|
get: function(){ return session.getALPN() || false; },
|
|
1539
2166
|
enumerable: true
|
|
@@ -1579,23 +2206,11 @@ function TLSSocket(duplex, options){
|
|
|
1579
2206
|
self.getPeerCertificate = function(){
|
|
1580
2207
|
let chain = session.getPeerCertificate();
|
|
1581
2208
|
if (!chain || chain.length === 0) return null;
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
issuer: x509.issuer,
|
|
1588
|
-
subjectaltname: x509.subjectAltName,
|
|
1589
|
-
valid_from: x509.validFrom,
|
|
1590
|
-
valid_to: x509.validTo,
|
|
1591
|
-
fingerprint: x509.fingerprint,
|
|
1592
|
-
fingerprint256: x509.fingerprint256,
|
|
1593
|
-
serialNumber: x509.serialNumber,
|
|
1594
|
-
raw: certDer
|
|
1595
|
-
};
|
|
1596
|
-
} catch(e) {
|
|
1597
|
-
return { raw: chain[0].cert };
|
|
1598
|
-
}
|
|
2209
|
+
// Subject and issuer as objects keyed by RDN type ({ C, ST, L, O,
|
|
2210
|
+
// OU, CN }), as Node documents them — not the newline-separated text
|
|
2211
|
+
// X509Certificate hands back, which stopped tls.checkServerIdentity()
|
|
2212
|
+
// (it reads cert.subject.CN) from working against this method.
|
|
2213
|
+
return certificateObject(chain[0].cert);
|
|
1599
2214
|
};
|
|
1600
2215
|
|
|
1601
2216
|
/** Node-compat: return the peer's leaf cert as a native X509Certificate
|
|
@@ -1625,8 +2240,7 @@ function TLSSocket(duplex, options){
|
|
|
1625
2240
|
* object (mirror of getPeerCertificate). Empty object {} if we didn't
|
|
1626
2241
|
* present a cert — this matches Node's observed behavior. */
|
|
1627
2242
|
self.getCertificate = function(){
|
|
1628
|
-
let
|
|
1629
|
-
let localChain = sctx && sctx.local_cert_chain;
|
|
2243
|
+
let localChain = session.getCertificate();
|
|
1630
2244
|
if (!localChain || localChain.length === 0) return {};
|
|
1631
2245
|
try {
|
|
1632
2246
|
let der = localChain[0].cert || localChain[0];
|
|
@@ -1775,12 +2389,89 @@ function TLSSocket(duplex, options){
|
|
|
1775
2389
|
enumerable: true
|
|
1776
2390
|
});
|
|
1777
2391
|
|
|
1778
|
-
/**
|
|
1779
|
-
|
|
2392
|
+
/** TLS fingerprints, { hash, raw } or null (see TLSSession): JA4 and JA5
|
|
2393
|
+
* of the ClientHello, JA4S of the ServerHello — in either role.
|
|
2394
|
+
* `opts.protocol` overrides the transport character (e.g. 'q' for QUIC). */
|
|
2395
|
+
self.getJA4 = function(opts){ return session.getJA4(opts); };
|
|
2396
|
+
self.getJA5 = function(opts){ return session.getJA5(opts); };
|
|
2397
|
+
self.getJA4S = function(opts){ return session.getJA4S(opts); };
|
|
2398
|
+
|
|
2399
|
+
/** Node-compat: set the key and certificate for THIS connection. Accepts a
|
|
2400
|
+
* context from createSecureContext() or a plain { key, cert }. Mainly for
|
|
2401
|
+
* picking a server certificate from inside ALPNCallback, once the
|
|
2402
|
+
* negotiated protocol is known. */
|
|
2403
|
+
self.setKeyCert = function(ctxOrOpts){
|
|
2404
|
+
if (session.setKeyCert) session.setKeyCert(ctxOrOpts);
|
|
2405
|
+
};
|
|
1780
2406
|
|
|
1781
2407
|
/** ECDHE shared secret (Buffer), or null. For research/advanced use. */
|
|
1782
2408
|
self.getSharedSecret = function(){ return session.getSharedSecret ? session.getSharedSecret() : null; };
|
|
1783
2409
|
|
|
2410
|
+
/** Node-compat: the Finished verify_data we sent, or undefined.
|
|
2411
|
+
* The session has kept this all along (context.local_finished_data); only
|
|
2412
|
+
* the proxy was missing, so the documented method did not exist on the
|
|
2413
|
+
* socket. Used for the tls-unique channel binding of RFC 5929. */
|
|
2414
|
+
self.getFinished = function(){
|
|
2415
|
+
let v = session.getFinished ? session.getFinished() : null;
|
|
2416
|
+
return v == null ? undefined : v; // Node returns undefined, not null
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
/** Node-compat: the peer's Finished verify_data, or undefined. */
|
|
2420
|
+
self.getPeerFinished = function(){
|
|
2421
|
+
let v = session.getPeerFinished ? session.getPeerFinished() : null;
|
|
2422
|
+
return v == null ? undefined : v;
|
|
2423
|
+
};
|
|
2424
|
+
|
|
2425
|
+
// This connection's ticket, from the first connection on: { data,
|
|
2426
|
+
// resumed, offered } (see TLSSession.getTicketInfo).
|
|
2427
|
+
self.getTicketInfo = function(){ return session.getTicketInfo(); };
|
|
2428
|
+
|
|
2429
|
+
// The underlying TLSSession, as the README documents (socket.session):
|
|
2430
|
+
// the state machine's own methods — setTicketState() and sendTicket(),
|
|
2431
|
+
// exportKeyingMaterial(), the secrets — on a TCP connection too. Read-only.
|
|
2432
|
+
Object.defineProperty(self, 'session', { get: function(){ return session; }, configurable: true, enumerable: false });
|
|
2433
|
+
|
|
2434
|
+
/** Node-compat: whether this connection resumed an earlier session.
|
|
2435
|
+
* Same value as the `isResumed` property; Node spells it as a method. */
|
|
2436
|
+
self.isSessionReused = function(){ return !!session.isResumed; };
|
|
2437
|
+
|
|
2438
|
+
/** Node-compat: bound address of the underlying transport,
|
|
2439
|
+
* `{ port, family, address }`. Empty object when not connected. */
|
|
2440
|
+
self.address = function(){
|
|
2441
|
+
let t = context.transport;
|
|
2442
|
+
if (t && typeof t.address === 'function') {
|
|
2443
|
+
try { return t.address() || {}; } catch(e) { return {}; }
|
|
2444
|
+
}
|
|
2445
|
+
return {};
|
|
2446
|
+
};
|
|
2447
|
+
|
|
2448
|
+
/** Node-compat: describes the ephemeral key agreement.
|
|
2449
|
+
*
|
|
2450
|
+
* Node returns {} when the agreement is not ephemeral, and null when
|
|
2451
|
+
* asked on a server socket — it only reports the peer's temporary key,
|
|
2452
|
+
* which a server does not receive. TLS 1.3 is always (EC)DHE, so on 1.3
|
|
2453
|
+
* there is always a group to report. */
|
|
2454
|
+
self.getEphemeralKeyInfo = function(){
|
|
2455
|
+
if (session.context && session.context.isServer) return null;
|
|
2456
|
+
let g = session.context ? session.context.selected_group : null;
|
|
2457
|
+
if (g == null) return {};
|
|
2458
|
+
let known = {
|
|
2459
|
+
0x0017: { name: 'prime256v1', size: 256 },
|
|
2460
|
+
0x0018: { name: 'secp384r1', size: 384 },
|
|
2461
|
+
0x0019: { name: 'secp521r1', size: 521 },
|
|
2462
|
+
0x001d: { name: 'x25519', size: 253 },
|
|
2463
|
+
0x001e: { name: 'x448', size: 448 }
|
|
2464
|
+
};
|
|
2465
|
+
let k = known[g];
|
|
2466
|
+
// Not an ECDH curve, so not in that table, but a group with a name.
|
|
2467
|
+
if (!k && ecdh_group_name(g)) return { type: 'TLSGroup', name: ecdh_group_name(g) };
|
|
2468
|
+
// Anything outside that table is still a negotiated TLS group — report
|
|
2469
|
+
// it as such rather than as an ECDH curve we cannot name, which is what
|
|
2470
|
+
// Node's 'TLSGroup' type is for.
|
|
2471
|
+
if (!k) return { type: 'TLSGroup', name: '0x' + g.toString(16) };
|
|
2472
|
+
return { type: 'ECDH', name: k.name, size: k.size };
|
|
2473
|
+
};
|
|
2474
|
+
|
|
1784
2475
|
/** Full negotiation result — all selected parameters in one object. */
|
|
1785
2476
|
self.getNegotiationResult = function(){ return session.getNegotiationResult ? session.getNegotiationResult() : null; };
|
|
1786
2477
|
|