lemon-tls 0.2.2 → 0.4.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 +258 -203
- package/index.d.ts +145 -14
- package/index.js +12 -0
- package/package.json +2 -2
- package/src/compat.js +290 -31
- package/src/crypto.js +127 -7
- package/src/dtls_socket.js +3 -0
- package/src/record.js +408 -61
- package/src/session/message.js +28 -3
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +820 -99
- package/src/tls_socket.js +815 -249
- package/src/wire.js +25 -0
package/src/tls_session.js
CHANGED
|
@@ -4,15 +4,21 @@ import { EventEmitter } from 'node:events';
|
|
|
4
4
|
import {
|
|
5
5
|
TLS_CIPHER_SUITES,
|
|
6
6
|
build_cert_verify_tbs,
|
|
7
|
+
build_cert_verify_tbs_with_hash,
|
|
7
8
|
get_handshake_finished,
|
|
9
|
+
get_handshake_finished_with_hash,
|
|
8
10
|
tls12_prf,
|
|
9
11
|
derive_handshake_traffic_secrets,
|
|
12
|
+
derive_handshake_traffic_secrets_with_hash,
|
|
10
13
|
derive_app_traffic_secrets,
|
|
14
|
+
derive_app_traffic_secrets_with_hash,
|
|
11
15
|
derive_resumption_master_secret,
|
|
16
|
+
derive_resumption_master_secret_with_hash,
|
|
12
17
|
derive_psk,
|
|
13
18
|
derive_binder_key,
|
|
14
19
|
compute_psk_binder,
|
|
15
20
|
derive_handshake_traffic_secrets_psk,
|
|
21
|
+
derive_handshake_traffic_secrets_psk_with_hash,
|
|
16
22
|
hkdf_expand_label,
|
|
17
23
|
getHashFn,
|
|
18
24
|
} from './crypto.js';
|
|
@@ -30,6 +36,17 @@ import { pick_scheme, sign_with_scheme } from './session/signing.js';
|
|
|
30
36
|
import createSecureContext from './secure_context.js';
|
|
31
37
|
import { x25519_get_public_key, x25519_get_shared_secret, p256_generate_keypair, p256_get_shared_secret, p384_generate_keypair, p384_get_shared_secret } from './session/ecdh.js';
|
|
32
38
|
import { build_tls_message, parse_tls_message } from './session/message.js';
|
|
39
|
+
import { encrypt_session_blob, decrypt_session_blob, encode_client_session, decode_client_session } from './session/ticket.js';
|
|
40
|
+
|
|
41
|
+
// Debug logging — enabled via LEMON_DEBUG=1 env var
|
|
42
|
+
const LEMON_DEBUG = typeof process !== 'undefined' && process.env && process.env.LEMON_DEBUG === '1';
|
|
43
|
+
function dbg(tag, ...args) { if (LEMON_DEBUG) console.error('[LEMON ' + tag + ']', ...args); }
|
|
44
|
+
function hexPreview(buf, max) {
|
|
45
|
+
if (!buf) return 'null';
|
|
46
|
+
let b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
47
|
+
let n = Math.min(b.length, max || 32);
|
|
48
|
+
return b.slice(0, n).toString('hex') + (b.length > n ? `... (${b.length} bytes)` : ` (${b.length} bytes)`);
|
|
49
|
+
}
|
|
33
50
|
|
|
34
51
|
|
|
35
52
|
function TLSSession(options){
|
|
@@ -45,7 +62,9 @@ function TLSSession(options){
|
|
|
45
62
|
ca: options.ca || null, // CA certificates (PEM strings or Buffers)
|
|
46
63
|
|
|
47
64
|
SNICallback: options.SNICallback || null,
|
|
48
|
-
ticketKeys: options.ticketKeys || null, // 48 bytes
|
|
65
|
+
ticketKeys: options.ticketKeys || null, // 48 bytes: [0:16]=key_name, [16:48]=AES-256-GCM key
|
|
66
|
+
ticketLifetime: options.ticketLifetime != null ? (options.ticketLifetime >>> 0) : 7200, // seconds
|
|
67
|
+
sessionTickets: options.sessionTickets !== false, // default true (was noTickets inverted)
|
|
49
68
|
|
|
50
69
|
// Advanced options
|
|
51
70
|
maxHandshakeSize: options.maxHandshakeSize || 0, // 0 = no limit
|
|
@@ -110,6 +129,18 @@ function TLSSession(options){
|
|
|
110
129
|
transcript: [],
|
|
111
130
|
transcriptHook: null, // DTLSSession sets this to transform transcript entries
|
|
112
131
|
|
|
132
|
+
// Incremental transcript hash — running crypto.Hash object that we update
|
|
133
|
+
// each time a handshake message is pushed. Replaces the previous pattern of
|
|
134
|
+
// `concatUint8Arrays(transcript)` + `hashFn(...)` on every key-derivation
|
|
135
|
+
// step, which re-hashed and re-allocated the entire transcript each time
|
|
136
|
+
// (~5 times per handshake, several KB each).
|
|
137
|
+
//
|
|
138
|
+
// The array `transcript` is still maintained in parallel for cases that
|
|
139
|
+
// need it (HRR rewind, TLS 1.2 EMS snapshot via transcript.length, logging).
|
|
140
|
+
// Only the HASH path uses this incremental object. Reset on HRR.
|
|
141
|
+
transcriptHash: null, // crypto.Hash object (lazy init when hashName is known)
|
|
142
|
+
transcriptHashName: null, // hash algorithm currently tracked ('sha256' / 'sha384')
|
|
143
|
+
|
|
113
144
|
|
|
114
145
|
//both
|
|
115
146
|
hello_sent: false,
|
|
@@ -171,23 +202,141 @@ function TLSSession(options){
|
|
|
171
202
|
resumption_master_secret: null,
|
|
172
203
|
ticket_nonce_counter: 0,
|
|
173
204
|
session_ticket_sent: false,
|
|
174
|
-
noTickets: !!options.noTickets,
|
|
175
205
|
psk_offered: null, // client: { identity, psk, cipher } offered in ClientHello
|
|
176
206
|
psk_accepted: false, // server accepted PSK → abbreviated handshake
|
|
177
|
-
isResumed: false, // true if PSK was accepted
|
|
207
|
+
isResumed: false, // true if PSK was accepted (1.3) or abbreviated handshake (1.2)
|
|
208
|
+
|
|
209
|
+
// TLS 1.2 resumption
|
|
210
|
+
tls12_abbreviated: false, // doing abbreviated handshake (server side)
|
|
211
|
+
tls12_resume_state: null, // loaded session state (from SessionID or Ticket): { version, cipher, master_secret, extended_master_secret, sni, alpn, timestamp }
|
|
212
|
+
tls12_session_ticket_requested: false, // client sent SessionTicket extension (empty or with data)
|
|
213
|
+
tls12_session_ticket_offered: null, // client sent non-empty SessionTicket (raw bytes) — server tries to decrypt
|
|
214
|
+
tls12_newsession_sent: false, // server sent NewSessionTicket message (TLS 1.2)
|
|
215
|
+
tls12_session_id_for_store: null, // session_id to emit with 'newSession' (32 bytes, server-generated)
|
|
216
|
+
tls12_session_id_emitted: false, // 'newSession' event already fired
|
|
217
|
+
tls12_client_session_emitted: false, // client-side 'session' event fired (TLS 1.2 Session ID or ticket)
|
|
218
|
+
tls12_resume_pending: false, // waiting for 'resumeSession' async callback
|
|
219
|
+
tls12_client_session: null, // client: saved session to resume with (parsed sessionData)
|
|
178
220
|
};
|
|
179
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Emit NSS SSLKEYLOGFILE lines on 'keylog' event (Node.js TLSSocket compat).
|
|
224
|
+
* Used by Wireshark and similar tools to decrypt TLS traffic.
|
|
225
|
+
*
|
|
226
|
+
* Format: "LABEL <client_random_hex> <secret_hex>\n"
|
|
227
|
+
* - TLS 1.2: CLIENT_RANDOM <cr> <master_secret>
|
|
228
|
+
* - TLS 1.3: CLIENT_HANDSHAKE_TRAFFIC_SECRET / SERVER_HANDSHAKE_TRAFFIC_SECRET
|
|
229
|
+
* CLIENT_TRAFFIC_SECRET_0 / SERVER_TRAFFIC_SECRET_0
|
|
230
|
+
*
|
|
231
|
+
* All three functions are zero-allocation when no 'keylog' listeners are attached
|
|
232
|
+
* (single listenerCount check at entry), so leaving this infrastructure in place
|
|
233
|
+
* has no measurable cost in production.
|
|
234
|
+
*/
|
|
235
|
+
function _emitKeylogPair(labelClient, labelServer, secretClient, secretServer) {
|
|
236
|
+
if (ev.listenerCount('keylog') === 0) return;
|
|
237
|
+
let clientRandom = context.isServer ? context.remote_random : context.local_random;
|
|
238
|
+
if (!clientRandom) return;
|
|
239
|
+
// Compute clientRandom hex once — both lines share it
|
|
240
|
+
let crHex = Buffer.from(clientRandom).toString('hex');
|
|
241
|
+
if (secretClient) {
|
|
242
|
+
let line = labelClient + ' ' + crHex + ' ' + Buffer.from(secretClient).toString('hex') + '\n';
|
|
243
|
+
ev.emit('keylog', Buffer.from(line));
|
|
244
|
+
}
|
|
245
|
+
if (secretServer) {
|
|
246
|
+
let line = labelServer + ' ' + crHex + ' ' + Buffer.from(secretServer).toString('hex') + '\n';
|
|
247
|
+
ev.emit('keylog', Buffer.from(line));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** TLS 1.3: emit CLIENT_HANDSHAKE_TRAFFIC_SECRET + SERVER_HANDSHAKE_TRAFFIC_SECRET. */
|
|
252
|
+
function _emitHandshakeKeylog() {
|
|
253
|
+
_emitKeylogPair(
|
|
254
|
+
'CLIENT_HANDSHAKE_TRAFFIC_SECRET', 'SERVER_HANDSHAKE_TRAFFIC_SECRET',
|
|
255
|
+
context.isServer ? context.remote_handshake_traffic_secret : context.local_handshake_traffic_secret,
|
|
256
|
+
context.isServer ? context.local_handshake_traffic_secret : context.remote_handshake_traffic_secret
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** TLS 1.3: emit CLIENT_TRAFFIC_SECRET_0 + SERVER_TRAFFIC_SECRET_0. */
|
|
261
|
+
function _emitAppKeylog() {
|
|
262
|
+
_emitKeylogPair(
|
|
263
|
+
'CLIENT_TRAFFIC_SECRET_0', 'SERVER_TRAFFIC_SECRET_0',
|
|
264
|
+
context.isServer ? context.remote_app_traffic_secret : context.local_app_traffic_secret,
|
|
265
|
+
context.isServer ? context.local_app_traffic_secret : context.remote_app_traffic_secret
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** TLS 1.2: emit CLIENT_RANDOM <client_random> <master_secret>. */
|
|
270
|
+
function _emitKeylog(label, secret) {
|
|
271
|
+
if (ev.listenerCount('keylog') === 0) return;
|
|
272
|
+
let clientRandom = context.isServer ? context.remote_random : context.local_random;
|
|
273
|
+
if (!clientRandom || !secret) return;
|
|
274
|
+
let line = label + ' ' +
|
|
275
|
+
Buffer.from(clientRandom).toString('hex') + ' ' +
|
|
276
|
+
Buffer.from(secret).toString('hex') + '\n';
|
|
277
|
+
ev.emit('keylog', Buffer.from(line));
|
|
278
|
+
}
|
|
279
|
+
|
|
180
280
|
/**
|
|
181
281
|
* Push a handshake message to the transcript.
|
|
182
282
|
* If a transcriptHook is set (by DTLSSession), it transforms the data first.
|
|
183
283
|
* This allows DTLS 1.2 to store DTLS-format entries (with reconstruction data)
|
|
184
284
|
* while TLS and DTLS 1.3 store standard TLS-format entries.
|
|
285
|
+
*
|
|
286
|
+
* Also updates the incremental transcript hash if it's been initialized,
|
|
287
|
+
* so subsequent calls to get_transcript_hash() run in O(1) clone+digest time
|
|
288
|
+
* instead of re-hashing the entire transcript.
|
|
185
289
|
*/
|
|
186
290
|
function pushTranscript(data) {
|
|
187
291
|
if (context.transcriptHook) {
|
|
188
292
|
data = context.transcriptHook(data);
|
|
189
293
|
}
|
|
190
294
|
context.transcript.push(data);
|
|
295
|
+
if (context.transcriptHash !== null) {
|
|
296
|
+
context.transcriptHash.update(data);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Returns the transcript hash using the incremental running hash object.
|
|
302
|
+
* Initializes the running hash on first call (replaying any pre-existing
|
|
303
|
+
* messages), then uses Hash.copy()+digest() on subsequent calls so the
|
|
304
|
+
* running hash keeps accepting more updates.
|
|
305
|
+
*
|
|
306
|
+
* Perf vs old pattern `getHashFn(h)(concatUint8Arrays(transcript))`:
|
|
307
|
+
* - Avoids concat — which allocates a buffer holding ALL transcript bytes
|
|
308
|
+
* - Avoids hashing the entire transcript from scratch every time
|
|
309
|
+
* - Hash.copy() duplicates only the hash state (~hashLen bytes)
|
|
310
|
+
*
|
|
311
|
+
* For a typical handshake with 6-8 messages of a few KB total and ~5 hash
|
|
312
|
+
* computations during key derivation, this saves ~20KB of allocations and
|
|
313
|
+
* re-hashes the same bytes 4 fewer times.
|
|
314
|
+
*/
|
|
315
|
+
function get_transcript_hash(hashName) {
|
|
316
|
+
if (context.transcriptHash !== null && context.transcriptHashName === hashName) {
|
|
317
|
+
return new Uint8Array(context.transcriptHash.copy().digest());
|
|
318
|
+
}
|
|
319
|
+
// Lazy init: create a fresh hash and replay existing transcript into it.
|
|
320
|
+
// After this, pushTranscript() updates the hash incrementally.
|
|
321
|
+
context.transcriptHash = crypto.createHash(hashName);
|
|
322
|
+
context.transcriptHashName = hashName;
|
|
323
|
+
for (let i = 0; i < context.transcript.length; i++) {
|
|
324
|
+
context.transcriptHash.update(context.transcript[i]);
|
|
325
|
+
}
|
|
326
|
+
return new Uint8Array(context.transcriptHash.copy().digest());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Reset the incremental transcript hash. Called after HRR reshape, where the
|
|
331
|
+
* transcript array is replaced with [message_hash(CH1), HRR] and the running
|
|
332
|
+
* hash must be restarted to match.
|
|
333
|
+
*/
|
|
334
|
+
function reset_transcript_hash(hashName) {
|
|
335
|
+
context.transcriptHash = crypto.createHash(hashName);
|
|
336
|
+
context.transcriptHashName = hashName;
|
|
337
|
+
for (let i = 0; i < context.transcript.length; i++) {
|
|
338
|
+
context.transcriptHash.update(context.transcript[i]);
|
|
339
|
+
}
|
|
191
340
|
}
|
|
192
341
|
|
|
193
342
|
function process_income_message(data){
|
|
@@ -202,7 +351,7 @@ function TLSSession(options){
|
|
|
202
351
|
return;
|
|
203
352
|
}
|
|
204
353
|
|
|
205
|
-
let message = parse_tls_message(data);
|
|
354
|
+
let message = parse_tls_message(data, context.selected_version);
|
|
206
355
|
|
|
207
356
|
// Emit 'handshakeMessage' hook for every message
|
|
208
357
|
ev.emit('handshakeMessage', message.type, data, message);
|
|
@@ -233,10 +382,20 @@ function TLSSession(options){
|
|
|
233
382
|
let pskIdentity = message.pre_shared_key.identities[0];
|
|
234
383
|
let pskBinder = message.pre_shared_key.binders ? message.pre_shared_key.binders[0] : null;
|
|
235
384
|
|
|
385
|
+
dbg('SRV-PSK', 'received identity:', hexPreview(pskIdentity.identity, 24),
|
|
386
|
+
'age:', (pskIdentity.age || 0) >>> 0,
|
|
387
|
+
'received binder:', hexPreview(pskBinder, 16));
|
|
388
|
+
|
|
236
389
|
let pskResult = null;
|
|
237
|
-
ev.emit('psk',
|
|
390
|
+
ev.emit('psk', {
|
|
391
|
+
identity: pskIdentity.identity,
|
|
392
|
+
obfuscatedAge: (pskIdentity.age || 0) >>> 0
|
|
393
|
+
}, function(result) {
|
|
238
394
|
pskResult = result;
|
|
239
395
|
});
|
|
396
|
+
|
|
397
|
+
dbg('SRV-PSK', 'pskResult:', pskResult ? `psk=${hexPreview(pskResult.psk, 8)} cipher=0x${pskResult.cipher?.toString(16)}` : 'null (decrypt failed)');
|
|
398
|
+
|
|
240
399
|
if (pskResult && pskResult.psk) {
|
|
241
400
|
let pskCipher = pskResult.cipher || 0x1301;
|
|
242
401
|
let hashName = TLS_CIPHER_SUITES[pskCipher] ? TLS_CIPHER_SUITES[pskCipher].hash : 'sha256';
|
|
@@ -247,6 +406,12 @@ function TLSSession(options){
|
|
|
247
406
|
let truncatedCH = data.slice(0, data.length - bindersSize);
|
|
248
407
|
let expectedBinder = compute_psk_binder(hashName, binder_key, truncatedCH);
|
|
249
408
|
|
|
409
|
+
dbg('SRV-PSK', 'hash:', hashName, 'hashLen:', hashLen,
|
|
410
|
+
'truncatedCH len:', truncatedCH.length,
|
|
411
|
+
'full CH len:', data.length);
|
|
412
|
+
dbg('SRV-PSK', 'expected binder:', hexPreview(expectedBinder, 16));
|
|
413
|
+
dbg('SRV-PSK', 'received binder:', hexPreview(pskBinder, 16));
|
|
414
|
+
|
|
250
415
|
let binderOk = pskBinder && expectedBinder.length === pskBinder.length;
|
|
251
416
|
if (binderOk) {
|
|
252
417
|
for (let bi = 0; bi < expectedBinder.length; bi++) {
|
|
@@ -254,6 +419,8 @@ function TLSSession(options){
|
|
|
254
419
|
}
|
|
255
420
|
}
|
|
256
421
|
|
|
422
|
+
dbg('SRV-PSK', binderOk ? '✓ BINDER MATCH — psk_accepted' : '✗ BINDER MISMATCH — full handshake');
|
|
423
|
+
|
|
257
424
|
if (binderOk) {
|
|
258
425
|
context.psk_accepted = true;
|
|
259
426
|
context.isResumed = true;
|
|
@@ -268,9 +435,12 @@ function TLSSession(options){
|
|
|
268
435
|
// Client: detect if server accepted PSK from ServerHello (BEFORE set_context)
|
|
269
436
|
if (!context.isServer && message.pre_shared_key && typeof message.pre_shared_key.selected === 'number') {
|
|
270
437
|
if (context.psk_offered) {
|
|
438
|
+
dbg('CLI-PSK', '✓ server accepted PSK, selected_identity:', message.pre_shared_key.selected);
|
|
271
439
|
context.psk_accepted = true;
|
|
272
440
|
context.isResumed = true;
|
|
273
441
|
}
|
|
442
|
+
} else if (!context.isServer && context.psk_offered && message.type === 'server_hello') {
|
|
443
|
+
dbg('CLI-PSK', '✗ server did NOT include pre_shared_key in SH — full handshake');
|
|
274
444
|
}
|
|
275
445
|
|
|
276
446
|
// Client: detect HelloRetryRequest (ServerHello with magic random)
|
|
@@ -292,6 +462,11 @@ function TLSSession(options){
|
|
|
292
462
|
let message_hash = wire.build_message(wire.TLS_MESSAGE_TYPE.MESSAGE_HASH, ch1_hash);
|
|
293
463
|
context.transcript = [message_hash, hrrData]; // message_hash + HRR
|
|
294
464
|
|
|
465
|
+
// After HRR, the running hash must be restarted to match the reshaped
|
|
466
|
+
// transcript. If any existing running hash was tracking the old (CH1 + HRR)
|
|
467
|
+
// sequence, it's now stale — we rebuild from the new 2-entry transcript.
|
|
468
|
+
reset_transcript_hash(hashName);
|
|
469
|
+
|
|
295
470
|
// Find the requested group from HRR key_share extension
|
|
296
471
|
// After wire.js fix, key_groups contains [{group: N, key_exchange: empty}] for HRR
|
|
297
472
|
let requestedGroup = null;
|
|
@@ -341,7 +516,7 @@ function TLSSession(options){
|
|
|
341
516
|
0x0401, 0x0501, 0x0601
|
|
342
517
|
] },
|
|
343
518
|
{ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
|
|
344
|
-
{ type:
|
|
519
|
+
{ type: 'EXTENDED_MASTER_SECRET', value: null },
|
|
345
520
|
];
|
|
346
521
|
|
|
347
522
|
// SNI (must be first)
|
|
@@ -358,7 +533,7 @@ function TLSSession(options){
|
|
|
358
533
|
}
|
|
359
534
|
|
|
360
535
|
// Custom extensions (e.g. QUIC transport params 0x39)
|
|
361
|
-
for (let ci
|
|
536
|
+
for (let ci = 0; ci < context.local_extensions.length; ci++) {
|
|
362
537
|
extensions.push(context.local_extensions[ci]);
|
|
363
538
|
}
|
|
364
539
|
|
|
@@ -397,6 +572,122 @@ function TLSSession(options){
|
|
|
397
572
|
add_remote_key_groups: message.key_groups || []
|
|
398
573
|
});
|
|
399
574
|
|
|
575
|
+
// Server: TLS 1.2 resumption detection (only makes sense from ClientHello).
|
|
576
|
+
// This runs BEFORE set_context's reactive loop picks a version, so we mark state
|
|
577
|
+
// but defer the decision. The reactive loop will check tls12_abbreviated once
|
|
578
|
+
// TLS 1.2 is actually selected.
|
|
579
|
+
if (context.isServer && message.type === 'client_hello') {
|
|
580
|
+
|
|
581
|
+
// Was SessionTicket extension present? (empty or with data)
|
|
582
|
+
if (message.session_ticket_supported) {
|
|
583
|
+
context.tls12_session_ticket_requested = true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 1) Try ticket-based resumption (RFC 5077) — stateless, preferred over session_id
|
|
587
|
+
if (message.session_ticket && message.session_ticket.length > 0 && context.ticketKeys && context.sessionTickets) {
|
|
588
|
+
let state = decrypt_session_blob(message.session_ticket, context.ticketKeys);
|
|
589
|
+
if (state && state.v === 12 && state.master_secret) {
|
|
590
|
+
// Honor ticket-based resumption: we'll proceed as abbreviated handshake once TLS 1.2 is selected.
|
|
591
|
+
context.tls12_resume_state = state;
|
|
592
|
+
context.tls12_session_ticket_offered = message.session_ticket;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 2) Try Session ID resumption — emit 'resumeSession' event (async-supported).
|
|
597
|
+
// Only if ticket-based didn't already succeed. Works regardless of sessionTickets
|
|
598
|
+
// setting: if the user registered a 'resumeSession' listener, they opted into
|
|
599
|
+
// Session ID-based resumption.
|
|
600
|
+
if (!context.tls12_resume_state && message.session_id && message.session_id.length > 0) {
|
|
601
|
+
// Fire synchronously-first; listener may resolve immediately OR asynchronously.
|
|
602
|
+
// If async, the listener's callback ends up calling set_context via a helper below.
|
|
603
|
+
let offeredId = message.session_id;
|
|
604
|
+
let resolved = false;
|
|
605
|
+
|
|
606
|
+
let resumeCb = function(err, sessionData) {
|
|
607
|
+
if (resolved) return;
|
|
608
|
+
resolved = true;
|
|
609
|
+
context.tls12_resume_pending = false;
|
|
610
|
+
|
|
611
|
+
if (!err && sessionData) {
|
|
612
|
+
// sessionData may be a structured state (user returned a decoded state)
|
|
613
|
+
// or an encrypted Buffer (user returned what we gave them in 'newSession').
|
|
614
|
+
let state = null;
|
|
615
|
+
if (sessionData instanceof Uint8Array || Buffer.isBuffer(sessionData)) {
|
|
616
|
+
state = decrypt_session_blob(sessionData, context.ticketKeys);
|
|
617
|
+
} else if (typeof sessionData === 'object' && sessionData.master_secret) {
|
|
618
|
+
state = sessionData;
|
|
619
|
+
}
|
|
620
|
+
if (state && state.v === 12 && state.master_secret) {
|
|
621
|
+
set_context({
|
|
622
|
+
tls12_resume_state: state,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// If no state resolved → full handshake. Reactive loop continues once pending clears.
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
context.tls12_resume_pending = true;
|
|
630
|
+
ev.emit('resumeSession', offeredId, resumeCb);
|
|
631
|
+
|
|
632
|
+
// If nobody listened (listenerCount === 0), immediately un-pend.
|
|
633
|
+
if (ev.listenerCount('resumeSession') === 0) {
|
|
634
|
+
resumeCb(null, null);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Client: TLS 1.2 resumption detection.
|
|
640
|
+
// Two cases trigger abbreviated handshake:
|
|
641
|
+
// (a) Session ID-based: server echoes the saved session_id
|
|
642
|
+
// (b) Ticket-based: per RFC 5077 §3.4, if server accepts the ticket AND the CH
|
|
643
|
+
// session_id is non-empty, it MUST echo the same session_id. So if our CH
|
|
644
|
+
// session_id appears back in SH and we offered a ticket, ticket was accepted.
|
|
645
|
+
if (!context.isServer && context.tls12_client_session && message.type === 'server_hello' &&
|
|
646
|
+
message.session_id && message.session_id.length > 0) {
|
|
647
|
+
|
|
648
|
+
let abbreviatedDetected = false;
|
|
649
|
+
let savedSid = context.tls12_client_session.session_id;
|
|
650
|
+
let sentSid = context.local_session_id;
|
|
651
|
+
let hasTicket = context.tls12_client_session.ticket && context.tls12_client_session.ticket.length > 0;
|
|
652
|
+
|
|
653
|
+
dbg('CLI-12RESUME', 'saved sid:', hexPreview(savedSid, 16),
|
|
654
|
+
'sent sid:', hexPreview(sentSid, 16),
|
|
655
|
+
'received sid:', hexPreview(message.session_id, 16),
|
|
656
|
+
'hasTicket:', hasTicket);
|
|
657
|
+
|
|
658
|
+
// Case (a): server's session_id equals the one we had stored from a prior connection
|
|
659
|
+
if (savedSid && savedSid.length > 0 && uint8Equal(message.session_id, savedSid)) {
|
|
660
|
+
abbreviatedDetected = true;
|
|
661
|
+
dbg('CLI-12RESUME', '✓ case (a) matched: SH echoes saved sid');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Case (b): we offered a ticket and server echoed our CH session_id
|
|
665
|
+
if (!abbreviatedDetected && hasTicket && sentSid && sentSid.length > 0 &&
|
|
666
|
+
uint8Equal(message.session_id, sentSid)) {
|
|
667
|
+
abbreviatedDetected = true;
|
|
668
|
+
dbg('CLI-12RESUME', '✓ case (b) matched: SH echoes CH sid after ticket offer');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!abbreviatedDetected) {
|
|
672
|
+
dbg('CLI-12RESUME', '✗ no match — full handshake expected');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (abbreviatedDetected) {
|
|
676
|
+
context.tls12_abbreviated = true;
|
|
677
|
+
context.isResumed = true;
|
|
678
|
+
// Load master_secret and EMS flag from saved session
|
|
679
|
+
set_context({
|
|
680
|
+
base_secret: context.tls12_client_session.master_secret,
|
|
681
|
+
use_extended_master_secret: !!context.tls12_client_session.extended_master_secret,
|
|
682
|
+
// Mark as if remote_hello_done arrived — we won't actually receive it in abbreviated flow,
|
|
683
|
+
// but the reactive loop uses this to gate CKE; we're skipping CKE anyway.
|
|
684
|
+
remote_hello_done: true,
|
|
685
|
+
// Pretend key_exchange_sent so Finished logic proceeds without real CKE
|
|
686
|
+
key_exchange_sent: true,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
400
691
|
ev.emit('hello');
|
|
401
692
|
|
|
402
693
|
if(context.isServer==true){
|
|
@@ -494,20 +785,63 @@ function TLSSession(options){
|
|
|
494
785
|
|
|
495
786
|
}else if(message.type=='new_session_ticket'){
|
|
496
787
|
|
|
497
|
-
// Client receives NewSessionTicket from server (post-handshake)
|
|
498
|
-
if(!context.isServer
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
788
|
+
// Client receives NewSessionTicket from server (post-handshake in TLS 1.3, pre-CCS in TLS 1.2)
|
|
789
|
+
if(!context.isServer){
|
|
790
|
+
if ((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.resumption_master_secret) {
|
|
791
|
+
// TLS 1.3: derive PSK from resumption_master_secret + ticket_nonce
|
|
792
|
+
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
793
|
+
let psk = derive_psk(hashName, context.resumption_master_secret, message.ticket_nonce);
|
|
794
|
+
|
|
795
|
+
dbg('CLI-NST', 'received TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
|
|
796
|
+
'hash:', hashName,
|
|
797
|
+
'transcript len:', concatUint8Arrays(context.transcript).length);
|
|
798
|
+
dbg('CLI-NST', 'ticket_nonce:', hexPreview(message.ticket_nonce, 4),
|
|
799
|
+
'age_add:', message.ticket_age_add,
|
|
800
|
+
'lifetime:', message.ticket_lifetime);
|
|
801
|
+
dbg('CLI-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
|
|
802
|
+
'derived psk:', hexPreview(psk, 8));
|
|
803
|
+
|
|
804
|
+
// Encode opaque client-side session Buffer (JSON — user is responsible for secure storage)
|
|
805
|
+
let session_blob = encode_client_session({
|
|
806
|
+
v: 13, // blob kind: TLS 1.3
|
|
807
|
+
version: context.selected_version,
|
|
808
|
+
cipher: context.selected_cipher_suite,
|
|
809
|
+
ticket: message.ticket,
|
|
810
|
+
psk: psk,
|
|
811
|
+
age_add: message.ticket_age_add,
|
|
812
|
+
lifetime: message.ticket_lifetime,
|
|
813
|
+
sni: context.local_sni || null,
|
|
814
|
+
alpn: context.selected_alpn || null,
|
|
815
|
+
created: Date.now(),
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
ev.emit('session', session_blob);
|
|
819
|
+
} else if (context.selected_version === wire.TLS_VERSION.TLS1_2 && context.base_secret) {
|
|
820
|
+
// TLS 1.2: NewSessionTicket is sent BEFORE server's CCS+Finished and is part of the
|
|
821
|
+
// handshake transcript (RFC 5077 §3.3). Server's Finished hash covers this message,
|
|
822
|
+
// so the client MUST include it in its transcript for verification to succeed.
|
|
823
|
+
pushTranscript(data);
|
|
824
|
+
|
|
825
|
+
// Save the raw ticket so getTLSTicket() can return it (Node compat).
|
|
826
|
+
context.tls12_received_ticket = message.ticket;
|
|
827
|
+
|
|
828
|
+
let session_blob = encode_client_session({
|
|
829
|
+
v: 12, // blob kind: TLS 1.2
|
|
830
|
+
version: context.selected_version,
|
|
831
|
+
cipher: context.selected_cipher_suite,
|
|
832
|
+
master_secret: context.base_secret,
|
|
833
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
834
|
+
ticket: message.ticket,
|
|
835
|
+
session_id: context.remote_session_id || null, // store for Session ID fallback
|
|
836
|
+
lifetime: message.ticket_lifetime_hint || context.ticketLifetime,
|
|
837
|
+
sni: context.local_sni || null,
|
|
838
|
+
alpn: context.selected_alpn || null,
|
|
839
|
+
created: Date.now(),
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
ev.emit('session', session_blob);
|
|
843
|
+
context.tls12_client_session_emitted = true;
|
|
844
|
+
}
|
|
511
845
|
}
|
|
512
846
|
|
|
513
847
|
}else if(message.type=='key_update'){
|
|
@@ -665,7 +999,7 @@ function TLSSession(options){
|
|
|
665
999
|
|
|
666
1000
|
|
|
667
1001
|
if('add_local_key_groups' in options){
|
|
668
|
-
for(let i
|
|
1002
|
+
for(let i = 0; i < options['add_local_key_groups'].length; i++){
|
|
669
1003
|
|
|
670
1004
|
let group=options['add_local_key_groups'][i].group;
|
|
671
1005
|
if(group in context.local_key_groups==false){
|
|
@@ -695,7 +1029,7 @@ function TLSSession(options){
|
|
|
695
1029
|
|
|
696
1030
|
|
|
697
1031
|
if('add_remote_key_groups' in options){
|
|
698
|
-
for(let i
|
|
1032
|
+
for(let i = 0; i < options['add_remote_key_groups'].length; i++){
|
|
699
1033
|
|
|
700
1034
|
let group=options['add_remote_key_groups'][i].group;
|
|
701
1035
|
if(group in context.remote_key_groups==false){
|
|
@@ -797,6 +1131,33 @@ function TLSSession(options){
|
|
|
797
1131
|
}
|
|
798
1132
|
}
|
|
799
1133
|
|
|
1134
|
+
if('tls12_resume_state' in options){
|
|
1135
|
+
if(context.tls12_resume_state !== options.tls12_resume_state){
|
|
1136
|
+
context.tls12_resume_state = options.tls12_resume_state;
|
|
1137
|
+
has_changed=true;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if('tls12_abbreviated' in options){
|
|
1142
|
+
if(context.tls12_abbreviated !== options.tls12_abbreviated){
|
|
1143
|
+
context.tls12_abbreviated = options.tls12_abbreviated;
|
|
1144
|
+
has_changed=true;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if('isResumed' in options){
|
|
1149
|
+
if(context.isResumed !== options.isResumed){
|
|
1150
|
+
context.isResumed = options.isResumed;
|
|
1151
|
+
has_changed=true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if('use_extended_master_secret' in options){
|
|
1156
|
+
if(context.use_extended_master_secret !== options.use_extended_master_secret){
|
|
1157
|
+
context.use_extended_master_secret = options.use_extended_master_secret;
|
|
1158
|
+
has_changed=true;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
800
1161
|
if('ecdhe_shared_secret' in options){
|
|
801
1162
|
if(context.ecdhe_shared_secret==null && options.ecdhe_shared_secret!==null){
|
|
802
1163
|
context.ecdhe_shared_secret=options.ecdhe_shared_secret;
|
|
@@ -809,6 +1170,11 @@ function TLSSession(options){
|
|
|
809
1170
|
if(options.base_secret !== context.base_secret){
|
|
810
1171
|
context.base_secret=options.base_secret;
|
|
811
1172
|
has_changed=true;
|
|
1173
|
+
// TLS 1.2: base_secret IS the master_secret. Emit NSS SSLKEYLOGFILE line.
|
|
1174
|
+
if (options.base_secret && (context.selected_version === wire.TLS_VERSION.TLS1_2 ||
|
|
1175
|
+
context.selected_version === wire.DTLS_VERSION.DTLS1_2)) {
|
|
1176
|
+
_emitKeylog('CLIENT_RANDOM', options.base_secret);
|
|
1177
|
+
}
|
|
812
1178
|
}
|
|
813
1179
|
}
|
|
814
1180
|
|
|
@@ -826,6 +1192,7 @@ function TLSSession(options){
|
|
|
826
1192
|
has_changed=true;
|
|
827
1193
|
if(context.local_handshake_traffic_secret!==null){
|
|
828
1194
|
ev.emit('handshakeSecrets', context.local_handshake_traffic_secret, context.remote_handshake_traffic_secret);
|
|
1195
|
+
_emitHandshakeKeylog();
|
|
829
1196
|
}
|
|
830
1197
|
}
|
|
831
1198
|
}
|
|
@@ -836,6 +1203,7 @@ function TLSSession(options){
|
|
|
836
1203
|
has_changed=true;
|
|
837
1204
|
if(context.remote_handshake_traffic_secret!==null){
|
|
838
1205
|
ev.emit('handshakeSecrets', context.local_handshake_traffic_secret, context.remote_handshake_traffic_secret);
|
|
1206
|
+
_emitHandshakeKeylog();
|
|
839
1207
|
}
|
|
840
1208
|
}
|
|
841
1209
|
}
|
|
@@ -846,6 +1214,7 @@ function TLSSession(options){
|
|
|
846
1214
|
has_changed=true;
|
|
847
1215
|
if(context.local_app_traffic_secret!==null){
|
|
848
1216
|
ev.emit('appSecrets', context.local_app_traffic_secret, context.remote_app_traffic_secret);
|
|
1217
|
+
_emitAppKeylog();
|
|
849
1218
|
}
|
|
850
1219
|
}
|
|
851
1220
|
}
|
|
@@ -856,6 +1225,7 @@ function TLSSession(options){
|
|
|
856
1225
|
has_changed=true;
|
|
857
1226
|
if(context.remote_app_traffic_secret!==null){
|
|
858
1227
|
ev.emit('appSecrets', context.local_app_traffic_secret, context.remote_app_traffic_secret);
|
|
1228
|
+
_emitAppKeylog();
|
|
859
1229
|
}
|
|
860
1230
|
}
|
|
861
1231
|
}
|
|
@@ -945,19 +1315,74 @@ function TLSSession(options){
|
|
|
945
1315
|
//select selected_cipher...
|
|
946
1316
|
if (context.selected_cipher_suite == null && context.local_supported_cipher_suites.length > 0 && context.remote_supported_cipher_suites.length > 0) {
|
|
947
1317
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1318
|
+
// If resuming TLS 1.2, force cipher to match stored state (if client still offers it)
|
|
1319
|
+
if (context.isServer && context.tls12_resume_state &&
|
|
1320
|
+
context.selected_version !== wire.TLS_VERSION.TLS1_3 &&
|
|
1321
|
+
context.selected_version !== wire.DTLS_VERSION.DTLS1_3) {
|
|
1322
|
+
let storedCipher = context.tls12_resume_state.cipher | 0;
|
|
1323
|
+
if (context.remote_supported_cipher_suites.indexOf(storedCipher) >= 0 &&
|
|
1324
|
+
context.local_supported_cipher_suites.indexOf(storedCipher) >= 0) {
|
|
1325
|
+
params_to_set['selected_cipher_suite'] = storedCipher;
|
|
1326
|
+
} else {
|
|
1327
|
+
// Client no longer offers this cipher → can't resume, drop state
|
|
1328
|
+
context.tls12_resume_state = null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// TLS 1.3 PSK resumption: per RFC 8446 §4.2.11, server MUST select a cipher compatible
|
|
1333
|
+
// with the selected PSK (same hash algorithm). Since we accepted the PSK based on its
|
|
1334
|
+
// stored cipher's hash (for binder verification), we force the same cipher here.
|
|
1335
|
+
if (context.isServer && context.psk_accepted && context.psk_offered && context.psk_offered.cipher &&
|
|
1336
|
+
(context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)) {
|
|
1337
|
+
let pskCipher = context.psk_offered.cipher | 0;
|
|
1338
|
+
if (context.remote_supported_cipher_suites.indexOf(pskCipher) >= 0 &&
|
|
1339
|
+
context.local_supported_cipher_suites.indexOf(pskCipher) >= 0) {
|
|
1340
|
+
params_to_set['selected_cipher_suite'] = pskCipher;
|
|
1341
|
+
}
|
|
1342
|
+
// If client no longer offers this cipher, PSK can't be used — but we already accepted it.
|
|
1343
|
+
// This is an edge case; fall through to normal selection and hope for the best.
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (!('selected_cipher_suite' in params_to_set)) {
|
|
1347
|
+
for (let i2 = 0; i2 < context.local_supported_cipher_suites.length; i2++) {
|
|
1348
|
+
let cs = context.local_supported_cipher_suites[i2] | 0;
|
|
1349
|
+
for (let j2 = 0; j2 < context.remote_supported_cipher_suites.length; j2++) {
|
|
1350
|
+
|
|
1351
|
+
if ((context.remote_supported_cipher_suites[j2] | 0) == cs) {
|
|
1352
|
+
params_to_set['selected_cipher_suite'] = cs;
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
955
1355
|
}
|
|
1356
|
+
if ('selected_cipher_suite' in params_to_set==true && params_to_set.selected_cipher_suite !== null) break;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if('selected_cipher_suite' in params_to_set==false || params_to_set.selected_cipher_suite==null){
|
|
956
1360
|
}
|
|
957
|
-
if ('selected_cipher_suite' in params_to_set==true && params_to_set.selected_cipher_suite !== null) break;
|
|
958
1361
|
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// TLS 1.2 abbreviated handshake setup: validate EMS match, seed base_secret, set flags.
|
|
1365
|
+
// Runs once when all prerequisites are met (version + cipher selected, resume state present).
|
|
1366
|
+
if (context.isServer && context.tls12_resume_state && !context.tls12_abbreviated &&
|
|
1367
|
+
params_to_set.selected_cipher_suite != null &&
|
|
1368
|
+
(context.selected_version === wire.TLS_VERSION.TLS1_2 || params_to_set.selected_version === wire.TLS_VERSION.TLS1_2)) {
|
|
1369
|
+
|
|
1370
|
+
let storedEMS = !!context.tls12_resume_state.extended_master_secret;
|
|
1371
|
+
let clientEMS = !!context.use_extended_master_secret;
|
|
959
1372
|
|
|
960
|
-
if(
|
|
1373
|
+
if (storedEMS !== clientEMS) {
|
|
1374
|
+
// EMS mismatch: per RFC 7627 can't resume. Fall through to full handshake.
|
|
1375
|
+
context.tls12_resume_state = null;
|
|
1376
|
+
} else {
|
|
1377
|
+
// OK, we can do abbreviated. Use params_to_set for all state flags
|
|
1378
|
+
// (triggers has_changed → reactive loop re-runs with new state).
|
|
1379
|
+
params_to_set['tls12_abbreviated'] = true;
|
|
1380
|
+
params_to_set['isResumed'] = true;
|
|
1381
|
+
params_to_set['base_secret'] = context.tls12_resume_state.master_secret;
|
|
1382
|
+
// Echo stored session_id if we had one from the client. Otherwise fresh.
|
|
1383
|
+
// For ticket-based resume: client's CH session_id is usually non-empty "random" bytes —
|
|
1384
|
+
// we echo it back (RFC 5077 §3.4). For ID-based: we already have context.remote_session_id.
|
|
1385
|
+
params_to_set['selected_session_id'] = context.remote_session_id || new Uint8Array(0);
|
|
961
1386
|
}
|
|
962
1387
|
}
|
|
963
1388
|
|
|
@@ -1101,6 +1526,9 @@ function TLSSession(options){
|
|
|
1101
1526
|
let message_hash = wire.build_message(wire.TLS_MESSAGE_TYPE.MESSAGE_HASH, ch1_hash);
|
|
1102
1527
|
context.transcript = [message_hash];
|
|
1103
1528
|
|
|
1529
|
+
// Rebuild the running hash to match the reshaped transcript.
|
|
1530
|
+
reset_transcript_hash(hashName);
|
|
1531
|
+
|
|
1104
1532
|
// Build and send HRR (it's a ServerHello with magic random)
|
|
1105
1533
|
let hrr_body = wire.build_hello_retry_request({
|
|
1106
1534
|
cipher_suite: context.selected_cipher_suite,
|
|
@@ -1137,7 +1565,10 @@ function TLSSession(options){
|
|
|
1137
1565
|
}
|
|
1138
1566
|
}
|
|
1139
1567
|
}else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
|
|
1140
|
-
|
|
1568
|
+
// Block ServerHello while waiting for async resumeSession decision
|
|
1569
|
+
if (!context.tls12_resume_pending) {
|
|
1570
|
+
can_send_hello=true;
|
|
1571
|
+
}
|
|
1141
1572
|
}
|
|
1142
1573
|
}
|
|
1143
1574
|
}
|
|
@@ -1194,19 +1625,44 @@ function TLSSession(options){
|
|
|
1194
1625
|
|
|
1195
1626
|
// Only echo extended_master_secret if client sent it
|
|
1196
1627
|
if (context.use_extended_master_secret) {
|
|
1197
|
-
ext_list.push({ type:
|
|
1628
|
+
ext_list.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
|
|
1198
1629
|
}
|
|
1199
1630
|
|
|
1200
|
-
if (context.
|
|
1631
|
+
if (context.selected_alpn) {
|
|
1201
1632
|
// RFC 7301: ServerHello echoes a single selected protocol
|
|
1202
|
-
ext_list.push({ type: 'ALPN', value: [ String(context.
|
|
1633
|
+
ext_list.push({ type: 'ALPN', value: [ String(context.selected_alpn) ] });
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// SESSION_TICKET: RFC 5077 §3.2 — server echoes empty extension to signal it will
|
|
1637
|
+
// send a NewSessionTicket later. Skip for abbreviated handshake (per §3.3).
|
|
1638
|
+
// Skip for DTLS — we don't emit NST in DTLS, so MUST NOT echo the ext either (§3.2).
|
|
1639
|
+
let isDtls12Here = (context.selected_version === wire.DTLS_VERSION.DTLS1_2);
|
|
1640
|
+
if (context.tls12_session_ticket_requested && !context.tls12_abbreviated && context.sessionTickets && !isDtls12Here) {
|
|
1641
|
+
ext_list.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Session ID: for abbreviated (resumed) handshake, MUST echo client's session_id
|
|
1645
|
+
// (RFC 5077 §3.4). For TLS 1.2 full handshake, generate a fresh 32-byte session_id
|
|
1646
|
+
// (matches OpenSSL/Node.js behavior for middlebox compatibility).
|
|
1647
|
+
// For DTLS 1.2 full handshake: just echo client's session_id (no middlebox concern,
|
|
1648
|
+
// and matches original lemon-tls behavior before my TLS 1.2 resumption changes).
|
|
1649
|
+
let sid_to_send;
|
|
1650
|
+
if (context.tls12_abbreviated) {
|
|
1651
|
+
sid_to_send = context.remote_session_id || new Uint8Array(0);
|
|
1652
|
+
} else if (context.selected_version === wire.DTLS_VERSION.DTLS1_2) {
|
|
1653
|
+
sid_to_send = context.remote_session_id || new Uint8Array(0);
|
|
1654
|
+
} else {
|
|
1655
|
+
if (!context.tls12_session_id_for_store) {
|
|
1656
|
+
context.tls12_session_id_for_store = new Uint8Array(crypto.randomBytes(32));
|
|
1657
|
+
}
|
|
1658
|
+
sid_to_send = context.tls12_session_id_for_store;
|
|
1203
1659
|
}
|
|
1204
1660
|
|
|
1205
1661
|
build_message_params = {
|
|
1206
1662
|
type: 'server_hello',
|
|
1207
1663
|
version: context.selected_version,
|
|
1208
1664
|
random: context.local_random,
|
|
1209
|
-
session_id:
|
|
1665
|
+
session_id: sid_to_send,
|
|
1210
1666
|
cipher_suite: context.selected_cipher_suite, // e.g. 0xC02F
|
|
1211
1667
|
// compression_method always 0
|
|
1212
1668
|
extensions: ext_list
|
|
@@ -1215,7 +1671,6 @@ function TLSSession(options){
|
|
|
1215
1671
|
|
|
1216
1672
|
|
|
1217
1673
|
|
|
1218
|
-
|
|
1219
1674
|
}
|
|
1220
1675
|
|
|
1221
1676
|
if(build_message_params!==null){
|
|
@@ -1247,12 +1702,14 @@ function TLSSession(options){
|
|
|
1247
1702
|
|
|
1248
1703
|
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1249
1704
|
let result;
|
|
1705
|
+
// Use incremental transcript hash — avoids concat+hash of full transcript
|
|
1706
|
+
let tx_hash = get_transcript_hash(hashName);
|
|
1250
1707
|
if (context.psk_accepted && context.psk_offered && context.psk_offered.psk) {
|
|
1251
1708
|
// PSK + ECDHE key schedule
|
|
1252
|
-
result =
|
|
1709
|
+
result = derive_handshake_traffic_secrets_psk_with_hash(hashName, context.psk_offered.psk, context.ecdhe_shared_secret, tx_hash);
|
|
1253
1710
|
} else {
|
|
1254
1711
|
// Standard key schedule (no PSK)
|
|
1255
|
-
result =
|
|
1712
|
+
result = derive_handshake_traffic_secrets_with_hash(hashName, context.ecdhe_shared_secret, tx_hash);
|
|
1256
1713
|
}
|
|
1257
1714
|
|
|
1258
1715
|
params_to_set['base_secret']=result.handshake_secret;
|
|
@@ -1322,7 +1779,7 @@ function TLSSession(options){
|
|
|
1322
1779
|
}
|
|
1323
1780
|
|
|
1324
1781
|
|
|
1325
|
-
for(let i
|
|
1782
|
+
for(let i = 0; i < context.local_extensions.length; i++){
|
|
1326
1783
|
extensions.push(context.local_extensions[i]);
|
|
1327
1784
|
}
|
|
1328
1785
|
|
|
@@ -1357,7 +1814,7 @@ function TLSSession(options){
|
|
|
1357
1814
|
context.message_sent_seq++;
|
|
1358
1815
|
}
|
|
1359
1816
|
|
|
1360
|
-
if(context.isServer==true && context.cert_sent==false && context.local_cert_chain!==null && !context.psk_accepted){
|
|
1817
|
+
if(context.isServer==true && context.cert_sent==false && context.local_cert_chain!==null && !context.psk_accepted && !context.tls12_abbreviated){
|
|
1361
1818
|
if(((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.encrypted_exts_sent==true && context.local_handshake_traffic_secret!==null) || ((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2) && context.hello_sent==true)){
|
|
1362
1819
|
|
|
1363
1820
|
let message_data = build_tls_message({
|
|
@@ -1390,7 +1847,8 @@ function TLSSession(options){
|
|
|
1390
1847
|
if (context.isServer==true && (context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
|
|
1391
1848
|
if(context.cert_sent==true && context.cert_verify_sent==false && context.local_cert_chain!==null && context.local_handshake_traffic_secret!==null && context.selected_cipher_suite!==null){
|
|
1392
1849
|
|
|
1393
|
-
let
|
|
1850
|
+
let tbsHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1851
|
+
let tbs_data = build_cert_verify_tbs_with_hash(tbsHashName, true, get_transcript_hash(tbsHashName));
|
|
1394
1852
|
|
|
1395
1853
|
let cert_private_key_obj = crypto.createPrivateKey({
|
|
1396
1854
|
key: Buffer.from(context.cert_private_key),
|
|
@@ -1647,6 +2105,41 @@ function TLSSession(options){
|
|
|
1647
2105
|
}
|
|
1648
2106
|
}
|
|
1649
2107
|
|
|
2108
|
+
// TLS 1.2 / DTLS 1.2 server: send CertificateRequest if mutual auth was
|
|
2109
|
+
// requested via { requestCert: true }. Per RFC 5246 §7.4.4 this message
|
|
2110
|
+
// goes between ServerKeyExchange and ServerHelloDone. The TLS 1.3 path
|
|
2111
|
+
// (line ~1805) sends CertificateRequest between EncryptedExtensions and
|
|
2112
|
+
// Certificate and is handled separately — version branching matters
|
|
2113
|
+
// because the wire formats differ (RFC 5246 §7.4.4 vs RFC 8446 §4.3.2).
|
|
2114
|
+
//
|
|
2115
|
+
// Without this block, a 1.2 server that sets requestCert never actually
|
|
2116
|
+
// requests the client's certificate, the client never sends one (TLS
|
|
2117
|
+
// clients only send a cert in response to CertificateRequest), and any
|
|
2118
|
+
// application-layer fingerprint check on the server fails with "peer
|
|
2119
|
+
// presented no certificate" — most notably breaking WebRTC, which
|
|
2120
|
+
// mandates mutual authentication (RFC 8827 §6.5) and pins to DTLS 1.2.
|
|
2121
|
+
if (context.isServer == true && context.requestCert == true &&
|
|
2122
|
+
!context.certificateRequestSent &&
|
|
2123
|
+
context.key_exchange_sent == true && !context.hello_done_sent &&
|
|
2124
|
+
(context.selected_version === wire.TLS_VERSION.TLS1_2 ||
|
|
2125
|
+
context.selected_version === wire.DTLS_VERSION.DTLS1_2)) {
|
|
2126
|
+
|
|
2127
|
+
let cr_body = wire.build_certificate_request({
|
|
2128
|
+
version: wire.TLS_VERSION.TLS1_2,
|
|
2129
|
+
// rsa_sign(1), ecdsa_sign(64) — accept both. WebRTC uses ECDSA,
|
|
2130
|
+
// most other 1.2 deployments use RSA. The client picks whichever
|
|
2131
|
+
// matches its certificate.
|
|
2132
|
+
certificate_types: [1, 64],
|
|
2133
|
+
signature_algorithms: context.local_supported_signature_algorithms || [],
|
|
2134
|
+
certificate_authorities: [], // empty: accept any CA
|
|
2135
|
+
});
|
|
2136
|
+
let cr_data = wire.build_message(wire.TLS_MESSAGE_TYPE.CERTIFICATE_REQUEST, cr_body);
|
|
2137
|
+
pushTranscript(cr_data);
|
|
2138
|
+
context.certificateRequestSent = true;
|
|
2139
|
+
ev.emit('message', 0, context.message_sent_seq, 'certificate_request', cr_data);
|
|
2140
|
+
context.message_sent_seq++;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1650
2143
|
//server hello done - 1.2 only...
|
|
1651
2144
|
if(context.isServer==true && (context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
|
|
1652
2145
|
if(context.hello_done_sent==false && context.key_exchange_sent==true){
|
|
@@ -1666,6 +2159,73 @@ function TLSSession(options){
|
|
|
1666
2159
|
|
|
1667
2160
|
|
|
1668
2161
|
|
|
2162
|
+
// TLS 1.2 server: send NewSessionTicket (RFC 5077) BEFORE our Finished.
|
|
2163
|
+
// Per RFC 5077 §3.3: "sent during the TLS handshake before the ChangeCipherSpec
|
|
2164
|
+
// message, after the server has successfully verified the client's Finished message."
|
|
2165
|
+
// Must run BEFORE the Finished send block below (which sets finished_sent=true).
|
|
2166
|
+
// NOTE: Only for FULL handshake. In abbreviated handshake, we'd need to signal renewal
|
|
2167
|
+
// via SESSION_TICKET ext in SH (which we don't add for abbreviated per RFC 5077 §3.2),
|
|
2168
|
+
// so sending NST would cause "unexpected message" errors on strict clients (e.g. openssl).
|
|
2169
|
+
// Renewal is optional per RFC 5077 §3.3 — safer to skip it in abbreviated.
|
|
2170
|
+
// Excluded for DTLS 1.2 — implementations (e.g. openssl s_server -dtls1_2) don't always
|
|
2171
|
+
// support it well. Revisit if needed.
|
|
2172
|
+
if (context.selected_version === wire.TLS_VERSION.TLS1_2 &&
|
|
2173
|
+
context.isServer && !context.tls12_newsession_sent && context.sessionTickets &&
|
|
2174
|
+
context.tls12_session_ticket_requested && !context.tls12_abbreviated &&
|
|
2175
|
+
context.base_secret) {
|
|
2176
|
+
|
|
2177
|
+
// Full handshake only: send NST after client's Finished verified, before server's Finished.
|
|
2178
|
+
let can_send_nst = context.remote_finished_ok && !context.finished_sent;
|
|
2179
|
+
|
|
2180
|
+
if (can_send_nst) {
|
|
2181
|
+
context.tls12_newsession_sent = true;
|
|
2182
|
+
|
|
2183
|
+
// Ensure ticketKeys is 48 bytes
|
|
2184
|
+
if (!context.ticketKeys || context.ticketKeys.length !== 48) {
|
|
2185
|
+
context.ticketKeys = crypto.randomBytes(48);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// Build session state to encrypt into ticket
|
|
2189
|
+
let ticket = encrypt_session_blob({
|
|
2190
|
+
v: 12, // blob kind: TLS 1.2
|
|
2191
|
+
version: context.selected_version,
|
|
2192
|
+
cipher: context.selected_cipher_suite,
|
|
2193
|
+
master_secret: context.base_secret,
|
|
2194
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2195
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2196
|
+
alpn: context.selected_alpn || null,
|
|
2197
|
+
created: Date.now(),
|
|
2198
|
+
}, context.ticketKeys);
|
|
2199
|
+
|
|
2200
|
+
let nst_data = build_tls_message({
|
|
2201
|
+
type: 'new_session_ticket_tls12',
|
|
2202
|
+
ticket_lifetime_hint: context.ticketLifetime,
|
|
2203
|
+
ticket: ticket,
|
|
2204
|
+
});
|
|
2205
|
+
|
|
2206
|
+
pushTranscript(nst_data);
|
|
2207
|
+
// epoch 0 = cleartext (server hasn't sent its CCS yet)
|
|
2208
|
+
ev.emit('message', 0, context.message_sent_seq, 'new_session_ticket', nst_data);
|
|
2209
|
+
context.message_sent_seq++;
|
|
2210
|
+
|
|
2211
|
+
// Server-side 'session' event for monitoring / backward compat with lemon-tls
|
|
2212
|
+
let server_session_blob = encode_client_session({
|
|
2213
|
+
v: 12,
|
|
2214
|
+
version: context.selected_version,
|
|
2215
|
+
cipher: context.selected_cipher_suite,
|
|
2216
|
+
master_secret: context.base_secret,
|
|
2217
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2218
|
+
ticket: ticket,
|
|
2219
|
+
session_id: context.remote_session_id || null,
|
|
2220
|
+
lifetime: context.ticketLifetime,
|
|
2221
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2222
|
+
alpn: context.selected_alpn || null,
|
|
2223
|
+
created: Date.now(),
|
|
2224
|
+
});
|
|
2225
|
+
ev.emit('session', server_session_blob);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
1669
2229
|
//send finished...
|
|
1670
2230
|
// Client: send Certificate + CertificateVerify before Finished (if server requested)
|
|
1671
2231
|
if(context.isServer==false && context.certificateRequested && !context.clientCertSent &&
|
|
@@ -1687,7 +2247,7 @@ function TLSSession(options){
|
|
|
1687
2247
|
|
|
1688
2248
|
// Send CertificateVerify
|
|
1689
2249
|
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1690
|
-
let transcript_hash =
|
|
2250
|
+
let transcript_hash = get_transcript_hash(hashName);
|
|
1691
2251
|
let scheme = pick_scheme(context.certificateRequestSigAlgs.length > 0 ? context.certificateRequestSigAlgs : context.local_supported_signature_algorithms, certCtx.privateKey);
|
|
1692
2252
|
let signature = sign_with_scheme(scheme, certCtx.privateKey, transcript_hash, false);
|
|
1693
2253
|
let cv_data = build_tls_message({
|
|
@@ -1720,7 +2280,8 @@ function TLSSession(options){
|
|
|
1720
2280
|
|
|
1721
2281
|
if((context.isServer==false && context.remote_finished_ok==true && context.local_app_traffic_secret!==null && context.remote_app_traffic_secret!==null) || (context.isServer==true && context.cert_verify_sent==true && context.local_cert_chain!==null) || (context.isServer==true && context.psk_accepted==true && context.encrypted_exts_sent==true)){
|
|
1722
2282
|
|
|
1723
|
-
let
|
|
2283
|
+
let finHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2284
|
+
let finished_data=get_handshake_finished_with_hash(finHashName,context.local_handshake_traffic_secret,get_transcript_hash(finHashName));
|
|
1724
2285
|
context.local_finished_data = finished_data;
|
|
1725
2286
|
|
|
1726
2287
|
let message_data = build_tls_message({
|
|
@@ -1740,16 +2301,34 @@ function TLSSession(options){
|
|
|
1740
2301
|
|
|
1741
2302
|
}else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
|
|
1742
2303
|
|
|
1743
|
-
|
|
2304
|
+
// Finished ordering differs between full and abbreviated handshake:
|
|
2305
|
+
// Full: client sends first (after CKE), then server (after client's Finished).
|
|
2306
|
+
// Abbreviated: server sends first (after ServerHello), then client (after server's Finished).
|
|
2307
|
+
let can_send_finished;
|
|
2308
|
+
if (context.tls12_abbreviated) {
|
|
2309
|
+
if (context.isServer) {
|
|
2310
|
+
can_send_finished = context.hello_sent == true; // after ServerHello
|
|
2311
|
+
} else {
|
|
2312
|
+
can_send_finished = context.remote_finished_ok == true; // after server's Finished
|
|
2313
|
+
}
|
|
2314
|
+
} else {
|
|
2315
|
+
if (context.isServer) {
|
|
2316
|
+
can_send_finished = context.remote_finished_ok == true;
|
|
2317
|
+
} else {
|
|
2318
|
+
can_send_finished = context.key_exchange_sent == true;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
if (can_send_finished) {
|
|
1744
2323
|
|
|
1745
|
-
let
|
|
1746
|
-
let transcript_hash =
|
|
2324
|
+
let finishedHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2325
|
+
let transcript_hash = get_transcript_hash(finishedHashName);
|
|
1747
2326
|
|
|
1748
2327
|
let finished_data;
|
|
1749
2328
|
if(context.isServer==true){
|
|
1750
|
-
finished_data=tls12_prf(context.base_secret, "server finished", transcript_hash, 12,
|
|
2329
|
+
finished_data=tls12_prf(context.base_secret, "server finished", transcript_hash, 12, finishedHashName);
|
|
1751
2330
|
}else{
|
|
1752
|
-
finished_data=tls12_prf(context.base_secret, "client finished", transcript_hash, 12,
|
|
2331
|
+
finished_data=tls12_prf(context.base_secret, "client finished", transcript_hash, 12, finishedHashName);
|
|
1753
2332
|
}
|
|
1754
2333
|
context.local_finished_data = finished_data;
|
|
1755
2334
|
|
|
@@ -1778,7 +2357,8 @@ function TLSSession(options){
|
|
|
1778
2357
|
|
|
1779
2358
|
if((context.isServer==true && context.finished_sent==true && context.remote_finished_ok==false) || (context.isServer==false && context.finished_sent==false && context.remote_finished_ok==true)){
|
|
1780
2359
|
|
|
1781
|
-
let
|
|
2360
|
+
let appHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2361
|
+
let result2 = derive_app_traffic_secrets_with_hash(appHashName, context.base_secret, get_transcript_hash(appHashName));
|
|
1782
2362
|
|
|
1783
2363
|
// Save master_secret for resumption before clearing
|
|
1784
2364
|
params_to_set['tls13_master_secret'] = result2.master_secret;
|
|
@@ -1803,7 +2383,8 @@ function TLSSession(options){
|
|
|
1803
2383
|
|
|
1804
2384
|
if((context.isServer==true && context.finished_sent==true) || (context.isServer==false && context.remote_finished !== null)){
|
|
1805
2385
|
|
|
1806
|
-
|
|
2386
|
+
let remoteFinHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2387
|
+
params_to_set['expected_remote_finished']=get_handshake_finished_with_hash(remoteFinHashName,context.remote_handshake_traffic_secret,get_transcript_hash(remoteFinHashName));
|
|
1807
2388
|
|
|
1808
2389
|
}
|
|
1809
2390
|
|
|
@@ -1812,13 +2393,13 @@ function TLSSession(options){
|
|
|
1812
2393
|
if(context.remote_finished!==null){
|
|
1813
2394
|
|
|
1814
2395
|
|
|
1815
|
-
let
|
|
1816
|
-
let transcript_hash =
|
|
2396
|
+
let tls12FinHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2397
|
+
let transcript_hash = get_transcript_hash(tls12FinHashName);
|
|
1817
2398
|
|
|
1818
2399
|
if(context.isServer==true){
|
|
1819
|
-
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "client finished", transcript_hash, 12,
|
|
2400
|
+
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "client finished", transcript_hash, 12, tls12FinHashName);
|
|
1820
2401
|
}else{
|
|
1821
|
-
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "server finished", transcript_hash, 12,
|
|
2402
|
+
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "server finished", transcript_hash, 12, tls12FinHashName);
|
|
1822
2403
|
}
|
|
1823
2404
|
|
|
1824
2405
|
|
|
@@ -1870,46 +2451,53 @@ function TLSSession(options){
|
|
|
1870
2451
|
// TLS 1.3: compute resumption_master_secret (both client and server need it)
|
|
1871
2452
|
if ((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.tls13_master_secret && !context.resumption_master_secret) {
|
|
1872
2453
|
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1873
|
-
context.resumption_master_secret =
|
|
1874
|
-
hashName, context.tls13_master_secret,
|
|
2454
|
+
context.resumption_master_secret = derive_resumption_master_secret_with_hash(
|
|
2455
|
+
hashName, context.tls13_master_secret, get_transcript_hash(hashName)
|
|
1875
2456
|
);
|
|
1876
2457
|
}
|
|
1877
2458
|
|
|
1878
2459
|
// TLS 1.3 server: send NewSessionTicket
|
|
1879
|
-
if ((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.isServer && !context.session_ticket_sent &&
|
|
2460
|
+
if ((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.isServer && !context.session_ticket_sent && context.sessionTickets && context.resumption_master_secret) {
|
|
1880
2461
|
context.session_ticket_sent = true;
|
|
1881
2462
|
|
|
1882
2463
|
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1883
2464
|
let ticket_nonce = new Uint8Array([context.ticket_nonce_counter++]);
|
|
1884
2465
|
let psk = derive_psk(hashName, context.resumption_master_secret, ticket_nonce);
|
|
1885
2466
|
let ticket_age_add = crypto.randomBytes(4).readUInt32BE(0);
|
|
1886
|
-
let ticket_lifetime =
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2467
|
+
let ticket_lifetime = context.ticketLifetime;
|
|
2468
|
+
|
|
2469
|
+
dbg('SRV-NST', 'issuing TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
|
|
2470
|
+
'hash:', hashName,
|
|
2471
|
+
'transcript len:', concatUint8Arrays(context.transcript).length);
|
|
2472
|
+
dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
|
|
2473
|
+
'age_add:', ticket_age_add,
|
|
2474
|
+
'lifetime:', ticket_lifetime);
|
|
2475
|
+
dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
|
|
2476
|
+
'derived psk:', hexPreview(psk, 8));
|
|
2477
|
+
|
|
2478
|
+
// Ensure ticketKeys is 48 bytes (key_name + aes_key)
|
|
2479
|
+
if (!context.ticketKeys || context.ticketKeys.length !== 48) {
|
|
2480
|
+
context.ticketKeys = crypto.randomBytes(48);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// Encrypt session state into opaque ticket (unified format: key_name(16) | IV(12) | CT | Tag(16))
|
|
2484
|
+
let ticket = encrypt_session_blob({
|
|
2485
|
+
v: 13, // blob kind: TLS 1.3 PSK
|
|
2486
|
+
version: context.selected_version,
|
|
1897
2487
|
cipher: context.selected_cipher_suite,
|
|
2488
|
+
psk: psk,
|
|
1898
2489
|
age_add: ticket_age_add,
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
ticket_cipher.final();
|
|
1904
|
-
let ticket_tag = ticket_cipher.getAuthTag();
|
|
1905
|
-
let ticket = Buffer.concat([ticket_iv, ticket_ct, ticket_tag]);
|
|
2490
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2491
|
+
alpn: context.selected_alpn || null,
|
|
2492
|
+
created: Date.now(),
|
|
2493
|
+
}, context.ticketKeys);
|
|
1906
2494
|
|
|
1907
2495
|
let nst_data = wire.build_message(wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET,
|
|
1908
2496
|
wire.build_new_session_ticket({
|
|
1909
2497
|
ticket_lifetime: ticket_lifetime,
|
|
1910
2498
|
ticket_age_add: ticket_age_add,
|
|
1911
2499
|
ticket_nonce: ticket_nonce,
|
|
1912
|
-
ticket:
|
|
2500
|
+
ticket: ticket,
|
|
1913
2501
|
extensions: []
|
|
1914
2502
|
})
|
|
1915
2503
|
);
|
|
@@ -1917,15 +2505,85 @@ function TLSSession(options){
|
|
|
1917
2505
|
ev.emit('message', 2, context.message_sent_seq, 'new_session_ticket', nst_data);
|
|
1918
2506
|
context.message_sent_seq++;
|
|
1919
2507
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2508
|
+
// Emit 'session' event on server side too — lets users track when tickets are
|
|
2509
|
+
// issued (e.g. for monitoring or metrics). Not part of Node.js API but useful.
|
|
2510
|
+
// Emits the same Buffer the client would receive via their 'session' event,
|
|
2511
|
+
// so server-side apps could also persist it if they want.
|
|
2512
|
+
let server_session_blob = encode_client_session({
|
|
2513
|
+
v: 13,
|
|
2514
|
+
version: context.selected_version,
|
|
1924
2515
|
cipher: context.selected_cipher_suite,
|
|
1925
|
-
|
|
2516
|
+
ticket: ticket,
|
|
2517
|
+
psk: psk,
|
|
1926
2518
|
age_add: ticket_age_add,
|
|
1927
|
-
|
|
2519
|
+
lifetime: ticket_lifetime,
|
|
2520
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2521
|
+
alpn: context.selected_alpn || null,
|
|
2522
|
+
created: Date.now(),
|
|
2523
|
+
});
|
|
2524
|
+
ev.emit('session', server_session_blob);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// TLS 1.2 server: emit 'newSession' for Session ID-based resumption.
|
|
2528
|
+
// Fires whenever we generated a session_id for this connection AND didn't issue
|
|
2529
|
+
// a NewSessionTicket (so the client can only resume via Session ID — we need the
|
|
2530
|
+
// user to store the session state). TLS 1.2 only (DTLS 1.2 excluded for now).
|
|
2531
|
+
if (context.selected_version === wire.TLS_VERSION.TLS1_2 &&
|
|
2532
|
+
context.isServer && !context.tls12_abbreviated && !context.tls12_newsession_sent &&
|
|
2533
|
+
context.tls12_session_id_for_store && !context.tls12_session_id_emitted && context.base_secret &&
|
|
2534
|
+
context.remote_finished_ok) {
|
|
2535
|
+
|
|
2536
|
+
context.tls12_session_id_emitted = true;
|
|
2537
|
+
|
|
2538
|
+
// Ensure ticketKeys is 48 bytes (used to encrypt stored session data)
|
|
2539
|
+
if (!context.ticketKeys || context.ticketKeys.length !== 48) {
|
|
2540
|
+
context.ticketKeys = crypto.randomBytes(48);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
let stored_blob = encrypt_session_blob({
|
|
2544
|
+
v: 12,
|
|
2545
|
+
version: context.selected_version,
|
|
2546
|
+
cipher: context.selected_cipher_suite,
|
|
2547
|
+
master_secret: context.base_secret,
|
|
2548
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2549
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2550
|
+
alpn: context.selected_alpn || null,
|
|
2551
|
+
created: Date.now(),
|
|
2552
|
+
}, context.ticketKeys);
|
|
2553
|
+
|
|
2554
|
+
// User stores this; returns it on next handshake via 'resumeSession' callback.
|
|
2555
|
+
ev.emit('newSession', context.tls12_session_id_for_store, stored_blob, function() {
|
|
2556
|
+
// Callback is advisory — we don't block on it in lemon-tls.
|
|
2557
|
+
// (Node.js blocks the handshake until callback is invoked, but our reactive
|
|
2558
|
+
// model decouples this: the session is marked for storage and we continue.)
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// TLS 1.2 client: emit 'session' for Session ID-only resumption (no ticket received).
|
|
2563
|
+
// Fires at secureConnect when the server gave us a non-empty session_id but no
|
|
2564
|
+
// NewSessionTicket — the client's only way to resume is via Session ID, so we must
|
|
2565
|
+
// give the user a blob containing session_id + master_secret to pass back later.
|
|
2566
|
+
// TLS 1.2 only (DTLS 1.2 excluded for now).
|
|
2567
|
+
if (context.selected_version === wire.TLS_VERSION.TLS1_2 &&
|
|
2568
|
+
!context.isServer && !context.tls12_abbreviated && !context.tls12_client_session_emitted &&
|
|
2569
|
+
context.remote_session_id && context.remote_session_id.length > 0 &&
|
|
2570
|
+
context.base_secret && context.remote_finished_ok) {
|
|
2571
|
+
|
|
2572
|
+
context.tls12_client_session_emitted = true;
|
|
2573
|
+
|
|
2574
|
+
let session_blob = encode_client_session({
|
|
2575
|
+
v: 12, // blob kind: TLS 1.2
|
|
2576
|
+
version: context.selected_version,
|
|
2577
|
+
cipher: context.selected_cipher_suite,
|
|
2578
|
+
master_secret: context.base_secret,
|
|
2579
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2580
|
+
ticket: null, // no ticket — Session ID only
|
|
2581
|
+
session_id: context.remote_session_id,
|
|
2582
|
+
sni: context.local_sni || null,
|
|
2583
|
+
alpn: context.selected_alpn || null,
|
|
2584
|
+
created: Date.now(),
|
|
1928
2585
|
});
|
|
2586
|
+
ev.emit('session', session_blob);
|
|
1929
2587
|
}
|
|
1930
2588
|
}
|
|
1931
2589
|
|
|
@@ -2093,7 +2751,7 @@ function TLSSession(options){
|
|
|
2093
2751
|
},
|
|
2094
2752
|
// TLS 1.2 compatibility
|
|
2095
2753
|
{ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
|
|
2096
|
-
{ type:
|
|
2754
|
+
{ type: 'EXTENDED_MASTER_SECRET', value: null }
|
|
2097
2755
|
];
|
|
2098
2756
|
|
|
2099
2757
|
// Add SNI if servername was provided
|
|
@@ -2107,39 +2765,51 @@ function TLSSession(options){
|
|
|
2107
2765
|
}
|
|
2108
2766
|
|
|
2109
2767
|
// Add custom extensions (e.g. QUIC transport params 0x39)
|
|
2110
|
-
for (let i
|
|
2768
|
+
for (let i = 0; i < context.local_extensions.length; i++) {
|
|
2111
2769
|
extensions.push(context.local_extensions[i]);
|
|
2112
2770
|
}
|
|
2113
2771
|
|
|
2114
|
-
//
|
|
2115
|
-
let
|
|
2772
|
+
// Resumption: check if session was provided (opaque Buffer) — decode to structured data
|
|
2773
|
+
let sessionData = null;
|
|
2774
|
+
if (options.session) {
|
|
2775
|
+
// options.session may be a Buffer/Uint8Array (Node.js style) or a plain object (legacy)
|
|
2776
|
+
if (options.session instanceof Uint8Array || Buffer.isBuffer(options.session)) {
|
|
2777
|
+
sessionData = decode_client_session(options.session);
|
|
2778
|
+
} else if (typeof options.session === 'object') {
|
|
2779
|
+
sessionData = options.session; // legacy: already-structured object
|
|
2780
|
+
}
|
|
2781
|
+
} else if (options.psk) {
|
|
2782
|
+
sessionData = options.psk; // legacy path
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2116
2785
|
let message_data;
|
|
2117
2786
|
|
|
2118
|
-
|
|
2787
|
+
// TLS 1.3 PSK resumption (sessionData contains psk)
|
|
2788
|
+
if (sessionData && sessionData.psk && sessionData.ticket && sessionData.cipher) {
|
|
2119
2789
|
// Add PSK key exchange modes (psk_dhe_ke = 1)
|
|
2120
2790
|
extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
|
|
2121
2791
|
|
|
2122
2792
|
// Save PSK for later verification
|
|
2123
2793
|
context.psk_offered = {
|
|
2124
|
-
identity:
|
|
2125
|
-
psk:
|
|
2126
|
-
cipher:
|
|
2127
|
-
age_add:
|
|
2794
|
+
identity: sessionData.ticket,
|
|
2795
|
+
psk: sessionData.psk instanceof Uint8Array ? sessionData.psk : new Uint8Array(sessionData.psk),
|
|
2796
|
+
cipher: sessionData.cipher,
|
|
2797
|
+
age_add: sessionData.age_add || 0,
|
|
2128
2798
|
};
|
|
2129
2799
|
|
|
2130
2800
|
// Compute obfuscated ticket age
|
|
2131
|
-
let ticketAge =
|
|
2132
|
-
let obfuscatedAge = ((ticketAge + (
|
|
2801
|
+
let ticketAge = sessionData.lifetime ? Math.min((Date.now() - (sessionData.created || Date.now())) / 1000, sessionData.lifetime) * 1000 : 0;
|
|
2802
|
+
let obfuscatedAge = ((ticketAge + (sessionData.age_add || 0)) & 0xFFFFFFFF) >>> 0;
|
|
2133
2803
|
|
|
2134
2804
|
// Build ClientHello with placeholder binder to compute truncated hash
|
|
2135
|
-
let hashName = TLS_CIPHER_SUITES[
|
|
2805
|
+
let hashName = TLS_CIPHER_SUITES[sessionData.cipher] ? TLS_CIPHER_SUITES[sessionData.cipher].hash : 'sha256';
|
|
2136
2806
|
let hashLen = getHashFn(hashName).outputLen;
|
|
2137
2807
|
let placeholderBinder = new Uint8Array(hashLen);
|
|
2138
2808
|
|
|
2139
2809
|
let pskExt = {
|
|
2140
2810
|
type: 'PRE_SHARED_KEY',
|
|
2141
2811
|
value: {
|
|
2142
|
-
identities: [{ identity:
|
|
2812
|
+
identities: [{ identity: sessionData.ticket, age: obfuscatedAge }],
|
|
2143
2813
|
binders: [placeholderBinder]
|
|
2144
2814
|
}
|
|
2145
2815
|
};
|
|
@@ -2165,12 +2835,63 @@ function TLSSession(options){
|
|
|
2165
2835
|
let binder_key = derive_binder_key(hashName, context.psk_offered.psk, false);
|
|
2166
2836
|
let binder = compute_psk_binder(hashName, binder_key, truncatedMessage);
|
|
2167
2837
|
|
|
2838
|
+
dbg('CLI-PSK', 'ticket:', hexPreview(sessionData.ticket, 24),
|
|
2839
|
+
'cipher:', '0x' + sessionData.cipher.toString(16),
|
|
2840
|
+
'hash:', hashName);
|
|
2841
|
+
dbg('CLI-PSK', 'psk:', hexPreview(sessionData.psk, 8),
|
|
2842
|
+
'age_add:', sessionData.age_add,
|
|
2843
|
+
'lifetime:', sessionData.lifetime,
|
|
2844
|
+
'ticketAge (ms):', ticketAge,
|
|
2845
|
+
'obfuscatedAge:', obfuscatedAge);
|
|
2846
|
+
dbg('CLI-PSK', 'truncatedMessage len:', truncatedMessage.length,
|
|
2847
|
+
'full CH len (after real binder):', 'see next');
|
|
2848
|
+
dbg('CLI-PSK', 'sent binder:', hexPreview(binder, 16));
|
|
2849
|
+
|
|
2168
2850
|
// Rebuild with real binder
|
|
2169
2851
|
pskExt.value.binders = [binder];
|
|
2170
2852
|
message_data = build_tls_message(build_message_params);
|
|
2171
2853
|
|
|
2854
|
+
} else if (sessionData && sessionData.v === 12 && sessionData.master_secret) {
|
|
2855
|
+
// TLS 1.2 resumption: session ID and/or SessionTicket
|
|
2856
|
+
// Save for later verification when ServerHello arrives
|
|
2857
|
+
context.tls12_client_session = sessionData;
|
|
2858
|
+
|
|
2859
|
+
// Only advertise SESSION_TICKET ext if we actually have a ticket to present.
|
|
2860
|
+
// If we only have a session_id (no ticket), don't include empty SESSION_TICKET ext:
|
|
2861
|
+
// servers with SSL_OP_NO_TICKET can behave inconsistently when the extension appears
|
|
2862
|
+
// alongside a session_id resumption attempt — they may skip the session_id lookup.
|
|
2863
|
+
if (sessionData.ticket && sessionData.ticket.length > 0) {
|
|
2864
|
+
extensions.push({ type: 'SESSION_TICKET', value: sessionData.ticket });
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// If we have a session_id → put it in ClientHello.session_id (overrides the random one)
|
|
2868
|
+
let sid = context.local_session_id;
|
|
2869
|
+
if (sessionData.session_id && sessionData.session_id.length > 0) {
|
|
2870
|
+
sid = sessionData.session_id;
|
|
2871
|
+
context.local_session_id = sid;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
let build_message_params = {
|
|
2875
|
+
type: 'client_hello',
|
|
2876
|
+
version: 0x0303,
|
|
2877
|
+
random: context.local_random,
|
|
2878
|
+
session_id: sid,
|
|
2879
|
+
cookie: context.dtls_cookie,
|
|
2880
|
+
cipher_suite: context.local_supported_cipher_suites,
|
|
2881
|
+
extensions: extensions
|
|
2882
|
+
};
|
|
2883
|
+
message_data = build_tls_message(build_message_params);
|
|
2884
|
+
|
|
2172
2885
|
} else {
|
|
2173
|
-
// No
|
|
2886
|
+
// No resumption — advertise empty SessionTicket extension to offer support.
|
|
2887
|
+
// Skip for DTLS (DTLS clients/servers often don't implement RFC 5077 fully,
|
|
2888
|
+
// and adding it caused interop issues with openssl s_server -dtls1_2).
|
|
2889
|
+
let isDtls = context.local_supported_versions && context.local_supported_versions.some(v => (v & 0xFF00) === 0xFE00);
|
|
2890
|
+
if (!isDtls && context.sessionTickets) {
|
|
2891
|
+
extensions.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// Standard ClientHello
|
|
2174
2895
|
let build_message_params = {
|
|
2175
2896
|
type: 'client_hello',
|
|
2176
2897
|
version: 0x0303,
|
|
@@ -2249,7 +2970,7 @@ function TLSSession(options){
|
|
|
2249
2970
|
|
|
2250
2971
|
/** Returns the negotiated ALPN protocol string (e.g. 'h2'), or null. */
|
|
2251
2972
|
getALPN: function(){
|
|
2252
|
-
return context.
|
|
2973
|
+
return context.selected_alpn || null;
|
|
2253
2974
|
},
|
|
2254
2975
|
|
|
2255
2976
|
/** Returns the remote certificate chain, or null. */
|