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.
@@ -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 for ticket encryption
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', pskIdentity.identity, function(result) {
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: 23, data: new Uint8Array(0) }, // extended_master_secret
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 in context.local_extensions) {
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 && context.resumption_master_secret){
499
- let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
500
- let psk = derive_psk(hashName, context.resumption_master_secret, message.ticket_nonce);
501
-
502
- ev.emit('session', {
503
- ticket: message.ticket,
504
- ticket_nonce: message.ticket_nonce,
505
- psk: psk,
506
- cipher: context.selected_cipher_suite,
507
- lifetime: message.ticket_lifetime,
508
- age_add: message.ticket_age_add,
509
- maxEarlyDataSize: 0,
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 in options['add_local_key_groups']){
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 in options['add_remote_key_groups']){
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
- for (let i2 = 0; i2 < context.local_supported_cipher_suites.length; i2++) {
949
- let cs = context.local_supported_cipher_suites[i2] | 0;
950
- for (let j2 = 0; j2 < context.remote_supported_cipher_suites.length; j2++) {
951
-
952
- if ((context.remote_supported_cipher_suites[j2] | 0) == cs) {
953
- params_to_set['selected_cipher_suite'] = cs;
954
- break;
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('selected_cipher_suite' in params_to_set==false || params_to_set.selected_cipher_suite==null){
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
- can_send_hello=true;
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: 23, data: new Uint8Array(0) });
1628
+ ext_list.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
1198
1629
  }
1199
1630
 
1200
- if (context.alpn_selected) {
1631
+ if (context.selected_alpn) {
1201
1632
  // RFC 7301: ServerHello echoes a single selected protocol
1202
- ext_list.push({ type: 'ALPN', value: [ String(context.alpn_selected) ] });
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: context.remote_session_id || new Uint8Array(0), // echo client 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 = derive_handshake_traffic_secrets_psk(hashName, context.psk_offered.psk, context.ecdhe_shared_secret, concatUint8Arrays(context.transcript));
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 = derive_handshake_traffic_secrets(hashName, context.ecdhe_shared_secret, concatUint8Arrays(context.transcript));
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 in context.local_extensions){
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 tbs_data = build_cert_verify_tbs(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash,true,concatUint8Arrays(context.transcript));
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 = getHashFn(hashName)(concatUint8Arrays(context.transcript));
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 finished_data=get_handshake_finished(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash,context.local_handshake_traffic_secret,concatUint8Arrays(context.transcript));
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
- if((context.isServer==true && context.remote_finished_ok==true) || (context.isServer==false && context.key_exchange_sent==true)){
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 hashFn = getHashFn(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1746
- let transcript_hash = hashFn(concatUint8Arrays(context.transcript));
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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 result2 = derive_app_traffic_secrets(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash, context.base_secret, concatUint8Arrays(context.transcript));
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
- params_to_set['expected_remote_finished']=get_handshake_finished(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash,context.remote_handshake_traffic_secret,concatUint8Arrays(context.transcript));
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 hashFn = getHashFn(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1816
- let transcript_hash = hashFn(concatUint8Arrays(context.transcript));
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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 = derive_resumption_master_secret(
1874
- hashName, context.tls13_master_secret, concatUint8Arrays(context.transcript)
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 && !context.noTickets && context.resumption_master_secret) {
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 = 7200; // 2 hours
1887
-
1888
- // Ticket = encrypted PSK + metadata using ticketKeys (or random fallback)
1889
- // Format: iv(12) || encrypted_json || tag(16)
1890
- // ticketKeys: 48 bytes = name(16) + aes_key(16) + hmac_key(16)
1891
- // We use first 32 bytes as AES-256-GCM key for simplicity
1892
- let tk = context.ticketKeys || crypto.randomBytes(48);
1893
- let ticket_enc_key = tk.length >= 32 ? tk.slice(0, 32) : Buffer.concat([tk, crypto.randomBytes(32 - tk.length)]);
1894
- let ticket_iv = crypto.randomBytes(12);
1895
- let ticket_plaintext = Buffer.from(JSON.stringify({
1896
- psk: Buffer.from(psk).toString('base64'),
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
- created: Date.now()
1900
- }));
1901
- let ticket_cipher = crypto.createCipheriv('aes-256-gcm', ticket_enc_key, ticket_iv);
1902
- let ticket_ct = ticket_cipher.update(ticket_plaintext);
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: new Uint8Array(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
- ev.emit('session', {
1921
- ticket: new Uint8Array(ticket),
1922
- ticket_nonce: ticket_nonce,
1923
- psk: psk,
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
- lifetime: ticket_lifetime,
2516
+ ticket: ticket,
2517
+ psk: psk,
1926
2518
  age_add: ticket_age_add,
1927
- maxEarlyDataSize: 0,
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: 23, data: new Uint8Array(0) } // extended_master_secret
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 in context.local_extensions) {
2768
+ for (let i = 0; i < context.local_extensions.length; i++) {
2111
2769
  extensions.push(context.local_extensions[i]);
2112
2770
  }
2113
2771
 
2114
- // PSK resumption: check if session/psk was provided
2115
- let pskData = options.session || options.psk || null;
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
- if (pskData && pskData.psk && pskData.ticket && pskData.cipher) {
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: pskData.ticket,
2125
- psk: pskData.psk instanceof Uint8Array ? pskData.psk : new Uint8Array(pskData.psk),
2126
- cipher: pskData.cipher,
2127
- age_add: pskData.age_add || 0,
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 = pskData.lifetime ? Math.min((Date.now() - (pskData.created || Date.now())) / 1000, pskData.lifetime) * 1000 : 0;
2132
- let obfuscatedAge = ((ticketAge + (pskData.age_add || 0)) & 0xFFFFFFFF) >>> 0;
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[pskData.cipher] ? TLS_CIPHER_SUITES[pskData.cipher].hash : 'sha256';
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: pskData.ticket, age: obfuscatedAge }],
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 PSKstandard ClientHello
2886
+ // No resumptionadvertise 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.alpn_selected || null;
2973
+ return context.selected_alpn || null;
2253
2974
  },
2254
2975
 
2255
2976
  /** Returns the remote certificate chain, or null. */