lemon-tls 0.2.1 → 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
@@ -108,6 +127,19 @@ function TLSSession(options){
108
127
 
109
128
 
110
129
  transcript: [],
130
+ transcriptHook: null, // DTLSSession sets this to transform transcript entries
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')
111
143
 
112
144
 
113
145
  //both
@@ -162,17 +194,151 @@ function TLSSession(options){
162
194
  // HelloRetryRequest
163
195
  helloRetried: false, // true if HRR was sent/received
164
196
 
197
+ // DTLS cookie (set by DTLSSession via set_context)
198
+ dtls_cookie: undefined, // Uint8Array or undefined
199
+
165
200
  // TLS 1.3 resumption
166
201
  tls13_master_secret: null,
167
202
  resumption_master_secret: null,
168
203
  ticket_nonce_counter: 0,
169
204
  session_ticket_sent: false,
170
- noTickets: !!options.noTickets,
171
205
  psk_offered: null, // client: { identity, psk, cipher } offered in ClientHello
172
206
  psk_accepted: false, // server accepted PSK → abbreviated handshake
173
- 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)
174
220
  };
175
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
+
280
+ /**
281
+ * Push a handshake message to the transcript.
282
+ * If a transcriptHook is set (by DTLSSession), it transforms the data first.
283
+ * This allows DTLS 1.2 to store DTLS-format entries (with reconstruction data)
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.
289
+ */
290
+ function pushTranscript(data) {
291
+ if (context.transcriptHook) {
292
+ data = context.transcriptHook(data);
293
+ }
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
+ }
340
+ }
341
+
176
342
  function process_income_message(data){
177
343
 
178
344
  // Track handshake start time
@@ -185,14 +351,14 @@ function TLSSession(options){
185
351
  return;
186
352
  }
187
353
 
188
- let message = parse_tls_message(data);
354
+ let message = parse_tls_message(data, context.selected_version);
189
355
 
190
356
  // Emit 'handshakeMessage' hook for every message
191
357
  ev.emit('handshakeMessage', message.type, data, message);
192
358
 
193
359
  if((context.isServer==false && message.type=='server_hello') || (context.isServer==true && message.type=='client_hello')){
194
360
 
195
- context.transcript.push(data);
361
+ pushTranscript(data);
196
362
 
197
363
  // Save raw ClientHello + emit event (server side)
198
364
  if (context.isServer && message.type === 'client_hello') {
@@ -216,10 +382,20 @@ function TLSSession(options){
216
382
  let pskIdentity = message.pre_shared_key.identities[0];
217
383
  let pskBinder = message.pre_shared_key.binders ? message.pre_shared_key.binders[0] : null;
218
384
 
385
+ dbg('SRV-PSK', 'received identity:', hexPreview(pskIdentity.identity, 24),
386
+ 'age:', (pskIdentity.age || 0) >>> 0,
387
+ 'received binder:', hexPreview(pskBinder, 16));
388
+
219
389
  let pskResult = null;
220
- ev.emit('psk', pskIdentity.identity, function(result) {
390
+ ev.emit('psk', {
391
+ identity: pskIdentity.identity,
392
+ obfuscatedAge: (pskIdentity.age || 0) >>> 0
393
+ }, function(result) {
221
394
  pskResult = result;
222
395
  });
396
+
397
+ dbg('SRV-PSK', 'pskResult:', pskResult ? `psk=${hexPreview(pskResult.psk, 8)} cipher=0x${pskResult.cipher?.toString(16)}` : 'null (decrypt failed)');
398
+
223
399
  if (pskResult && pskResult.psk) {
224
400
  let pskCipher = pskResult.cipher || 0x1301;
225
401
  let hashName = TLS_CIPHER_SUITES[pskCipher] ? TLS_CIPHER_SUITES[pskCipher].hash : 'sha256';
@@ -230,6 +406,12 @@ function TLSSession(options){
230
406
  let truncatedCH = data.slice(0, data.length - bindersSize);
231
407
  let expectedBinder = compute_psk_binder(hashName, binder_key, truncatedCH);
232
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
+
233
415
  let binderOk = pskBinder && expectedBinder.length === pskBinder.length;
234
416
  if (binderOk) {
235
417
  for (let bi = 0; bi < expectedBinder.length; bi++) {
@@ -237,6 +419,8 @@ function TLSSession(options){
237
419
  }
238
420
  }
239
421
 
422
+ dbg('SRV-PSK', binderOk ? '✓ BINDER MATCH — psk_accepted' : '✗ BINDER MISMATCH — full handshake');
423
+
240
424
  if (binderOk) {
241
425
  context.psk_accepted = true;
242
426
  context.isResumed = true;
@@ -251,9 +435,12 @@ function TLSSession(options){
251
435
  // Client: detect if server accepted PSK from ServerHello (BEFORE set_context)
252
436
  if (!context.isServer && message.pre_shared_key && typeof message.pre_shared_key.selected === 'number') {
253
437
  if (context.psk_offered) {
438
+ dbg('CLI-PSK', '✓ server accepted PSK, selected_identity:', message.pre_shared_key.selected);
254
439
  context.psk_accepted = true;
255
440
  context.isResumed = true;
256
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');
257
444
  }
258
445
 
259
446
  // Client: detect HelloRetryRequest (ServerHello with magic random)
@@ -275,6 +462,11 @@ function TLSSession(options){
275
462
  let message_hash = wire.build_message(wire.TLS_MESSAGE_TYPE.MESSAGE_HASH, ch1_hash);
276
463
  context.transcript = [message_hash, hrrData]; // message_hash + HRR
277
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
+
278
470
  // Find the requested group from HRR key_share extension
279
471
  // After wire.js fix, key_groups contains [{group: N, key_exchange: empty}] for HRR
280
472
  let requestedGroup = null;
@@ -324,7 +516,7 @@ function TLSSession(options){
324
516
  0x0401, 0x0501, 0x0601
325
517
  ] },
326
518
  { type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
327
- { type: 23, data: new Uint8Array(0) }, // extended_master_secret
519
+ { type: 'EXTENDED_MASTER_SECRET', value: null },
328
520
  ];
329
521
 
330
522
  // SNI (must be first)
@@ -350,11 +542,12 @@ function TLSSession(options){
350
542
  version: 0x0303,
351
543
  random: context.local_random,
352
544
  session_id: context.local_session_id,
545
+ cookie: context.dtls_cookie,
353
546
  cipher_suite: context.local_supported_cipher_suites,
354
547
  extensions: extensions,
355
548
  });
356
549
 
357
- context.transcript.push(ch2);
550
+ pushTranscript(ch2);
358
551
  ev.emit('message', 0, context.message_sent_seq, 'hello', ch2);
359
552
  context.message_sent_seq++;
360
553
  }
@@ -368,7 +561,9 @@ function TLSSession(options){
368
561
  remote_random: message.random || null,
369
562
  remote_sni: message.sni || null,
370
563
  remote_session_id: message.session_id || null,
371
- remote_supported_versions: message.supported_versions || [],
564
+ remote_supported_versions: (message.supported_versions && message.supported_versions.length > 0)
565
+ ? message.supported_versions
566
+ : (message.legacy_version ? [message.legacy_version] : []),
372
567
  remote_supported_alpns: message.alpn || [],
373
568
  remote_supported_cipher_suites: message.cipher_suites || [],
374
569
  remote_supported_signature_algorithms: message.signature_algorithms || [],
@@ -377,6 +572,122 @@ function TLSSession(options){
377
572
  add_remote_key_groups: message.key_groups || []
378
573
  });
379
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
+
380
691
  ev.emit('hello');
381
692
 
382
693
  if(context.isServer==true){
@@ -396,21 +707,27 @@ function TLSSession(options){
396
707
 
397
708
  }else if(message.type=='client_key_exchange' || message.type=='server_key_exchange'){
398
709
 
399
- context.transcript.push(data);
710
+ pushTranscript(data);
400
711
 
401
712
  if ([0xC02F,0xC02B,0xC030,0xC02C,0xC013,0xC014,0xC009,0xC00A].includes(context.selected_cipher_suite)==true) {//ECDHE
402
713
 
403
714
  // ServerKeyExchange carries the group; ClientKeyExchange does not (server already chose it)
404
715
  let kex_group = message.group || context.selected_group;
405
716
 
406
- set_context({
717
+ let kex_updates = {
407
718
  add_remote_key_groups: [
408
719
  {
409
720
  group: kex_group,
410
721
  public_key: message.public_key
411
722
  }
412
723
  ],
413
- });
724
+ };
725
+ // TLS 1.2 client: selected_group isn't set from ServerHello (no supported_groups ext).
726
+ // Set it from the SKE group so the reactive loop can generate a keypair and build CKE.
727
+ if (context.selected_group === null && kex_group) {
728
+ kex_updates.selected_group = kex_group;
729
+ }
730
+ set_context(kex_updates);
414
731
 
415
732
  }else if ([0x009E,0x009F,0x0033,0x0039,0x0067,0x006B].includes(context.selected_cipher_suite)==true) {//DHE
416
733
 
@@ -426,7 +743,7 @@ function TLSSession(options){
426
743
 
427
744
  }else if(message.type=='server_hello_done'){
428
745
 
429
- context.transcript.push(data);
746
+ pushTranscript(data);
430
747
 
431
748
 
432
749
  set_context({
@@ -435,7 +752,7 @@ function TLSSession(options){
435
752
 
436
753
  }else if(message.type=='encrypted_extensions'){
437
754
 
438
- context.transcript.push(data);
755
+ pushTranscript(data);
439
756
 
440
757
  set_context({
441
758
  remote_supported_groups: message.supported_groups || [],
@@ -443,7 +760,7 @@ function TLSSession(options){
443
760
 
444
761
  }else if(message.type=='certificate'){
445
762
 
446
- context.transcript.push(data);
763
+ pushTranscript(data);
447
764
 
448
765
  set_context({
449
766
  remote_cert_chain: message.entries,
@@ -458,7 +775,7 @@ function TLSSession(options){
458
775
 
459
776
  }else if(message.type=='certificate_verify'){
460
777
 
461
- context.transcript.push(data);
778
+ pushTranscript(data);
462
779
 
463
780
  }else if(message.type=='finished'){
464
781
 
@@ -468,26 +785,69 @@ function TLSSession(options){
468
785
 
469
786
  }else if(message.type=='new_session_ticket'){
470
787
 
471
- // Client receives NewSessionTicket from server (post-handshake)
472
- if(!context.isServer && context.resumption_master_secret){
473
- let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
474
- let psk = derive_psk(hashName, context.resumption_master_secret, message.ticket_nonce);
475
-
476
- ev.emit('session', {
477
- ticket: message.ticket,
478
- ticket_nonce: message.ticket_nonce,
479
- psk: psk,
480
- cipher: context.selected_cipher_suite,
481
- lifetime: message.ticket_lifetime,
482
- age_add: message.ticket_age_add,
483
- maxEarlyDataSize: 0,
484
- });
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
+ }
485
845
  }
486
846
 
487
847
  }else if(message.type=='key_update'){
488
848
 
489
849
  // Peer is updating their traffic secret (we update our read key)
490
- if(context.state==='connected' && context.selected_version === wire.TLS_VERSION.TLS1_3){
850
+ if(context.state==='connected' && (context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
491
851
  let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
492
852
  let hashLen = getHashLen(hashName);
493
853
  let newRemoteSecret = hkdf_expand_label(hashName, context.remote_app_traffic_secret, 'traffic upd', new Uint8Array(0), hashLen);
@@ -512,7 +872,7 @@ function TLSSession(options){
512
872
 
513
873
  // Server is requesting a client certificate (TLS 1.3)
514
874
  if(!context.isServer){
515
- context.transcript.push(data);
875
+ pushTranscript(data);
516
876
  context.certificateRequested = true;
517
877
  context.certificateRequestContext = message.certificate_request_context || new Uint8Array(0);
518
878
  context.certificateRequestSigAlgs = message.signature_algorithms || [];
@@ -771,6 +1131,33 @@ function TLSSession(options){
771
1131
  }
772
1132
  }
773
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
+ }
774
1161
  if('ecdhe_shared_secret' in options){
775
1162
  if(context.ecdhe_shared_secret==null && options.ecdhe_shared_secret!==null){
776
1163
  context.ecdhe_shared_secret=options.ecdhe_shared_secret;
@@ -783,6 +1170,11 @@ function TLSSession(options){
783
1170
  if(options.base_secret !== context.base_secret){
784
1171
  context.base_secret=options.base_secret;
785
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
+ }
786
1178
  }
787
1179
  }
788
1180
 
@@ -800,6 +1192,7 @@ function TLSSession(options){
800
1192
  has_changed=true;
801
1193
  if(context.local_handshake_traffic_secret!==null){
802
1194
  ev.emit('handshakeSecrets', context.local_handshake_traffic_secret, context.remote_handshake_traffic_secret);
1195
+ _emitHandshakeKeylog();
803
1196
  }
804
1197
  }
805
1198
  }
@@ -810,6 +1203,7 @@ function TLSSession(options){
810
1203
  has_changed=true;
811
1204
  if(context.remote_handshake_traffic_secret!==null){
812
1205
  ev.emit('handshakeSecrets', context.local_handshake_traffic_secret, context.remote_handshake_traffic_secret);
1206
+ _emitHandshakeKeylog();
813
1207
  }
814
1208
  }
815
1209
  }
@@ -820,6 +1214,7 @@ function TLSSession(options){
820
1214
  has_changed=true;
821
1215
  if(context.local_app_traffic_secret!==null){
822
1216
  ev.emit('appSecrets', context.local_app_traffic_secret, context.remote_app_traffic_secret);
1217
+ _emitAppKeylog();
823
1218
  }
824
1219
  }
825
1220
  }
@@ -830,6 +1225,7 @@ function TLSSession(options){
830
1225
  has_changed=true;
831
1226
  if(context.remote_app_traffic_secret!==null){
832
1227
  ev.emit('appSecrets', context.local_app_traffic_secret, context.remote_app_traffic_secret);
1228
+ _emitAppKeylog();
833
1229
  }
834
1230
  }
835
1231
  }
@@ -871,7 +1267,10 @@ function TLSSession(options){
871
1267
  }
872
1268
  }
873
1269
 
874
-
1270
+ if('dtls_cookie' in options){
1271
+ context.dtls_cookie=options.dtls_cookie;
1272
+ has_changed=true;
1273
+ }
875
1274
 
876
1275
 
877
1276
  }
@@ -901,24 +1300,89 @@ function TLSSession(options){
901
1300
 
902
1301
  if('selected_version' in params_to_set==false || params_to_set.selected_version==null){
903
1302
  }
1303
+
1304
+ // TLS 1.2: clear key_share groups from ClientHello.
1305
+ // key_share is a TLS 1.3 extension; in TLS 1.2, keys come from CKE/SKE.
1306
+ // Without this, the server would compute the shared secret too early
1307
+ // (using CH key_share instead of waiting for CKE).
1308
+ if (context.isServer && params_to_set.selected_version !== null &&
1309
+ params_to_set.selected_version !== wire.TLS_VERSION.TLS1_3 &&
1310
+ params_to_set.selected_version !== wire.DTLS_VERSION.DTLS1_3) {
1311
+ context.remote_key_groups = {};
1312
+ }
904
1313
  }
905
1314
 
906
1315
  //select selected_cipher...
907
1316
  if (context.selected_cipher_suite == null && context.local_supported_cipher_suites.length > 0 && context.remote_supported_cipher_suites.length > 0) {
908
1317
 
909
- for (let i2 = 0; i2 < context.local_supported_cipher_suites.length; i2++) {
910
- let cs = context.local_supported_cipher_suites[i2] | 0;
911
- for (let j2 = 0; j2 < context.remote_supported_cipher_suites.length; j2++) {
912
-
913
- if ((context.remote_supported_cipher_suites[j2] | 0) == cs) {
914
- params_to_set['selected_cipher_suite'] = cs;
915
- 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
+ }
916
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){
917
1360
  }
918
- if ('selected_cipher_suite' in params_to_set==true && params_to_set.selected_cipher_suite !== null) break;
919
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;
920
1372
 
921
- 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);
922
1386
  }
923
1387
  }
924
1388
 
@@ -1050,7 +1514,7 @@ function TLSSession(options){
1050
1514
  if(context.isServer==true){
1051
1515
 
1052
1516
  // HelloRetryRequest: if we selected a group but client didn't send a key_share for it
1053
- if(context.hello_sent==false && !context.helloRetried && context.selected_version === wire.TLS_VERSION.TLS1_3 &&
1517
+ if(context.hello_sent==false && !context.helloRetried && (context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) &&
1054
1518
  context.selected_group !== null && context.selected_cipher_suite !== null &&
1055
1519
  !(context.selected_group in context.remote_key_groups)){
1056
1520
 
@@ -1062,15 +1526,18 @@ function TLSSession(options){
1062
1526
  let message_hash = wire.build_message(wire.TLS_MESSAGE_TYPE.MESSAGE_HASH, ch1_hash);
1063
1527
  context.transcript = [message_hash];
1064
1528
 
1529
+ // Rebuild the running hash to match the reshaped transcript.
1530
+ reset_transcript_hash(hashName);
1531
+
1065
1532
  // Build and send HRR (it's a ServerHello with magic random)
1066
1533
  let hrr_body = wire.build_hello_retry_request({
1067
1534
  cipher_suite: context.selected_cipher_suite,
1068
- selected_version: wire.TLS_VERSION.TLS1_3,
1535
+ selected_version: context.selected_version,
1069
1536
  selected_group: context.selected_group,
1070
1537
  session_id: context.remote_session_id,
1071
1538
  });
1072
1539
  let hrr_data = wire.build_message(wire.TLS_MESSAGE_TYPE.SERVER_HELLO, hrr_body);
1073
- context.transcript.push(hrr_data);
1540
+ pushTranscript(hrr_data);
1074
1541
 
1075
1542
  ev.emit('message', 0, context.message_sent_seq, 'hello_retry_request', hrr_data);
1076
1543
  context.message_sent_seq++;
@@ -1090,15 +1557,18 @@ function TLSSession(options){
1090
1557
  if(context.hello_sent==false){
1091
1558
 
1092
1559
  if(context.selected_version!==null && context.selected_cipher_suite!==null && context.selected_session_id!==null){
1093
- if(context.selected_version === wire.TLS_VERSION.TLS1_3){
1560
+ if((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
1094
1561
  if(context.selected_group in context.local_key_groups==true && context.local_key_groups[context.selected_group].public_key!==null){
1095
1562
  // After HRR, don't send ServerHello until CH2 provides the requested key_share
1096
1563
  if (!context.helloRetried || (context.selected_group in context.remote_key_groups)) {
1097
1564
  can_send_hello=true;
1098
1565
  }
1099
1566
  }
1100
- }else if(context.selected_version === wire.TLS_VERSION.TLS1_2){
1101
- can_send_hello=true;
1567
+ }else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
1568
+ // Block ServerHello while waiting for async resumeSession decision
1569
+ if (!context.tls12_resume_pending) {
1570
+ can_send_hello=true;
1571
+ }
1102
1572
  }
1103
1573
  }
1104
1574
  }
@@ -1111,12 +1581,12 @@ function TLSSession(options){
1111
1581
 
1112
1582
  let build_message_params=null;
1113
1583
 
1114
- if(context.selected_version==wire.TLS_VERSION.TLS1_3){
1584
+ if((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
1115
1585
 
1116
1586
  let shExtensions = [
1117
1587
  {
1118
1588
  type: 'SUPPORTED_VERSIONS',
1119
- value: wire.TLS_VERSION.TLS1_3
1589
+ value: context.selected_version
1120
1590
  },
1121
1591
  {
1122
1592
  type: 'KEY_SHARE',
@@ -1142,7 +1612,7 @@ function TLSSession(options){
1142
1612
  };
1143
1613
 
1144
1614
 
1145
- }else if(context.selected_version==wire.TLS_VERSION.TLS1_2){
1615
+ }else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
1146
1616
 
1147
1617
 
1148
1618
  // TLS 1.2 ServerHello: no SUPPORTED_VERSIONS or KEY_SHARE.
@@ -1155,19 +1625,44 @@ function TLSSession(options){
1155
1625
 
1156
1626
  // Only echo extended_master_secret if client sent it
1157
1627
  if (context.use_extended_master_secret) {
1158
- ext_list.push({ type: 23, data: new Uint8Array(0) });
1628
+ ext_list.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
1159
1629
  }
1160
1630
 
1161
- if (context.alpn_selected) {
1631
+ if (context.selected_alpn) {
1162
1632
  // RFC 7301: ServerHello echoes a single selected protocol
1163
- 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;
1164
1659
  }
1165
1660
 
1166
1661
  build_message_params = {
1167
1662
  type: 'server_hello',
1168
1663
  version: context.selected_version,
1169
1664
  random: context.local_random,
1170
- session_id: context.remote_session_id || new Uint8Array(0), // echo client session_id
1665
+ session_id: sid_to_send,
1171
1666
  cipher_suite: context.selected_cipher_suite, // e.g. 0xC02F
1172
1667
  // compression_method always 0
1173
1668
  extensions: ext_list
@@ -1176,7 +1671,6 @@ function TLSSession(options){
1176
1671
 
1177
1672
 
1178
1673
 
1179
-
1180
1674
  }
1181
1675
 
1182
1676
  if(build_message_params!==null){
@@ -1184,7 +1678,7 @@ function TLSSession(options){
1184
1678
 
1185
1679
  let message_data = build_tls_message(build_message_params);
1186
1680
 
1187
- context.transcript.push(message_data);
1681
+ pushTranscript(message_data);
1188
1682
 
1189
1683
  context.hello_sent=true;
1190
1684
 
@@ -1204,16 +1698,18 @@ function TLSSession(options){
1204
1698
 
1205
1699
  //get base_secret
1206
1700
  if (context.base_secret==null && context.selected_cipher_suite !== null){
1207
- if(context.selected_version == wire.TLS_VERSION.TLS1_3 && (context.ecdhe_shared_secret !== null)){
1701
+ if((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && (context.ecdhe_shared_secret !== null)){
1208
1702
 
1209
1703
  let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
1210
1704
  let result;
1705
+ // Use incremental transcript hash — avoids concat+hash of full transcript
1706
+ let tx_hash = get_transcript_hash(hashName);
1211
1707
  if (context.psk_accepted && context.psk_offered && context.psk_offered.psk) {
1212
1708
  // PSK + ECDHE key schedule
1213
- 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);
1214
1710
  } else {
1215
1711
  // Standard key schedule (no PSK)
1216
- 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);
1217
1713
  }
1218
1714
 
1219
1715
  params_to_set['base_secret']=result.handshake_secret;
@@ -1226,7 +1722,7 @@ function TLSSession(options){
1226
1722
  params_to_set['remote_handshake_traffic_secret']=result.server_handshake_traffic_secret;
1227
1723
  }
1228
1724
 
1229
- }else if(context.selected_version === wire.TLS_VERSION.TLS1_2 && context.local_random!==null && context.remote_random!==null){
1725
+ }else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2) && context.local_random!==null && context.remote_random!==null){
1230
1726
  if(context.ecdhe_shared_secret !== null){
1231
1727
 
1232
1728
 
@@ -1246,7 +1742,11 @@ function TLSSession(options){
1246
1742
  if(context.isServer || context.key_exchange_sent){
1247
1743
  let hashFn = getHashFn(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1248
1744
 
1249
- let transcript_hash = hashFn(concatUint8Arrays(context.transcript));
1745
+ // Use snapshot up to CKE if available (excludes CertificateVerify)
1746
+ let emsTranscript = context._emsTranscriptLen
1747
+ ? context.transcript.slice(0, context._emsTranscriptLen)
1748
+ : context.transcript;
1749
+ let transcript_hash = hashFn(concatUint8Arrays(emsTranscript));
1250
1750
 
1251
1751
  let master_secret = tls12_prf(context.ecdhe_shared_secret, "extended master secret", transcript_hash, 48, TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1252
1752
 
@@ -1270,7 +1770,7 @@ function TLSSession(options){
1270
1770
 
1271
1771
 
1272
1772
  //send encrypted_extensions...
1273
- if (context.isServer==true && context.selected_version === wire.TLS_VERSION.TLS1_3){
1773
+ if (context.isServer==true && (context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
1274
1774
  if(context.encrypted_exts_sent==false && context.hello_sent==true && context.local_handshake_traffic_secret!==null){
1275
1775
 
1276
1776
  let extensions=[];
@@ -1288,7 +1788,7 @@ function TLSSession(options){
1288
1788
  extensions: extensions
1289
1789
  });
1290
1790
 
1291
- context.transcript.push(message_data);
1791
+ pushTranscript(message_data);
1292
1792
 
1293
1793
  context.encrypted_exts_sent=true;
1294
1794
 
@@ -1302,31 +1802,31 @@ function TLSSession(options){
1302
1802
 
1303
1803
  //send certificate... (skip for PSK resumption — no cert needed)
1304
1804
  // But first: send CertificateRequest if requestCert is set (TLS 1.3 only, between EE and Cert)
1305
- if(context.isServer==true && context.requestCert==true && !context.certificateRequestSent && context.encrypted_exts_sent==true && context.local_handshake_traffic_secret!==null && context.selected_version === wire.TLS_VERSION.TLS1_3 && !context.psk_accepted){
1805
+ if(context.isServer==true && context.requestCert==true && !context.certificateRequestSent && context.encrypted_exts_sent==true && context.local_handshake_traffic_secret!==null && (context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && !context.psk_accepted){
1306
1806
  let cr_data = build_tls_message({
1307
1807
  type: 'certificate_request',
1308
1808
  certificate_request_context: new Uint8Array(0),
1309
1809
  signature_algorithms: context.local_supported_signature_algorithms,
1310
1810
  });
1311
- context.transcript.push(cr_data);
1811
+ pushTranscript(cr_data);
1312
1812
  context.certificateRequestSent = true;
1313
1813
  ev.emit('message', 1, context.message_sent_seq, 'certificate_request', cr_data);
1314
1814
  context.message_sent_seq++;
1315
1815
  }
1316
1816
 
1317
- if(context.isServer==true && context.cert_sent==false && context.local_cert_chain!==null && !context.psk_accepted){
1318
- if((context.selected_version === wire.TLS_VERSION.TLS1_3 && context.encrypted_exts_sent==true && context.local_handshake_traffic_secret!==null) || (context.selected_version === wire.TLS_VERSION.TLS1_2 && context.hello_sent==true)){
1817
+ if(context.isServer==true && context.cert_sent==false && context.local_cert_chain!==null && !context.psk_accepted && !context.tls12_abbreviated){
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)){
1319
1819
 
1320
1820
  let message_data = build_tls_message({
1321
1821
  type: 'certificate',
1322
1822
  version: context.selected_version,
1323
1823
  entries: context.local_cert_chain
1324
1824
  });
1325
- context.transcript.push(message_data);
1825
+ pushTranscript(message_data);
1326
1826
 
1327
1827
  context.cert_sent=true;
1328
1828
 
1329
- if (context.selected_version === wire.TLS_VERSION.TLS1_3){
1829
+ if ((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
1330
1830
  ev.emit('message',1,context.message_sent_seq,'certificate',message_data);
1331
1831
  }else{
1332
1832
  ev.emit('message',0,context.message_sent_seq,'certificate',message_data);
@@ -1344,10 +1844,11 @@ function TLSSession(options){
1344
1844
 
1345
1845
 
1346
1846
  //send certificate verify...
1347
- if (context.isServer==true && context.selected_version === wire.TLS_VERSION.TLS1_3){
1847
+ if (context.isServer==true && (context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
1348
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){
1349
1849
 
1350
- 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));
1351
1852
 
1352
1853
  let cert_private_key_obj = crypto.createPrivateKey({
1353
1854
  key: Buffer.from(context.cert_private_key),
@@ -1454,7 +1955,7 @@ function TLSSession(options){
1454
1955
 
1455
1956
 
1456
1957
 
1457
- context.transcript.push(message_data);
1958
+ pushTranscript(message_data);
1458
1959
 
1459
1960
  context.cert_verify_sent=true;
1460
1961
 
@@ -1477,26 +1978,90 @@ function TLSSession(options){
1477
1978
 
1478
1979
 
1479
1980
  // client/server key exchange - 1.2 only...
1480
- if (context.key_exchange_sent == false && context.selected_version == wire.TLS_VERSION.TLS1_2) {
1981
+ if (context.key_exchange_sent == false && (context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)) {
1481
1982
  if(context.selected_group!==null && context.selected_group in context.local_key_groups==true && context.local_key_groups[context.selected_group].public_key!==null){
1482
1983
 
1483
1984
  if (context.isServer==false && context.remote_hello_done==true) {
1484
1985
 
1986
+ // TLS 1.2: send Certificate before CKE if server requested client auth
1987
+ if (context.certificateRequested && !context.clientCertSent) {
1988
+ context.clientCertSent = true;
1989
+
1990
+ // Build TLS 1.2 Certificate message
1991
+ let certEntries = [];
1992
+ if (context.local_cert_chain && context.local_cert_chain.length > 0) {
1993
+ certEntries = context.local_cert_chain;
1994
+ }
1995
+ // TLS 1.2 Certificate: certificate_list<0..2^24-1>
1996
+ // Each entry: cert_length<3> + cert_der
1997
+ let totalLen = 0;
1998
+ for (let ci = 0; ci < certEntries.length; ci++) {
1999
+ totalLen += 3 + certEntries[ci].cert.length;
2000
+ }
2001
+ let certBody = new Uint8Array(3 + totalLen);
2002
+ certBody[0] = (totalLen >> 16) & 0xff;
2003
+ certBody[1] = (totalLen >> 8) & 0xff;
2004
+ certBody[2] = totalLen & 0xff;
2005
+ let off = 3;
2006
+ for (let ci = 0; ci < certEntries.length; ci++) {
2007
+ let der = certEntries[ci].cert;
2008
+ certBody[off] = (der.length >> 16) & 0xff;
2009
+ certBody[off+1] = (der.length >> 8) & 0xff;
2010
+ certBody[off+2] = der.length & 0xff;
2011
+ certBody.set(der, off + 3);
2012
+ off += 3 + der.length;
2013
+ }
2014
+ let cert_data = wire.build_message(wire.TLS_MESSAGE_TYPE.CERTIFICATE, certBody);
2015
+ pushTranscript(cert_data);
2016
+ ev.emit('message', 0, context.message_sent_seq, 'certificate', cert_data);
2017
+ context.message_sent_seq++;
2018
+ }
2019
+
1485
2020
  let public_key = context.local_key_groups[context.selected_group].public_key;
1486
2021
 
1487
2022
  let message_data = build_tls_message({
1488
2023
  type: 'client_key_exchange',
1489
2024
  public_key: public_key,
1490
2025
  });
1491
- context.transcript.push(message_data);
2026
+ pushTranscript(message_data);
1492
2027
 
1493
2028
  // Set via params_to_set to trigger re-evaluation (EMS needs this)
1494
2029
  params_to_set['key_exchange_sent'] = true;
2030
+
2031
+ // Save transcript length for EMS: session_hash includes up to CKE only (RFC 7627)
2032
+ context._emsTranscriptLen = context.transcript.length;
1495
2033
 
1496
2034
  ev.emit('message', 0, context.message_sent_seq, 'client_key_exchange', message_data);
1497
2035
 
1498
2036
  context.message_sent_seq++;
1499
2037
 
2038
+ // TLS 1.2 CertificateVerify: if we sent a non-empty Certificate, prove we own the private key
2039
+ if (context.certificateRequested && context.cert_private_key && context.local_cert_chain && context.local_cert_chain.length > 0) {
2040
+ // sign_with_scheme hashes internally, so pass RAW transcript (not pre-hashed)
2041
+ let transcript_data = concatUint8Arrays(context.transcript);
2042
+
2043
+ // Pick scheme matching our cert + server's requested algorithms
2044
+ let cert_key_obj = crypto.createPrivateKey({ key: Buffer.from(context.cert_private_key), format: 'der', type: 'pkcs8' });
2045
+ let reqAlgs = context.certificateRequestSigAlgs.length > 0
2046
+ ? context.certificateRequestSigAlgs
2047
+ : context.local_supported_signature_algorithms;
2048
+ let scheme = pick_scheme(wire.TLS_VERSION.TLS1_2, cert_key_obj, reqAlgs);
2049
+ let signature = sign_with_scheme(wire.TLS_VERSION.TLS1_2, scheme, transcript_data, cert_key_obj);
2050
+
2051
+ // Build CertificateVerify: scheme(2) + sig_length(2) + sig
2052
+ let cvBody = new Uint8Array(2 + 2 + signature.length);
2053
+ cvBody[0] = (scheme >> 8) & 0xff;
2054
+ cvBody[1] = scheme & 0xff;
2055
+ cvBody[2] = (signature.length >> 8) & 0xff;
2056
+ cvBody[3] = signature.length & 0xff;
2057
+ cvBody.set(signature, 4);
2058
+
2059
+ let cv_data = wire.build_message(wire.TLS_MESSAGE_TYPE.CERTIFICATE_VERIFY, cvBody);
2060
+ pushTranscript(cv_data);
2061
+ ev.emit('message', 0, context.message_sent_seq, 'certificate_verify', cv_data);
2062
+ context.message_sent_seq++;
2063
+ }
2064
+
1500
2065
 
1501
2066
  }else if (context.isServer==true && context.cert_sent == true) {
1502
2067
 
@@ -1528,7 +2093,7 @@ function TLSSession(options){
1528
2093
  sig_alg: scheme12,
1529
2094
  signature: sig_data
1530
2095
  });
1531
- context.transcript.push(message_data);
2096
+ pushTranscript(message_data);
1532
2097
 
1533
2098
  context.key_exchange_sent = true;
1534
2099
 
@@ -1541,16 +2106,16 @@ function TLSSession(options){
1541
2106
  }
1542
2107
 
1543
2108
  //server hello done - 1.2 only...
1544
- if(context.isServer==true && context.selected_version == wire.TLS_VERSION.TLS1_2){
2109
+ if(context.isServer==true && (context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
1545
2110
  if(context.hello_done_sent==false && context.key_exchange_sent==true){
1546
2111
 
1547
2112
  let message_data = build_tls_message({
1548
2113
  type: 'server_hello_done'});
1549
- context.transcript.push(message_data);
2114
+ pushTranscript(message_data);
1550
2115
 
1551
2116
  context.hello_done_sent=true;
1552
2117
 
1553
- ev.emit('message',0,context.message_sent_seq,'certificate',message_data);
2118
+ ev.emit('message',0,context.message_sent_seq,'server_hello_done',message_data);
1554
2119
 
1555
2120
  context.message_sent_seq++;
1556
2121
 
@@ -1559,6 +2124,73 @@ function TLSSession(options){
1559
2124
 
1560
2125
 
1561
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
+
1562
2194
  //send finished...
1563
2195
  // Client: send Certificate + CertificateVerify before Finished (if server requested)
1564
2196
  if(context.isServer==false && context.certificateRequested && !context.clientCertSent &&
@@ -1574,13 +2206,13 @@ function TLSSession(options){
1574
2206
  entries: certCtx.certificateChain,
1575
2207
  certificate_request_context: context.certificateRequestContext || new Uint8Array(0),
1576
2208
  });
1577
- context.transcript.push(cert_data);
2209
+ pushTranscript(cert_data);
1578
2210
  ev.emit('message', 1, context.message_sent_seq, 'certificate', cert_data);
1579
2211
  context.message_sent_seq++;
1580
2212
 
1581
2213
  // Send CertificateVerify
1582
2214
  let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
1583
- let transcript_hash = getHashFn(hashName)(concatUint8Arrays(context.transcript));
2215
+ let transcript_hash = get_transcript_hash(hashName);
1584
2216
  let scheme = pick_scheme(context.certificateRequestSigAlgs.length > 0 ? context.certificateRequestSigAlgs : context.local_supported_signature_algorithms, certCtx.privateKey);
1585
2217
  let signature = sign_with_scheme(scheme, certCtx.privateKey, transcript_hash, false);
1586
2218
  let cv_data = build_tls_message({
@@ -1588,7 +2220,7 @@ function TLSSession(options){
1588
2220
  scheme: scheme,
1589
2221
  signature: signature,
1590
2222
  });
1591
- context.transcript.push(cv_data);
2223
+ pushTranscript(cv_data);
1592
2224
  ev.emit('message', 1, context.message_sent_seq, 'certificate_verify', cv_data);
1593
2225
  context.message_sent_seq++;
1594
2226
  } else {
@@ -1599,7 +2231,7 @@ function TLSSession(options){
1599
2231
  entries: [],
1600
2232
  certificate_request_context: context.certificateRequestContext || new Uint8Array(0),
1601
2233
  });
1602
- context.transcript.push(cert_data);
2234
+ pushTranscript(cert_data);
1603
2235
  ev.emit('message', 1, context.message_sent_seq, 'certificate', cert_data);
1604
2236
  context.message_sent_seq++;
1605
2237
  }
@@ -1609,11 +2241,12 @@ function TLSSession(options){
1609
2241
  // base_secret may be null after app secrets are derived, so we also check handshake secret.
1610
2242
  if (context.finished_sent==false && context.selected_cipher_suite!==null && (context.base_secret!==null || context.local_handshake_traffic_secret!==null)){
1611
2243
 
1612
- if(context.selected_version === wire.TLS_VERSION.TLS1_3 && context.local_handshake_traffic_secret!==null){
2244
+ if((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.local_handshake_traffic_secret!==null){
1613
2245
 
1614
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)){
1615
2247
 
1616
- 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));
1617
2250
  context.local_finished_data = finished_data;
1618
2251
 
1619
2252
  let message_data = build_tls_message({
@@ -1621,7 +2254,7 @@ function TLSSession(options){
1621
2254
  data: finished_data
1622
2255
  });
1623
2256
 
1624
- context.transcript.push(message_data);
2257
+ pushTranscript(message_data);
1625
2258
 
1626
2259
  context.finished_sent=true;
1627
2260
 
@@ -1631,18 +2264,36 @@ function TLSSession(options){
1631
2264
 
1632
2265
  }
1633
2266
 
1634
- }else if(context.selected_version === wire.TLS_VERSION.TLS1_2){
2267
+ }else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
1635
2268
 
1636
- 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
+ }
1637
2286
 
1638
- let hashFn = getHashFn(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1639
- 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);
1640
2291
 
1641
2292
  let finished_data;
1642
2293
  if(context.isServer==true){
1643
- 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);
1644
2295
  }else{
1645
- 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);
1646
2297
  }
1647
2298
  context.local_finished_data = finished_data;
1648
2299
 
@@ -1651,7 +2302,7 @@ function TLSSession(options){
1651
2302
  data: finished_data
1652
2303
  });
1653
2304
 
1654
- context.transcript.push(message_data);
2305
+ pushTranscript(message_data);
1655
2306
 
1656
2307
  context.finished_sent=true;
1657
2308
 
@@ -1666,12 +2317,13 @@ function TLSSession(options){
1666
2317
  }
1667
2318
 
1668
2319
  //get app traffic secret...
1669
- if (context.selected_version === wire.TLS_VERSION.TLS1_3){
2320
+ if ((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3)){
1670
2321
  if(context.base_secret!==null && context.local_app_traffic_secret==null && context.remote_app_traffic_secret==null){
1671
2322
 
1672
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)){
1673
2324
 
1674
- 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));
1675
2327
 
1676
2328
  // Save master_secret for resumption before clearing
1677
2329
  params_to_set['tls13_master_secret'] = result2.master_secret;
@@ -1692,26 +2344,27 @@ function TLSSession(options){
1692
2344
  //expected_remote_finished...
1693
2345
  if (context.expected_remote_finished==null && context.selected_cipher_suite!==null){
1694
2346
 
1695
- if(context.selected_version == wire.TLS_VERSION.TLS1_3 && context.remote_handshake_traffic_secret!==null){
2347
+ if((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.remote_handshake_traffic_secret!==null){
1696
2348
 
1697
2349
  if((context.isServer==true && context.finished_sent==true) || (context.isServer==false && context.remote_finished !== null)){
1698
2350
 
1699
- 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));
1700
2353
 
1701
2354
  }
1702
2355
 
1703
- }else if(context.selected_version === wire.TLS_VERSION.TLS1_2 && context.base_secret!==null){
2356
+ }else if((context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2) && context.base_secret!==null){
1704
2357
 
1705
2358
  if(context.remote_finished!==null){
1706
2359
 
1707
2360
 
1708
- let hashFn = getHashFn(TLS_CIPHER_SUITES[context.selected_cipher_suite].hash);
1709
- 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);
1710
2363
 
1711
2364
  if(context.isServer==true){
1712
- 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);
1713
2366
  }else{
1714
- 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);
1715
2368
  }
1716
2369
 
1717
2370
 
@@ -1736,7 +2389,7 @@ function TLSSession(options){
1736
2389
  data: context.remote_finished
1737
2390
  });
1738
2391
 
1739
- context.transcript.push(message_data);
2392
+ pushTranscript(message_data);
1740
2393
 
1741
2394
  params_to_set['remote_finished_ok']=true;
1742
2395
 
@@ -1755,54 +2408,61 @@ function TLSSession(options){
1755
2408
 
1756
2409
 
1757
2410
 
1758
- if(context.state!=='connected' && context.remote_finished_ok==true && ((context.selected_version === wire.TLS_VERSION.TLS1_3 && context.local_app_traffic_secret!==null && context.remote_app_traffic_secret!==null) || context.selected_version === wire.TLS_VERSION.TLS1_2)){
2411
+ if(context.state!=='connected' && context.remote_finished_ok==true && (((context.selected_version === wire.TLS_VERSION.TLS1_3 || context.selected_version === wire.DTLS_VERSION.DTLS1_3) && context.local_app_traffic_secret!==null && context.remote_app_traffic_secret!==null) || (context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2))){
1759
2412
  context.state='connected';
1760
2413
  context.handshakeEndTime = Date.now();
1761
2414
  ev.emit('secureConnect');
1762
2415
 
1763
2416
  // TLS 1.3: compute resumption_master_secret (both client and server need it)
1764
- if (context.selected_version === wire.TLS_VERSION.TLS1_3 && context.tls13_master_secret && !context.resumption_master_secret) {
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) {
1765
2418
  let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
1766
- context.resumption_master_secret = derive_resumption_master_secret(
1767
- 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)
1768
2421
  );
1769
2422
  }
1770
2423
 
1771
2424
  // TLS 1.3 server: send NewSessionTicket
1772
- if (context.selected_version === wire.TLS_VERSION.TLS1_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) {
1773
2426
  context.session_ticket_sent = true;
1774
2427
 
1775
2428
  let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
1776
2429
  let ticket_nonce = new Uint8Array([context.ticket_nonce_counter++]);
1777
2430
  let psk = derive_psk(hashName, context.resumption_master_secret, ticket_nonce);
1778
2431
  let ticket_age_add = crypto.randomBytes(4).readUInt32BE(0);
1779
- let ticket_lifetime = 7200; // 2 hours
1780
-
1781
- // Ticket = encrypted PSK + metadata using ticketKeys (or random fallback)
1782
- // Format: iv(12) || encrypted_json || tag(16)
1783
- // ticketKeys: 48 bytes = name(16) + aes_key(16) + hmac_key(16)
1784
- // We use first 32 bytes as AES-256-GCM key for simplicity
1785
- let tk = context.ticketKeys || crypto.randomBytes(48);
1786
- let ticket_enc_key = tk.length >= 32 ? tk.slice(0, 32) : Buffer.concat([tk, crypto.randomBytes(32 - tk.length)]);
1787
- let ticket_iv = crypto.randomBytes(12);
1788
- let ticket_plaintext = Buffer.from(JSON.stringify({
1789
- 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,
1790
2452
  cipher: context.selected_cipher_suite,
2453
+ psk: psk,
1791
2454
  age_add: ticket_age_add,
1792
- created: Date.now()
1793
- }));
1794
- let ticket_cipher = crypto.createCipheriv('aes-256-gcm', ticket_enc_key, ticket_iv);
1795
- let ticket_ct = ticket_cipher.update(ticket_plaintext);
1796
- ticket_cipher.final();
1797
- let ticket_tag = ticket_cipher.getAuthTag();
1798
- 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);
1799
2459
 
1800
2460
  let nst_data = wire.build_message(wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET,
1801
2461
  wire.build_new_session_ticket({
1802
2462
  ticket_lifetime: ticket_lifetime,
1803
2463
  ticket_age_add: ticket_age_add,
1804
2464
  ticket_nonce: ticket_nonce,
1805
- ticket: new Uint8Array(ticket),
2465
+ ticket: ticket,
1806
2466
  extensions: []
1807
2467
  })
1808
2468
  );
@@ -1810,15 +2470,85 @@ function TLSSession(options){
1810
2470
  ev.emit('message', 2, context.message_sent_seq, 'new_session_ticket', nst_data);
1811
2471
  context.message_sent_seq++;
1812
2472
 
1813
- ev.emit('session', {
1814
- ticket: new Uint8Array(ticket),
1815
- ticket_nonce: ticket_nonce,
1816
- 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,
1817
2480
  cipher: context.selected_cipher_suite,
1818
- lifetime: ticket_lifetime,
2481
+ ticket: ticket,
2482
+ psk: psk,
1819
2483
  age_add: ticket_age_add,
1820
- 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(),
1821
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(),
2550
+ });
2551
+ ev.emit('session', session_blob);
1822
2552
  }
1823
2553
  }
1824
2554
 
@@ -1986,7 +2716,7 @@ function TLSSession(options){
1986
2716
  },
1987
2717
  // TLS 1.2 compatibility
1988
2718
  { type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
1989
- { type: 23, data: new Uint8Array(0) } // extended_master_secret
2719
+ { type: 'EXTENDED_MASTER_SECRET', value: null }
1990
2720
  ];
1991
2721
 
1992
2722
  // Add SNI if servername was provided
@@ -2004,35 +2734,47 @@ function TLSSession(options){
2004
2734
  extensions.push(context.local_extensions[i]);
2005
2735
  }
2006
2736
 
2007
- // PSK resumption: check if session/psk was provided
2008
- 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
+
2009
2750
  let message_data;
2010
2751
 
2011
- 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) {
2012
2754
  // Add PSK key exchange modes (psk_dhe_ke = 1)
2013
2755
  extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
2014
2756
 
2015
2757
  // Save PSK for later verification
2016
2758
  context.psk_offered = {
2017
- identity: pskData.ticket,
2018
- psk: pskData.psk instanceof Uint8Array ? pskData.psk : new Uint8Array(pskData.psk),
2019
- cipher: pskData.cipher,
2020
- 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,
2021
2763
  };
2022
2764
 
2023
2765
  // Compute obfuscated ticket age
2024
- let ticketAge = pskData.lifetime ? Math.min((Date.now() - (pskData.created || Date.now())) / 1000, pskData.lifetime) * 1000 : 0;
2025
- 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;
2026
2768
 
2027
2769
  // Build ClientHello with placeholder binder to compute truncated hash
2028
- 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';
2029
2771
  let hashLen = getHashFn(hashName).outputLen;
2030
2772
  let placeholderBinder = new Uint8Array(hashLen);
2031
2773
 
2032
2774
  let pskExt = {
2033
2775
  type: 'PRE_SHARED_KEY',
2034
2776
  value: {
2035
- identities: [{ identity: pskData.ticket, age: obfuscatedAge }],
2777
+ identities: [{ identity: sessionData.ticket, age: obfuscatedAge }],
2036
2778
  binders: [placeholderBinder]
2037
2779
  }
2038
2780
  };
@@ -2044,6 +2786,7 @@ function TLSSession(options){
2044
2786
  version: 0x0303,
2045
2787
  random: context.local_random,
2046
2788
  session_id: context.local_session_id,
2789
+ cookie: context.dtls_cookie,
2047
2790
  cipher_suite: context.local_supported_cipher_suites,
2048
2791
  extensions: extensions
2049
2792
  };
@@ -2057,24 +2800,76 @@ function TLSSession(options){
2057
2800
  let binder_key = derive_binder_key(hashName, context.psk_offered.psk, false);
2058
2801
  let binder = compute_psk_binder(hashName, binder_key, truncatedMessage);
2059
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
+
2060
2815
  // Rebuild with real binder
2061
2816
  pskExt.value.binders = [binder];
2062
2817
  message_data = build_tls_message(build_message_params);
2063
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
+
2064
2850
  } else {
2065
- // 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
2066
2860
  let build_message_params = {
2067
2861
  type: 'client_hello',
2068
2862
  version: 0x0303,
2069
2863
  random: context.local_random,
2070
2864
  session_id: context.local_session_id,
2865
+ cookie: context.dtls_cookie,
2071
2866
  cipher_suite: context.local_supported_cipher_suites,
2072
2867
  extensions: extensions
2073
2868
  };
2074
2869
  message_data = build_tls_message(build_message_params);
2075
2870
  }
2076
2871
 
2077
- context.transcript.push(message_data);
2872
+ pushTranscript(message_data);
2078
2873
 
2079
2874
  context.hello_sent=true;
2080
2875
 
@@ -2140,7 +2935,7 @@ function TLSSession(options){
2140
2935
 
2141
2936
  /** Returns the negotiated ALPN protocol string (e.g. 'h2'), or null. */
2142
2937
  getALPN: function(){
2143
- return context.alpn_selected || null;
2938
+ return context.selected_alpn || null;
2144
2939
  },
2145
2940
 
2146
2941
  /** Returns the remote certificate chain, or null. */
@@ -2218,7 +3013,7 @@ function TLSSession(options){
2218
3013
  let cipherInfo = context.selected_cipher_suite ? TLS_CIPHER_SUITES[context.selected_cipher_suite] : null;
2219
3014
  return {
2220
3015
  version: context.selected_version,
2221
- versionName: context.selected_version === 0x0304 ? 'TLSv1.3' : context.selected_version === 0x0303 ? 'TLSv1.2' : null,
3016
+ versionName: context.selected_version === 0x0304 ? 'TLSv1.3' : context.selected_version === 0xFEFC ? 'DTLSv1.3' : context.selected_version === 0x0303 ? 'TLSv1.2' : context.selected_version === 0xFEFD ? 'DTLSv1.2' : null,
2222
3017
  cipher: context.selected_cipher_suite,
2223
3018
  cipherName: cipherInfo ? cipherInfo.name : null,
2224
3019
  group: context.selected_group,
@@ -2255,7 +3050,7 @@ function TLSSession(options){
2255
3050
 
2256
3051
  /** Request a TLS 1.3 Key Update. requestPeer=true means ask the other side to update too. */
2257
3052
  requestKeyUpdate: function(requestPeer){
2258
- if (context.state !== 'connected' || context.selected_version !== wire.TLS_VERSION.TLS1_3) return;
3053
+ if (context.state !== 'connected' || (context.selected_version !== wire.TLS_VERSION.TLS1_3 && context.selected_version !== wire.DTLS_VERSION.DTLS1_3)) return;
2259
3054
  let hashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
2260
3055
  let hashLen = getHashLen(hashName);
2261
3056