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.
@@ -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)
@@ -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'){
@@ -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;
@@ -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),
@@ -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 = getHashFn(hashName)(concatUint8Arrays(context.transcript));
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 finished_data=get_handshake_finished(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash,context.local_handshake_traffic_secret,concatUint8Arrays(context.transcript));
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
- if((context.isServer==true && context.remote_finished_ok==true) || (context.isServer==false && context.key_exchange_sent==true)){
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
- let hashFn = getHashFn(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1746
- let transcript_hash = hashFn(concatUint8Arrays(context.transcript));
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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 result2 = derive_app_traffic_secrets(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash, context.base_secret, concatUint8Arrays(context.transcript));
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
- 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));
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 hashFn = getHashFn(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1816
- let transcript_hash = hashFn(concatUint8Arrays(context.transcript));
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
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 = derive_resumption_master_secret(
1874
- hashName, context.tls13_master_secret, concatUint8Arrays(context.transcript)
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 && !context.noTickets && context.resumption_master_secret) {
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 = 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'),
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
- 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]);
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: new Uint8Array(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
- ev.emit('session', {
1921
- ticket: new Uint8Array(ticket),
1922
- ticket_nonce: ticket_nonce,
1923
- psk: psk,
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
- lifetime: ticket_lifetime,
2481
+ ticket: ticket,
2482
+ psk: psk,
1926
2483
  age_add: ticket_age_add,
1927
- maxEarlyDataSize: 0,
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: 23, data: new Uint8Array(0) } // extended_master_secret
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
- // PSK resumption: check if session/psk was provided
2115
- let pskData = options.session || options.psk || null;
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
- if (pskData && pskData.psk && pskData.ticket && pskData.cipher) {
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: 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,
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 = 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;
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[pskData.cipher] ? TLS_CIPHER_SUITES[pskData.cipher].hash : 'sha256';
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: pskData.ticket, age: obfuscatedAge }],
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 PSKstandard ClientHello
2851
+ // No resumptionadvertise 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.alpn_selected || null;
2938
+ return context.selected_alpn || null;
2253
2939
  },
2254
2940
 
2255
2941
  /** Returns the remote certificate chain, or null. */