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.
- package/README.md +258 -203
- package/index.d.ts +145 -14
- package/index.js +33 -6
- package/package.json +3 -10
- package/src/compat.js +290 -31
- package/src/crypto.js +139 -8
- package/src/dtls_session.js +865 -0
- package/src/dtls_socket.js +263 -0
- package/src/record.js +894 -65
- package/src/session/message.js +33 -5
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +945 -150
- package/src/tls_socket.js +815 -249
- package/src/wire.js +167 -11
package/src/tls_session.js
CHANGED
|
@@ -4,15 +4,21 @@ import { EventEmitter } from 'node:events';
|
|
|
4
4
|
import {
|
|
5
5
|
TLS_CIPHER_SUITES,
|
|
6
6
|
build_cert_verify_tbs,
|
|
7
|
+
build_cert_verify_tbs_with_hash,
|
|
7
8
|
get_handshake_finished,
|
|
9
|
+
get_handshake_finished_with_hash,
|
|
8
10
|
tls12_prf,
|
|
9
11
|
derive_handshake_traffic_secrets,
|
|
12
|
+
derive_handshake_traffic_secrets_with_hash,
|
|
10
13
|
derive_app_traffic_secrets,
|
|
14
|
+
derive_app_traffic_secrets_with_hash,
|
|
11
15
|
derive_resumption_master_secret,
|
|
16
|
+
derive_resumption_master_secret_with_hash,
|
|
12
17
|
derive_psk,
|
|
13
18
|
derive_binder_key,
|
|
14
19
|
compute_psk_binder,
|
|
15
20
|
derive_handshake_traffic_secrets_psk,
|
|
21
|
+
derive_handshake_traffic_secrets_psk_with_hash,
|
|
16
22
|
hkdf_expand_label,
|
|
17
23
|
getHashFn,
|
|
18
24
|
} from './crypto.js';
|
|
@@ -30,6 +36,17 @@ import { pick_scheme, sign_with_scheme } from './session/signing.js';
|
|
|
30
36
|
import createSecureContext from './secure_context.js';
|
|
31
37
|
import { x25519_get_public_key, x25519_get_shared_secret, p256_generate_keypair, p256_get_shared_secret, p384_generate_keypair, p384_get_shared_secret } from './session/ecdh.js';
|
|
32
38
|
import { build_tls_message, parse_tls_message } from './session/message.js';
|
|
39
|
+
import { encrypt_session_blob, decrypt_session_blob, encode_client_session, decode_client_session } from './session/ticket.js';
|
|
40
|
+
|
|
41
|
+
// Debug logging — enabled via LEMON_DEBUG=1 env var
|
|
42
|
+
const LEMON_DEBUG = typeof process !== 'undefined' && process.env && process.env.LEMON_DEBUG === '1';
|
|
43
|
+
function dbg(tag, ...args) { if (LEMON_DEBUG) console.error('[LEMON ' + tag + ']', ...args); }
|
|
44
|
+
function hexPreview(buf, max) {
|
|
45
|
+
if (!buf) return 'null';
|
|
46
|
+
let b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
47
|
+
let n = Math.min(b.length, max || 32);
|
|
48
|
+
return b.slice(0, n).toString('hex') + (b.length > n ? `... (${b.length} bytes)` : ` (${b.length} bytes)`);
|
|
49
|
+
}
|
|
33
50
|
|
|
34
51
|
|
|
35
52
|
function TLSSession(options){
|
|
@@ -45,7 +62,9 @@ function TLSSession(options){
|
|
|
45
62
|
ca: options.ca || null, // CA certificates (PEM strings or Buffers)
|
|
46
63
|
|
|
47
64
|
SNICallback: options.SNICallback || null,
|
|
48
|
-
ticketKeys: options.ticketKeys || null, // 48 bytes
|
|
65
|
+
ticketKeys: options.ticketKeys || null, // 48 bytes: [0:16]=key_name, [16:48]=AES-256-GCM key
|
|
66
|
+
ticketLifetime: options.ticketLifetime != null ? (options.ticketLifetime >>> 0) : 7200, // seconds
|
|
67
|
+
sessionTickets: options.sessionTickets !== false, // default true (was noTickets inverted)
|
|
49
68
|
|
|
50
69
|
// Advanced options
|
|
51
70
|
maxHandshakeSize: options.maxHandshakeSize || 0, // 0 = no limit
|
|
@@ -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
|
-
|
|
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',
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
1628
|
+
ext_list.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
|
|
1159
1629
|
}
|
|
1160
1630
|
|
|
1161
|
-
if (context.
|
|
1631
|
+
if (context.selected_alpn) {
|
|
1162
1632
|
// RFC 7301: ServerHello echoes a single selected protocol
|
|
1163
|
-
ext_list.push({ type: 'ALPN', value: [ String(context.
|
|
1633
|
+
ext_list.push({ type: 'ALPN', value: [ String(context.selected_alpn) ] });
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// SESSION_TICKET: RFC 5077 §3.2 — server echoes empty extension to signal it will
|
|
1637
|
+
// send a NewSessionTicket later. Skip for abbreviated handshake (per §3.3).
|
|
1638
|
+
// Skip for DTLS — we don't emit NST in DTLS, so MUST NOT echo the ext either (§3.2).
|
|
1639
|
+
let isDtls12Here = (context.selected_version === wire.DTLS_VERSION.DTLS1_2);
|
|
1640
|
+
if (context.tls12_session_ticket_requested && !context.tls12_abbreviated && context.sessionTickets && !isDtls12Here) {
|
|
1641
|
+
ext_list.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Session ID: for abbreviated (resumed) handshake, MUST echo client's session_id
|
|
1645
|
+
// (RFC 5077 §3.4). For TLS 1.2 full handshake, generate a fresh 32-byte session_id
|
|
1646
|
+
// (matches OpenSSL/Node.js behavior for middlebox compatibility).
|
|
1647
|
+
// For DTLS 1.2 full handshake: just echo client's session_id (no middlebox concern,
|
|
1648
|
+
// and matches original lemon-tls behavior before my TLS 1.2 resumption changes).
|
|
1649
|
+
let sid_to_send;
|
|
1650
|
+
if (context.tls12_abbreviated) {
|
|
1651
|
+
sid_to_send = context.remote_session_id || new Uint8Array(0);
|
|
1652
|
+
} else if (context.selected_version === wire.DTLS_VERSION.DTLS1_2) {
|
|
1653
|
+
sid_to_send = context.remote_session_id || new Uint8Array(0);
|
|
1654
|
+
} else {
|
|
1655
|
+
if (!context.tls12_session_id_for_store) {
|
|
1656
|
+
context.tls12_session_id_for_store = new Uint8Array(crypto.randomBytes(32));
|
|
1657
|
+
}
|
|
1658
|
+
sid_to_send = context.tls12_session_id_for_store;
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2114
|
+
pushTranscript(message_data);
|
|
1550
2115
|
|
|
1551
2116
|
context.hello_done_sent=true;
|
|
1552
2117
|
|
|
1553
|
-
ev.emit('message',0,context.message_sent_seq,'
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1639
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1709
|
-
let transcript_hash =
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
1767
|
-
hashName, context.tls13_master_secret,
|
|
2419
|
+
context.resumption_master_secret = derive_resumption_master_secret_with_hash(
|
|
2420
|
+
hashName, context.tls13_master_secret, get_transcript_hash(hashName)
|
|
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 &&
|
|
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 =
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
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:
|
|
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
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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
|
-
|
|
2481
|
+
ticket: ticket,
|
|
2482
|
+
psk: psk,
|
|
1819
2483
|
age_add: ticket_age_add,
|
|
1820
|
-
|
|
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:
|
|
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
|
-
//
|
|
2008
|
-
let
|
|
2737
|
+
// Resumption: check if session was provided (opaque Buffer) — decode to structured data
|
|
2738
|
+
let sessionData = null;
|
|
2739
|
+
if (options.session) {
|
|
2740
|
+
// options.session may be a Buffer/Uint8Array (Node.js style) or a plain object (legacy)
|
|
2741
|
+
if (options.session instanceof Uint8Array || Buffer.isBuffer(options.session)) {
|
|
2742
|
+
sessionData = decode_client_session(options.session);
|
|
2743
|
+
} else if (typeof options.session === 'object') {
|
|
2744
|
+
sessionData = options.session; // legacy: already-structured object
|
|
2745
|
+
}
|
|
2746
|
+
} else if (options.psk) {
|
|
2747
|
+
sessionData = options.psk; // legacy path
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2009
2750
|
let message_data;
|
|
2010
2751
|
|
|
2011
|
-
|
|
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:
|
|
2018
|
-
psk:
|
|
2019
|
-
cipher:
|
|
2020
|
-
age_add:
|
|
2759
|
+
identity: sessionData.ticket,
|
|
2760
|
+
psk: sessionData.psk instanceof Uint8Array ? sessionData.psk : new Uint8Array(sessionData.psk),
|
|
2761
|
+
cipher: sessionData.cipher,
|
|
2762
|
+
age_add: sessionData.age_add || 0,
|
|
2021
2763
|
};
|
|
2022
2764
|
|
|
2023
2765
|
// Compute obfuscated ticket age
|
|
2024
|
-
let ticketAge =
|
|
2025
|
-
let obfuscatedAge = ((ticketAge + (
|
|
2766
|
+
let ticketAge = sessionData.lifetime ? Math.min((Date.now() - (sessionData.created || Date.now())) / 1000, sessionData.lifetime) * 1000 : 0;
|
|
2767
|
+
let obfuscatedAge = ((ticketAge + (sessionData.age_add || 0)) & 0xFFFFFFFF) >>> 0;
|
|
2026
2768
|
|
|
2027
2769
|
// Build ClientHello with placeholder binder to compute truncated hash
|
|
2028
|
-
let hashName = TLS_CIPHER_SUITES[
|
|
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:
|
|
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
|
|
2851
|
+
// No resumption — advertise empty SessionTicket extension to offer support.
|
|
2852
|
+
// Skip for DTLS (DTLS clients/servers often don't implement RFC 5077 fully,
|
|
2853
|
+
// and adding it caused interop issues with openssl s_server -dtls1_2).
|
|
2854
|
+
let isDtls = context.local_supported_versions && context.local_supported_versions.some(v => (v & 0xFF00) === 0xFE00);
|
|
2855
|
+
if (!isDtls && context.sessionTickets) {
|
|
2856
|
+
extensions.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// Standard ClientHello
|
|
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
|
-
|
|
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.
|
|
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
|
|