lemon-tls 0.2.2 → 0.3.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 +1 -1
- package/src/compat.js +290 -31
- package/src/crypto.js +127 -7
- package/src/record.js +408 -61
- package/src/session/message.js +27 -2
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +780 -94
- 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)
|
|
@@ -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'){
|
|
@@ -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;
|
|
@@ -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),
|
|
@@ -1666,6 +2124,73 @@ function TLSSession(options){
|
|
|
1666
2124
|
|
|
1667
2125
|
|
|
1668
2126
|
|
|
2127
|
+
// TLS 1.2 server: send NewSessionTicket (RFC 5077) BEFORE our Finished.
|
|
2128
|
+
// Per RFC 5077 §3.3: "sent during the TLS handshake before the ChangeCipherSpec
|
|
2129
|
+
// message, after the server has successfully verified the client's Finished message."
|
|
2130
|
+
// Must run BEFORE the Finished send block below (which sets finished_sent=true).
|
|
2131
|
+
// NOTE: Only for FULL handshake. In abbreviated handshake, we'd need to signal renewal
|
|
2132
|
+
// via SESSION_TICKET ext in SH (which we don't add for abbreviated per RFC 5077 §3.2),
|
|
2133
|
+
// so sending NST would cause "unexpected message" errors on strict clients (e.g. openssl).
|
|
2134
|
+
// Renewal is optional per RFC 5077 §3.3 — safer to skip it in abbreviated.
|
|
2135
|
+
// Excluded for DTLS 1.2 — implementations (e.g. openssl s_server -dtls1_2) don't always
|
|
2136
|
+
// support it well. Revisit if needed.
|
|
2137
|
+
if (context.selected_version === wire.TLS_VERSION.TLS1_2 &&
|
|
2138
|
+
context.isServer && !context.tls12_newsession_sent && context.sessionTickets &&
|
|
2139
|
+
context.tls12_session_ticket_requested && !context.tls12_abbreviated &&
|
|
2140
|
+
context.base_secret) {
|
|
2141
|
+
|
|
2142
|
+
// Full handshake only: send NST after client's Finished verified, before server's Finished.
|
|
2143
|
+
let can_send_nst = context.remote_finished_ok && !context.finished_sent;
|
|
2144
|
+
|
|
2145
|
+
if (can_send_nst) {
|
|
2146
|
+
context.tls12_newsession_sent = true;
|
|
2147
|
+
|
|
2148
|
+
// Ensure ticketKeys is 48 bytes
|
|
2149
|
+
if (!context.ticketKeys || context.ticketKeys.length !== 48) {
|
|
2150
|
+
context.ticketKeys = crypto.randomBytes(48);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// Build session state to encrypt into ticket
|
|
2154
|
+
let ticket = encrypt_session_blob({
|
|
2155
|
+
v: 12, // blob kind: TLS 1.2
|
|
2156
|
+
version: context.selected_version,
|
|
2157
|
+
cipher: context.selected_cipher_suite,
|
|
2158
|
+
master_secret: context.base_secret,
|
|
2159
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2160
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2161
|
+
alpn: context.selected_alpn || null,
|
|
2162
|
+
created: Date.now(),
|
|
2163
|
+
}, context.ticketKeys);
|
|
2164
|
+
|
|
2165
|
+
let nst_data = build_tls_message({
|
|
2166
|
+
type: 'new_session_ticket_tls12',
|
|
2167
|
+
ticket_lifetime_hint: context.ticketLifetime,
|
|
2168
|
+
ticket: ticket,
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
pushTranscript(nst_data);
|
|
2172
|
+
// epoch 0 = cleartext (server hasn't sent its CCS yet)
|
|
2173
|
+
ev.emit('message', 0, context.message_sent_seq, 'new_session_ticket', nst_data);
|
|
2174
|
+
context.message_sent_seq++;
|
|
2175
|
+
|
|
2176
|
+
// Server-side 'session' event for monitoring / backward compat with lemon-tls
|
|
2177
|
+
let server_session_blob = encode_client_session({
|
|
2178
|
+
v: 12,
|
|
2179
|
+
version: context.selected_version,
|
|
2180
|
+
cipher: context.selected_cipher_suite,
|
|
2181
|
+
master_secret: context.base_secret,
|
|
2182
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2183
|
+
ticket: ticket,
|
|
2184
|
+
session_id: context.remote_session_id || null,
|
|
2185
|
+
lifetime: context.ticketLifetime,
|
|
2186
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2187
|
+
alpn: context.selected_alpn || null,
|
|
2188
|
+
created: Date.now(),
|
|
2189
|
+
});
|
|
2190
|
+
ev.emit('session', server_session_blob);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1669
2194
|
//send finished...
|
|
1670
2195
|
// Client: send Certificate + CertificateVerify before Finished (if server requested)
|
|
1671
2196
|
if(context.isServer==false && context.certificateRequested && !context.clientCertSent &&
|
|
@@ -1687,7 +2212,7 @@ function TLSSession(options){
|
|
|
1687
2212
|
|
|
1688
2213
|
// Send CertificateVerify
|
|
1689
2214
|
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1690
|
-
let transcript_hash =
|
|
2215
|
+
let transcript_hash = get_transcript_hash(hashName);
|
|
1691
2216
|
let scheme = pick_scheme(context.certificateRequestSigAlgs.length > 0 ? context.certificateRequestSigAlgs : context.local_supported_signature_algorithms, certCtx.privateKey);
|
|
1692
2217
|
let signature = sign_with_scheme(scheme, certCtx.privateKey, transcript_hash, false);
|
|
1693
2218
|
let cv_data = build_tls_message({
|
|
@@ -1720,7 +2245,8 @@ function TLSSession(options){
|
|
|
1720
2245
|
|
|
1721
2246
|
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
2247
|
|
|
1723
|
-
let
|
|
2248
|
+
let finHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2249
|
+
let finished_data=get_handshake_finished_with_hash(finHashName,context.local_handshake_traffic_secret,get_transcript_hash(finHashName));
|
|
1724
2250
|
context.local_finished_data = finished_data;
|
|
1725
2251
|
|
|
1726
2252
|
let message_data = build_tls_message({
|
|
@@ -1740,16 +2266,34 @@ function TLSSession(options){
|
|
|
1740
2266
|
|
|
1741
2267
|
}else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
|
|
1742
2268
|
|
|
1743
|
-
|
|
2269
|
+
// Finished ordering differs between full and abbreviated handshake:
|
|
2270
|
+
// Full: client sends first (after CKE), then server (after client's Finished).
|
|
2271
|
+
// Abbreviated: server sends first (after ServerHello), then client (after server's Finished).
|
|
2272
|
+
let can_send_finished;
|
|
2273
|
+
if (context.tls12_abbreviated) {
|
|
2274
|
+
if (context.isServer) {
|
|
2275
|
+
can_send_finished = context.hello_sent == true; // after ServerHello
|
|
2276
|
+
} else {
|
|
2277
|
+
can_send_finished = context.remote_finished_ok == true; // after server's Finished
|
|
2278
|
+
}
|
|
2279
|
+
} else {
|
|
2280
|
+
if (context.isServer) {
|
|
2281
|
+
can_send_finished = context.remote_finished_ok == true;
|
|
2282
|
+
} else {
|
|
2283
|
+
can_send_finished = context.key_exchange_sent == true;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
1744
2286
|
|
|
1745
|
-
|
|
1746
|
-
|
|
2287
|
+
if (can_send_finished) {
|
|
2288
|
+
|
|
2289
|
+
let finishedHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2290
|
+
let transcript_hash = get_transcript_hash(finishedHashName);
|
|
1747
2291
|
|
|
1748
2292
|
let finished_data;
|
|
1749
2293
|
if(context.isServer==true){
|
|
1750
|
-
finished_data=tls12_prf(context.base_secret, "server finished", transcript_hash, 12,
|
|
2294
|
+
finished_data=tls12_prf(context.base_secret, "server finished", transcript_hash, 12, finishedHashName);
|
|
1751
2295
|
}else{
|
|
1752
|
-
finished_data=tls12_prf(context.base_secret, "client finished", transcript_hash, 12,
|
|
2296
|
+
finished_data=tls12_prf(context.base_secret, "client finished", transcript_hash, 12, finishedHashName);
|
|
1753
2297
|
}
|
|
1754
2298
|
context.local_finished_data = finished_data;
|
|
1755
2299
|
|
|
@@ -1778,7 +2322,8 @@ function TLSSession(options){
|
|
|
1778
2322
|
|
|
1779
2323
|
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
2324
|
|
|
1781
|
-
let
|
|
2325
|
+
let appHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2326
|
+
let result2 = derive_app_traffic_secrets_with_hash(appHashName, context.base_secret, get_transcript_hash(appHashName));
|
|
1782
2327
|
|
|
1783
2328
|
// Save master_secret for resumption before clearing
|
|
1784
2329
|
params_to_set['tls13_master_secret'] = result2.master_secret;
|
|
@@ -1803,7 +2348,8 @@ function TLSSession(options){
|
|
|
1803
2348
|
|
|
1804
2349
|
if((context.isServer==true && context.finished_sent==true) || (context.isServer==false && context.remote_finished !== null)){
|
|
1805
2350
|
|
|
1806
|
-
|
|
2351
|
+
let remoteFinHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2352
|
+
params_to_set['expected_remote_finished']=get_handshake_finished_with_hash(remoteFinHashName,context.remote_handshake_traffic_secret,get_transcript_hash(remoteFinHashName));
|
|
1807
2353
|
|
|
1808
2354
|
}
|
|
1809
2355
|
|
|
@@ -1812,13 +2358,13 @@ function TLSSession(options){
|
|
|
1812
2358
|
if(context.remote_finished!==null){
|
|
1813
2359
|
|
|
1814
2360
|
|
|
1815
|
-
let
|
|
1816
|
-
let transcript_hash =
|
|
2361
|
+
let tls12FinHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2362
|
+
let transcript_hash = get_transcript_hash(tls12FinHashName);
|
|
1817
2363
|
|
|
1818
2364
|
if(context.isServer==true){
|
|
1819
|
-
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "client finished", transcript_hash, 12,
|
|
2365
|
+
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "client finished", transcript_hash, 12, tls12FinHashName);
|
|
1820
2366
|
}else{
|
|
1821
|
-
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "server finished", transcript_hash, 12,
|
|
2367
|
+
params_to_set['expected_remote_finished']=tls12_prf(context.base_secret, "server finished", transcript_hash, 12, tls12FinHashName);
|
|
1822
2368
|
}
|
|
1823
2369
|
|
|
1824
2370
|
|
|
@@ -1870,46 +2416,53 @@ function TLSSession(options){
|
|
|
1870
2416
|
// TLS 1.3: compute resumption_master_secret (both client and server need it)
|
|
1871
2417
|
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
2418
|
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1873
|
-
context.resumption_master_secret =
|
|
1874
|
-
hashName, context.tls13_master_secret,
|
|
2419
|
+
context.resumption_master_secret = derive_resumption_master_secret_with_hash(
|
|
2420
|
+
hashName, context.tls13_master_secret, get_transcript_hash(hashName)
|
|
1875
2421
|
);
|
|
1876
2422
|
}
|
|
1877
2423
|
|
|
1878
2424
|
// 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 &&
|
|
2425
|
+
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
2426
|
context.session_ticket_sent = true;
|
|
1881
2427
|
|
|
1882
2428
|
let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
1883
2429
|
let ticket_nonce = new Uint8Array([context.ticket_nonce_counter++]);
|
|
1884
2430
|
let psk = derive_psk(hashName, context.resumption_master_secret, ticket_nonce);
|
|
1885
2431
|
let ticket_age_add = crypto.randomBytes(4).readUInt32BE(0);
|
|
1886
|
-
let ticket_lifetime =
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2432
|
+
let ticket_lifetime = context.ticketLifetime;
|
|
2433
|
+
|
|
2434
|
+
dbg('SRV-NST', 'issuing TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
|
|
2435
|
+
'hash:', hashName,
|
|
2436
|
+
'transcript len:', concatUint8Arrays(context.transcript).length);
|
|
2437
|
+
dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
|
|
2438
|
+
'age_add:', ticket_age_add,
|
|
2439
|
+
'lifetime:', ticket_lifetime);
|
|
2440
|
+
dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
|
|
2441
|
+
'derived psk:', hexPreview(psk, 8));
|
|
2442
|
+
|
|
2443
|
+
// Ensure ticketKeys is 48 bytes (key_name + aes_key)
|
|
2444
|
+
if (!context.ticketKeys || context.ticketKeys.length !== 48) {
|
|
2445
|
+
context.ticketKeys = crypto.randomBytes(48);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// Encrypt session state into opaque ticket (unified format: key_name(16) | IV(12) | CT | Tag(16))
|
|
2449
|
+
let ticket = encrypt_session_blob({
|
|
2450
|
+
v: 13, // blob kind: TLS 1.3 PSK
|
|
2451
|
+
version: context.selected_version,
|
|
1897
2452
|
cipher: context.selected_cipher_suite,
|
|
2453
|
+
psk: psk,
|
|
1898
2454
|
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]);
|
|
2455
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2456
|
+
alpn: context.selected_alpn || null,
|
|
2457
|
+
created: Date.now(),
|
|
2458
|
+
}, context.ticketKeys);
|
|
1906
2459
|
|
|
1907
2460
|
let nst_data = wire.build_message(wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET,
|
|
1908
2461
|
wire.build_new_session_ticket({
|
|
1909
2462
|
ticket_lifetime: ticket_lifetime,
|
|
1910
2463
|
ticket_age_add: ticket_age_add,
|
|
1911
2464
|
ticket_nonce: ticket_nonce,
|
|
1912
|
-
ticket:
|
|
2465
|
+
ticket: ticket,
|
|
1913
2466
|
extensions: []
|
|
1914
2467
|
})
|
|
1915
2468
|
);
|
|
@@ -1917,15 +2470,85 @@ function TLSSession(options){
|
|
|
1917
2470
|
ev.emit('message', 2, context.message_sent_seq, 'new_session_ticket', nst_data);
|
|
1918
2471
|
context.message_sent_seq++;
|
|
1919
2472
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2473
|
+
// Emit 'session' event on server side too — lets users track when tickets are
|
|
2474
|
+
// issued (e.g. for monitoring or metrics). Not part of Node.js API but useful.
|
|
2475
|
+
// Emits the same Buffer the client would receive via their 'session' event,
|
|
2476
|
+
// so server-side apps could also persist it if they want.
|
|
2477
|
+
let server_session_blob = encode_client_session({
|
|
2478
|
+
v: 13,
|
|
2479
|
+
version: context.selected_version,
|
|
1924
2480
|
cipher: context.selected_cipher_suite,
|
|
1925
|
-
|
|
2481
|
+
ticket: ticket,
|
|
2482
|
+
psk: psk,
|
|
1926
2483
|
age_add: ticket_age_add,
|
|
1927
|
-
|
|
2484
|
+
lifetime: ticket_lifetime,
|
|
2485
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2486
|
+
alpn: context.selected_alpn || null,
|
|
2487
|
+
created: Date.now(),
|
|
2488
|
+
});
|
|
2489
|
+
ev.emit('session', server_session_blob);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// TLS 1.2 server: emit 'newSession' for Session ID-based resumption.
|
|
2493
|
+
// Fires whenever we generated a session_id for this connection AND didn't issue
|
|
2494
|
+
// a NewSessionTicket (so the client can only resume via Session ID — we need the
|
|
2495
|
+
// user to store the session state). TLS 1.2 only (DTLS 1.2 excluded for now).
|
|
2496
|
+
if (context.selected_version === wire.TLS_VERSION.TLS1_2 &&
|
|
2497
|
+
context.isServer && !context.tls12_abbreviated && !context.tls12_newsession_sent &&
|
|
2498
|
+
context.tls12_session_id_for_store && !context.tls12_session_id_emitted && context.base_secret &&
|
|
2499
|
+
context.remote_finished_ok) {
|
|
2500
|
+
|
|
2501
|
+
context.tls12_session_id_emitted = true;
|
|
2502
|
+
|
|
2503
|
+
// Ensure ticketKeys is 48 bytes (used to encrypt stored session data)
|
|
2504
|
+
if (!context.ticketKeys || context.ticketKeys.length !== 48) {
|
|
2505
|
+
context.ticketKeys = crypto.randomBytes(48);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
let stored_blob = encrypt_session_blob({
|
|
2509
|
+
v: 12,
|
|
2510
|
+
version: context.selected_version,
|
|
2511
|
+
cipher: context.selected_cipher_suite,
|
|
2512
|
+
master_secret: context.base_secret,
|
|
2513
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2514
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
2515
|
+
alpn: context.selected_alpn || null,
|
|
2516
|
+
created: Date.now(),
|
|
2517
|
+
}, context.ticketKeys);
|
|
2518
|
+
|
|
2519
|
+
// User stores this; returns it on next handshake via 'resumeSession' callback.
|
|
2520
|
+
ev.emit('newSession', context.tls12_session_id_for_store, stored_blob, function() {
|
|
2521
|
+
// Callback is advisory — we don't block on it in lemon-tls.
|
|
2522
|
+
// (Node.js blocks the handshake until callback is invoked, but our reactive
|
|
2523
|
+
// model decouples this: the session is marked for storage and we continue.)
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// TLS 1.2 client: emit 'session' for Session ID-only resumption (no ticket received).
|
|
2528
|
+
// Fires at secureConnect when the server gave us a non-empty session_id but no
|
|
2529
|
+
// NewSessionTicket — the client's only way to resume is via Session ID, so we must
|
|
2530
|
+
// give the user a blob containing session_id + master_secret to pass back later.
|
|
2531
|
+
// TLS 1.2 only (DTLS 1.2 excluded for now).
|
|
2532
|
+
if (context.selected_version === wire.TLS_VERSION.TLS1_2 &&
|
|
2533
|
+
!context.isServer && !context.tls12_abbreviated && !context.tls12_client_session_emitted &&
|
|
2534
|
+
context.remote_session_id && context.remote_session_id.length > 0 &&
|
|
2535
|
+
context.base_secret && context.remote_finished_ok) {
|
|
2536
|
+
|
|
2537
|
+
context.tls12_client_session_emitted = true;
|
|
2538
|
+
|
|
2539
|
+
let session_blob = encode_client_session({
|
|
2540
|
+
v: 12, // blob kind: TLS 1.2
|
|
2541
|
+
version: context.selected_version,
|
|
2542
|
+
cipher: context.selected_cipher_suite,
|
|
2543
|
+
master_secret: context.base_secret,
|
|
2544
|
+
extended_master_secret: !!context.use_extended_master_secret,
|
|
2545
|
+
ticket: null, // no ticket — Session ID only
|
|
2546
|
+
session_id: context.remote_session_id,
|
|
2547
|
+
sni: context.local_sni || null,
|
|
2548
|
+
alpn: context.selected_alpn || null,
|
|
2549
|
+
created: Date.now(),
|
|
1928
2550
|
});
|
|
2551
|
+
ev.emit('session', session_blob);
|
|
1929
2552
|
}
|
|
1930
2553
|
}
|
|
1931
2554
|
|
|
@@ -2093,7 +2716,7 @@ function TLSSession(options){
|
|
|
2093
2716
|
},
|
|
2094
2717
|
// TLS 1.2 compatibility
|
|
2095
2718
|
{ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
|
|
2096
|
-
{ type:
|
|
2719
|
+
{ type: 'EXTENDED_MASTER_SECRET', value: null }
|
|
2097
2720
|
];
|
|
2098
2721
|
|
|
2099
2722
|
// Add SNI if servername was provided
|
|
@@ -2111,35 +2734,47 @@ function TLSSession(options){
|
|
|
2111
2734
|
extensions.push(context.local_extensions[i]);
|
|
2112
2735
|
}
|
|
2113
2736
|
|
|
2114
|
-
//
|
|
2115
|
-
let
|
|
2737
|
+
// Resumption: check if session was provided (opaque Buffer) — decode to structured data
|
|
2738
|
+
let sessionData = null;
|
|
2739
|
+
if (options.session) {
|
|
2740
|
+
// options.session may be a Buffer/Uint8Array (Node.js style) or a plain object (legacy)
|
|
2741
|
+
if (options.session instanceof Uint8Array || Buffer.isBuffer(options.session)) {
|
|
2742
|
+
sessionData = decode_client_session(options.session);
|
|
2743
|
+
} else if (typeof options.session === 'object') {
|
|
2744
|
+
sessionData = options.session; // legacy: already-structured object
|
|
2745
|
+
}
|
|
2746
|
+
} else if (options.psk) {
|
|
2747
|
+
sessionData = options.psk; // legacy path
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2116
2750
|
let message_data;
|
|
2117
2751
|
|
|
2118
|
-
|
|
2752
|
+
// TLS 1.3 PSK resumption (sessionData contains psk)
|
|
2753
|
+
if (sessionData && sessionData.psk && sessionData.ticket && sessionData.cipher) {
|
|
2119
2754
|
// Add PSK key exchange modes (psk_dhe_ke = 1)
|
|
2120
2755
|
extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
|
|
2121
2756
|
|
|
2122
2757
|
// Save PSK for later verification
|
|
2123
2758
|
context.psk_offered = {
|
|
2124
|
-
identity:
|
|
2125
|
-
psk:
|
|
2126
|
-
cipher:
|
|
2127
|
-
age_add:
|
|
2759
|
+
identity: sessionData.ticket,
|
|
2760
|
+
psk: sessionData.psk instanceof Uint8Array ? sessionData.psk : new Uint8Array(sessionData.psk),
|
|
2761
|
+
cipher: sessionData.cipher,
|
|
2762
|
+
age_add: sessionData.age_add || 0,
|
|
2128
2763
|
};
|
|
2129
2764
|
|
|
2130
2765
|
// Compute obfuscated ticket age
|
|
2131
|
-
let ticketAge =
|
|
2132
|
-
let obfuscatedAge = ((ticketAge + (
|
|
2766
|
+
let ticketAge = sessionData.lifetime ? Math.min((Date.now() - (sessionData.created || Date.now())) / 1000, sessionData.lifetime) * 1000 : 0;
|
|
2767
|
+
let obfuscatedAge = ((ticketAge + (sessionData.age_add || 0)) & 0xFFFFFFFF) >>> 0;
|
|
2133
2768
|
|
|
2134
2769
|
// Build ClientHello with placeholder binder to compute truncated hash
|
|
2135
|
-
let hashName = TLS_CIPHER_SUITES[
|
|
2770
|
+
let hashName = TLS_CIPHER_SUITES[sessionData.cipher] ? TLS_CIPHER_SUITES[sessionData.cipher].hash : 'sha256';
|
|
2136
2771
|
let hashLen = getHashFn(hashName).outputLen;
|
|
2137
2772
|
let placeholderBinder = new Uint8Array(hashLen);
|
|
2138
2773
|
|
|
2139
2774
|
let pskExt = {
|
|
2140
2775
|
type: 'PRE_SHARED_KEY',
|
|
2141
2776
|
value: {
|
|
2142
|
-
identities: [{ identity:
|
|
2777
|
+
identities: [{ identity: sessionData.ticket, age: obfuscatedAge }],
|
|
2143
2778
|
binders: [placeholderBinder]
|
|
2144
2779
|
}
|
|
2145
2780
|
};
|
|
@@ -2165,12 +2800,63 @@ function TLSSession(options){
|
|
|
2165
2800
|
let binder_key = derive_binder_key(hashName, context.psk_offered.psk, false);
|
|
2166
2801
|
let binder = compute_psk_binder(hashName, binder_key, truncatedMessage);
|
|
2167
2802
|
|
|
2803
|
+
dbg('CLI-PSK', 'ticket:', hexPreview(sessionData.ticket, 24),
|
|
2804
|
+
'cipher:', '0x' + sessionData.cipher.toString(16),
|
|
2805
|
+
'hash:', hashName);
|
|
2806
|
+
dbg('CLI-PSK', 'psk:', hexPreview(sessionData.psk, 8),
|
|
2807
|
+
'age_add:', sessionData.age_add,
|
|
2808
|
+
'lifetime:', sessionData.lifetime,
|
|
2809
|
+
'ticketAge (ms):', ticketAge,
|
|
2810
|
+
'obfuscatedAge:', obfuscatedAge);
|
|
2811
|
+
dbg('CLI-PSK', 'truncatedMessage len:', truncatedMessage.length,
|
|
2812
|
+
'full CH len (after real binder):', 'see next');
|
|
2813
|
+
dbg('CLI-PSK', 'sent binder:', hexPreview(binder, 16));
|
|
2814
|
+
|
|
2168
2815
|
// Rebuild with real binder
|
|
2169
2816
|
pskExt.value.binders = [binder];
|
|
2170
2817
|
message_data = build_tls_message(build_message_params);
|
|
2171
2818
|
|
|
2819
|
+
} else if (sessionData && sessionData.v === 12 && sessionData.master_secret) {
|
|
2820
|
+
// TLS 1.2 resumption: session ID and/or SessionTicket
|
|
2821
|
+
// Save for later verification when ServerHello arrives
|
|
2822
|
+
context.tls12_client_session = sessionData;
|
|
2823
|
+
|
|
2824
|
+
// Only advertise SESSION_TICKET ext if we actually have a ticket to present.
|
|
2825
|
+
// If we only have a session_id (no ticket), don't include empty SESSION_TICKET ext:
|
|
2826
|
+
// servers with SSL_OP_NO_TICKET can behave inconsistently when the extension appears
|
|
2827
|
+
// alongside a session_id resumption attempt — they may skip the session_id lookup.
|
|
2828
|
+
if (sessionData.ticket && sessionData.ticket.length > 0) {
|
|
2829
|
+
extensions.push({ type: 'SESSION_TICKET', value: sessionData.ticket });
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// If we have a session_id → put it in ClientHello.session_id (overrides the random one)
|
|
2833
|
+
let sid = context.local_session_id;
|
|
2834
|
+
if (sessionData.session_id && sessionData.session_id.length > 0) {
|
|
2835
|
+
sid = sessionData.session_id;
|
|
2836
|
+
context.local_session_id = sid;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
let build_message_params = {
|
|
2840
|
+
type: 'client_hello',
|
|
2841
|
+
version: 0x0303,
|
|
2842
|
+
random: context.local_random,
|
|
2843
|
+
session_id: sid,
|
|
2844
|
+
cookie: context.dtls_cookie,
|
|
2845
|
+
cipher_suite: context.local_supported_cipher_suites,
|
|
2846
|
+
extensions: extensions
|
|
2847
|
+
};
|
|
2848
|
+
message_data = build_tls_message(build_message_params);
|
|
2849
|
+
|
|
2172
2850
|
} else {
|
|
2173
|
-
// No
|
|
2851
|
+
// No resumption — advertise empty SessionTicket extension to offer support.
|
|
2852
|
+
// Skip for DTLS (DTLS clients/servers often don't implement RFC 5077 fully,
|
|
2853
|
+
// and adding it caused interop issues with openssl s_server -dtls1_2).
|
|
2854
|
+
let isDtls = context.local_supported_versions && context.local_supported_versions.some(v => (v & 0xFF00) === 0xFE00);
|
|
2855
|
+
if (!isDtls && context.sessionTickets) {
|
|
2856
|
+
extensions.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// Standard ClientHello
|
|
2174
2860
|
let build_message_params = {
|
|
2175
2861
|
type: 'client_hello',
|
|
2176
2862
|
version: 0x0303,
|
|
@@ -2249,7 +2935,7 @@ function TLSSession(options){
|
|
|
2249
2935
|
|
|
2250
2936
|
/** Returns the negotiated ALPN protocol string (e.g. 'h2'), or null. */
|
|
2251
2937
|
getALPN: function(){
|
|
2252
|
-
return context.
|
|
2938
|
+
return context.selected_alpn || null;
|
|
2253
2939
|
},
|
|
2254
2940
|
|
|
2255
2941
|
/** Returns the remote certificate chain, or null. */
|