lemon-tls 0.6.0 → 0.9.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.
@@ -20,8 +20,10 @@ import {
20
20
  compute_psk_binder,
21
21
  derive_handshake_traffic_secrets_psk,
22
22
  derive_handshake_traffic_secrets_psk_with_hash,
23
+ derive_client_early_traffic_secret,
23
24
  hkdf_expand_label,
24
25
  getHashFn,
26
+ getHashLen,
25
27
  derive_exporter_master_secret_with_hash,
26
28
  tls13_exporter,
27
29
  tls12_exporter,
@@ -35,18 +37,24 @@ import {
35
37
  concatUint8Arrays,
36
38
  arraysEqual,
37
39
  uint8Equal,
38
- timingSafeEqualU8
40
+ timingSafeEqualU8,
41
+ certificateObject,
39
42
  } from './utils.js';
40
43
 
41
44
  import * as wire from './wire.js';
42
45
 
43
46
  // Extracted modules
44
- import { pick_scheme, sign_with_scheme, verify_with_scheme, default_signature_schemes } from './session/signing.js';
47
+ import { pick_scheme, sign_with_scheme, verify_with_scheme, default_signature_schemes, scheme_name } from './session/signing.js';
45
48
  import createSecureContext from './secure_context.js';
46
- import { x25519_get_public_key, SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS, generate_keypair as ecdh_generate_keypair, get_shared_secret as ecdh_get_shared_secret, is_supported_group
49
+ import { x25519_get_public_key, SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS, generate_keypair as ecdh_generate_keypair, get_shared_secret as ecdh_get_shared_secret, is_supported_group,
50
+ is_kem_group, kem_server_exchange, group_name as ecdh_group_name
47
51
  } from './session/ecdh.js';
48
52
  import { build_tls_message, parse_tls_message } from './session/message.js';
49
53
  import { encrypt_session_blob, decrypt_session_blob, encode_client_session, decode_client_session } from './session/ticket.js';
54
+ import { parse_cert_compression, supported_cert_compression, compress_certificate_body, decompress_certificate_body, cert_compression_name } from './session/cert_compression.js';
55
+ import { prepare_ech_server, server_process_client_hello, accept_confirmation, EXT_ECH,
56
+ parse_ech_config_list, prepare_ech_client, client_hpke_setup, client_outer_ext_data, client_encode_inner,
57
+ client_seal_outer, grease_ech_state, grease_ech_ext_data, hrr_ech_offset } from './session/ech.js';
50
58
 
51
59
  // RFC 8446 §4.1.3 downgrade-protection sentinels. A TLS 1.3-capable server that
52
60
  // negotiates a lower version stamps one of these into the last 8 bytes of
@@ -58,6 +66,16 @@ const DOWNGRADE_SENTINEL_TLS12 = new Uint8Array([0x44,0x4F,0x57,0x4E,0x47,0x52,0
58
66
  const DOWNGRADE_SENTINEL_TLS11 = new Uint8Array([0x44,0x4F,0x57,0x4E,0x47,0x52,0x44,0x00]); // "DOWNGRD\x00" (TLS 1.1 and below)
59
67
 
60
68
  // Debug logging — enabled via LEMON_DEBUG=1 env var
69
+ // A (D)TLS 1.2 suite whose key exchange is ephemeral elliptic-curve
70
+ // Diffie-Hellman — ECDHE_RSA or ECDHE_ECDSA. Read from the suite table rather
71
+ // than listed by code: a hand-kept list here had fallen behind it, missing
72
+ // both ChaCha20 suites, and a client that preferred them got a ServerHello
73
+ // and then a dead handshake.
74
+ function is_ecdhe_suite(cs) {
75
+ let meta = TLS_CIPHER_SUITES[cs];
76
+ return !!meta && typeof meta.kex === 'string' && meta.kex.indexOf('ECDHE_') === 0;
77
+ }
78
+
61
79
  const LEMON_DEBUG = typeof process !== 'undefined' && process.env && process.env.LEMON_DEBUG === '1';
62
80
  function dbg(tag, ...args) { if (LEMON_DEBUG) console.error('[LEMON ' + tag + ']', ...args); }
63
81
  function hexPreview(buf, max) {
@@ -68,6 +86,187 @@ function hexPreview(buf, max) {
68
86
  }
69
87
 
70
88
 
89
+ const MAX_CHAIN_DEPTH = 10;
90
+
91
+ // 0-RTT anti-replay, the default (earlyDataReplay): the tickets that have
92
+ // carried early data, in this process, each for its time — a second use
93
+ // within it gets no 0-RTT (RFC 8446 §8.1: single-use tickets). A cluster
94
+ // shares its own store instead. Bounded: full, early data is refused.
95
+ const EARLY_REPLAY_MAX = 100000;
96
+ const earlyReplaySeen = new Map(); // id → until (ms)
97
+ // What it remembers began here: a ticket issued before (by this process's
98
+ // predecessor, under ticket keys kept across the restart) may have carried
99
+ // early data then, and that flight could be sent again now. Such a ticket
100
+ // gets no 0-RTT for as long as a flight stays fresh (2 × earlyDataMaxAge)
101
+ // — one handshake in 1-RTT, and its new ticket is fine.
102
+ const EARLY_REPLAY_START = Date.now();
103
+ function defaultEarlyReplay(id, ttl, cb) {
104
+ let now = Date.now();
105
+ if (earlyReplaySeen.size >= EARLY_REPLAY_MAX) {
106
+ for (const [k, until] of earlyReplaySeen) { if (until > now) break; earlyReplaySeen.delete(k); }
107
+ if (earlyReplaySeen.size >= EARLY_REPLAY_MAX) { cb(null, false); return; }
108
+ }
109
+ let until = earlyReplaySeen.get(id);
110
+ if (until !== undefined && until > now) { cb(null, false); return; }
111
+ earlyReplaySeen.delete(id);
112
+ earlyReplaySeen.set(id, now + ttl);
113
+ cb(null, true);
114
+ }
115
+ const EARLY_REPLAY_TIMEOUT_MS = 100;
116
+
117
+ // Our certificate's private key, parsed: by the bytes it was parsed from
118
+ // (local_private_key). Gone with them.
119
+ const PRIVATE_KEY_OBJECTS = new WeakMap();
120
+
121
+ function _x509(x) {
122
+ try { return new crypto.X509Certificate(Buffer.isBuffer(x) ? x : Buffer.from(x)); }
123
+ catch (e) { return null; }
124
+ }
125
+ // Trust anchors, parsed once. The anchors are the same certificates on every
126
+ // handshake — the platform bundle (~150 roots) or the caller's `ca` — and
127
+ // parsing all of them per handshake cost 5–8 ms of blocking CPU on the client,
128
+ // plus as many native objects for the GC to finalize afterwards. The peer's
129
+ // own chain is still parsed fresh each time: it is untrusted input.
130
+ //
131
+ // PEM strings are cached by value (the bundle is a list of strings); Buffers
132
+ // and other objects by identity. The string cache is bounded so an
133
+ // application that builds a new `ca` string per connection cannot grow it
134
+ // without limit — it just falls back to parsing.
135
+ const ANCHOR_CACHE_MAX = 1024;
136
+ const _anchorByPem = new Map();
137
+ const _anchorByObj = new WeakMap();
138
+ function _anchorX509(a) {
139
+ if (typeof a === 'string') {
140
+ let hit = _anchorByPem.get(a);
141
+ if (hit !== undefined) return hit;
142
+ hit = _x509(a);
143
+ if (_anchorByPem.size >= ANCHOR_CACHE_MAX) _anchorByPem.clear();
144
+ _anchorByPem.set(a, hit);
145
+ return hit;
146
+ }
147
+ if (a && typeof a === 'object') {
148
+ let hit = _anchorByObj.get(a);
149
+ if (hit !== undefined) return hit;
150
+ hit = _x509(a);
151
+ _anchorByObj.set(a, hit);
152
+ return hit;
153
+ }
154
+ return null;
155
+ }
156
+ function _sameCert(a, b) { return a.raw.length === b.raw.length && a.raw.equals(b.raw); }
157
+ function _validNow(x509, now) {
158
+ return !(now < new Date(x509.validFrom) || now > new Date(x509.validTo));
159
+ }
160
+ // The node:tls (OpenSSL) code for a certificate outside its validity window.
161
+ function _validityError(x509, now) {
162
+ return now < new Date(x509.validFrom) ? 'CERT_NOT_YET_VALID' : 'CERT_HAS_EXPIRED';
163
+ }
164
+ function _selfSigned(x509) {
165
+ try { return !!x509.checkIssued(x509) && x509.verify(x509.publicKey); } catch (e) { return false; }
166
+ }
167
+
168
+ /**
169
+ * Build and verify a certificate chain from `chainDer` (leaf first) up to one
170
+ * of `anchors`.
171
+ *
172
+ * This replaces a check that compared ONLY the leaf against each configured CA,
173
+ * which was wrong in both directions:
174
+ *
175
+ * - A real chain (leaf <- intermediate <- root) FAILED, because nothing ever
176
+ * walked the intermediates the peer sent.
177
+ * - The whole check sat behind `if (context.ca)`, so when no CA was
178
+ * configured a peer presenting ANY self-signed certificate was authorized
179
+ * outright — with rejectUnauthorized defaulting to true, which reads as if
180
+ * verification were on.
181
+ *
182
+ * Each link has its signature verified, each non-leaf must actually assert
183
+ * CA:TRUE, and every certificate in the path must be inside its validity
184
+ * window. Intermediates come from the peer and are never trusted on their own;
185
+ * only reaching an anchor ends the walk.
186
+ *
187
+ * Returns { ok: true, depth } or { ok: false, error }. The error codes are
188
+ * the ones node:tls reports for the same failure (OpenSSL's X509_V_ERR_*
189
+ * names), so an application can test err.code the same way on both:
190
+ *
191
+ * DEPTH_ZERO_SELF_SIGNED_CERT the leaf is self-signed and not trusted
192
+ * UNABLE_TO_VERIFY_LEAF_SIGNATURE the leaf alone, its issuer unknown
193
+ * SELF_SIGNED_CERT_IN_CHAIN the chain ends in an untrusted root
194
+ * UNABLE_TO_GET_ISSUER_CERT_LOCALLY the chain stops at an unknown issuer
195
+ * INVALID_CA an issuer that is not a CA
196
+ * CERT_HAS_EXPIRED / CERT_NOT_YET_VALID a certificate in the path
197
+ */
198
+ function verify_cert_chain(chainDer, anchors, now) {
199
+ if (!chainDer || chainDer.length === 0) return { ok: false, error: 'NO_PEER_CERTIFICATE' };
200
+ now = now || new Date();
201
+
202
+ const leaf = _x509(chainDer[0].cert || chainDer[0]);
203
+ if (!leaf) return { ok: false, error: 'CERTIFICATE_PARSE_ERROR' };
204
+
205
+ const pool = [];
206
+ for (let i = 1; i < chainDer.length; i++) {
207
+ const x = _x509(chainDer[i].cert || chainDer[i]);
208
+ if (x) pool.push(x);
209
+ }
210
+
211
+ const trusted = [];
212
+ for (let i = 0; i < (anchors || []).length; i++) {
213
+ const x = _anchorX509(anchors[i]);
214
+ if (x) trusted.push(x);
215
+ }
216
+ if (trusted.length === 0) return { ok: false, error: 'NO_TRUST_ANCHORS' };
217
+
218
+ let current = leaf;
219
+ const used = new Set();
220
+
221
+ for (let depth = 0; depth < MAX_CHAIN_DEPTH; depth++) {
222
+ // Explicitly trusted certificate (pinned leaf, or a self-signed cert the
223
+ // caller deliberately placed in the trust store).
224
+ for (const a of trusted) if (_sameCert(current, a)) return { ok: true, depth: depth };
225
+
226
+ for (const a of trusted) {
227
+ try {
228
+ if (current.checkIssued(a) && current.verify(a.publicKey)) {
229
+ if (!_validNow(a, now)) return { ok: false, error: _validityError(a, now) };
230
+ return { ok: true, depth: depth + 1 };
231
+ }
232
+ } catch (e) { /* try next anchor */ }
233
+ }
234
+
235
+ let next = null;
236
+ for (let i = 0; i < pool.length; i++) {
237
+ if (used.has(i)) continue; // cycle guard
238
+ const cand = pool[i];
239
+ try {
240
+ if (!current.checkIssued(cand)) continue;
241
+ if (!current.verify(cand.publicKey)) continue;
242
+ if (cand.ca !== true) return { ok: false, error: 'INVALID_CA' };
243
+ if (!_validNow(cand, now)) return { ok: false, error: _validityError(cand, now) };
244
+ used.add(i); next = cand; break;
245
+ } catch (e) { /* try next candidate */ }
246
+ }
247
+
248
+ if (!next) {
249
+ let selfSigned = _selfSigned(current);
250
+ return { ok: false, error: depth === 0
251
+ ? (selfSigned ? 'DEPTH_ZERO_SELF_SIGNED_CERT' : 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')
252
+ : (selfSigned ? 'SELF_SIGNED_CERT_IN_CHAIN' : 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY') };
253
+ }
254
+ current = next;
255
+ }
256
+
257
+ return { ok: false, error: 'CERT_CHAIN_TOO_LONG' };
258
+ }
259
+
260
+
261
+ // An IPv4 or IPv6 address literal (IPv6 with or without brackets) — what
262
+ // SNI may never carry, and what a certificate is checked against by IP SAN.
263
+ function is_ip_literal(host) {
264
+ host = String(host);
265
+ return /^(\d{1,3}\.){3}\d{1,3}$/.test(host) ||
266
+ (host.indexOf(':') >= 0 && /^[0-9a-fA-F:.\[\]]+$/.test(host));
267
+ }
268
+
269
+
71
270
  function TLSSession(options){
72
271
  if (!(this instanceof TLSSession)) return new TLSSession(options);
73
272
  options = options || {};
@@ -78,20 +277,152 @@ function TLSSession(options){
78
277
  state: 'new', //new | negotiating | ...
79
278
  isServer: !!options.isServer,
80
279
  rejectUnauthorized: options.rejectUnauthorized !== false, // default true
280
+ // Node parity: replaces the default RFC 6125 identity check. Returns
281
+ // undefined to accept, an Error to reject. See validatePeerCertificate().
282
+ checkServerIdentity: typeof options.checkServerIdentity === 'function'
283
+ ? options.checkServerIdentity : null,
284
+ // When no `ca` is configured, chain against the platform trust store
285
+ // instead of authorizing everything. Opt out with useDefaultCA:false —
286
+ // DTLS does exactly that, since WebRTC peers are self-signed by design
287
+ // and authenticated out of band via the SDP fingerprint.
288
+ useDefaultCA: options.useDefaultCA !== false,
289
+ defaultCA: typeof options.defaultCA === 'function' ? options.defaultCA : null,
290
+ // Node parity: synchronous ALPN selector, server side. Receives
291
+ // { servername, protocols } and returns the chosen protocol, or undefined
292
+ // to refuse with no_application_protocol. Mutually exclusive with
293
+ // ALPNProtocols — compat.js rejects passing both, as Node does.
294
+ ALPNCallback: typeof options.ALPNCallback === 'function' ? options.ALPNCallback : null,
81
295
  ca: options.ca || null, // CA certificates (PEM strings or Buffers)
82
296
 
83
297
  SNICallback: options.SNICallback || null,
84
298
  ticketKeys: options.ticketKeys || null, // 48 bytes: [0:16]=key_name, [16:48]=AES-256-GCM key
85
- ticketLifetime: options.ticketLifetime != null ? (options.ticketLifetime >>> 0) : 7200, // seconds
299
+ // Server, TLS 1.2: whether a session ID issued now will be kept anywhere —
300
+ // a function, asked when the ServerHello is built (listeners are added
301
+ // after the session exists). A session ID is a promise to resume from it;
302
+ // with nothing storing the session (no 'newSession' listener) the promise
303
+ // is false, and every client that comes back with the ID is turned to a
304
+ // full handshake. With no store the ServerHello carries an empty
305
+ // session_id instead, which RFC 5246 §7.4.1.3 defines as "this session
306
+ // will not be cached". Not given: IDs are issued, as before.
307
+ sessionIdStore: typeof options.sessionIdStore === 'function' ? options.sessionIdStore : null,
308
+ // Seconds. RFC 8446 §4.6.1: "Servers MUST NOT use any value greater than
309
+ // 604800 seconds (7 days)" — capped here rather than trusted.
310
+ ticketLifetime: Math.min(options.ticketLifetime != null ? (options.ticketLifetime >>> 0) : 7200, 604800),
311
+ // Server: what the application wants carried inside the tickets it issues.
312
+ // Called as ticketData(resumed) each time a ticket is issued, where
313
+ // `resumed` is the data of the ticket this connection resumed with (null
314
+ // on a full handshake); its return value — JSON-safe, small — rides inside
315
+ // the ticket, encrypted with ticketKeys, and comes back in getTicketInfo()
316
+ // on the connection that presents it. The application decides what carries
317
+ // over from one ticket to the next; this layer only carries it.
318
+ ticketData: typeof options.ticketData === 'function' ? options.ticketData : null,
319
+ // Server: what the tickets this session issues are for, beyond what TLS
320
+ // itself checks — a ticket issued under one context is not accepted
321
+ // under another (full handshake), as OpenSSL's session id context. A
322
+ // QUIC stack puts its version here (RFC 9369 §5: a ticket from a v1
323
+ // connection MUST NOT resume a v2 one); setTicketContext() changes it
324
+ // while the ClientHello is read, once the version is decided. '' = none.
325
+ ticketContext: typeof options.ticketContext === 'string' ? options.ticketContext : '',
326
+ // 0-RTT (RFC 8446 §4.2.10, early data). Server: issue tickets that allow
327
+ // it (max_early_data_size: maxEarlyData — QUIC's 0xffffffff, RFC 9001
328
+ // §4.6.1) and accept it when a ticket of ours comes back fresh (within
329
+ // earlyDataMaxAge of the age the client states), for the first time
330
+ // (earlyDataReplay), under the same earlyDataContext (what the layer
331
+ // above needs unchanged: QUIC's transport parameters), with the same
332
+ // ALPN and cipher suite, and with no HelloRetryRequest. Client: offer it
333
+ // with a ticket that allows it. Off on both unless asked for; the secret
334
+ // is handed out ('earlySecret'), the records are the layer above's.
335
+ earlyData: options.earlyData === true,
336
+ // TLS over TCP: early data ends with EndOfEarlyData (RFC 8446 §4.5),
337
+ // which the client sends under the early key before its second flight
338
+ // and the server must have before the client's Finished. QUIC has none
339
+ // (RFC 9001 §8.3): off unless the record layer asks for it.
340
+ endOfEarlyData: options.endOfEarlyData === true,
341
+ earlyDataMaxAge: (typeof options.earlyDataMaxAge === 'number' && options.earlyDataMaxAge > 0) ? options.earlyDataMaxAge : 10000,
342
+ earlyDataReplay: typeof options.earlyDataReplay === 'function' ? options.earlyDataReplay : null,
343
+ earlyDataContext: typeof options.earlyDataContext === 'string' ? options.earlyDataContext : '',
344
+ maxEarlyData: (typeof options.maxEarlyData === 'number' && options.maxEarlyData >= 0) ? (options.maxEarlyData >>> 0) : 0xffffffff,
345
+ // A client's own data kept in the session it saves (QUIC: the server's
346
+ // transport parameters, which 0-RTT must respect, RFC 9000 §7.4.1), and
347
+ // given back with the early secret on a resumed connection.
348
+ sessionAppData: options.sessionAppData != null ? options.sessionAppData : null,
349
+ // Certificate compression (RFC 8879), TLS 1.3: the algorithms this side
350
+ // decompresses — offered in the ClientHello (client) or the
351
+ // CertificateRequest (server) — and compresses with, for a peer that
352
+ // offered them. ['brotli', 'zlib', 'zstd'], true (all) or false. Server:
353
+ // every one this Node has, unless set; client: none unless set — what a
354
+ // ClientHello carries is its fingerprint, and changing it is the
355
+ // application's call.
356
+ cert_compression: parse_cert_compression(options.certificateCompression) ||
357
+ (options.isServer ? supported_cert_compression() : []),
358
+ peer_cert_compression: null, // what the peer can decompress (its ClientHello / CertificateRequest)
359
+ cert_compression_sent: null, // the algorithm our Certificate went in, null if it went plain or not at all
360
+ cert_compression_received: null, // the algorithm the peer's came in
361
+ // OCSP stapling (RFC 6066 §8; TLS 1.3: RFC 8446 §4.4.2.1).
362
+ // Server: ocspRequest(certificate, issuer, cb) — node:tls's 'OCSPRequest'
363
+ // — asked when a client requests status and a certificate will be sent;
364
+ // cb(err, response): a DER OCSP response to staple, or null for none.
365
+ // Client: requestOCSP — ask for it; what comes is emitted 'OCSPResponse'.
366
+ ocspRequest: (options.isServer && typeof options.ocspRequest === 'function') ? options.ocspRequest : null,
367
+ requestOCSP: !options.isServer && options.requestOCSP === true,
368
+ ocsp_requested: false,
369
+ ocsp_pending: false,
370
+ ocsp_response: null,
371
+ // Encrypted Client Hello (RFC 9849), server: the keys, and this
372
+ // connection's outcome — status null (not offered) | 'accepted' | 'rejected'.
373
+ ech_server: options.isServer ? prepare_ech_server(options.ech) : null,
374
+ // Client: the config to encrypt the real ClientHello to (an
375
+ // ECHConfigList, `ech`), or GREASE ECH without one (`echGrease`). Status
376
+ // null (none) | 'grease' | 'accepted' | 'rejected'; the rest of `ech` is
377
+ // the client's state for this connection (see ech_split onwards).
378
+ ech_client: options.isServer ? null : prepare_ech_client(options.ech),
379
+ ech_grease: !options.isServer && options.echGrease === true,
380
+ ech: { status: null },
86
381
  sessionTickets: options.sessionTickets !== false, // default true (was noTickets inverted)
87
382
 
88
383
  // Advanced options
89
384
  maxHandshakeSize: options.maxHandshakeSize || 0, // 0 = no limit
90
385
  customExtensions: options.customExtensions || [], // [{type:0xNN, data:Uint8Array}]
386
+
387
+ // Shuffle ClientHello extension order (Chrome 110+ behaviour). Off by
388
+ // default: a fixed order keeps handshakes diffable against each other and
389
+ // avoids waking up middleboxes that ossified on one. See
390
+ // permute_extensions() for what this does and does not buy.
391
+ permuteExtensions: !!options.permuteExtensions,
392
+ // The order chosen for CH1, replayed on a post-HRR CH2.
393
+ extension_order: null,
394
+
395
+ // Insert GREASE (RFC 8701) reserved values into the ClientHello. Off by
396
+ // default: a peer that mishandles an unknown value fails the handshake
397
+ // with no clue as to why, and GREASE changes no fingerprint (JA4 and JA5
398
+ // strip it), so the cost of being wrong outweighs the benefit of being
399
+ // right. Safe and worth enabling against public HTTPS servers; leave off
400
+ // for DTLS/WebRTC peers until tested. See apply_grease().
401
+ grease: !!options.grease,
402
+ // The cipher-list GREASE value picked for CH1, reused on a post-HRR CH2.
403
+ grease_cipher: null,
91
404
  handshakeBytes: 0,
92
405
  handshakeStartTime: null,
93
406
  handshakeEndTime: null,
94
- rawClientHello: null, // saved for JA3/JA4 and 'clienthello' event
407
+ // Raw hello messages, stored regardless of role: whichever side sent it,
408
+ // both are visible to both peers, and every fingerprint below is computed
409
+ // from one of them. rawClientHello also backs the 'clienthello' event.
410
+ //
411
+ // ClientHello keeps the FIRST one seen — after a HelloRetryRequest the
412
+ // client sends a second, amended ClientHello, and the original offer is
413
+ // what a passive observer fingerprints. ServerHello keeps the LAST one,
414
+ // because HRR is itself encoded as a ServerHello and would otherwise
415
+ // shadow the real one.
416
+ rawClientHello: null,
417
+ rawServerHello: null,
418
+
419
+ // One cache slot per fingerprint, each filled the first time it is asked
420
+ // for. getJA4() is legitimately called from the 'clienthello' handler,
421
+ // which fires before the ServerHello exists; a slot is only filled once
422
+ // its hello exists, so ja4s is not frozen at null by an early call.
423
+ parsedClientHello: null, // null = not tried, false = threw, object = parsed
424
+ parsedServerHello: null,
425
+ fpCache: {},
95
426
 
96
427
  //local stuff...
97
428
  local_sni: options.servername || null,
@@ -224,11 +555,20 @@ function TLSSession(options){
224
555
  selected_cert: null,
225
556
 
226
557
  cert_private_key: null,
558
+ // Server: choose the cipher suite by our order, not the client's
559
+ // (node:tls's honorCipherOrder; default false, the client's).
560
+ honor_cipher_order: options.honorCipherOrder === true,
561
+ local_cert_alternates: null, // other key/certificate pairs (key/cert arrays): [{ certificateChain, privateKey }]
562
+ cert_chosen: false, // which of them this handshake presents, decided
227
563
 
228
564
  // Client certificate authentication
229
565
  requestCert: !!options.requestCert, // server: send CertificateRequest?
230
566
  clientCert: options.cert || null, // client: cert to send if requested
231
567
  clientKey: options.key || null, // client: private key for CertificateVerify
568
+ // Needed to decrypt an encrypted PEM key. createSecureContext() has always
569
+ // accepted this, but nothing forwarded it here, so an encrypted client key
570
+ // threw on parse no matter what the caller passed.
571
+ clientKeyPassphrase: options.passphrase || null,
232
572
  certificateRequested: false, // client: server sent CertificateRequest?
233
573
  certificateRequestSent: false, // server: we sent CertificateRequest?
234
574
  certificateRequestContext: null,
@@ -237,6 +577,7 @@ function TLSSession(options){
237
577
 
238
578
  // HelloRetryRequest
239
579
  helloRetried: false, // true if HRR was sent/received
580
+ client_hellos_received: 0, // server: one, or two after a HelloRetryRequest
240
581
  // What a received HelloRetryRequest committed the server to; the second
241
582
  // ServerHello is validated against these (RFC 8446 §4.1.4).
242
583
  hrr_version: null,
@@ -253,8 +594,26 @@ function TLSSession(options){
253
594
  ticket_nonce_counter: 0,
254
595
  session_ticket_sent: false,
255
596
  psk_offered: null, // client: { identity, psk, cipher } offered in ClientHello
597
+ // 0-RTT on this connection. offered: the client sent early_data;
598
+ // candidate: the server found it acceptable but for ALPN and suite,
599
+ // { cipher, alpn, secret }; pending: the replay check is out;
600
+ // status: null | 'accepted' | 'rejected'; appData: the client's saved
601
+ // sessionAppData, given back.
602
+ early: { offered: false, candidate: null, pending: false, status: null, alpn: null, appData: null, clientOffered: false, eoed: false },
256
603
  psk_accepted: false, // server accepted PSK → abbreviated handshake
257
604
  isResumed: false, // true if PSK was accepted (1.3) or abbreviated handshake (1.2)
605
+ hrr_hash: null, // client: the hash of the suite a HelloRetryRequest named
606
+ resumed_ticket_data: null, // application data of the ticket resumed with (see ticketData)
607
+ // The protocol layer's own slot in a ticket, apart from the application's
608
+ // ticketData: what setTicketState() last set, carried by the tickets
609
+ // issued from then on, and what the ticket resumed with carried
610
+ // (getTicketInfo().state). A QUIC stack keeps its path measurements there,
611
+ // to start a returning client's connection where the last one left off.
612
+ ticket_state: null,
613
+ resumed_ticket_state: null,
614
+ resumed_peer_chain: null, // the client's certificate, from the ticket resumed with
615
+ ticket_offered: false, // the client presented a ticket, used or not (see getTicketInfo)
616
+ app_ticket_data: undefined, // what this connection's ticket carries: decided once (ticket_app_data)
258
617
 
259
618
  // TLS 1.2 resumption
260
619
  tls12_abbreviated: false, // doing abbreviated handshake (server side)
@@ -266,6 +625,19 @@ function TLSSession(options){
266
625
  tls12_session_id_emitted: false, // 'newSession' event already fired
267
626
  tls12_client_session_emitted: false, // client-side 'session' event fired (TLS 1.2 Session ID or ticket)
268
627
  tls12_resume_pending: false, // waiting for 'resumeSession' async callback
628
+
629
+ // Waiting for SNICallback to hand back a certificate.
630
+ //
631
+ // Node documents SNICallback as (servername, cb) precisely so a server can
632
+ // look a certificate up — from disk, from a cache, from another process —
633
+ // and any virtual-host server must, because it cannot know every
634
+ // certificate in advance. The lookup may therefore answer later, and until
635
+ // it does there is nothing to sign or send with.
636
+ //
637
+ // Same shape and same reason as tls12_resume_pending directly above: a
638
+ // gate the reactive loop honours, cleared by the callback, which then lets
639
+ // the loop run again with the answer in context.
640
+ sni_pending: false,
269
641
  tls12_client_session: null, // client: saved session to resume with (parsed sessionData)
270
642
  };
271
643
 
@@ -354,15 +726,13 @@ function TLSSession(options){
354
726
  * (D)TLS 1.3 and 1.2 are treated as one version each because the record layer
355
727
  * differences live in record.js / the transports, not here.
356
728
  */
357
- function is13() {
358
- return context.selected_version === wire.TLS_VERSION.TLS1_3 ||
359
- context.selected_version === wire.DTLS_VERSION.DTLS1_3;
360
- }
361
-
362
- function is12() {
363
- return context.selected_version === wire.TLS_VERSION.TLS1_2 ||
364
- context.selected_version === wire.DTLS_VERSION.DTLS1_2;
365
- }
729
+ // Delegate rather than restate: wire.js owns the "which message format does
730
+ // this version constant mean" question, because that is where getting it
731
+ // wrong silently produced a TLS 1.2 Certificate on a DTLS 1.3 connection.
732
+ // Introducing is_tls13_wire there and leaving a second copy of the same test
733
+ // here would have recreated exactly the drift it was added to remove.
734
+ function is13() { return wire.is_tls13_wire(context.selected_version); }
735
+ function is12() { return wire.is_tls12_wire(context.selected_version); }
366
736
 
367
737
  /**
368
738
  * Assemble the ClientHello extension list.
@@ -378,6 +748,8 @@ function TLSSession(options){
378
748
  * @param {object} opts
379
749
  * keyShareGroup — NamedGroup for the key_share entry (null = omit)
380
750
  * keySharePublic — public key bytes for that group
751
+ * extraKeyShares — further { group, key_exchange } entries (CH1 only:
752
+ * after a HelloRetryRequest there is exactly one)
381
753
  * cookie — HRR cookie to echo (RFC 8446 §4.2.2), or null
382
754
  */
383
755
  function build_client_hello_extensions(opts) {
@@ -396,7 +768,11 @@ function TLSSession(options){
396
768
  let extensions = [];
397
769
 
398
770
  if (offers13) {
399
- extensions.push({ type: 'SUPPORTED_VERSIONS', value: context.local_supported_versions });
771
+ // With ECH, TLS 1.3 only (RFC 9849 §6.1: ECH is a TLS 1.3 mechanism; a
772
+ // server steering to 1.2 would carry the hello's real name nowhere and
773
+ // the public name in the clear).
774
+ extensions.push({ type: 'SUPPORTED_VERSIONS', value: ech_on() ? context.local_supported_versions.filter(function (v) { return v === wire.TLS_VERSION.TLS1_3; })
775
+ : context.local_supported_versions });
400
776
  }
401
777
  // supported_groups is a CAPABILITY claim, not a copy of configuration.
402
778
  //
@@ -410,14 +786,17 @@ function TLSSession(options){
410
786
  let advertisable = [];
411
787
  for (let gi = 0; gi < context.local_supported_groups.length; gi++) {
412
788
  let g = context.local_supported_groups[gi];
413
- if (is_supported_group(g)) advertisable.push(g);
789
+ if (!is_supported_group(g)) continue;
790
+ // A KEM group only in a hello that offers TLS 1.3: TLS 1.2's key
791
+ // exchange cannot carry one, and a 1.2-only hello should look like one.
792
+ if (!offers13 && is_kem_group(g)) continue;
793
+ advertisable.push(g);
414
794
  }
415
795
  extensions.push({ type: 'SUPPORTED_GROUPS', value: advertisable });
416
796
  if (offers13 && opts.keySharePublic) {
417
- extensions.push({
418
- type: 'KEY_SHARE',
419
- value: [{ group: opts.keyShareGroup, key_exchange: opts.keySharePublic }]
420
- });
797
+ let shares = [{ group: opts.keyShareGroup, key_exchange: opts.keySharePublic }];
798
+ if (opts.extraKeyShares) shares = shares.concat(opts.extraKeyShares);
799
+ extensions.push({ type: 'KEY_SHARE', value: shares });
421
800
  }
422
801
  extensions.push({
423
802
  type: 'SIGNATURE_ALGORITHMS',
@@ -429,14 +808,35 @@ function TLSSession(options){
429
808
  // in a 1.3-only hello.
430
809
  extensions.push({ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) });
431
810
  extensions.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
432
- } else {
811
+ }
812
+ if (!context.isServer && context.requestOCSP) {
813
+ // status_request (RFC 6066 §8): an OCSP response for the server's
814
+ // certificate, stapled — node:tls's requestOCSP.
815
+ extensions.push({ type: 'STATUS_REQUEST', value: { ocsp: true } });
816
+ }
817
+ if (offers13 && !context.isServer && context.cert_compression.length > 0) {
818
+ // compress_certificate (RFC 8879): the server's chain may come
819
+ // compressed. TLS 1.3 only.
820
+ extensions.push({ type: 'COMPRESS_CERTIFICATE', value: context.cert_compression });
821
+ }
822
+ if (offers13) {
433
823
  // psk_key_exchange_modes: a server MUST NOT send NewSessionTicket to a
434
- // client that omitted it (RFC 8446 §4.2.9).
824
+ // client that omitted it (RFC 8446 §4.2.9) — in any hello that offers
825
+ // 1.3, as browsers and OpenSSL send it. It was in 1.3-only hellos alone:
826
+ // a client offering 1.2 and 1.3 asked for no tickets, and the
827
+ // resumption path then added it a second time to a 1.3-only one —
828
+ // duplicate extension 45, which a server refuses (decode_error from
829
+ // lemon-tls, illegal_parameter from OpenSSL): a saved TLS 1.3 session
830
+ // never resumed. It is here once, for both.
435
831
  extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
436
832
  }
437
833
 
438
- // SNI must come first.
439
- if (context.local_sni) {
834
+ // SNI must come first. A host NAME only: RFC 6066 §3 — "Literal IPv4 and
835
+ // IPv6 addresses are not permitted in HostName". local_sni holds whatever
836
+ // the caller connected to, an address included, because the certificate
837
+ // is still checked against it (see the identity check); it is only kept
838
+ // off the wire. node:tls does the same.
839
+ if (context.local_sni && !is_ip_literal(context.local_sni)) {
440
840
  extensions.unshift({ type: 'SERVER_NAME', value: context.local_sni });
441
841
  }
442
842
 
@@ -485,10 +885,220 @@ function TLSSession(options){
485
885
  extensions.push(context.local_extensions[i]);
486
886
  }
487
887
 
888
+ // Encrypted Client Hello (RFC 9849). With a config: this list is the
889
+ // inner hello's, marked as such (§5: the inner extension, type 1) — the
890
+ // outer one is made from it (ech_split). Without one, and echGrease:
891
+ // a GREASE extension (§6.2), a CH2's with an empty enc.
892
+ if (!context.isServer && offers13 && !isDtlsProfile) {
893
+ if (ech_on()) {
894
+ extensions.push({ type: EXT_ECH, data: new Uint8Array([1]) });
895
+ } else if (context.ech_grease) {
896
+ if (!context.ech.grease) { context.ech.grease = grease_ech_state(); context.ech.status = 'grease'; }
897
+ extensions.push({ type: EXT_ECH, data: grease_ech_ext_data(context.ech.grease, context.helloRetried === true) });
898
+ }
899
+ }
900
+
901
+ if (context.grease) apply_grease(extensions);
902
+ if (context.permuteExtensions) extensions = permute_extensions(extensions);
903
+
488
904
  note_offered_extensions(extensions);
489
905
  return extensions;
490
906
  }
491
907
 
908
+ /* --------------------------------- GREASE -------------------------------- */
909
+
910
+ /** RFC 8701 §2: the sixteen two-byte reserved values. */
911
+ const GREASE_U16 = [
912
+ 0x0A0A, 0x1A1A, 0x2A2A, 0x3A3A, 0x4A4A, 0x5A5A, 0x6A6A, 0x7A7A,
913
+ 0x8A8A, 0x9A9A, 0xAAAA, 0xBABA, 0xCACA, 0xDADA, 0xEAEA, 0xFAFA
914
+ ];
915
+ /** RFC 8701 §2: psk_key_exchange_modes is a vector of SINGLE bytes and
916
+ * therefore has its own, entirely separate reserved table. Reusing the u16
917
+ * values here would emit a mode the registry does not reserve. */
918
+ const GREASE_U8 = [0x0B, 0x2A, 0x49, 0x68, 0x87, 0xA6, 0xC5, 0xE4];
919
+
920
+ function is_grease_u16(v) {
921
+ return typeof v === 'number' && (v & 0x0f0f) === 0x0a0a && ((v >>> 8) & 0xff) === (v & 0xff);
922
+ }
923
+ function is_grease_u8(v) { return GREASE_U8.indexOf(v) >= 0; }
924
+
925
+ function pick_from(table) {
926
+ return table[crypto.randomBytes(4).readUInt32BE(0) % table.length];
927
+ }
928
+
929
+ /**
930
+ * Insert GREASE (RFC 8701) into the ClientHello — opt-in via `grease`.
931
+ *
932
+ * The point is anti-ossification: a peer that cannot tolerate an unknown
933
+ * value in a list breaks immediately and visibly, instead of quietly
934
+ * calcifying the protocol until a real value is deployed years later.
935
+ *
936
+ * Note that this does NOT hide you. JA4 and JA5 both strip GREASE before
937
+ * hashing, so every fingerprint is identical with this on or off. What
938
+ * changes is only that a ClientHello carrying zero GREASE reads as
939
+ * "not a browser" to anyone looking.
940
+ *
941
+ * COLLISION HANDLING — the reason this is not a blind push. A caller can
942
+ * already inject GREASE by hand through customExtensions, local_extensions,
943
+ * or the local_supported_* lists. Adding a second value on top of that would
944
+ * at best be redundant and at worst fatal: RFC 8446 §4.2 forbids two
945
+ * extensions of the same type outright, and this library's own parser
946
+ * rejects that with decode_error. So every list is inspected first, and a
947
+ * list that already carries a GREASE value is left exactly as it is. Manual
948
+ * configuration always wins.
949
+ */
950
+ function apply_grease(extensions) {
951
+ let byType = {};
952
+ for (let i = 0; i < extensions.length; i++) {
953
+ let t = extensions[i] && extensions[i].type;
954
+ if (t != null) byType[String(t)] = extensions[i];
955
+ }
956
+ let find = function (name, code) { return byType[name] || byType[String(code)] || null; };
957
+
958
+ // --- a GREASE extension type of its own ---
959
+ let hasGreaseExt = false;
960
+ for (let i = 0; i < extensions.length; i++) {
961
+ if (is_grease_u16(extensions[i] && extensions[i].type)) { hasGreaseExt = true; break; }
962
+ }
963
+ if (!hasGreaseExt) {
964
+ // Body length is deliberately varied: a peer that tolerates an unknown
965
+ // extension only when it is empty is still ossified.
966
+ let n = crypto.randomBytes(1)[0] % 3;
967
+ extensions.push({ type: pick_from(GREASE_U16), value: new Uint8Array(n) });
968
+ }
969
+
970
+ // --- value lists that take u16 entries ---
971
+ let u16Lists = [
972
+ ['SUPPORTED_GROUPS', 10], ['SIGNATURE_ALGORITHMS', 13], ['SUPPORTED_VERSIONS', 43]
973
+ ];
974
+ for (let i = 0; i < u16Lists.length; i++) {
975
+ let e = find(u16Lists[i][0], u16Lists[i][1]);
976
+ if (!e || !Array.isArray(e.value) || e.value.length === 0) continue;
977
+ if (e.value.some(is_grease_u16)) continue; // caller already did it
978
+ e.value = [pick_from(GREASE_U16)].concat(e.value);
979
+ }
980
+
981
+ // --- psk_key_exchange_modes: single bytes, separate table ---
982
+ let pk = find('PSK_KEY_EXCHANGE_MODES', 45);
983
+ if (pk && Array.isArray(pk.value) && !pk.value.some(is_grease_u8)) {
984
+ pk.value = [pick_from(GREASE_U8)].concat(pk.value);
985
+ }
986
+
987
+ // --- ALPN: a two-BYTE protocol name, not a u16 ---
988
+ let alpn = find('ALPN', 16);
989
+ if (alpn && Array.isArray(alpn.value) && alpn.value.length > 0) {
990
+ let already = alpn.value.some(function (p) {
991
+ if (typeof p !== 'string' || p.length !== 2) return false;
992
+ return is_grease_u16((p.charCodeAt(0) << 8) | p.charCodeAt(1));
993
+ });
994
+ if (!already) {
995
+ let g = pick_from(GREASE_U16);
996
+ alpn.value = [String.fromCharCode((g >>> 8) & 0xff) + String.fromCharCode(g & 0xff)]
997
+ .concat(alpn.value);
998
+ }
999
+ }
1000
+
1001
+ // --- key_share: a GREASE group carrying an arbitrary key ---
1002
+ // This is the one that actually exercises a server: it must skip an entry
1003
+ // whose group it does not recognise rather than try to parse the key.
1004
+ let ks = find('KEY_SHARE', 51);
1005
+ if (ks && Array.isArray(ks.value) && ks.value.length > 0) {
1006
+ let already = ks.value.some(function (k) { return is_grease_u16(k && k.group); });
1007
+ if (!already) {
1008
+ // Prefer the group we just advertised, so the offer stays self-consistent.
1009
+ let sg = find('SUPPORTED_GROUPS', 10);
1010
+ let g = null;
1011
+ if (sg && Array.isArray(sg.value)) {
1012
+ for (let i = 0; i < sg.value.length; i++) {
1013
+ if (is_grease_u16(sg.value[i])) { g = sg.value[i]; break; }
1014
+ }
1015
+ }
1016
+ if (g === null) g = pick_from(GREASE_U16);
1017
+ ks.value = [{ group: g, key_exchange: new Uint8Array(1) }].concat(ks.value);
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ /** GREASE for the cipher_suites list, applied at the build site because the
1023
+ * list is not part of the extension array. Same rule as everywhere else: a
1024
+ * caller-supplied GREASE value is left alone.
1025
+ *
1026
+ * The chosen value is remembered, because CH1 and CH2 must be identical
1027
+ * across a HelloRetryRequest (RFC 8446 §4.1.2) — re-rolling on the retry
1028
+ * would change the offered cipher list and could be rejected. */
1029
+ function grease_cipher_suites(list) {
1030
+ if (!context.grease || !Array.isArray(list) || list.length === 0) return list;
1031
+ if (list.some(is_grease_u16)) return list;
1032
+ if (context.grease_cipher === null) context.grease_cipher = pick_from(GREASE_U16);
1033
+ return [context.grease_cipher].concat(list);
1034
+ }
1035
+
1036
+ /**
1037
+ * Shuffle ClientHello extension order (opt-in via `permuteExtensions`).
1038
+ *
1039
+ * Chrome has done this since 110 as an anti-ossification measure: middleboxes
1040
+ * that hardcode a fixed extension order break loudly rather than silently
1041
+ * calcifying the protocol. It is a separate mechanism from GREASE (RFC 8701),
1042
+ * which inserts reserved dummy VALUES; this reorders real extensions.
1043
+ *
1044
+ * What it changes, for anyone reaching for it as evasion: nothing this
1045
+ * library computes. JA4 and JA5 sort extensions before hashing, so both are
1046
+ * byte-for-byte identical with this on or off. It defeats only JA3, the 2017
1047
+ * fingerprint, which hashed extensions in wire order — and a client whose
1048
+ * JA3 differs on every connection is itself a distinctive pattern.
1049
+ *
1050
+ * Two ordering rules are preserved:
1051
+ *
1052
+ * - pre_shared_key MUST come last (RFC 8446 §4.2.11) — its binders are
1053
+ * computed over everything before it. It is appended after assembly, so it
1054
+ * is not in this array yet and cannot be displaced.
1055
+ *
1056
+ * - CH1 and CH2 must stay identical across a HelloRetryRequest
1057
+ * (RFC 8446 §4.1.2), which lists the permitted changes and does not
1058
+ * include reordering. The permutation is therefore computed once per
1059
+ * session and replayed by type on the retry, rather than re-rolled — a
1060
+ * fresh shuffle on CH2 would be a spec violation that strict servers
1061
+ * could reject.
1062
+ */
1063
+ function permute_extensions(list) {
1064
+ let key = function (e) { return String(e && e.type); };
1065
+
1066
+ // Replay the order chosen for CH1 rather than re-rolling.
1067
+ if (context.extension_order !== null) {
1068
+ let byType = {};
1069
+ for (let i = 0; i < list.length; i++) {
1070
+ let k = key(list[i]);
1071
+ if (!byType[k]) byType[k] = [];
1072
+ byType[k].push(list[i]);
1073
+ }
1074
+ let out = [];
1075
+ for (let i = 0; i < context.extension_order.length; i++) {
1076
+ let bucket = byType[context.extension_order[i]];
1077
+ if (bucket && bucket.length > 0) out.push(bucket.shift());
1078
+ }
1079
+ // Anything CH2 added that CH1 never had (the HRR cookie) keeps its
1080
+ // position at the end; dropping it would corrupt the retry.
1081
+ for (let k in byType) {
1082
+ if (Object.prototype.hasOwnProperty.call(byType, k)) {
1083
+ for (let j = 0; j < byType[k].length; j++) out.push(byType[k][j]);
1084
+ }
1085
+ }
1086
+ return out;
1087
+ }
1088
+
1089
+ // Fisher-Yates over a copy, with a CSPRNG rather than Math.random: the
1090
+ // order is observable on the wire, and a predictable shuffle would be a
1091
+ // weaker signal than no shuffle at all.
1092
+ let out = list.slice();
1093
+ for (let i = out.length - 1; i > 0; i--) {
1094
+ let j = crypto.randomBytes(4).readUInt32BE(0) % (i + 1);
1095
+ let tmp = out[i]; out[i] = out[j]; out[j] = tmp;
1096
+ }
1097
+
1098
+ context.extension_order = out.map(key);
1099
+ return out;
1100
+ }
1101
+
492
1102
  /**
493
1103
  * Remember which extension types we actually offered.
494
1104
  *
@@ -533,6 +1143,16 @@ function TLSSession(options){
533
1143
  for (let i = 0; i < message.extensions.length; i++) {
534
1144
  let et = message.extensions[i] && message.extensions[i].type;
535
1145
  if (typeof et !== 'number') continue;
1146
+
1147
+ // RFC 8701 §3.3 lets a SERVER advertise GREASE extensions of its own in
1148
+ // EncryptedExtensions, CertificateRequest, Certificate and
1149
+ // NewSessionTicket — the whole point being that we could not have asked
1150
+ // for them. Testing those against offered_extension_types therefore
1151
+ // rejects a peer that is behaving exactly as specified, which is the
1152
+ // same class of over-strictness as the 1.2 regression noted above, only
1153
+ // with us on the rejecting side this time.
1154
+ if (is_grease_u16(et)) continue;
1155
+
536
1156
  if (context.offered_extension_types.indexOf(et) < 0) {
537
1157
  fatalAlert(wire.TLS_ALERT.UNSUPPORTED_EXTENSION,
538
1158
  where + ' carried extension ' + et + ' which we did not offer');
@@ -592,6 +1212,156 @@ function TLSSession(options){
592
1212
  return null;
593
1213
  }
594
1214
 
1215
+ /* ====================== Encrypted Client Hello: client ===================== */
1216
+
1217
+ // A config to use, on a hello that offers TLS 1.3 (not DTLS: RFC 9849 is
1218
+ // TLS's).
1219
+ function ech_on() {
1220
+ return !context.isServer && context.ech_client !== null &&
1221
+ context.local_supported_versions.indexOf(wire.TLS_VERSION.TLS1_3) >= 0;
1222
+ }
1223
+
1224
+ function ext_code(e) { return typeof e.type === 'number' ? e.type : wire.TLS_EXT[e.type]; }
1225
+
1226
+ /**
1227
+ * The assembled list → the inner hello's, in place, and what the outer
1228
+ * one is made of. Inner: server_name, then every other extension, then
1229
+ * the inner ECH marker (pre_shared_key, when there is one, goes last after
1230
+ * this — the PSK path adds it). Outer: the config's public_name, then the
1231
+ * same extensions, the same objects — so the inner hello can name them
1232
+ * instead of carrying them (ech_outer_extensions, §5.1): one block, in
1233
+ * the outer's order. It is the key_share above all that this saves; with
1234
+ * a post-quantum share, a ClientHello carrying it twice would not fit a
1235
+ * QUIC Initial packet.
1236
+ */
1237
+ function ech_split(extensions) {
1238
+ let sni = null, marker = null, psk = null, block = [];
1239
+ for (let i = 0; i < extensions.length; i++) {
1240
+ let e = extensions[i], c = ext_code(e);
1241
+ if (c === 0) sni = e;
1242
+ else if (c === EXT_ECH) marker = e;
1243
+ else if (c === 41) psk = e;
1244
+ else block.push(e);
1245
+ }
1246
+ extensions.length = 0;
1247
+ if (sni) extensions.push(sni);
1248
+ for (let i = 0; i < block.length; i++) extensions.push(block[i]);
1249
+ extensions.push(marker);
1250
+ if (psk) extensions.push(psk);
1251
+ return {
1252
+ compressed: block.map(ext_code),
1253
+ outer: [{ type: 'SERVER_NAME', value: context.ech_client.config.public_name }].concat(block),
1254
+ sniLength: sni ? Buffer.byteLength(String(context.local_sni)) : null
1255
+ };
1256
+ }
1257
+
1258
+ /**
1259
+ * The inner hello (a handshake message, as the transcript has it) → the
1260
+ * outer one that goes on the wire, carrying it encrypted. The first
1261
+ * hello sets up the HPKE context; the second (after a HelloRetryRequest)
1262
+ * uses it again with an empty enc (§6.1.5).
1263
+ */
1264
+ function ech_outer_hello(innerMsg, split, second) {
1265
+ let prep = context.ech_client, st = context.ech;
1266
+ if (!second) { let hs = client_hpke_setup(prep); st.hpke = hs.context; st.enc = hs.enc; }
1267
+ let encoded = client_encode_inner(innerMsg, split.compressed, prep.config.maximum_name_length, split.sniLength);
1268
+ let extData = client_outer_ext_data(prep.kdf_id, prep.aead_id, prep.config.config_id,
1269
+ second ? new Uint8Array(0) : st.enc, encoded.length + prep.suite.Nt);
1270
+ let outerMsg = build_tls_message({
1271
+ type: 'client_hello',
1272
+ version: 0x0303,
1273
+ random: st.outer_random,
1274
+ session_id: context.local_session_id,
1275
+ cookie: context.dtls_cookie,
1276
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
1277
+ cipher_suite: context.local_supported_cipher_suites,
1278
+ extensions: split.outer.concat([{ type: EXT_ECH, data: extData }], split.outerPsk ? [split.outerPsk] : []),
1279
+ });
1280
+ return client_seal_outer(outerMsg, st.hpke, encoded);
1281
+ }
1282
+
1283
+ /**
1284
+ * The server did not take the inner hello (§6.1.6): the handshake goes on
1285
+ * as the outer one — its transcript, its random — and the server must
1286
+ * prove itself for the public_name, which it then holds keys for. It ends,
1287
+ * whatever happens, with ech_required (ech_abort_rejected).
1288
+ */
1289
+ function ech_rejected() {
1290
+ let st = context.ech;
1291
+ st.status = 'rejected';
1292
+ st.decided = true;
1293
+ st.verify_name = context.ech_client.config.public_name;
1294
+ context.local_random = st.outer_random;
1295
+ context.transcript = [st.outer_ch1];
1296
+ context.transcriptHash = null; // rebuilt from the outer transcript when next asked
1297
+ context.transcriptHashName = null;
1298
+ }
1299
+
1300
+ /**
1301
+ * §7.2, §7.2.1: did the server take the inner hello? Asked of a
1302
+ * HelloRetryRequest (its encrypted_client_hello extension) and of the
1303
+ * ServerHello (the last 8 bytes of its random), before either joins the
1304
+ * transcript. False when the handshake has been aborted.
1305
+ */
1306
+ function ech_decide(message, data) {
1307
+ let st = context.ech;
1308
+ let suite = TLS_CIPHER_SUITES[hello_cipher_suite(message)];
1309
+ let hashName = suite ? suite.hash : null;
1310
+ if (!hashName) return true; // the hello's own checks will refuse it
1311
+ let ver = Array.isArray(message.supported_versions) ? message.supported_versions[0] : message.supported_versions;
1312
+ let prefix = label_prefix_for(ver);
1313
+ if (message.random && uint8Equal(message.random, wire.TLS13_HRR_RANDOM)) {
1314
+ let off;
1315
+ try { off = hrr_ech_offset(data); }
1316
+ catch (e) { fatalAlert(e.alertDesc || wire.TLS_ALERT.DECODE_ERROR, e.message); return false; }
1317
+ if (off >= 0) {
1318
+ let z = new Uint8Array(data); z.fill(0, off, off + 8);
1319
+ let mh = wire.build_message(wire.TLS_MESSAGE_TYPE.MESSAGE_HASH, getHashFn(hashName)(concatUint8Arrays(context.transcript)));
1320
+ let th = getHashFn(hashName)(concatUint8Arrays([mh, z]));
1321
+ if (timingSafeEqualU8(accept_confirmation(hashName, st.inner_random, th, true, prefix), data.subarray(off, off + 8))) {
1322
+ st.hrr_accepted = true;
1323
+ return true;
1324
+ }
1325
+ }
1326
+ ech_rejected();
1327
+ return true;
1328
+ }
1329
+ let z = new Uint8Array(data); z.fill(0, 4 + 2 + 24, 4 + 2 + 32);
1330
+ let th = getHashFn(hashName)(concatUint8Arrays(context.transcript.concat([z])));
1331
+ if (timingSafeEqualU8(accept_confirmation(hashName, st.inner_random, th, false, prefix), data.subarray(4 + 2 + 24, 4 + 2 + 32))) {
1332
+ st.status = 'accepted';
1333
+ st.decided = true;
1334
+ return true;
1335
+ }
1336
+ if (st.hrr_accepted) {
1337
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'ECH: accepted in the HelloRetryRequest, not in the ServerHello');
1338
+ return false;
1339
+ }
1340
+ ech_rejected();
1341
+ return true;
1342
+ }
1343
+
1344
+ /**
1345
+ * §6.1.6: ECH was rejected, and the server has proven itself for the
1346
+ * public_name (its Certificate, CertificateVerify and Finished checked).
1347
+ * The connection is not used: ech_required, and an error carrying the
1348
+ * server's new keys — `retryConfigs`, an ECHConfigList to connect again
1349
+ * with — or null: the server has turned ECH off (a connection without it
1350
+ * is then the caller's decision, not this library's). Keys from a server
1351
+ * that was not authenticated (rejectUnauthorized false) are not trusted.
1352
+ */
1353
+ function ech_abort_rejected() {
1354
+ if (context.state === 'error' || context.state === 'closed') return;
1355
+ let retry = context.ech.retry_configs && context.peerAuthorized === true ? context.ech.retry_configs : null;
1356
+ sendAlert(2, wire.TLS_ALERT.ECH_REQUIRED);
1357
+ let err = new Error('ECH rejected by the server (it answered as ' + JSON.stringify(context.ech.verify_name) + ')' +
1358
+ (retry ? ': it sent new keys — reconnect with err.retryConfigs' : ': it sent no keys to retry with'));
1359
+ err.code = 'ERR_ECH_REJECTED';
1360
+ err.retryConfigs = retry ? Buffer.from(retry) : null;
1361
+ err.publicName = context.ech.verify_name;
1362
+ ev.emit('error', err);
1363
+ }
1364
+
595
1365
  /**
596
1366
  * Build and send the second ClientHello after a HelloRetryRequest.
597
1367
  *
@@ -613,20 +1383,78 @@ function TLSSession(options){
613
1383
  keySharePublic: keySharePublic,
614
1384
  cookie: cookie
615
1385
  });
1386
+ let echSplit = ech_on() && context.ech.outer_ch1 ? ech_split(extensions) : null;
1387
+
1388
+ // The PSK offered in the first hello goes in this one too, its age and
1389
+ // binder made again (RFC 8446 §4.1.2) — the binder over message_hash,
1390
+ // the HelloRetryRequest and this hello (§4.2.11.2). It was left out:
1391
+ // a resumption that met a HelloRetryRequest became a full handshake —
1392
+ // the certificate sent and checked — or, against a server that kept the
1393
+ // first hello's PSK, a ServerHello selecting an identity this hello
1394
+ // never offered. Dropped when the server's suite has another hash
1395
+ // (§4.1.4: the PSK cannot serve it), or when ECH was refused (the outer
1396
+ // hello carries no ticket). No early data: none after a retry (§4.2.10).
1397
+ let pskExt = null, pskHash = null;
1398
+ let po = context.psk_offered;
1399
+ if (po && po.identity && !(echSplit && context.ech.status === 'rejected')) {
1400
+ pskHash = TLS_CIPHER_SUITES[po.cipher] ? TLS_CIPHER_SUITES[po.cipher].hash : 'sha256';
1401
+ if (context.hrr_hash && pskHash !== context.hrr_hash) {
1402
+ context.psk_offered = null;
1403
+ } else {
1404
+ let hashLen = getHashFn(pskHash).outputLen;
1405
+ let ticketAge = po.lifetime ? Math.min((Date.now() - (po.created || Date.now())) / 1000, po.lifetime) * 1000 : 0;
1406
+ pskExt = {
1407
+ type: 'PRE_SHARED_KEY',
1408
+ value: {
1409
+ identities: [{ identity: po.identity, age: ((ticketAge + (po.age_add || 0)) & 0xFFFFFFFF) >>> 0 }],
1410
+ binders: [new Uint8Array(hashLen)]
1411
+ }
1412
+ };
1413
+ extensions.push(pskExt); // last
1414
+ if (echSplit) {
1415
+ echSplit.outerPsk = {
1416
+ type: 'PRE_SHARED_KEY',
1417
+ value: {
1418
+ identities: [{ identity: new Uint8Array(crypto.randomBytes(po.identity.length)), age: crypto.randomBytes(4).readUInt32BE(0) }],
1419
+ binders: [new Uint8Array(crypto.randomBytes(hashLen))]
1420
+ }
1421
+ };
1422
+ }
1423
+ }
1424
+ } else if (po) {
1425
+ context.psk_offered = null;
1426
+ }
616
1427
 
617
- let ch2 = build_tls_message({
1428
+ let ch2Params = {
618
1429
  type: 'client_hello',
619
1430
  version: 0x0303,
620
- random: context.local_random,
1431
+ // Each hello repeats its own first random (RFC 8446 §4.1.2) — the
1432
+ // inner one's, with ECH.
1433
+ random: echSplit ? context.ech.inner_random : context.local_random,
621
1434
  session_id: context.local_session_id,
622
1435
  cookie: context.dtls_cookie,
623
- cipher_suites: context.local_supported_cipher_suites,
1436
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
624
1437
  cipher_suite: context.local_supported_cipher_suites,
625
1438
  extensions: extensions,
626
- });
1439
+ };
1440
+ let ch2 = build_tls_message(ch2Params);
1441
+ if (pskExt) {
1442
+ let hashLen = getHashFn(pskHash).outputLen;
1443
+ let truncated = concatUint8Arrays(context.transcript.concat([ch2.slice(0, ch2.length - (2 + 1 + hashLen))]));
1444
+ let binder_key = derive_binder_key(pskHash, context.psk_offered.psk, false, label_prefix());
1445
+ pskExt.value.binders = [compute_psk_binder(pskHash, binder_key, truncated, label_prefix())];
1446
+ ch2 = build_tls_message(ch2Params);
1447
+ }
627
1448
 
1449
+ // ECH: the outer hello goes; the transcript takes the inner one — or the
1450
+ // outer, when the HelloRetryRequest said the server did not decrypt.
1451
+ let onWire = ch2;
1452
+ if (echSplit) {
1453
+ onWire = ech_outer_hello(ch2, echSplit, true);
1454
+ if (context.ech.status === 'rejected') ch2 = onWire;
1455
+ }
628
1456
  pushTranscript(ch2);
629
- ev.emit('message', 0, context.message_sent_seq, 'hello', ch2);
1457
+ ev.emit('message', 0, context.message_sent_seq, 'hello', onWire);
630
1458
  context.message_sent_seq++;
631
1459
  }
632
1460
 
@@ -676,12 +1504,20 @@ function TLSSession(options){
676
1504
  * and nowhere else.
677
1505
  */
678
1506
  function local_private_key() {
679
- if (!context.cert_private_key) return null;
680
- return crypto.createPrivateKey({
681
- key: Buffer.from(context.cert_private_key),
682
- format: 'der',
683
- type: 'pkcs8',
684
- });
1507
+ return private_key_object(context.cert_private_key);
1508
+ }
1509
+ function private_key_object(der) {
1510
+ if (!der) return null;
1511
+ // Parsed once per key, not once per handshake: the bytes come from the
1512
+ // secure context (or the SNICallback's), the same array for every
1513
+ // session that uses it, and a KeyObject is immutable. Parsing the PKCS#8
1514
+ // was a few percent of a server's CPU per connection.
1515
+ let keyObj = PRIVATE_KEY_OBJECTS.get(der);
1516
+ if (!keyObj) {
1517
+ keyObj = crypto.createPrivateKey({ key: Buffer.from(der), format: 'der', type: 'pkcs8' });
1518
+ PRIVATE_KEY_OBJECTS.set(der, keyObj);
1519
+ }
1520
+ return keyObj;
685
1521
  }
686
1522
 
687
1523
  /**
@@ -693,10 +1529,454 @@ function TLSSession(options){
693
1529
  * handshake where every message parses and every transcript matches, and only
694
1530
  * the keys differ — which surfaces as "bad record MAC" with no other clue.
695
1531
  */
1532
+ // ── Tickets: what goes in, what is accepted back ────────────────────────
1533
+ //
1534
+ // The session both issues its tickets and opens them, with one ticketKeys:
1535
+ // format and key live in one place. (Opening TLS 1.3 tickets used to live
1536
+ // in TLSSocket, so a TLSSession used on its own — QUIC — issued tickets it
1537
+ // could not accept, and every returning QUIC client paid a full handshake.)
1538
+
1539
+ // A ticket past its lifetime is not accepted. The time of issue was always
1540
+ // written into the ticket and never read, so a ticket stayed valid for as
1541
+ // long as its key did — which, with keys kept across restarts, is forever,
1542
+ // and a stolen ticket with it. A ticket from before `lifetime` was recorded
1543
+ // falls back to the configured one.
1544
+ // A ticket is accepted only under the context it was issued for (ticketContext).
1545
+ function ticket_context_ok(state) {
1546
+ return (state.cx || '') === (context.ticketContext || '');
1547
+ }
1548
+
1549
+ function ticket_expired(state) {
1550
+ if (typeof state.created !== 'number') return true;
1551
+ let lifetime = (typeof state.lifetime === 'number' ? state.lifetime : context.ticketLifetime) * 1000;
1552
+ let age = Date.now() - state.created;
1553
+ return age < -60000 || age > lifetime; // a minute of clock skew either way
1554
+ }
1555
+
1556
+ // ── 0-RTT, the server's decision ────────────────────────────────────────
1557
+ //
1558
+ // The ClientHello offered early data with a ticket we accepted. Whether
1559
+ // the early data is taken is settled here as far as the hello allows —
1560
+ // the ticket allowed it, and it is fresh:
1561
+ // the age the client states (obfuscated_ticket_age − ticket_age_add) within
1562
+ // earlyDataMaxAge of what our clock says (RFC 8446 §8.3) — then by the
1563
+ // replay check, asynchronous: the ServerHello waits for it, as for an
1564
+ // asynchronous SNICallback. No answer within 100 ms, or an error: no 0-RTT
1565
+ // — the handshake goes on in full, nothing is lost. The rest (the ALPN,
1566
+ // the cipher suite and the early-data context are the ticket's) is checked
1567
+ // when EncryptedExtensions is built (early_decide).
1568
+ function early_consider(pskResult, pskIdentity, hashName) {
1569
+ context.early.offered = true;
1570
+ let e = pskResult.early;
1571
+ if (!context.earlyData || !e || context.helloRetried) return;
1572
+ // The context is compared at EncryptedExtensions (early_decide): the
1573
+ // application may still set it from the SNICallback, which comes after.
1574
+ let clientAge = (((pskIdentity.age >>> 0) - (e.age_add >>> 0)) >>> 0);
1575
+ let serverAge = Date.now() - e.created;
1576
+ if (!(Math.abs(serverAge - clientAge) <= context.earlyDataMaxAge)) { dbg('SRV-0RTT', 'ticket age off by', serverAge - clientAge, 'ms — no 0-RTT'); return; }
1577
+ if (!context.earlyDataReplay && e.created < EARLY_REPLAY_START && Date.now() - EARLY_REPLAY_START < 2 * context.earlyDataMaxAge) {
1578
+ dbg('SRV-0RTT', 'ticket from before this process, within the replay window — no 0-RTT');
1579
+ return;
1580
+ }
1581
+ let secret = derive_client_early_traffic_secret(hashName, context.psk_offered.psk, get_transcript_hash(hashName), label_prefix());
1582
+ let candidate = { cipher: pskResult.cipher, alpn: e.alpn, secret: secret, context: e.context || '' };
1583
+ let id = crypto.createHash('sha256').update(pskIdentity.identity).digest('hex');
1584
+ let ttl = 2 * context.earlyDataMaxAge;
1585
+ let store = context.earlyDataReplay || defaultEarlyReplay;
1586
+ let sync = true, done = false;
1587
+ context.early.pending = true;
1588
+ let timer = setTimeout(function () { finish(new Error('no answer within ' + EARLY_REPLAY_TIMEOUT_MS + ' ms'), false); }, EARLY_REPLAY_TIMEOUT_MS);
1589
+ if (timer.unref) timer.unref();
1590
+ function finish(err, fresh) {
1591
+ if (done) return;
1592
+ done = true;
1593
+ clearTimeout(timer);
1594
+ context.early.pending = false;
1595
+ if (!err && fresh === true) context.early.candidate = candidate;
1596
+ else dbg('SRV-0RTT', err ? 'replay check failed: ' + (err.message || err) : 'ticket already used for 0-RTT', '— no 0-RTT');
1597
+ if (!sync && context.state !== 'error' && context.state !== 'closed') set_context({ run_loop: true });
1598
+ }
1599
+ try { store(id, ttl, finish); } catch (err) { finish(err, false); }
1600
+ sync = false;
1601
+ }
1602
+
1603
+ // At EncryptedExtensions: taken (the secret handed out — 'earlySecret',
1604
+ // then 'earlyData' true) or not ('earlyData' false). Once.
1605
+ function early_decide() {
1606
+ if (!context.isServer || !context.early.offered) return false;
1607
+ if (context.early.status !== null) return context.early.status === 'accepted';
1608
+ let c = context.early.candidate;
1609
+ // The context again: it may have changed since the hello was read (QUIC
1610
+ // decides a connection's transport parameters in the SNICallback, after
1611
+ // the PSK was looked at — setEarlyDataContext).
1612
+ if (c && !context.helloRetried && c.cipher === context.selected_cipher_suite &&
1613
+ (c.alpn || null) === (context.selected_alpn || null) &&
1614
+ c.context === (context.earlyDataContext || '')) {
1615
+ context.early.status = 'accepted';
1616
+ _emitKeylogSingle('CLIENT_EARLY_TRAFFIC_SECRET', c.secret);
1617
+ ev.emit('earlySecret', c.secret);
1618
+ ev.emit('earlyData', true);
1619
+ return true;
1620
+ }
1621
+ context.early.status = 'rejected';
1622
+ ev.emit('earlyData', false);
1623
+ return false;
1624
+ }
1625
+
1626
+ function _emitKeylogSingle(label, secret) {
1627
+ if (ev.listenerCount('keylog') === 0 || !secret) return;
1628
+ let clientRandom = context.isServer ? context.remote_random : context.local_random;
1629
+ if (!clientRandom) return;
1630
+ ev.emit('keylog', Buffer.from(label + ' ' + Buffer.from(clientRandom).toString('hex') + ' ' + Buffer.from(secret).toString('hex') + '\n'));
1631
+ }
1632
+
1633
+ // The client's: rejected (a HelloRetryRequest, or no early_data in
1634
+ // EncryptedExtensions) or accepted — told once ('earlyData').
1635
+ function early_client_result(accepted) {
1636
+ if (context.isServer || !context.early.offered || context.early.status !== null) return;
1637
+ context.early.status = accepted ? 'accepted' : 'rejected';
1638
+ ev.emit('earlyData', accepted);
1639
+ }
1640
+
1641
+ // The default answer to a TLS 1.3 PSK identity: one of our own tickets.
1642
+ function open_ticket_13(identity) {
1643
+ if (!context.sessionTickets || !context.ticketKeys) return null;
1644
+ let state = decrypt_session_blob(identity, context.ticketKeys);
1645
+ if (!state || state.v !== 13 || !state.psk || !state.cipher) return null;
1646
+ if (ticket_expired(state)) {
1647
+ dbg('SRV-PSK', 'ticket expired — full handshake');
1648
+ return null;
1649
+ }
1650
+ if (!ticket_context_ok(state)) {
1651
+ dbg('SRV-PSK', 'ticket issued under another context — full handshake');
1652
+ return null;
1653
+ }
1654
+ if (!ticket_peer_acceptable(state)) {
1655
+ dbg('SRV-PSK', 'ticket without an authorized client certificate, one is required — full handshake');
1656
+ return null;
1657
+ }
1658
+ return { psk: state.psk, cipher: state.cipher, data: state.app != null ? state.app : null, sni: state.sni != null ? state.sni : null,
1659
+ state: state.st != null ? state.st : null, peer: state,
1660
+ early: state.ed === 1 ? { context: state.edc || '', alpn: state.alpn || null, created: state.created, age_add: state.age_add >>> 0 } : null };
1661
+ }
1662
+
1663
+ // A session is resumed only under the name it was established for. RFC 6066
1664
+ // §3: a server "MUST NOT accept the request to resume the session if the
1665
+ // server_name extension contains a different name. Instead, it proceeds
1666
+ // with a full handshake". The ticket kept the name and nothing read it: a
1667
+ // ticket from one site resumed on another, skipping that site's
1668
+ // certificate — and its client-certificate check, which a resumed
1669
+ // handshake does not repeat (nginx's CVE-2025-23419 is this). Compared as
1670
+ // DNS names are: case-insensitively, a trailing dot ignored; no name on
1671
+ // both sides (a connection to an address) is the same name. A ticket that
1672
+ // does not say (an application's own 'psk' answer without one) is not held
1673
+ // to it.
1674
+ function same_server_name(ticketSni, helloSni) {
1675
+ if (ticketSni === undefined) return true;
1676
+ let norm = function (n) { return n == null || n === '' ? '' : String(n).toLowerCase().replace(/\.$/, ''); };
1677
+ return norm(ticketSni) === norm(helloSni);
1678
+ }
1679
+
1680
+ // ── The client's certificate, kept with the session ──────────────────────
1681
+ //
1682
+ // A resumed handshake has no Certificate and no CertificateRequest
1683
+ // (RFC 8446 §4.6.1 / §4.3.2: not with a PSK), so whatever the client proved
1684
+ // on its full handshake has to come back from the ticket — as OpenSSL keeps
1685
+ // the peer certificate and its verify result in the SSL_SESSION. Nothing
1686
+ // was kept: every resumed connection reported `authorized: false` and no
1687
+ // peer certificate, so an mTLS server turned away its own returning
1688
+ // clients; and the client-certificate requirement is checked on full
1689
+ // handshakes only, so a ticket issued when none was required (before the
1690
+ // server turned requestCert on, or one with rejectUnauthorized: false)
1691
+ // resumed into a server that now requires one, with no certificate at all.
1692
+ //
1693
+ // So the ticket carries the leaf certificate, whether it was authorized and
1694
+ // why not; a server that requires an authorized client (requestCert +
1695
+ // rejectUnauthorized) accepts only a ticket that says so — anything else is
1696
+ // a full handshake, where the certificate is asked for again.
1697
+ function ticket_peer_fields() {
1698
+ if (!context.isServer) return {};
1699
+ // This handshake's certificate, or — resumed — the one the ticket
1700
+ // carried in: a ticket issued on a resumed connection carries it on.
1701
+ let chain = (context.remote_cert_chain && context.remote_cert_chain.length > 0) ? context.remote_cert_chain : context.resumed_peer_chain;
1702
+ let leaf = chain && chain.length > 0 && chain[0] ? chain[0].cert : null;
1703
+ return {
1704
+ pc: leaf || null,
1705
+ pa: !!(leaf && context.peerAuthorized),
1706
+ pe: leaf ? (context.authorizationError || null) : 'NO_PEER_CERTIFICATE',
1707
+ };
1708
+ }
1709
+ function ticket_peer_acceptable(state) {
1710
+ if (!(context.requestCert && context.rejectUnauthorized)) return true;
1711
+ return !!(state && state.pa === true && state.pc);
1712
+ }
1713
+ // Kept apart from remote_cert_chain, which means "a Certificate arrived in
1714
+ // THIS handshake" — and so has to be proved by a CertificateVerify in it.
1715
+ // getPeerCertificate() answers with either.
1716
+ function restore_ticket_peer(state) {
1717
+ if (!state || !state.pc) return;
1718
+ context.resumed_peer_chain = [{ cert: state.pc }];
1719
+ context.peerAuthorized = state.pa === true;
1720
+ context.authorizationError = state.pa === true ? null : (state.pe || 'UNABLE_TO_VERIFY_LEAF_SIGNATURE');
1721
+ }
1722
+
1723
+ // Our Certificate message — compressed (RFC 8879) when the peer offered an
1724
+ // algorithm we have, by our preference, in TLS 1.3. The compressed form
1725
+ // is cached per chain and algorithm (cert_compression.js).
1726
+ // ---- OCSP stapling (server) ----
1727
+
1728
+ /** The client asked for an OCSP response (status_request, type ocsp). */
1729
+ function client_requested_ocsp() {
1730
+ let ex = context.remote_extensions || [];
1731
+ for (let i = 0; i < ex.length; i++) {
1732
+ if (ex[i] && ex[i].type === wire.TLS_EXT.STATUS_REQUEST && ex[i].data && ex[i].data.length > 0 && ex[i].data[0] === 1) return true;
1733
+ }
1734
+ return false;
1735
+ }
1736
+
1737
+ /**
1738
+ * Ask the application for the OCSP response — once, when a client asked
1739
+ * for it, our certificate is settled (after the SNI lookup) and it will be
1740
+ * sent (not a resumption). As node:tls's 'OCSPRequest': (certificate,
1741
+ * issuer, callback), DER both, issuer null when the chain has none. The
1742
+ * ServerHello waits for the answer, as it waits for SNICallback: in TLS
1743
+ * 1.2 it must say whether a CertificateStatus follows.
1744
+ */
1745
+ function maybe_request_ocsp(version) {
1746
+ if (!context.isServer || context.ocsp_requested || !context.ocspRequest) return;
1747
+ if (context.sni_pending || context.local_cert_chain === null || context.local_cert_chain.length === 0) return;
1748
+ if (context.psk_accepted || context.tls12_abbreviated || context.tls12_resume_pending) return;
1749
+ // A TLS 1.2 resumption (ticket or session ID) sends no Certificate. Its
1750
+ // decision is taken in this same pass and committed only at its end, so
1751
+ // tls12_abbreviated is not yet set here: the loaded state says it. It is
1752
+ // dropped (null) when the resumption is refused, and the next pass asks.
1753
+ // `version` is the one being chosen now — a 1.3 handshake whose client
1754
+ // also carried a 1.2 ticket is a full one.
1755
+ let is12Now = version === wire.TLS_VERSION.TLS1_2 || version === wire.DTLS_VERSION.DTLS1_2;
1756
+ if (is12Now && context.tls12_resume_state) return;
1757
+ if (!client_requested_ocsp()) return;
1758
+ context.ocsp_requested = true;
1759
+ context.ocsp_pending = true;
1760
+ let done = false, sync = true;
1761
+ let chain = context.local_cert_chain;
1762
+ let issuer = chain.length > 1 && chain[1] && chain[1].cert ? chain[1].cert : null;
1763
+ try {
1764
+ context.ocspRequest(chain[0].cert, issuer, function (err, response) {
1765
+ if (done) return;
1766
+ done = true;
1767
+ context.ocsp_pending = false;
1768
+ if (err) {
1769
+ fatalAlert(wire.TLS_ALERT.INTERNAL_ERROR, 'OCSPRequest: ' + (err.message || err));
1770
+ return;
1771
+ }
1772
+ if (response && response.length > 0) context.ocsp_response = new Uint8Array(response);
1773
+ // Answered later: the pass that would have sent the ServerHello has
1774
+ // already stopped at the gate, so run the loop again.
1775
+ if (!sync) set_context({ run_loop: true });
1776
+ });
1777
+ } catch (e) {
1778
+ done = true;
1779
+ context.ocsp_pending = false;
1780
+ fatalAlert(wire.TLS_ALERT.INTERNAL_ERROR, 'OCSPRequest: ' + (e.message || e));
1781
+ }
1782
+ sync = false;
1783
+ }
1784
+
1785
+ function certificate_message(entries, requestContext) {
1786
+ let plain = build_tls_message({
1787
+ type: 'certificate',
1788
+ version: context.selected_version || wire.TLS_VERSION.TLS1_3,
1789
+ entries: entries,
1790
+ certificate_request_context: requestContext,
1791
+ });
1792
+ let peer = context.peer_cert_compression;
1793
+ if (!is13() || !Array.isArray(peer) || entries.length === 0) return plain;
1794
+ let alg = null;
1795
+ for (let i = 0; i < context.cert_compression.length && alg === null; i++) {
1796
+ if (peer.indexOf(context.cert_compression[i]) >= 0) alg = context.cert_compression[i];
1797
+ }
1798
+ if (alg === null) return plain;
1799
+ let body = plain.subarray(4); // the handshake header off
1800
+ let compressed = compress_certificate_body(body, alg);
1801
+ if (!compressed || compressed.length >= body.length) return plain;
1802
+ context.cert_compression_sent = alg;
1803
+ return build_tls_message({ type: 'compressed_certificate', algorithm: alg, uncompressed_length: body.length, compressed: compressed });
1804
+ }
1805
+
1806
+ // The client's side of the same. A resumed handshake has no Certificate:
1807
+ // the server is who it was when the session was saved, and nothing else
1808
+ // says who that was. The session ('session' event) left the certificate
1809
+ // out, so a resumed connection answered getPeerCertificate() with nothing
1810
+ // and `authorized` false — node:tls answers both from the session — and a
1811
+ // session saved with rejectUnauthorized: false (an unverified server, a
1812
+ // self-signed one) resumed into a connection that requires verification,
1813
+ // with nothing verified: the PSK proves the server is the one that issued
1814
+ // the ticket, not that it is the one the caller trusts.
1815
+ //
1816
+ // So the session carries the server's chain and the verdict on it; a
1817
+ // connection that requires an authorized server offers only a session that
1818
+ // says so.
1819
+ function client_session_peer_fields() {
1820
+ let chain = (context.remote_cert_chain && context.remote_cert_chain.length > 0) ? context.remote_cert_chain : context.resumed_peer_chain;
1821
+ let certs = chain ? chain.map(function (e) { return e.cert; }).filter(function (c) { return !!c; }) : [];
1822
+ return {
1823
+ peer_chain: certs.length > 0 ? certs : null,
1824
+ peer_authorized: certs.length > 0 && context.peerAuthorized === true,
1825
+ peer_error: certs.length > 0 ? (context.authorizationError || null) : 'NO_PEER_CERTIFICATE',
1826
+ };
1827
+ }
1828
+ function restore_client_session_peer(sessionData) {
1829
+ if (!sessionData) return; // an external PSK: no saved server
1830
+ let certs = sessionData && Array.isArray(sessionData.peer_chain) ? sessionData.peer_chain : null;
1831
+ context.resumed_peer_chain = certs ? certs.map(function (c) { return { cert: c }; }) : null;
1832
+ context.peerAuthorized = !!certs && sessionData.peer_authorized === true;
1833
+ context.authorizationError = context.peerAuthorized ? null : (sessionData.peer_error || 'UNABLE_TO_VERIFY_LEAF_SIGNATURE');
1834
+ }
1835
+ // Whether a saved session may be offered on this connection. Not one for
1836
+ // another server name (RFC 8446 §4.6.1: resume only with the SNI it was
1837
+ // made with), not one past its lifetime (§4.6.1: not cached longer than
1838
+ // ticket_lifetime — the server refuses it anyway, after a wasted binder),
1839
+ // and not one whose server was not verified when verification is required.
1840
+ function client_session_usable(sessionData) {
1841
+ if (!sessionData) return false;
1842
+ let sni = sessionData.sni != null ? String(sessionData.sni).toLowerCase() : null;
1843
+ let want = context.local_sni != null ? String(context.local_sni).toLowerCase() : null;
1844
+ if (sni !== want) { dbg('CLI-PSK', 'saved session is for another server name — not offered'); return false; }
1845
+ if (typeof sessionData.lifetime === 'number' && typeof sessionData.created === 'number' &&
1846
+ Date.now() > sessionData.created + sessionData.lifetime * 1000) {
1847
+ dbg('CLI-PSK', 'saved session expired — not offered'); return false;
1848
+ }
1849
+ if (context.rejectUnauthorized && sessionData.peer_authorized !== true) {
1850
+ dbg('CLI-PSK', 'saved session has no verified server — not offered'); return false;
1851
+ }
1852
+ return true;
1853
+ }
1854
+
1855
+ // A TLS 1.3 NewSessionTicket, sent now: the one every full handshake ends
1856
+ // with, and any issued later (sendTicket) — each with its own nonce, so its
1857
+ // own PSK (RFC 8446 §4.6.1), carrying the application's ticketData and the
1858
+ // protocol layer's ticket state as they are at the time.
1859
+ function issue_ticket_13() {
1860
+ let hashName = negotiated_hash();
1861
+ let ticket_nonce = new Uint8Array([context.ticket_nonce_counter++]);
1862
+ let psk = derive_psk(hashName, context.resumption_master_secret, ticket_nonce, label_prefix());
1863
+ let ticket_age_add = crypto.randomBytes(4).readUInt32BE(0);
1864
+ let ticket_lifetime = context.ticketLifetime;
1865
+
1866
+ dbg('SRV-NST', 'issuing TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
1867
+ 'hash:', hashName,
1868
+ 'transcript len:', concatUint8Arrays(context.transcript).length);
1869
+ dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
1870
+ 'age_add:', ticket_age_add,
1871
+ 'lifetime:', ticket_lifetime);
1872
+ dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
1873
+ 'derived psk:', hexPreview(psk, 8));
1874
+
1875
+ // Ensure ticketKeys is 48 bytes (key_name + aes_key)
1876
+ if (!context.ticketKeys || context.ticketKeys.length !== 48) {
1877
+ context.ticketKeys = crypto.randomBytes(48);
1878
+ }
1879
+
1880
+ // Encrypt session state into opaque ticket (unified format: key_name(16) | IV(12) | CT | Tag(16))
1881
+ let ticket = encrypt_session_blob({
1882
+ v: 13, // blob kind: TLS 1.3 PSK
1883
+ version: context.selected_version,
1884
+ cipher: context.selected_cipher_suite,
1885
+ psk: psk,
1886
+ age_add: ticket_age_add,
1887
+ sni: context.selected_sni || context.remote_sni || null,
1888
+ alpn: context.selected_alpn || null,
1889
+ created: Date.now(),
1890
+ lifetime: ticket_lifetime,
1891
+ app: ticket_app_data(),
1892
+ st: context.ticket_state != null ? context.ticket_state : undefined,
1893
+ cx: context.ticketContext || undefined,
1894
+ // 0-RTT allowed with it, and under what (open_ticket_13, early_eligible).
1895
+ ed: context.earlyData ? 1 : undefined,
1896
+ edc: context.earlyData ? context.earlyDataContext : undefined,
1897
+ ...ticket_peer_fields(),
1898
+ }, context.ticketKeys);
1899
+
1900
+ // early_data in the ticket (RFC 8446 §4.2.10): max_early_data_size.
1901
+ let nstExts = [];
1902
+ if (context.earlyData) {
1903
+ let m = context.maxEarlyData >>> 0;
1904
+ nstExts.push({ type: wire.TLS_EXT.EARLY_DATA, data: new Uint8Array([m >>> 24 & 255, m >>> 16 & 255, m >>> 8 & 255, m & 255]) });
1905
+ }
1906
+
1907
+ let nst_data = wire.build_message(wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET,
1908
+ wire.build_new_session_ticket({
1909
+ ticket_lifetime: ticket_lifetime,
1910
+ ticket_age_add: ticket_age_add,
1911
+ ticket_nonce: ticket_nonce,
1912
+ ticket: ticket,
1913
+ extensions: nstExts
1914
+ })
1915
+ );
1916
+
1917
+ ev.emit('message', 2, context.message_sent_seq, 'new_session_ticket', nst_data);
1918
+ context.message_sent_seq++;
1919
+
1920
+ // Emit 'session' event on server side too — lets users track when tickets are
1921
+ // issued (e.g. for monitoring or metrics). Not part of Node.js API but useful.
1922
+ // Emits the same Buffer the client would receive via their 'session' event,
1923
+ // so server-side apps could also persist it if they want.
1924
+ let server_session_blob = encode_client_session({
1925
+ v: 13,
1926
+ version: context.selected_version,
1927
+ cipher: context.selected_cipher_suite,
1928
+ ticket: ticket,
1929
+ psk: psk,
1930
+ age_add: ticket_age_add,
1931
+ lifetime: ticket_lifetime,
1932
+ sni: context.selected_sni || context.remote_sni || null,
1933
+ alpn: context.selected_alpn || null,
1934
+ created: Date.now(),
1935
+ });
1936
+ ev.emit('session', server_session_blob);
1937
+ }
1938
+
1939
+ // What the application wants in the ticket being issued (ticketData).
1940
+ // Never allowed to break a handshake: a throw, something that does not
1941
+ // serialize, or anything too big means a ticket without application data.
1942
+ // Too big because a ticket travels in every ClientHello that presents it —
1943
+ // over QUIC that ClientHello should fit the first packet.
1944
+ const TICKET_DATA_MAX = 256; // bytes, serialized
1945
+ const TICKET_STATE_MAX = 128; // setTicketState's, apart from it
1946
+ //
1947
+ // Asked once per connection, and the answer kept: the ticket issued and
1948
+ // getTicketInfo() — asked by the application for this very connection —
1949
+ // must carry the same value. Asked twice, ticketData minting a new id on a
1950
+ // full handshake would give the connection one id and its ticket another,
1951
+ // and the first connection would never be tied to the ones resuming it.
1952
+ function ticket_app_data() {
1953
+ if (context.app_ticket_data !== undefined) return context.app_ticket_data;
1954
+ context.app_ticket_data = ticket_app_data_ask();
1955
+ return context.app_ticket_data;
1956
+ }
1957
+ function ticket_app_data_ask() {
1958
+ if (!context.ticketData) return null;
1959
+ let data;
1960
+ try { data = context.ticketData(context.isResumed ? context.resumed_ticket_data : null); }
1961
+ catch (e) { dbg('SRV-NST', 'ticketData threw:', e && e.message); return null; }
1962
+ if (data == null) return null;
1963
+ let size;
1964
+ try { size = Buffer.byteLength(JSON.stringify(data)); } catch (e) { return null; }
1965
+ if (!(size <= TICKET_DATA_MAX)) {
1966
+ dbg('SRV-NST', 'ticketData is ' + size + ' bytes, over ' + TICKET_DATA_MAX + ' — left out');
1967
+ return null;
1968
+ }
1969
+ return data;
1970
+ }
1971
+
696
1972
  function label_prefix() {
697
- return context.selected_version === wire.DTLS_VERSION.DTLS1_3
698
- ? LABEL_PREFIX_DTLS13
699
- : LABEL_PREFIX_TLS13;
1973
+ return label_prefix_for(context.selected_version);
1974
+ }
1975
+
1976
+ // For a version chosen in this pass but not yet committed to context
1977
+ // (the HelloRetryRequest is built before the pass commits).
1978
+ function label_prefix_for(version) {
1979
+ return version === wire.DTLS_VERSION.DTLS1_3 ? LABEL_PREFIX_DTLS13 : LABEL_PREFIX_TLS13;
700
1980
  }
701
1981
 
702
1982
  /**
@@ -738,6 +2018,7 @@ function TLSSession(options){
738
2018
  }
739
2019
 
740
2020
  function pushTranscript(data) {
2021
+
741
2022
  if (context.transcriptHook) {
742
2023
  data = context.transcriptHook(data);
743
2024
  }
@@ -763,6 +2044,7 @@ function TLSSession(options){
763
2044
  * re-hashes the same bytes 4 fewer times.
764
2045
  */
765
2046
  function get_transcript_hash(hashName) {
2047
+
766
2048
  if (context.transcriptHash !== null && context.transcriptHashName === hashName) {
767
2049
  return new Uint8Array(context.transcriptHash.copy().digest());
768
2050
  }
@@ -844,17 +2126,99 @@ function TLSSession(options){
844
2126
  let t = message.type;
845
2127
  let serverOnly = t === 'server_hello' || t === 'encrypted_extensions' ||
846
2128
  t === 'server_hello_done' || t === 'certificate_request' ||
847
- t === 'server_key_exchange' || t === 'new_session_ticket';
848
- let clientOnly = t === 'client_hello' || t === 'client_key_exchange';
2129
+ t === 'server_key_exchange' || t === 'new_session_ticket' ||
2130
+ t === 'certificate_status';
2131
+ let clientOnly = t === 'client_hello' || t === 'client_key_exchange' || t === 'end_of_early_data';
849
2132
  if ((context.isServer && serverOnly) || (!context.isServer && clientOnly)) {
850
2133
  fatalAlert(wire.TLS_ALERT.UNEXPECTED_MESSAGE, 'Unexpected ' + t + ' from ' + (context.isServer ? 'client' : 'server'));
851
2134
  return;
852
2135
  }
853
2136
  }
854
2137
 
2138
+ // A server takes one ClientHello, and a second only in answer to its
2139
+ // HelloRetryRequest (RFC 8446 §4.1.2, §4.1.4). There is no
2140
+ // renegotiation here, so any other — after the ServerHello, a second
2141
+ // with no retry asked, a third — is unexpected_message. Before, it was
2142
+ // run through the handshake again: the SNI callback fired anew, and
2143
+ // with ECH the decrypted state of this connection was overwritten.
2144
+ if (context.isServer && message.type === 'client_hello') {
2145
+ let n = context.client_hellos_received;
2146
+ if (!(n === 0 || (n === 1 && context.helloRetried === true && context.hello_sent === false))) {
2147
+ fatalAlert(wire.TLS_ALERT.UNEXPECTED_MESSAGE, 'Unexpected ClientHello (' + (n + 1) + ')');
2148
+ return;
2149
+ }
2150
+ context.client_hellos_received = n + 1;
2151
+ }
2152
+
2153
+ // Encrypted Client Hello (RFC 9849 §7.1): the hello that arrived may
2154
+ // carry the real one encrypted. Decrypted, it takes the place of the
2155
+ // outer hello from here on — transcript, SNI, ALPN, key share, every
2156
+ // check below — exactly as if it had come in the clear; the outer one
2157
+ // is kept only as the wire's fingerprint. Not decryptable, the outer
2158
+ // hello goes on as it came, and EncryptedExtensions will carry
2159
+ // retry_configs.
2160
+ if (context.isServer && message.type === 'client_hello' && context.ech_server) {
2161
+ if (context.rawClientHello === null) context.rawClientHello = data;
2162
+ let echResult;
2163
+ try {
2164
+ echResult = server_process_client_hello(context.ech_server, context.ech, data, context.helloRetried === true);
2165
+ } catch (e) {
2166
+ fatalAlert(e.alertDesc || wire.TLS_ALERT.ILLEGAL_PARAMETER, e.message);
2167
+ return;
2168
+ }
2169
+ if (echResult) {
2170
+ try {
2171
+ message = parse_tls_message(echResult.data, context.selected_version);
2172
+ } catch (e) {
2173
+ fatalAlert(e.alertDesc || wire.TLS_ALERT.DECODE_ERROR, 'Malformed ClientHelloInner: ' + e.message);
2174
+ return;
2175
+ }
2176
+ data = echResult.data;
2177
+ }
2178
+ }
2179
+
855
2180
  // Emit 'handshakeMessage' hook for every message
856
2181
  ev.emit('handshakeMessage', message.type, data, message);
857
2182
 
2183
+ // What the peer can decompress (RFC 8879): from its ClientHello, or —
2184
+ // for our client certificate — its CertificateRequest.
2185
+ if (context.isServer && message.type === 'client_hello') {
2186
+ if ('early_data' in message) context.early.clientOffered = true;
2187
+ context.peer_cert_compression = Array.isArray(message.compress_certificate) ? message.compress_certificate : null;
2188
+ } else if (!context.isServer && message.type === 'certificate_request' && Array.isArray(message.extensions)) {
2189
+ for (let ei = 0; ei < message.extensions.length; ei++) {
2190
+ let ce = message.extensions[ei];
2191
+ if (ce && (ce.type === wire.TLS_EXT.COMPRESS_CERTIFICATE || ce.name === 'COMPRESS_CERTIFICATE') && Array.isArray(ce.value)) context.peer_cert_compression = ce.value;
2192
+ }
2193
+ }
2194
+
2195
+ // A CompressedCertificate (RFC 8879 §4) is read as the Certificate it
2196
+ // holds; the transcript keeps the message as it came (`data`). Only in
2197
+ // TLS 1.3, only with an algorithm this side offered (illegal_parameter),
2198
+ // and only if it decompresses to exactly the length it declares
2199
+ // (bad_certificate) — never past the handshake size limit.
2200
+ if (message.type === 'compressed_certificate') {
2201
+ if (!is13() || context.cert_compression.indexOf(message.algorithm) < 0) {
2202
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'CompressedCertificate with an algorithm not offered (' + message.algorithm + ')');
2203
+ return;
2204
+ }
2205
+ // Never past HANDSHAKE_MESSAGE_MAX (wire.js): the declared length was
2206
+ // the 24-bit field's 16 MB, and a few hundred compressed bytes could
2207
+ // be inflated to it — in memory and time, before anything was verified.
2208
+ let limit = context.maxHandshakeSize > 0 ? Math.min(context.maxHandshakeSize, wire.HANDSHAKE_MESSAGE_MAX) : wire.HANDSHAKE_MESSAGE_MAX;
2209
+ let body = decompress_certificate_body(message.algorithm, message.compressed, message.uncompressed_length, limit);
2210
+ if (!body) {
2211
+ fatalAlert(wire.TLS_ALERT.BAD_CERTIFICATE, 'CompressedCertificate does not decompress to its declared length');
2212
+ return;
2213
+ }
2214
+ let algorithm = message.algorithm;
2215
+ try { message = wire.parse_certificate(body); }
2216
+ catch (e) { fatalAlert(wire.TLS_ALERT.BAD_CERTIFICATE, 'CompressedCertificate: ' + e.message); return; }
2217
+ message.type = 'certificate';
2218
+ message.compressed = true;
2219
+ context.cert_compression_received = algorithm;
2220
+ }
2221
+
858
2222
  if((context.isServer==false && message.type=='server_hello') || (context.isServer==true && message.type=='client_hello')){
859
2223
 
860
2224
  // Server-side ClientHello policy checks — BEFORE the transcript push,
@@ -894,11 +2258,26 @@ function TLSSession(options){
894
2258
  }
895
2259
  }
896
2260
 
2261
+ // ECH (RFC 9849 §6.1.4): which hello the server answered — before this
2262
+ // message joins the transcript, which it decides.
2263
+ if (!context.isServer && message.type === 'server_hello' && context.ech.outer_ch1 && !context.ech.decided) {
2264
+ if (!ech_decide(message, data)) return;
2265
+ }
2266
+
897
2267
  pushTranscript(data);
898
2268
 
2269
+ // Record the peer's hello for fingerprinting, whichever side we are.
2270
+ // A client sees the ServerHello here; a server sees the ClientHello.
2271
+ if (message.type === 'client_hello') {
2272
+ if (context.rawClientHello === null) context.rawClientHello = data;
2273
+ } else if (message.type === 'server_hello') {
2274
+ context.rawServerHello = data;
2275
+ context.parsedServerHello = null; // HRR then real SH: re-parse
2276
+ context.fpCache.ja4s = undefined;
2277
+ }
2278
+
899
2279
  // Save raw ClientHello + emit event (server side)
900
2280
  if (context.isServer && message.type === 'client_hello') {
901
- context.rawClientHello = data;
902
2281
  // Make the peer's extensions readable INSIDE the 'clienthello'
903
2282
  // handler (getRemoteExtension/getRemoteExtensions) — the main
904
2283
  // set_context that stores them runs after this emit, and the
@@ -923,24 +2302,47 @@ function TLSSession(options){
923
2302
  }
924
2303
 
925
2304
  // Server: detect and validate PSK BEFORE set_context (so reactive loop sees psk_accepted)
926
- if (context.isServer && message.pre_shared_key && message.pre_shared_key.identities && message.pre_shared_key.identities.length > 0) {
2305
+ //
2306
+ // RFC 8446 §4.2.9: a client offering pre_shared_key without
2307
+ // psk_key_exchange_modes is to be refused outright; and a PSK may only be
2308
+ // used in a mode the client listed. This server resumes in one mode only,
2309
+ // psk_dhe_ke (the key schedule always mixes in a fresh ECDHE secret, so
2310
+ // resumption keeps forward secrecy) — a client that did not offer it is
2311
+ // given a full handshake instead.
2312
+ if (context.isServer && message.pre_shared_key && !Array.isArray(message.psk_modes)) {
2313
+ fatalAlert(wire.TLS_ALERT.MISSING_EXTENSION, 'pre_shared_key without psk_key_exchange_modes');
2314
+ return;
2315
+ }
2316
+ let pskDheOffered = Array.isArray(message.psk_modes) && message.psk_modes.indexOf(1) >= 0;
2317
+ if (context.isServer && pskDheOffered && message.pre_shared_key && message.pre_shared_key.identities && message.pre_shared_key.identities.length > 0) {
927
2318
  let pskIdentity = message.pre_shared_key.identities[0];
2319
+ context.ticket_offered = true;
928
2320
  let pskBinder = message.pre_shared_key.binders ? message.pre_shared_key.binders[0] : null;
929
2321
 
930
2322
  dbg('SRV-PSK', 'received identity:', hexPreview(pskIdentity.identity, 24),
931
2323
  'age:', (pskIdentity.age || 0) >>> 0,
932
2324
  'received binder:', hexPreview(pskBinder, 16));
933
2325
 
934
- let pskResult = null;
2326
+ // A 'psk' listener decides, when there is one and it answers. When
2327
+ // nobody answers, this session opens the ticket itself — it issued it,
2328
+ // with its own ticketKeys (see open_ticket_13). That default used to
2329
+ // live in TLSSocket, so a TLSSession used on its own (QUIC) issued
2330
+ // tickets it could never accept.
2331
+ let pskResult;
935
2332
  ev.emit('psk', {
936
2333
  identity: pskIdentity.identity,
937
2334
  obfuscatedAge: (pskIdentity.age || 0) >>> 0
938
2335
  }, function(result) {
939
2336
  pskResult = result;
940
2337
  });
2338
+ if (pskResult === undefined) pskResult = open_ticket_13(pskIdentity.identity);
941
2339
 
942
2340
  dbg('SRV-PSK', 'pskResult:', pskResult ? `psk=${hexPreview(pskResult.psk, 8)} cipher=0x${pskResult.cipher?.toString(16)}` : 'null (decrypt failed)');
943
2341
 
2342
+ if (pskResult && pskResult.psk && !same_server_name(pskResult.sni, message.sni)) {
2343
+ dbg('SRV-PSK', 'ticket for another server name — full handshake');
2344
+ pskResult = null;
2345
+ }
944
2346
  if (pskResult && pskResult.psk) {
945
2347
  let pskCipher = pskResult.cipher || 0x1301;
946
2348
  let hashName = TLS_CIPHER_SUITES[pskCipher] ? TLS_CIPHER_SUITES[pskCipher].hash : 'sha256';
@@ -948,7 +2350,12 @@ function TLSSession(options){
948
2350
 
949
2351
  let hashLen = getHashFn(hashName).outputLen;
950
2352
  let bindersSize = 2 + 1 + hashLen;
2353
+ // After a HelloRetryRequest the binder covers message_hash and the
2354
+ // retry too (§4.2.11.2): what the transcript holds before this hello.
951
2355
  let truncatedCH = data.slice(0, data.length - bindersSize);
2356
+ if (context.helloRetried && context.transcript.length > 1) {
2357
+ truncatedCH = concatUint8Arrays(context.transcript.slice(0, -1).concat([truncatedCH]));
2358
+ }
952
2359
  let expectedBinder = compute_psk_binder(hashName, binder_key, truncatedCH, label_prefix());
953
2360
 
954
2361
  dbg('SRV-PSK', 'hash:', hashName, 'hashLen:', hashLen,
@@ -967,6 +2374,9 @@ function TLSSession(options){
967
2374
  if (binderOk) {
968
2375
  context.psk_accepted = true;
969
2376
  context.isResumed = true;
2377
+ context.resumed_ticket_data = pskResult.data != null ? pskResult.data : null;
2378
+ context.resumed_ticket_state = pskResult.state != null ? pskResult.state : null;
2379
+ if (pskResult.peer) restore_ticket_peer(pskResult.peer);
970
2380
  context.psk_offered = {
971
2381
  psk: pskResult.psk instanceof Uint8Array ? pskResult.psk : new Uint8Array(pskResult.psk),
972
2382
  cipher: pskCipher,
@@ -975,17 +2385,32 @@ function TLSSession(options){
975
2385
  // under. Stored here so selection does not have to re-derive it.
976
2386
  hash: hashName,
977
2387
  };
2388
+ if ('early_data' in message) early_consider(pskResult, pskIdentity, hashName);
978
2389
  }
979
2390
  }
980
2391
  }
981
2392
 
982
2393
  // Client: detect if server accepted PSK from ServerHello (BEFORE set_context)
983
2394
  if (!context.isServer && message.pre_shared_key && typeof message.pre_shared_key.selected === 'number') {
984
- if (context.psk_offered) {
985
- dbg('CLI-PSK', '✓ server accepted PSK, selected_identity:', message.pre_shared_key.selected);
986
- context.psk_accepted = true;
987
- context.isResumed = true;
2395
+ // RFC 8446 §4.2.11: the server picks one of the identities offered
2396
+ // (one, here), with a suite of the PSK's hash — anything else is
2397
+ // illegal_parameter. A pre_shared_key nobody offered is an
2398
+ // extension we did not send: unsupported_extension (§4.2).
2399
+ if (!context.psk_offered) {
2400
+ fatalAlert(wire.TLS_ALERT.UNSUPPORTED_EXTENSION, 'pre_shared_key in ServerHello without one offered');
2401
+ return;
988
2402
  }
2403
+ let shSuite = hello_cipher_suite(message);
2404
+ let pskHash = TLS_CIPHER_SUITES[context.psk_offered.cipher] ? TLS_CIPHER_SUITES[context.psk_offered.cipher].hash : null;
2405
+ let shHash = TLS_CIPHER_SUITES[shSuite] ? TLS_CIPHER_SUITES[shSuite].hash : null;
2406
+ if (message.pre_shared_key.selected !== 0 || (shSuite !== null && pskHash !== shHash)) {
2407
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'ServerHello selected a PSK identity or suite that was not offered');
2408
+ return;
2409
+ }
2410
+ dbg('CLI-PSK', '✓ server accepted PSK, selected_identity:', message.pre_shared_key.selected);
2411
+ context.psk_accepted = true;
2412
+ context.isResumed = true;
2413
+ restore_client_session_peer(context.psk_offered.session);
989
2414
  } else if (!context.isServer && context.psk_offered && message.type === 'server_hello') {
990
2415
  dbg('CLI-PSK', '✗ server did NOT include pre_shared_key in SH — full handshake');
991
2416
  }
@@ -993,6 +2418,7 @@ function TLSSession(options){
993
2418
  // Client: detect HelloRetryRequest (ServerHello with magic random)
994
2419
  if (!context.isServer && message.random && uint8Equal(message.random, wire.TLS13_HRR_RANDOM)) {
995
2420
  context.helloRetried = true;
2421
+ early_client_result(false); // §4.2.10: no early data after a HelloRetryRequest
996
2422
 
997
2423
  // Get cipher from HRR to determine hash
998
2424
  let hrrCipher = null;
@@ -1000,6 +2426,7 @@ function TLSSession(options){
1000
2426
  else if (message.cipher_suite) hrrCipher = message.cipher_suite;
1001
2427
  if (!hrrCipher) hrrCipher = 0x1301;
1002
2428
  let hashName = TLS_CIPHER_SUITES[hrrCipher] ? TLS_CIPHER_SUITES[hrrCipher].hash : 'sha256';
2429
+ context.hrr_hash = hashName; // the second hello's PSK must use it
1003
2430
 
1004
2431
  // Replace transcript: CH1 → message_hash (RFC 8446 §4.4.1)
1005
2432
  // BUG FIX: The HRR was already pushed to transcript at the top of this block (line 195).
@@ -1317,6 +2744,41 @@ function TLSSession(options){
1317
2744
  }
1318
2745
  }
1319
2746
 
2747
+ // ── Server side: the client's key shares (RFC 8446 §4.2.8) ──
2748
+ // "Clients MUST NOT offer multiple KeyShareEntry values for the same
2749
+ // group. Clients MUST NOT offer any KeyShareEntry values for groups not
2750
+ // listed in the client's 'supported_groups' extension. Servers MAY check
2751
+ // for violations of these rules and abort the handshake with an
2752
+ // 'illegal_parameter' alert." Checked, as OpenSSL and BoringSSL do.
2753
+ // Unchecked, a share for a group the client never listed was used —
2754
+ // a handshake in a group it had not agreed to — and of two shares for
2755
+ // one group the first was taken, the second silently ignored: two
2756
+ // implementations reading one ClientHello two ways.
2757
+ if (context.isServer === true && message.type === 'client_hello' &&
2758
+ Array.isArray(message.key_groups) && message.key_groups.length > 0) {
2759
+ // §9.2: "If containing a 'key_share' extension, it MUST also contain
2760
+ // a 'supported_groups' extension" — missing_extension otherwise.
2761
+ if (!Array.isArray(message.supported_groups)) {
2762
+ fatalAlert(wire.TLS_ALERT.MISSING_EXTENSION, 'ClientHello has key_share without supported_groups');
2763
+ return;
2764
+ }
2765
+ let offered = message.supported_groups;
2766
+ let seenShare = {};
2767
+ for (let ki = 0; ki < message.key_groups.length; ki++) {
2768
+ let g = message.key_groups[ki] && message.key_groups[ki].group;
2769
+ if (typeof g !== 'number') continue;
2770
+ if (seenShare[g]) {
2771
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'ClientHello has two key shares for group 0x' + (g >>> 0).toString(16));
2772
+ return;
2773
+ }
2774
+ seenShare[g] = true;
2775
+ if (offered.indexOf(g) < 0) {
2776
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'ClientHello has a key share for group 0x' + (g >>> 0).toString(16) + ', not in its supported_groups');
2777
+ return;
2778
+ }
2779
+ }
2780
+ }
2781
+
1320
2782
  // Resolve our certificate BEFORE the set_context below.
1321
2783
  //
1322
2784
  // The reactive loop derives the cipher suite from the inputs present when
@@ -1326,18 +2788,63 @@ function TLSSession(options){
1326
2788
  // ran with no key in hand, so a server holding an ECDSA certificate could
1327
2789
  // pick an ECDHE_RSA suite and then present a certificate the peer rejects
1328
2790
  // as "wrong certificate type". Inputs first, then the loop.
2791
+ // No server_name in the hello: Node does not call SNICallback at all
2792
+ // and presents the default certificate. Same here when there is one.
2793
+ // With no default certificate, the callback is still asked (with null)
2794
+ // — the only chance left to pick a certificate rather than fail a
2795
+ // handshake Node would also fail.
1329
2796
  if (context.isServer === true && message.type === 'client_hello' &&
1330
- typeof context.SNICallback === 'function') {
2797
+ typeof context.SNICallback === 'function' &&
2798
+ (message.sni || context.local_cert_chain === null)) {
2799
+
2800
+ // Raised BEFORE calling out, so the set_context below — which runs
2801
+ // whether or not the callback has answered — finds the gate closed and
2802
+ // stops short of the ServerHello. A synchronous callback lowers it
2803
+ // again before that line is reached and nothing is deferred at all.
2804
+ context.sni_pending = true;
2805
+
2806
+ let sniResolved = false;
2807
+ // True only while the call below is still on the stack.
2808
+ let sniSync = true;
2809
+
1331
2810
  context.SNICallback(message.sni || null, function (err, creds) {
2811
+ // A lookup that times out and then answers late would otherwise
2812
+ // re-enter the handshake a second time.
2813
+ if (sniResolved) return;
2814
+ sniResolved = true;
2815
+ context.sni_pending = false;
2816
+
1332
2817
  if (!err && creds) {
1333
- // Direct assignment, not set_context: we are deliberately seeding
1334
- // inputs ahead of the single set_context below, which is what
1335
- // actually runs the loop. Going through set_context here would run
1336
- // it twice, once with a half-populated hello.
2818
+ // Direct assignment, not set_context: these are inputs seeded
2819
+ // ahead of the loop, not a reason to run it.
1337
2820
  context.local_cert_chain = creds.certificateChain;
1338
2821
  context.cert_private_key = creds.privateKey;
2822
+ context.local_cert_alternates = creds.alternates || null;
2823
+
2824
+ // Whether to run the loop depends on WHEN this fired:
2825
+ //
2826
+ // Synchronously — the set_context that processes this hello has
2827
+ // not run yet. It will, in a moment, carrying the certificate
2828
+ // with it. Running the loop here as well would process the same
2829
+ // hello twice.
2830
+ //
2831
+ // Asynchronously — that set_context has already run and stopped
2832
+ // at the closed gate. Nothing else will run the loop, so it has
2833
+ // to be nudged.
2834
+ if (!sniSync) set_context({ run_loop: true });
2835
+ return;
1339
2836
  }
2837
+
2838
+ // No certificate for this name. Refusing is the only honest answer:
2839
+ // leaving the gate raised means a handshake that never completes and
2840
+ // never fails either — a timeout for the client and silence in the
2841
+ // log. unrecognized_name (112) is what RFC 6066 §3 defines for it.
2842
+ fatalAlert(112, 'No certificate for server name ' +
2843
+ JSON.stringify(message.sni || '') +
2844
+ (err ? ': ' + (err.message || err) : ''));
1340
2845
  });
2846
+
2847
+ sniSync = false;
1341
2848
  }
1342
2849
 
1343
2850
  set_context({
@@ -1393,10 +2900,13 @@ function TLSSession(options){
1393
2900
 
1394
2901
  // 1) Try ticket-based resumption (RFC 5077) — stateless, preferred over session_id
1395
2902
  if (message.session_ticket && message.session_ticket.length > 0 && context.ticketKeys && context.sessionTickets) {
2903
+ context.ticket_offered = true;
1396
2904
  let state = decrypt_session_blob(message.session_ticket, context.ticketKeys);
1397
- if (state && state.v === 12 && state.master_secret) {
2905
+ if (state && state.v === 12 && state.master_secret && !ticket_expired(state) && ticket_context_ok(state) && same_server_name(state.sni, message.sni) && ticket_peer_acceptable(state)) {
1398
2906
  // Honor ticket-based resumption: we'll proceed as abbreviated handshake once TLS 1.2 is selected.
1399
2907
  context.tls12_resume_state = state;
2908
+ restore_ticket_peer(state);
2909
+ context.resumed_ticket_data = state.app != null ? state.app : null;
1400
2910
  context.tls12_session_ticket_offered = message.session_ticket;
1401
2911
  }
1402
2912
  }
@@ -1425,7 +2935,11 @@ function TLSSession(options){
1425
2935
  } else if (typeof sessionData === 'object' && sessionData.master_secret) {
1426
2936
  state = sessionData;
1427
2937
  }
1428
- if (state && state.v === 12 && state.master_secret) {
2938
+ if (state && state.v === 12 && state.master_secret && !ticket_expired(state) && ticket_context_ok(state) && same_server_name(state.sni, message.sni) && ticket_peer_acceptable(state)) {
2939
+ // Directly, not through set_context: it acts only on the fields
2940
+ // it knows, and drops anything else without a word.
2941
+ context.resumed_ticket_data = state.app != null ? state.app : null;
2942
+ restore_ticket_peer(state);
1429
2943
  set_context({
1430
2944
  tls12_resume_state: state,
1431
2945
  });
@@ -1483,6 +2997,7 @@ function TLSSession(options){
1483
2997
  if (abbreviatedDetected) {
1484
2998
  context.tls12_abbreviated = true;
1485
2999
  context.isResumed = true;
3000
+ restore_client_session_peer(context.tls12_client_session);
1486
3001
  // Load master_secret and EMS flag from saved session
1487
3002
  set_context({
1488
3003
  base_secret: context.tls12_client_session.master_secret,
@@ -1503,6 +3018,7 @@ function TLSSession(options){
1503
3018
 
1504
3019
  }else if(message.type=='client_key_exchange' || message.type=='server_key_exchange'){
1505
3020
 
3021
+ if (message.type === 'server_key_exchange') context.ske_received = true; // CertificateStatus comes before it
1506
3022
  pushTranscript(data);
1507
3023
 
1508
3024
  // TLS 1.2 ECDHE: the ServerKeyExchange is signed over
@@ -1552,7 +3068,7 @@ function TLSSession(options){
1552
3068
  context.peerSignatureScheme = message.sig_alg;
1553
3069
  }
1554
3070
 
1555
- if ([0xC02F,0xC02B,0xC030,0xC02C,0xC013,0xC014,0xC009,0xC00A].includes(context.selected_cipher_suite)==true) {//ECDHE
3071
+ if (is_ecdhe_suite(context.selected_cipher_suite)) {//ECDHE
1556
3072
 
1557
3073
  // ServerKeyExchange carries the group; ClientKeyExchange does not (server already chose it)
1558
3074
  let kex_group = message.group || context.selected_group;
@@ -1586,6 +3102,7 @@ function TLSSession(options){
1586
3102
 
1587
3103
  }else if(message.type=='server_hello_done'){
1588
3104
 
3105
+ context.shd_received = true; // CertificateStatus comes before it
1589
3106
  pushTranscript(data);
1590
3107
 
1591
3108
 
@@ -1627,8 +3144,42 @@ function TLSSession(options){
1627
3144
  }
1628
3145
  }
1629
3146
 
3147
+ // ECH retry_configs (RFC 9849 §6.1.6): kept when ECH was rejected — a
3148
+ // well-formed ECHConfigList, or decode_error. After an acceptance the
3149
+ // server has no reason to send them (§6.1.4: unsupported_extension).
3150
+ // To a GREASE client they mean nothing (§6.2).
3151
+ if (!context.isServer && context.ech.outer_ch1) {
3152
+ let eeAll = Array.isArray(message.extensions) ? message.extensions : [];
3153
+ for (let ri = 0; ri < eeAll.length; ri++) {
3154
+ if (!eeAll[ri] || eeAll[ri].type !== EXT_ECH) continue;
3155
+ if (context.ech.status === 'accepted') {
3156
+ fatalAlert(wire.TLS_ALERT.UNSUPPORTED_EXTENSION, 'ECH retry_configs after ECH was accepted');
3157
+ return;
3158
+ }
3159
+ try { parse_ech_config_list(eeAll[ri].data); }
3160
+ catch (e) { fatalAlert(wire.TLS_ALERT.DECODE_ERROR, 'ECH retry_configs malformed: ' + e.message); return; }
3161
+ context.ech.retry_configs = new Uint8Array(eeAll[ri].data);
3162
+ }
3163
+ }
3164
+
1630
3165
  pushTranscript(data);
1631
3166
 
3167
+ // 0-RTT: early_data in EncryptedExtensions is the server taking it —
3168
+ // with the ALPN the ticket was made with, or the handshake is broken
3169
+ // (RFC 8446 §4.2.10: the client MUST abort with illegal_parameter).
3170
+ if (!context.isServer && context.early.offered) {
3171
+ let edIn = false, eeList = Array.isArray(message.extensions) ? message.extensions : [];
3172
+ for (let ei2 = 0; ei2 < eeList.length; ei2++) if (eeList[ei2] && eeList[ei2].type === wire.TLS_EXT.EARLY_DATA) edIn = true;
3173
+ if (edIn) {
3174
+ let eeAlpn = message.alpn != null ? (Array.isArray(message.alpn) ? message.alpn[0] : message.alpn) : null;
3175
+ if ((eeAlpn || null) !== (context.early.alpn || null)) {
3176
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'early data accepted with another ALPN than the ticket\'s');
3177
+ return;
3178
+ }
3179
+ }
3180
+ early_client_result(edIn);
3181
+ }
3182
+
1632
3183
  // TLS 1.3: most server extensions arrive HERE, not in the
1633
3184
  // ServerHello (RFC 8446 §4.3.1) — including application-protocol
1634
3185
  // ones like use_srtp (RFC 8842 requires it in EncryptedExtensions).
@@ -1649,9 +3200,54 @@ function TLSSession(options){
1649
3200
  context.remote_extensions = _mergedExts;
1650
3201
  }
1651
3202
 
1652
- set_context({
1653
- remote_supported_groups: message.supported_groups || [],
1654
- });
3203
+ // The server's ALPN choice lives HERE in TLS 1.3, not in the ServerHello:
3204
+ // RFC 8446 §4.3.1 moved every non-key-establishment extension into
3205
+ // EncryptedExtensions, and RFC 7301 negotiation is one of them.
3206
+ //
3207
+ // Reading only supported_groups left selected_alpn null on the client:
3208
+ // the server picked 'h2', said so, and the client reported
3209
+ // alpnProtocol === false. Nothing failed, so nothing pointed here — the
3210
+ // connection simply behaved as though ALPN had never been negotiated,
3211
+ // which for HTTP/2 means it cannot be used at all (RFC 9113 §3.2).
3212
+ let eeUpdate = { remote_supported_groups: message.supported_groups || [] };
3213
+ if (message.alpn !== undefined && message.alpn !== null) {
3214
+ // The extension carries a list, but a server names exactly one
3215
+ // protocol in its answer (RFC 7301 §3.2).
3216
+ let chosenAlpn = Array.isArray(message.alpn) ? message.alpn[0] : message.alpn;
3217
+ if (typeof chosenAlpn === 'string' && chosenAlpn.length > 0) {
3218
+ // A server may only select something we offered; anything else is
3219
+ // unnegotiable and must not be accepted.
3220
+ if (context.local_supported_alpns &&
3221
+ context.local_supported_alpns.indexOf(chosenAlpn) < 0) {
3222
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER,
3223
+ 'Server selected an ALPN protocol that was not offered: ' + chosenAlpn);
3224
+ return;
3225
+ }
3226
+ eeUpdate.selected_alpn = chosenAlpn;
3227
+ }
3228
+ }
3229
+ set_context(eeUpdate);
3230
+
3231
+ }else if(message.type=='certificate_status'){
3232
+
3233
+ // TLS 1.2 (RFC 6066 §8): only after a status_request we sent, and the
3234
+ // server's ServerHello said one follows.
3235
+ let shSaid = false;
3236
+ for (let ri = 0; ri < (context.remote_extensions || []).length; ri++) {
3237
+ if (context.remote_extensions[ri] && context.remote_extensions[ri].type === wire.TLS_EXT.STATUS_REQUEST) shSaid = true;
3238
+ }
3239
+ // Once, right after the server's Certificate (before ServerKeyExchange
3240
+ // and ServerHelloDone), and never in a resumption, which has none.
3241
+ let inPlace = !context.tls12_abbreviated && context.remote_cert_chain !== null &&
3242
+ !context.cert_status_received && !context.ske_received && !context.shd_received;
3243
+ if (!is12() || !context.requestOCSP || !shSaid || !inPlace) {
3244
+ fatalAlert(wire.TLS_ALERT.UNEXPECTED_MESSAGE, 'Unexpected CertificateStatus');
3245
+ return;
3246
+ }
3247
+ context.cert_status_received = true;
3248
+ pushTranscript(data);
3249
+ if (message.status_type === 1) ev.emit('OCSPResponse', message.response);
3250
+ set_context({ run_loop: true });
1655
3251
 
1656
3252
  }else if(message.type=='certificate'){
1657
3253
 
@@ -1659,6 +3255,25 @@ function TLSSession(options){
1659
3255
 
1660
3256
  let entries = message.entries || [];
1661
3257
 
3258
+ // TLS 1.3: a stapled OCSP response is in the leaf's entry (RFC 8446
3259
+ // §4.4.2.1) — read on the client that asked for it.
3260
+ if (!context.isServer && is13() && entries.length > 0 && Array.isArray(entries[0].extensions)) {
3261
+ for (let oi = 0; oi < entries[0].extensions.length; oi++) {
3262
+ let oe = entries[0].extensions[oi];
3263
+ if (oe && oe.type === wire.TLS_EXT.STATUS_REQUEST) {
3264
+ // Not asked for: RFC 8446 §4.2 — unsupported_extension.
3265
+ if (!context.requestOCSP) {
3266
+ fatalAlert(wire.TLS_ALERT.UNSUPPORTED_EXTENSION, 'status_request in a Certificate the client did not ask for');
3267
+ return;
3268
+ }
3269
+ let st;
3270
+ try { st = wire.parse_certificate_status(oe.data); }
3271
+ catch (e) { fatalAlert(wire.TLS_ALERT.DECODE_ERROR, 'Bad CertificateStatus in Certificate: ' + e.message); return; }
3272
+ if (st.status_type === 1) ev.emit('OCSPResponse', st.response);
3273
+ }
3274
+ }
3275
+ }
3276
+
1662
3277
  // RFC 8446 §4.4.2.4 / RFC 5246 §7.4.6.
1663
3278
  // - From a SERVER an empty certificate_list is always illegal: the
1664
3279
  // server has nothing else to authenticate with, so this is fatal here
@@ -1774,6 +3389,12 @@ function TLSSession(options){
1774
3389
 
1775
3390
  }else if(message.type=='finished'){
1776
3391
 
3392
+ // Early data taken over TCP: its EndOfEarlyData comes first (§4.5).
3393
+ if (context.isServer && context.endOfEarlyData && context.early.status === 'accepted' && !context.early.eoed) {
3394
+ fatalAlert(wire.TLS_ALERT.UNEXPECTED_MESSAGE, 'Finished before EndOfEarlyData');
3395
+ return;
3396
+ }
3397
+
1777
3398
  // ── Order enforcement: Finished may not follow a bare Certificate ──
1778
3399
  //
1779
3400
  // RFC 8446 §4.4.3: a peer that sends a non-empty Certificate MUST follow
@@ -1821,6 +3442,14 @@ function TLSSession(options){
1821
3442
  'derived psk:', hexPreview(psk, 8));
1822
3443
 
1823
3444
  // Encode opaque client-side session Buffer (JSON — user is responsible for secure storage)
3445
+ // early_data in the ticket: 0-RTT allowed, up to this many bytes.
3446
+ let maxEarly = 0, nstExts = Array.isArray(message.extensions) ? message.extensions : [];
3447
+ for (let ni = 0; ni < nstExts.length; ni++) {
3448
+ let ne = nstExts[ni];
3449
+ if (ne && ne.type === wire.TLS_EXT.EARLY_DATA && ne.data && ne.data.length === 4) {
3450
+ maxEarly = ((ne.data[0] << 24) | (ne.data[1] << 16) | (ne.data[2] << 8) | ne.data[3]) >>> 0;
3451
+ }
3452
+ }
1824
3453
  let session_blob = encode_client_session({
1825
3454
  v: 13, // blob kind: TLS 1.3
1826
3455
  version: context.selected_version,
@@ -1832,6 +3461,9 @@ function TLSSession(options){
1832
3461
  sni: context.local_sni || null,
1833
3462
  alpn: context.selected_alpn || null,
1834
3463
  created: Date.now(),
3464
+ max_early_data: maxEarly || undefined,
3465
+ qa: context.sessionAppData != null ? context.sessionAppData : undefined,
3466
+ ...client_session_peer_fields(),
1835
3467
  });
1836
3468
 
1837
3469
  ev.emit('session', session_blob);
@@ -1856,6 +3488,7 @@ function TLSSession(options){
1856
3488
  sni: context.local_sni || null,
1857
3489
  alpn: context.selected_alpn || null,
1858
3490
  created: Date.now(),
3491
+ ...client_session_peer_fields(),
1859
3492
  });
1860
3493
 
1861
3494
  ev.emit('session', session_blob);
@@ -1863,19 +3496,32 @@ function TLSSession(options){
1863
3496
  }
1864
3497
  }
1865
3498
 
3499
+ }else if(message.type=='end_of_early_data'){
3500
+
3501
+ // RFC 8446 §4.5: after the early data the server took, before the
3502
+ // client's second flight — and only then.
3503
+ if (!context.isServer || !context.endOfEarlyData || context.early.status !== 'accepted' ||
3504
+ context.early.eoed || context.remote_finished !== null || context.state === 'connected') {
3505
+ fatalAlert(wire.TLS_ALERT.UNEXPECTED_MESSAGE, 'Unexpected EndOfEarlyData');
3506
+ return;
3507
+ }
3508
+ pushTranscript(data);
3509
+ context.early.eoed = true;
3510
+ ev.emit('endOfEarlyData');
3511
+
1866
3512
  }else if(message.type=='key_update'){
1867
3513
 
1868
3514
  // Peer is updating their traffic secret (we update our read key)
1869
3515
  if(context.state==='connected' && (is13())){
1870
3516
  let hashName = negotiated_hash();
1871
3517
  let hashLen = getHashLen(hashName);
1872
- let newRemoteSecret = hkdf_expand_label(hashName, context.remote_app_traffic_secret, 'traffic upd', new Uint8Array(0, label_prefix()), hashLen);
3518
+ let newRemoteSecret = hkdf_expand_label(hashName, context.remote_app_traffic_secret, 'traffic upd', new Uint8Array(0), hashLen, label_prefix());
1873
3519
  context.remote_app_traffic_secret = newRemoteSecret;
1874
3520
  ev.emit('keyUpdate', { direction: 'receive', secret: newRemoteSecret });
1875
3521
 
1876
3522
  // If peer requested us to update too
1877
3523
  if(message.request_update === 1){
1878
- let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0, label_prefix()), hashLen);
3524
+ let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0), hashLen, label_prefix());
1879
3525
  context.local_app_traffic_secret = newLocalSecret;
1880
3526
 
1881
3527
  // Send our KeyUpdate (not requesting back)
@@ -1929,6 +3575,14 @@ function TLSSession(options){
1929
3575
  if (options && typeof options === 'object'){
1930
3576
 
1931
3577
 
3578
+ // A bare nudge: carries no state, exists only to set has_changed so the
3579
+ // loop runs again. Used where an input was assigned directly and there
3580
+ // is nothing else to carry it in: an SNI callback or an OCSPRequest
3581
+ // answering asynchronously, a CertificateStatus read.
3582
+ if('run_loop' in options){
3583
+ has_changed=true;
3584
+ }
3585
+
1932
3586
  if('local_supported_versions' in options){
1933
3587
  if(arraysEqual(options.local_supported_versions,context.local_supported_versions)==false){
1934
3588
  context.local_supported_versions=options.local_supported_versions;
@@ -2402,6 +4056,39 @@ function TLSSession(options){
2402
4056
  if('selected_version' in params_to_set==false || params_to_set.selected_version==null){
2403
4057
  }
2404
4058
 
4059
+ // TLS_FALLBACK_SCSV (RFC 7507). A client that retried with a lower
4060
+ // version after a failed attempt says so with the 0x5600 value in its
4061
+ // cipher suites. If we support something higher than the highest it
4062
+ // now offers, the retry was caused by an attacker breaking the first
4063
+ // attempt, not by us: refuse with inappropriate_fallback rather than
4064
+ // accept the downgrade. (TLS 1.3's own protection is the DOWNGRD
4065
+ // sentinel in our random; this covers clients that fall back.)
4066
+ //
4067
+ // Checked whether or not a version in common was found. A client that
4068
+ // fell back below anything we speak (TLS 1.1, 1.0) got protocol_version
4069
+ // (70) from the check further down — which RFC 7507 allows, but it
4070
+ // hides the one thing the client asked about: this was a fallback, and
4071
+ // we support better. Scanners test exactly that case and read 70 as
4072
+ // "SCSV not supported". inappropriate_fallback (86) comes first.
4073
+ if (context.isServer &&
4074
+ (context.remote_supported_cipher_suites || []).indexOf(0x5600) >= 0) {
4075
+ let ourMax = 0, theirMax = 0;
4076
+ for (let i = 0; i < context.local_supported_versions.length; i++) {
4077
+ let v = context.local_supported_versions[i] | 0;
4078
+ if (v >= 0x0300 && v < 0x0400 && v > ourMax) ourMax = v; // TLS, not DTLS
4079
+ }
4080
+ for (let i = 0; i < context.remote_supported_versions.length; i++) {
4081
+ let v = context.remote_supported_versions[i] | 0;
4082
+ if (v >= 0x0300 && v < 0x0400 && v > theirMax) theirMax = v;
4083
+ }
4084
+ if (ourMax && theirMax && ourMax > theirMax) {
4085
+ fatalAlert(wire.TLS_ALERT.INAPPROPRIATE_FALLBACK,
4086
+ 'TLS_FALLBACK_SCSV: the client fell back to 0x' + theirMax.toString(16) +
4087
+ ', and 0x' + ourMax.toString(16) + ' is supported here');
4088
+ return;
4089
+ }
4090
+ }
4091
+
2405
4092
  // TLS 1.2: clear key_share groups from ClientHello.
2406
4093
  // key_share is a TLS 1.3 extension; in TLS 1.2, keys come from CKE/SKE.
2407
4094
  // Without this, the server would compute the shared secret too early
@@ -2413,6 +4100,35 @@ function TLSSession(options){
2413
4100
  }
2414
4101
  }
2415
4102
 
4103
+ //select certificate... (several key/certificate pairs)
4104
+ //
4105
+ // The one the client asks for: the first scheme in ITS
4106
+ // signature_algorithms (its order is its preference, RFC 8446 §4.2.3)
4107
+ // that one of our keys can sign with at this version — an ML-DSA
4108
+ // certificate for a client that puts ML-DSA first, the ECDSA or RSA one
4109
+ // for a browser, which offers no ML-DSA. None: the default pair, as
4110
+ // with one. Decided once the version and the server name's certificate
4111
+ // are known, before the cipher suite (which, in TLS 1.2, depends on
4112
+ // the key's type).
4113
+ if (context.isServer && !context.cert_chosen && context.local_cert_alternates && context.local_cert_alternates.length > 0 &&
4114
+ !context.sni_pending && pending('selected_version') != null && context.cert_private_key) {
4115
+ context.cert_chosen = true;
4116
+ let cv = pending('selected_version');
4117
+ let sv = (cv === wire.TLS_VERSION.TLS1_3 || cv === wire.DTLS_VERSION.DTLS1_3) ? wire.TLS_VERSION.TLS1_3 : wire.TLS_VERSION.TLS1_2;
4118
+ let cands = [{ certificateChain: context.local_cert_chain, privateKey: context.cert_private_key }].concat(context.local_cert_alternates);
4119
+ let peerAlgs = context.remote_supported_signature_algorithms || [];
4120
+ let chosen = null;
4121
+ for (let i = 0; i < peerAlgs.length && chosen === null; i++) {
4122
+ for (let j = 0; j < cands.length; j++) {
4123
+ if (pick_scheme(sv, private_key_object(cands[j].privateKey), [peerAlgs[i]], context.local_supported_signature_algorithms) !== null) { chosen = cands[j]; break; }
4124
+ }
4125
+ }
4126
+ if (chosen !== null && chosen !== cands[0]) {
4127
+ context.local_cert_chain = chosen.certificateChain;
4128
+ context.cert_private_key = chosen.privateKey;
4129
+ }
4130
+ }
4131
+
2416
4132
  //select selected_cipher...
2417
4133
  if (context.selected_cipher_suite == null && context.local_supported_cipher_suites.length > 0 && context.remote_supported_cipher_suites.length > 0) {
2418
4134
 
@@ -2521,8 +4237,19 @@ function TLSSession(options){
2521
4237
  return true;
2522
4238
  };
2523
4239
 
2524
- for (let i2 = 0; i2 < context.local_supported_cipher_suites.length; i2++) {
2525
- let cs = context.local_supported_cipher_suites[i2] | 0;
4240
+ // Whose order: the client's, as node:tls (OpenSSL) chooses by
4241
+ // default — honorCipherOrder: true for ours. It was always ours: a
4242
+ // client that put AES-256 first (CNSA 2.0 requires it) was given
4243
+ // AES-128 because our list had it first. Browsers list AES-128
4244
+ // first, and get it either way.
4245
+ let suiteOrder = context.local_supported_cipher_suites;
4246
+ if (context.isServer && !context.honor_cipher_order) {
4247
+ suiteOrder = context.remote_supported_cipher_suites.filter(function (c) {
4248
+ return context.local_supported_cipher_suites.indexOf(c | 0) >= 0;
4249
+ });
4250
+ }
4251
+ for (let i2 = 0; i2 < suiteOrder.length; i2++) {
4252
+ let cs = suiteOrder[i2] | 0;
2526
4253
  if (!suiteAuthOk(cs)) continue;
2527
4254
  // A suite belongs to exactly one protocol generation (RFC 8446
2528
4255
  // §B.4). Selecting a 1.3 suite while negotiating 1.2 — which a
@@ -2577,17 +4304,68 @@ function TLSSession(options){
2577
4304
  }
2578
4305
 
2579
4306
  //select alpn...
2580
- if (context.selected_alpn == null && context.local_supported_alpns && context.remote_supported_alpns) {
2581
- // iterate local list by preference order
2582
- for (let a = 0; a < context.local_supported_alpns.length; a++) {
2583
- let cand = context.local_supported_alpns[a];
2584
- for (let b = 0; b < context.remote_supported_alpns.length; b++) {
2585
- if (context.remote_supported_alpns[b] === cand) {
2586
- params_to_set['selected_alpn'] = cand;
2587
- break;
4307
+ //
4308
+ // Two ways to choose, matching Node:
4309
+ //
4310
+ // ALPNCallback — synchronous, gets { servername, protocols } and
4311
+ // returns the chosen protocol, or undefined to refuse.
4312
+ // Lets the choice depend on SNI or anything else known
4313
+ // at this point, which a fixed list cannot do.
4314
+ // ALPNProtocols — the static list; server preference order wins.
4315
+ //
4316
+ // RFC 7301 §3.2: if the client offered ALPN and no protocol is agreed,
4317
+ // the server MUST abort with no_application_protocol(120). Falling
4318
+ // through silently — which is what this did before — leaves the client
4319
+ // believing ALPN was simply not supported, and it then speaks whatever
4320
+ // it would have defaulted to. For a server that only serves h2 that is a
4321
+ // protocol confusion, not a graceful degradation.
4322
+ if (context.isServer && context.selected_alpn == null &&
4323
+ Array.isArray(context.remote_supported_alpns) &&
4324
+ context.remote_supported_alpns.length > 0) {
4325
+
4326
+ let chosen = null;
4327
+ let refused = false;
4328
+
4329
+ if (typeof context.ALPNCallback === 'function') {
4330
+ let picked;
4331
+ try {
4332
+ picked = context.ALPNCallback({
4333
+ servername: context.remote_sni || null,
4334
+ protocols: context.remote_supported_alpns.slice()
4335
+ });
4336
+ } catch (e) {
4337
+ // A throwing selector refuses. Treating it as "no preference"
4338
+ // would hand the peer a protocol the application rejected.
4339
+ picked = undefined;
4340
+ }
4341
+ if (typeof picked === 'string' && picked.length > 0) {
4342
+ // Only a protocol the client actually offered may be selected
4343
+ // (RFC 7301 §3.2) — echoing anything else is unnegotiable.
4344
+ if (context.remote_supported_alpns.indexOf(picked) >= 0) chosen = picked;
4345
+ else refused = true;
4346
+ } else {
4347
+ refused = true;
4348
+ }
4349
+
4350
+ } else if (context.local_supported_alpns && context.local_supported_alpns.length > 0) {
4351
+ for (let a = 0; a < context.local_supported_alpns.length && chosen === null; a++) {
4352
+ let cand = context.local_supported_alpns[a];
4353
+ for (let b = 0; b < context.remote_supported_alpns.length; b++) {
4354
+ if (context.remote_supported_alpns[b] === cand) { chosen = cand; break; }
2588
4355
  }
2589
4356
  }
2590
- if ('selected_alpn' in params_to_set==true && params_to_set.selected_alpn !== null) break;
4357
+ if (chosen === null) refused = true;
4358
+ }
4359
+ // No callback and no configured list at all means this server does not
4360
+ // do ALPN. That is not a failed negotiation, so no alert: stay silent
4361
+ // and let the client fall back, as a pre-ALPN server would.
4362
+
4363
+ if (chosen !== null) {
4364
+ params_to_set['selected_alpn'] = chosen;
4365
+ } else if (refused) {
4366
+ fatalAlert(wire.TLS_ALERT.NO_APPLICATION_PROTOCOL,
4367
+ 'no overlap between offered ALPN protocols and this server');
4368
+ return;
2591
4369
  }
2592
4370
  }
2593
4371
 
@@ -2605,9 +4383,59 @@ function TLSSession(options){
2605
4383
  //select group...
2606
4384
  if (context.selected_group == null){
2607
4385
  if(context.local_supported_groups.length > 0 && context.remote_supported_groups.length > 0) {
2608
- for (let i = 0; i < context.local_supported_groups.length; i++) {
4386
+ // Two passes over our preference order. First: a group the client
4387
+ // already sent a key share for — the handshake goes on in one round
4388
+ // trip. Only if there is none: the first group both sides support,
4389
+ // for which a HelloRetryRequest will ask. Choosing by preference
4390
+ // alone asked a client that listed the post-quantum group, but sent
4391
+ // its share for X25519, to start over — one wasted round trip on
4392
+ // every such connection (BoringSSL chooses the same way).
4393
+ //
4394
+ // A KEM group only in TLS 1.3: TLS 1.2's key exchange is ECDHE in a
4395
+ // ServerKeyExchange, and a KEM does not fit it.
4396
+ let selIs13 = pending('selected_version') === wire.TLS_VERSION.TLS1_3 ||
4397
+ pending('selected_version') === wire.DTLS_VERSION.DTLS1_3;
4398
+
4399
+ // Post-quantum before everything: a client that supports a KEM group
4400
+ // gets it even without a key share for it — the HelloRetryRequest
4401
+ // asks for one. Choosing by key share alone gave X25519 to a client
4402
+ // that listed the hybrid group but sent its share for X25519, so the
4403
+ // connection was not quantum-safe although both sides could have
4404
+ // made it so (and a scanner testing exactly that reported no PQ
4405
+ // support). It costs a round trip only for such clients: browsers
4406
+ // send the hybrid share up front.
4407
+ //
4408
+ // Among the post-quantum groups, the CLIENT's order — the first it
4409
+ // lists that we have, whether or not it sent a key share for it (a
4410
+ // HelloRetryRequest asks for one then). Each is post-quantum, so none
4411
+ // is weaker for us to accept, and the client has the reason for its
4412
+ // order we cannot see: CNSA 2.0's TLS profile has a client list
4413
+ // ML-KEM-1024 first and expects to get it; a browser lists
4414
+ // X25519MLKEM768 first, with its share, and gets it in one round
4415
+ // trip. Taking the first one it sent a share for gave a CNSA client
4416
+ // that sent its share for X25519MLKEM768 (or none) the hybrid
4417
+ // instead — a scanner reported exactly that.
4418
+ for (let i = 0; selIs13 && i < context.remote_supported_groups.length; i++) {
4419
+ let g = context.remote_supported_groups[i];
4420
+ if (!is_kem_group(g) || !is_supported_group(g) || context.local_supported_groups.indexOf(g) < 0) continue;
4421
+ params_to_set['selected_group'] = g;
4422
+ break;
4423
+ }
4424
+
4425
+ for (let i = 0; !('selected_group' in params_to_set) && i < context.local_supported_groups.length; i++) {
2609
4426
  let g = context.local_supported_groups[i];
2610
4427
  if (context.remote_supported_groups.indexOf(g) < 0) continue;
4428
+ if (!is_supported_group(g)) continue;
4429
+ if (is_kem_group(g) && !selIs13) continue;
4430
+ let rk = context.remote_key_groups[g];
4431
+ if (!rk || !rk.public_key) continue;
4432
+ params_to_set['selected_group'] = g;
4433
+ break;
4434
+ }
4435
+ for (let i = 0; !('selected_group' in params_to_set) && i < context.local_supported_groups.length; i++) {
4436
+ let g = context.local_supported_groups[i];
4437
+ if (is_kem_group(g) && !selIs13) continue;
4438
+ if (context.remote_supported_groups.indexOf(g) < 0) continue;
2611
4439
 
2612
4440
  // Mutual support is not enough: we must be able to actually PRODUCE
2613
4441
  // a key share for the group. ecdh.js is the single source of truth
@@ -2700,7 +4528,32 @@ function TLSSession(options){
2700
4528
  // Curve-specific keygen lives in ecdh.js (single source of truth for
2701
4529
  // which groups exist and how their keys are formed); the loop only
2702
4530
  // decides WHEN a key is needed.
2703
- if(context.selected_group !== null && context.selected_group in context.local_key_groups==false){
4531
+ if(context.selected_group !== null && context.selected_group in context.local_key_groups==false &&
4532
+ context.isServer && is_kem_group(context.selected_group)){
4533
+
4534
+ // A KEM group, server side: there is no key pair of our own to make.
4535
+ // Our share is the ciphertext encapsulated to the client's key, and it
4536
+ // comes out of the same step as the secret — so both are set here,
4537
+ // and the ECDHE step below (which needs a private key of ours) is not
4538
+ // reached. Without the client's share yet, nothing: the
4539
+ // HelloRetryRequest below asks for it.
4540
+ let rk = context.remote_key_groups[context.selected_group];
4541
+ if (rk && rk.public_key) {
4542
+ try {
4543
+ let ex = kem_server_exchange(context.selected_group, rk.public_key);
4544
+ params_to_set['add_local_key_groups'] = [{
4545
+ group: context.selected_group,
4546
+ private_key: new Uint8Array(0),
4547
+ public_key: ex.public_key
4548
+ }];
4549
+ params_to_set['ecdhe_shared_secret'] = ex.shared_secret;
4550
+ } catch (e) {
4551
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'Invalid peer key share for group 0x' + context.selected_group.toString(16) + ': ' + e.message);
4552
+ return;
4553
+ }
4554
+ }
4555
+
4556
+ } else if(context.selected_group !== null && context.selected_group in context.local_key_groups==false){
2704
4557
 
2705
4558
  let kp = ecdh_generate_keypair(context.selected_group);
2706
4559
  if (kp) {
@@ -2793,17 +4646,45 @@ function TLSSession(options){
2793
4646
  // Build and send HRR (it's a ServerHello with magic random)
2794
4647
  let hrr_body = wire.build_hello_retry_request({
2795
4648
  cipher_suite: hrrCipherNow,
2796
- selected_version: context.selected_version,
4649
+ // The version chosen in THIS pass (hrrVersionNow, above): it is
4650
+ // not in `context` until the pass commits. context.selected_version
4651
+ // was still null here, and the builder's fallback is TLS 1.3 —
4652
+ // right for TLS by accident, wrong for DTLS: the retry carried
4653
+ // 0x0304 where DTLS 1.3 is 0xfefc (RFC 9147), and a client that
4654
+ // checks (this one does, RFC 8446 §4.1.4) refused the ServerHello
4655
+ // that followed.
4656
+ selected_version: hrrVersionNow,
2797
4657
  selected_group: hrrGroupNow,
2798
4658
  session_id: context.remote_session_id,
4659
+ // ECH (RFC 9849 §7.2.1): accepted → 8 bytes confirming it,
4660
+ // computed below over this very message; rejected → 8 random
4661
+ // bytes, so a retry looks the same either way (§7.1, §10.10.4).
4662
+ // Last in the message, so they are its last 8 bytes.
4663
+ other_exts: context.ech.status ? [{ type: EXT_ECH, data: context.ech.status === 'accepted'
4664
+ ? new Uint8Array(8) : new Uint8Array(crypto.randomBytes(8)) }] : undefined,
2799
4665
  });
2800
4666
  let hrr_data = wire.build_message(wire.TLS_MESSAGE_TYPE.SERVER_HELLO, hrr_body);
4667
+ if (context.ech.status === 'accepted') {
4668
+ let th = getHashFn(hashName)(concatUint8Arrays(context.transcript.concat([hrr_data])));
4669
+ hrr_data.set(accept_confirmation(hashName, context.ech.inner_random, th, true, label_prefix_for(hrrVersionNow)), hrr_data.length - 8);
4670
+ }
2801
4671
  pushTranscript(hrr_data);
2802
4672
 
2803
4673
  ev.emit('message', 0, context.message_sent_seq, 'hello_retry_request', hrr_data);
2804
4674
  context.message_sent_seq++;
2805
4675
 
2806
- // Reset for second ClientHello
4676
+ // Reset for second ClientHello. The PSK too: it is decided again
4677
+ // from the second hello, its binder over this retry (RFC 8446
4678
+ // §4.2.11.2). It was kept from the first: a second hello with no
4679
+ // PSK, or another, still resumed with the first's — accepted on a
4680
+ // binder that never covered the hello it answered.
4681
+ context.psk_accepted = false;
4682
+ context.isResumed = false;
4683
+ context.psk_offered = null;
4684
+ context.resumed_ticket_data = null;
4685
+ context.resumed_ticket_state = null;
4686
+ context.early.candidate = null;
4687
+ if (context.resumed_peer_chain) { context.resumed_peer_chain = null; context.peerAuthorized = false; context.authorizationError = null; }
2807
4688
  context.remote_random = null;
2808
4689
  context.remote_extensions = [];
2809
4690
  context.remote_supported_versions = [];
@@ -2815,7 +4696,21 @@ function TLSSession(options){
2815
4696
 
2816
4697
  let can_send_hello=false;
2817
4698
 
2818
- if(context.hello_sent==false){
4699
+ // Nothing goes out while the certificate is still being looked up.
4700
+ //
4701
+ // The ServerHello COMMITS this connection to a cipher suite, and which
4702
+ // suites are even possible depends on the key type of the certificate
4703
+ // about to be presented — an ECDSA certificate cannot serve an
4704
+ // ECDHE_RSA suite. Sending it first is not merely early, it is a
4705
+ // decision made without its input.
4706
+ //
4707
+ // What that looked like in practice, with an asynchronous SNICallback:
4708
+ // ServerHello and EncryptedExtensions went out, the certificate
4709
+ // arrived a moment later, and by then nothing was left to run the
4710
+ // reactive loop again — so Certificate was never sent at all. The
4711
+ // handshake simply stopped, with no alert on either side, and the peer
4712
+ // waited until it timed out.
4713
+ if(context.hello_sent==false && !context.sni_pending && !context.early.pending){
2819
4714
 
2820
4715
  if(context.selected_version!==null && context.selected_cipher_suite!==null && context.selected_session_id!==null){
2821
4716
  if((is13())){
@@ -2834,6 +4729,18 @@ function TLSSession(options){
2834
4729
  }
2835
4730
  }
2836
4731
 
4732
+ // The OCSP response (node:tls 'OCSPRequest') is asked for at the
4733
+ // moment the ServerHello could go — version, certificate and the
4734
+ // resumption decision all settled — and the ServerHello waits for it.
4735
+ // Asked any earlier (when the ClientHello's extensions arrive), a
4736
+ // TLS 1.2 resumption not yet decided was taken for a full handshake:
4737
+ // the application was asked, and the ServerHello promised a
4738
+ // CertificateStatus that never came.
4739
+ if (can_send_hello) {
4740
+ maybe_request_ocsp(context.selected_version);
4741
+ if (context.ocsp_pending) can_send_hello = false;
4742
+ }
4743
+
2837
4744
  if(can_send_hello==true){
2838
4745
 
2839
4746
  if(context.local_random==null){
@@ -2919,11 +4826,44 @@ function TLSSession(options){
2919
4826
  { type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) }
2920
4827
  ];
2921
4828
 
4829
+ // status_request, empty: a CertificateStatus follows the
4830
+ // Certificate (RFC 6066 §8). Only when there is one to send.
4831
+ if (context.ocsp_response) {
4832
+ ext_list.push({ type: wire.TLS_EXT.STATUS_REQUEST, data: new Uint8Array(0) });
4833
+ }
4834
+
2922
4835
  // Only echo extended_master_secret if client sent it
2923
4836
  if (context.use_extended_master_secret) {
2924
4837
  ext_list.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
2925
4838
  }
2926
4839
 
4840
+ // ec_point_formats (RFC 8422 §5.2): "included in a ServerHello
4841
+ // message in response to a ClientHello message containing the
4842
+ // Supported Point Formats Extension when negotiating an ECC cipher
4843
+ // suite", listing the formats we parse — uncompressed only, the
4844
+ // one every peer must support. Leaving it out cost nothing with
4845
+ // browsers, but it is a MUST, and scanners flag the server as
4846
+ // non-compliant (NIST SP 800-52) for it.
4847
+ if (is_ecdhe_suite(context.selected_cipher_suite)) {
4848
+ let clientFormats = null;
4849
+ for (let pfi = 0; pfi < context.remote_extensions.length; pfi++) {
4850
+ if (context.remote_extensions[pfi].type === wire.TLS_EXT.EC_POINT_FORMATS) {
4851
+ clientFormats = context.remote_extensions[pfi].value;
4852
+ break;
4853
+ }
4854
+ }
4855
+ if (clientFormats) {
4856
+ // §5.1.2: a client that lists formats without uncompressed,
4857
+ // having offered curves defined there, "MUST" be refused with
4858
+ // illegal_parameter: nothing else can carry its key share.
4859
+ if (clientFormats.indexOf(0) < 0) {
4860
+ fatalAlert(wire.TLS_ALERT.ILLEGAL_PARAMETER, 'ec_point_formats without uncompressed');
4861
+ return;
4862
+ }
4863
+ ext_list.push({ type: 'EC_POINT_FORMATS', value: [0] });
4864
+ }
4865
+ }
4866
+
2927
4867
  if (context.selected_alpn) {
2928
4868
  // RFC 7301: ServerHello echoes a single selected protocol
2929
4869
  ext_list.push({ type: 'ALPN', value: [ String(context.selected_alpn) ] });
@@ -2969,6 +4909,8 @@ function TLSSession(options){
2969
4909
  sid_to_send = context.remote_session_id || new Uint8Array(0);
2970
4910
  } else if (context.selected_version === wire.DTLS_VERSION.DTLS1_2) {
2971
4911
  sid_to_send = context.remote_session_id || new Uint8Array(0);
4912
+ } else if (context.sessionIdStore && !context.sessionIdStore()) {
4913
+ sid_to_send = new Uint8Array(0); // nothing would keep it: see sessionIdStore
2972
4914
  } else {
2973
4915
  if (!context.tls12_session_id_for_store) {
2974
4916
  context.tls12_session_id_for_store = new Uint8Array(crypto.randomBytes(32));
@@ -2994,8 +4936,33 @@ function TLSSession(options){
2994
4936
  if(build_message_params!==null){
2995
4937
 
2996
4938
 
4939
+ // ECH accepted (RFC 9849 §7.2): the last 8 bytes of the random
4940
+ // confirm it — zero while the transcript up to this ServerHello
4941
+ // is hashed, then that hash's confirmation.
4942
+ let echConfirm = build_message_params.type === 'server_hello' && is13() && context.ech.status === 'accepted';
4943
+ if (echConfirm) build_message_params.random.fill(0, 24);
4944
+
2997
4945
  let message_data = build_tls_message(build_message_params);
2998
4946
 
4947
+ if (echConfirm) {
4948
+ let echHash = negotiated_hash();
4949
+ let th = getHashFn(echHash)(concatUint8Arrays(context.transcript.concat([message_data])));
4950
+ let conf = accept_confirmation(echHash, context.ech.inner_random, th, false, label_prefix());
4951
+ message_data.set(conf, 4 + 2 + 24); // header, legacy_version, random[24..32]
4952
+ context.local_random.set(conf, 24);
4953
+ }
4954
+
4955
+ // Our own hello counts too: on a server this is the ServerHello a
4956
+ // client would fingerprint us by, so getJA4S() answers in both
4957
+ // roles.
4958
+ if (build_message_params.type === 'server_hello') {
4959
+ context.rawServerHello = message_data;
4960
+ context.parsedServerHello = null;
4961
+ context.fpCache.ja4s = undefined;
4962
+ } else if (build_message_params.type === 'client_hello' && context.rawClientHello === null) {
4963
+ context.rawClientHello = message_data;
4964
+ }
4965
+
2999
4966
  pushTranscript(message_data);
3000
4967
 
3001
4968
  context.hello_sent=true;
@@ -3015,7 +4982,24 @@ function TLSSession(options){
3015
4982
 
3016
4983
 
3017
4984
  //get base_secret
3018
- if (context.base_secret==null && context.selected_cipher_suite !== null){
4985
+ //
4986
+ // Guarded by hello_sent on the server, and this is not a formality.
4987
+ //
4988
+ // RFC 8446 §7.1 derives the handshake traffic secrets over
4989
+ // ClientHello || ServerHello — both of them. Running before our
4990
+ // ServerHello has been pushed to the transcript hashes ONE message
4991
+ // instead of two, so the server derives one set of keys and the client,
4992
+ // which has seen both, derives another. Nothing detects the mismatch:
4993
+ // the handshake completes and the first encrypted record fails with
4994
+ // bad_record_mac, which names the symptom and hides the cause entirely.
4995
+ //
4996
+ // Before an asynchronous SNICallback existed this could not happen —
4997
+ // hello_sent and the key derivation were reached on the same pass, in
4998
+ // that order. Holding the ServerHello back for a certificate lookup
4999
+ // separates them, and only then does the ordering become load-bearing.
5000
+ var _keysReadyToDerive = context.isServer ? (context.hello_sent === true) : true;
5001
+
5002
+ if (context.base_secret==null && context.selected_cipher_suite !== null && _keysReadyToDerive){
3019
5003
  if((is13()) && (context.ecdhe_shared_secret !== null)){
3020
5004
 
3021
5005
  let hashName = negotiated_hash();
@@ -3095,6 +5079,8 @@ function TLSSession(options){
3095
5079
  if(context.selected_alpn!==null){
3096
5080
  extensions.push({ type: 'ALPN', value: [context.selected_alpn] });
3097
5081
  }
5082
+ // 0-RTT accepted (early_decide): early_data, empty (§4.2.10).
5083
+ if (early_decide()) extensions.push({ type: wire.TLS_EXT.EARLY_DATA, data: new Uint8Array(0) });
3098
5084
 
3099
5085
  // server_name acknowledgement (RFC 6066 §3, RFC 8446 §4.4.2).
3100
5086
  // When a client sent SNI and we accepted it, the server confirms by
@@ -3114,6 +5100,21 @@ function TLSSession(options){
3114
5100
  }
3115
5101
 
3116
5102
 
5103
+ // The groups this server takes, in its preference (RFC 8446
5104
+ // §4.2.7): "servers ... MAY send supported_groups" in
5105
+ // EncryptedExtensions, for the client to start its NEXT connection
5106
+ // with a key share for one the server prefers — a HelloRetryRequest
5107
+ // round trip saved, or a stronger group chosen. The client may not
5108
+ // act on it in this handshake. Only what this build can carry out.
5109
+ {
5110
+ let eeGroups = [];
5111
+ for (let gi = 0; gi < context.local_supported_groups.length; gi++) {
5112
+ let g = context.local_supported_groups[gi];
5113
+ if (is_supported_group(g)) eeGroups.push(g);
5114
+ }
5115
+ if (eeGroups.length > 0) extensions.push({ type: 'SUPPORTED_GROUPS', value: eeGroups });
5116
+ }
5117
+
3117
5118
  // DTLS-SRTP answer (RFC 5764 §4.1). One profile, empty MKI.
3118
5119
  let srtpSel = negotiate_srtp_profile();
3119
5120
  if (srtpSel !== null) {
@@ -3122,6 +5123,13 @@ function TLSSession(options){
3122
5123
  value: { profiles: [srtpSel], mki: new Uint8Array(0) } });
3123
5124
  }
3124
5125
 
5126
+ // ECH offered and not decrypted (RFC 9849 §7.1): the keys to use
5127
+ // instead. The client checks our certificate for the public name
5128
+ // and reconnects with them.
5129
+ if (context.ech.status === 'rejected' && context.ech_server && context.ech_server.retry_config_list) {
5130
+ extensions.push({ type: EXT_ECH, data: context.ech_server.retry_config_list });
5131
+ }
5132
+
3125
5133
  for(let i = 0; i < context.local_extensions.length; i++){
3126
5134
  extensions.push(context.local_extensions[i]);
3127
5135
  }
@@ -3150,6 +5158,8 @@ function TLSSession(options){
3150
5158
  type: 'certificate_request',
3151
5159
  certificate_request_context: new Uint8Array(0),
3152
5160
  signature_algorithms: context.local_supported_signature_algorithms,
5161
+ // The client's certificate may come compressed (RFC 8879).
5162
+ extensions: context.cert_compression.length > 0 ? [{ type: 'COMPRESS_CERTIFICATE', value: context.cert_compression }] : undefined,
3153
5163
  });
3154
5164
  pushTranscript(cr_data);
3155
5165
  context.certificateRequestSent = true;
@@ -3168,15 +5178,18 @@ function TLSSession(options){
3168
5178
 
3169
5179
  if(certDue13 || certDue12){
3170
5180
 
3171
- let message_data = build_tls_message({
3172
- type: 'certificate',
3173
- version: context.selected_version,
3174
- entries: context.local_cert_chain
3175
- });
5181
+ // TLS 1.3: the OCSP response rides the leaf's CertificateEntry
5182
+ // (RFC 8446 §4.4.2.1) — inside the encrypted Certificate.
5183
+ let certEntries = context.local_cert_chain;
5184
+ if (is13() && context.ocsp_response) {
5185
+ certEntries = certEntries.slice();
5186
+ certEntries[0] = Object.assign({}, certEntries[0], {
5187
+ extensions: [{ type: wire.TLS_EXT.STATUS_REQUEST, data: wire.build_certificate_status(context.ocsp_response) }]
5188
+ });
5189
+ }
5190
+ let message_data = certificate_message(certEntries, new Uint8Array(0));
3176
5191
  pushTranscript(message_data);
3177
5192
 
3178
- context.cert_sent=true;
3179
-
3180
5193
  if ((is13())){
3181
5194
  ev.emit('message',1,context.message_sent_seq,'certificate',message_data);
3182
5195
  }else{
@@ -3184,6 +5197,18 @@ function TLSSession(options){
3184
5197
  }
3185
5198
 
3186
5199
  context.message_sent_seq++;
5200
+
5201
+ // TLS 1.2: its own message, right after the Certificate and before
5202
+ // ServerKeyExchange (RFC 6066 §8) — sent here, before cert_sent
5203
+ // lets the key exchange go.
5204
+ if (is12() && context.ocsp_response) {
5205
+ let cs_data = build_tls_message({ type: 'certificate_status', response: context.ocsp_response });
5206
+ pushTranscript(cs_data);
5207
+ ev.emit('message', 0, context.message_sent_seq, 'certificate_status', cs_data);
5208
+ context.message_sent_seq++;
5209
+ }
5210
+
5211
+ context.cert_sent=true;
3187
5212
 
3188
5213
 
3189
5214
  }
@@ -3214,7 +5239,8 @@ function TLSSession(options){
3214
5239
  let selected_scheme = pick_scheme(
3215
5240
  wire.TLS_VERSION.TLS1_3,
3216
5241
  cert_private_key_obj,
3217
- context.remote_supported_signature_algorithms || []
5242
+ context.remote_supported_signature_algorithms || [],
5243
+ context.local_supported_signature_algorithms
3218
5244
  );
3219
5245
 
3220
5246
  let sig_data = null;
@@ -3234,6 +5260,7 @@ function TLSSession(options){
3234
5260
  }
3235
5261
 
3236
5262
  if(sig_data){
5263
+ context.selected_signature_algorithm = selected_scheme; // reported (getNegotiationResult)
3237
5264
 
3238
5265
  let message_data = build_tls_message({
3239
5266
  type: 'certificate_verify',
@@ -3331,7 +5358,7 @@ function TLSSession(options){
3331
5358
  // Pick scheme matching our cert + server's requested algorithms
3332
5359
  let cert_key_obj = local_private_key();
3333
5360
  let reqAlgs = peer_requested_sig_algs();
3334
- let scheme = pick_scheme(wire.TLS_VERSION.TLS1_2, cert_key_obj, reqAlgs);
5361
+ let scheme = pick_scheme(wire.TLS_VERSION.TLS1_2, cert_key_obj, reqAlgs, context.local_supported_signature_algorithms);
3335
5362
  let signature = sign_with_scheme(wire.TLS_VERSION.TLS1_2, scheme, transcript_data, cert_key_obj);
3336
5363
 
3337
5364
  // Build CertificateVerify: scheme(2) + sig_length(2) + sig
@@ -3363,7 +5390,7 @@ function TLSSession(options){
3363
5390
 
3364
5391
  let cert_private_key_obj = local_private_key();
3365
5392
 
3366
- let scheme12 = pick_scheme(wire.TLS_VERSION.TLS1_2, cert_private_key_obj, context.remote_supported_signature_algorithms);
5393
+ let scheme12 = pick_scheme(wire.TLS_VERSION.TLS1_2, cert_private_key_obj, context.remote_supported_signature_algorithms, context.local_supported_signature_algorithms);
3367
5394
 
3368
5395
  let sig_data = sign_with_scheme(wire.TLS_VERSION.TLS1_2, scheme12, tbs_data, cert_private_key_obj);
3369
5396
 
@@ -3477,6 +5504,10 @@ function TLSSession(options){
3477
5504
  sni: context.selected_sni || context.remote_sni || null,
3478
5505
  alpn: context.selected_alpn || null,
3479
5506
  created: Date.now(),
5507
+ lifetime: context.ticketLifetime,
5508
+ app: ticket_app_data(),
5509
+ cx: context.ticketContext || undefined,
5510
+ ...ticket_peer_fields(),
3480
5511
  }, context.ticketKeys);
3481
5512
 
3482
5513
  let nst_data = build_tls_message({
@@ -3508,6 +5539,26 @@ function TLSSession(options){
3508
5539
  }
3509
5540
  }
3510
5541
 
5542
+ // ECH rejected (RFC 9849 §6.1.6): the server's flight has been checked
5543
+ // — for the public_name — and nothing of ours follows it.
5544
+ if (context.isServer === false && context.ech.status === 'rejected' && context.ech.outer_ch1 &&
5545
+ context.remote_finished_ok === true && context.finished_sent === false) {
5546
+ ech_abort_rejected();
5547
+ return;
5548
+ }
5549
+
5550
+ // Client, TLS over TCP, early data taken: EndOfEarlyData under the
5551
+ // early key, first in the second flight (RFC 8446 §4.5) — epoch 3 for
5552
+ // the record layer.
5553
+ if (!context.isServer && is13() && context.endOfEarlyData && context.early.status === 'accepted' &&
5554
+ !context.early.eoed && context.remote_finished_ok === true) {
5555
+ context.early.eoed = true;
5556
+ let eoed = build_tls_message({ type: 'end_of_early_data' });
5557
+ pushTranscript(eoed);
5558
+ ev.emit('message', 3, context.message_sent_seq, 'end_of_early_data', eoed);
5559
+ context.message_sent_seq++;
5560
+ }
5561
+
3511
5562
  //send finished...
3512
5563
  // Client: send Certificate + CertificateVerify before Finished (if server requested)
3513
5564
  if(context.isServer==false && context.certificateRequested && !context.clientCertSent &&
@@ -3516,13 +5567,12 @@ function TLSSession(options){
3516
5567
 
3517
5568
  if(context.clientCert && context.clientKey){
3518
5569
  // Send client certificate
3519
- let certCtx = createSecureContext({ key: context.clientKey, cert: context.clientCert });
3520
- let cert_data = build_tls_message({
3521
- type: 'certificate',
3522
- version: wire.TLS_VERSION.TLS1_3,
3523
- entries: certCtx.certificateChain,
3524
- certificate_request_context: context.certificateRequestContext || new Uint8Array(0),
3525
- });
5570
+ // Only include passphrase when there is one — node:crypto treats an
5571
+ // explicit null as an error rather than as "no passphrase".
5572
+ let certOpts = { key: context.clientKey, cert: context.clientCert };
5573
+ if (context.clientKeyPassphrase) certOpts.passphrase = context.clientKeyPassphrase;
5574
+ let certCtx = createSecureContext(certOpts);
5575
+ let cert_data = certificate_message(certCtx.certificateChain, context.certificateRequestContext || new Uint8Array(0));
3526
5576
  pushTranscript(cert_data);
3527
5577
  ev.emit('message', 1, context.message_sent_seq, 'certificate', cert_data);
3528
5578
  context.message_sent_seq++;
@@ -3542,7 +5592,7 @@ function TLSSession(options){
3542
5592
  });
3543
5593
  let peerSigAlgs = peer_requested_sig_algs();
3544
5594
 
3545
- let scheme = pick_scheme(wire.TLS_VERSION.TLS1_3, clientKeyObj, peerSigAlgs);
5595
+ let scheme = pick_scheme(wire.TLS_VERSION.TLS1_3, clientKeyObj, peerSigAlgs, context.local_supported_signature_algorithms);
3546
5596
  if (scheme === null) {
3547
5597
  fatalAlert(wire.TLS_ALERT.HANDSHAKE_FAILURE, 'No signature scheme shared with server for client certificate');
3548
5598
  return;
@@ -3607,7 +5657,17 @@ function TLSSession(options){
3607
5657
  if(clientFinishedDue || serverCertFinishedDue || serverPskFinishedDue){
3608
5658
 
3609
5659
  let finHashName = negotiated_hash();
3610
- let finished_data=get_handshake_finished_with_hash(finHashName,context.local_handshake_traffic_secret,get_transcript_hash(finHashName, label_prefix()));
5660
+ // THE LABEL PREFIX BELONGS IN THE *FOURTH* ARGUMENT. It was being
5661
+ // passed to get_transcript_hash(), which takes only a hash name and
5662
+ // silently ignored it — so get_handshake_finished_with_hash received
5663
+ // labelPrefix=undefined and derived finished_key with the TLS 1.3
5664
+ // prefix "tls13 " instead of DTLS 1.3's "dtls13" (RFC 9147 §5.9).
5665
+ // Both peers running this library make the same substitution and
5666
+ // their Finished MACs agree, so it is invisible in a self-test and
5667
+ // shows up only against a conforming peer as
5668
+ // "Finished verify_data mismatch" — after every message parsed and
5669
+ // every signature verified.
5670
+ let finished_data=get_handshake_finished_with_hash(finHashName,context.local_handshake_traffic_secret,get_transcript_hash(finHashName), label_prefix());
3611
5671
  context.local_finished_data = finished_data;
3612
5672
 
3613
5673
  let message_data = build_tls_message({
@@ -3750,7 +5810,57 @@ function TLSSession(options){
3750
5810
  if(context.remote_finished !== null){
3751
5811
 
3752
5812
  let remoteFinHashName = negotiated_hash();
3753
- params_to_set['expected_remote_finished']=get_handshake_finished_with_hash(remoteFinHashName,context.remote_handshake_traffic_secret,get_transcript_hash(remoteFinHashName, label_prefix()));
5813
+ // Same fix as the local Finished above: the prefix is the FOURTH
5814
+ // argument, not an argument to get_transcript_hash().
5815
+ params_to_set['expected_remote_finished']=get_handshake_finished_with_hash(remoteFinHashName,context.remote_handshake_traffic_secret,get_transcript_hash(remoteFinHashName), label_prefix());
5816
+
5817
+ // ── DIAGNOSTIC (LEMON_DEBUG=1 or WEBRTC_DEBUG=1) ──────────────
5818
+ // verify_data is a MAC over three inputs, and a mismatch means
5819
+ // ONE of them differs from what the peer computed. Two peers
5820
+ // running this same library make the SAME choice and agree with
5821
+ // each other, so a self-test can never expose it — only a real
5822
+ // peer (BoringSSL) can. Print all three so the difference is
5823
+ // visible instead of inferred:
5824
+ // • which handshake messages went into the transcript, in
5825
+ // order, with their lengths — a missing or duplicated
5826
+ // message is the usual cause;
5827
+ // • the transcript hash and the label prefix used;
5828
+ // • both verify_data values.
5829
+ try {
5830
+ if (typeof process !== 'undefined' && process.env &&
5831
+ (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1')) {
5832
+ var _hsNames = { 1:'ClientHello', 2:'ServerHello', 4:'NewSessionTicket',
5833
+ 5:'EndOfEarlyData', 8:'EncryptedExtensions', 11:'Certificate',
5834
+ 13:'CertificateRequest', 15:'CertificateVerify',
5835
+ 20:'Finished', 24:'KeyUpdate', 254:'MessageHash' };
5836
+ var _t = context.transcript || [];
5837
+ var _list = [];
5838
+ for (var _ti = 0; _ti < _t.length; _ti++) {
5839
+ var _e = _t[_ti];
5840
+ if (!_e || !_e.length) { _list.push('?'); continue; }
5841
+ _list.push((_hsNames[_e[0]] || ('type' + _e[0])) + '(' + _e.length + ')');
5842
+ }
5843
+ var _hex = function (u8) {
5844
+ if (!u8) return 'null';
5845
+ var s = '';
5846
+ for (var _i = 0; _i < u8.length && _i < 16; _i++) {
5847
+ s += ('0' + u8[_i].toString(16)).slice(-2);
5848
+ }
5849
+ return s + (u8.length > 16 ? '..' : '');
5850
+ };
5851
+ console.log('[lemon-fin] role=' + (context.is_server ? 'server' : 'client') +
5852
+ ' version=0x' + (context.selected_version || 0).toString(16) +
5853
+ ' labelPrefix="' + label_prefix() + '"' +
5854
+ ' hash=' + remoteFinHashName);
5855
+ console.log('[lemon-fin] transcript(' + _t.length + '): ' + _list.join(' → '));
5856
+ console.log('[lemon-fin] transcriptHash=' +
5857
+ _hex(get_transcript_hash(remoteFinHashName, label_prefix())));
5858
+ console.log('[lemon-fin] remoteSecret=' + _hex(context.remote_handshake_traffic_secret));
5859
+ console.log('[lemon-fin] peerSent=' + _hex(context.remote_finished));
5860
+ console.log('[lemon-fin] weExpect=' + _hex(params_to_set['expected_remote_finished']));
5861
+ }
5862
+ } catch (_eDiag) {}
5863
+ // ─────────────────────────────────────────────────────────────
3754
5864
 
3755
5865
  }
3756
5866
 
@@ -3903,8 +6013,13 @@ function TLSSession(options){
3903
6013
 
3904
6014
  context.state='connected';
3905
6015
  context.handshakeEndTime = Date.now();
3906
- ev.emit('secureConnect');
3907
6016
 
6017
+ // The TLS 1.3 ticket goes out BEFORE secureConnect, as OpenSSL (and so
6018
+ // Node) sends it. Emitted after, a server that answered and closed from
6019
+ // its secure handler — a health check, a redirect — closed before the
6020
+ // ticket was written, and its clients never received one; and one that
6021
+ // wrote from the handler put its data ahead of the ticket. Both records
6022
+ // use the application keys, which exist from the server's Finished on.
3908
6023
  // TLS 1.3: compute resumption_master_secret (both client and server need it)
3909
6024
  if ((is13()) && context.tls13_master_secret && !context.resumption_master_secret) {
3910
6025
  let hashName = negotiated_hash();
@@ -3914,73 +6029,19 @@ function TLSSession(options){
3914
6029
  }
3915
6030
 
3916
6031
  // TLS 1.3 server: send NewSessionTicket
6032
+ // Two after a full handshake, one after a resumption — as OpenSSL and
6033
+ // BoringSSL issue them: a client uses each ticket once (RFC 8446 §C.4:
6034
+ // a ticket reused links the connections, and its 0-RTT is refused as
6035
+ // a replay), so a client opening two connections at once — or the
6036
+ // next before this one ends — has one for each.
3917
6037
  if ((is13()) && context.isServer && !context.session_ticket_sent && context.sessionTickets && context.resumption_master_secret) {
3918
6038
  context.session_ticket_sent = true;
3919
-
3920
- let hashName = negotiated_hash();
3921
- let ticket_nonce = new Uint8Array([context.ticket_nonce_counter++]);
3922
- let psk = derive_psk(hashName, context.resumption_master_secret, ticket_nonce, label_prefix());
3923
- let ticket_age_add = crypto.randomBytes(4).readUInt32BE(0);
3924
- let ticket_lifetime = context.ticketLifetime;
3925
-
3926
- dbg('SRV-NST', 'issuing TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
3927
- 'hash:', hashName,
3928
- 'transcript len:', concatUint8Arrays(context.transcript).length);
3929
- dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
3930
- 'age_add:', ticket_age_add,
3931
- 'lifetime:', ticket_lifetime);
3932
- dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
3933
- 'derived psk:', hexPreview(psk, 8));
3934
-
3935
- // Ensure ticketKeys is 48 bytes (key_name + aes_key)
3936
- if (!context.ticketKeys || context.ticketKeys.length !== 48) {
3937
- context.ticketKeys = crypto.randomBytes(48);
3938
- }
3939
-
3940
- // Encrypt session state into opaque ticket (unified format: key_name(16) | IV(12) | CT | Tag(16))
3941
- let ticket = encrypt_session_blob({
3942
- v: 13, // blob kind: TLS 1.3 PSK
3943
- version: context.selected_version,
3944
- cipher: context.selected_cipher_suite,
3945
- psk: psk,
3946
- age_add: ticket_age_add,
3947
- sni: context.selected_sni || context.remote_sni || null,
3948
- alpn: context.selected_alpn || null,
3949
- created: Date.now(),
3950
- }, context.ticketKeys);
3951
-
3952
- let nst_data = wire.build_message(wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET,
3953
- wire.build_new_session_ticket({
3954
- ticket_lifetime: ticket_lifetime,
3955
- ticket_age_add: ticket_age_add,
3956
- ticket_nonce: ticket_nonce,
3957
- ticket: ticket,
3958
- extensions: []
3959
- })
3960
- );
3961
-
3962
- ev.emit('message', 2, context.message_sent_seq, 'new_session_ticket', nst_data);
3963
- context.message_sent_seq++;
3964
-
3965
- // Emit 'session' event on server side too — lets users track when tickets are
3966
- // issued (e.g. for monitoring or metrics). Not part of Node.js API but useful.
3967
- // Emits the same Buffer the client would receive via their 'session' event,
3968
- // so server-side apps could also persist it if they want.
3969
- let server_session_blob = encode_client_session({
3970
- v: 13,
3971
- version: context.selected_version,
3972
- cipher: context.selected_cipher_suite,
3973
- ticket: ticket,
3974
- psk: psk,
3975
- age_add: ticket_age_add,
3976
- lifetime: ticket_lifetime,
3977
- sni: context.selected_sni || context.remote_sni || null,
3978
- alpn: context.selected_alpn || null,
3979
- created: Date.now(),
3980
- });
3981
- ev.emit('session', server_session_blob);
6039
+ issue_ticket_13();
6040
+ if (!context.psk_accepted) issue_ticket_13();
3982
6041
  }
3983
6042
 
6043
+ ev.emit('secureConnect');
6044
+
3984
6045
  // TLS 1.2 server: emit 'newSession' for Session ID-based resumption.
3985
6046
  // Fires whenever we generated a session_id for this connection AND didn't issue
3986
6047
  // a NewSessionTicket (so the client can only resume via Session ID — we need the
@@ -4006,6 +6067,10 @@ function TLSSession(options){
4006
6067
  sni: context.selected_sni || context.remote_sni || null,
4007
6068
  alpn: context.selected_alpn || null,
4008
6069
  created: Date.now(),
6070
+ lifetime: context.ticketLifetime,
6071
+ app: ticket_app_data(),
6072
+ cx: context.ticketContext || undefined,
6073
+ ...ticket_peer_fields(),
4009
6074
  }, context.ticketKeys);
4010
6075
 
4011
6076
  // User stores this; returns it on next handshake via 'resumeSession' callback.
@@ -4039,6 +6104,7 @@ function TLSSession(options){
4039
6104
  sni: context.local_sni || null,
4040
6105
  alpn: context.selected_alpn || null,
4041
6106
  created: Date.now(),
6107
+ ...client_session_peer_fields(),
4042
6108
  });
4043
6109
  ev.emit('session', session_blob);
4044
6110
  }
@@ -4050,6 +6116,75 @@ function TLSSession(options){
4050
6116
  }
4051
6117
  }
4052
6118
 
6119
+ /**
6120
+ * The peer's leaf certificate shaped the way Node's checkServerIdentity
6121
+ * expects: `subject` / `subjectaltname` as OpenSSL-formatted strings, plus
6122
+ * the fingerprints an app needs to pin against.
6123
+ *
6124
+ * Deliberately the same shape TLSSocket#getPeerCertificate() returns, so a
6125
+ * check written against one works against the other.
6126
+ */
6127
+ function peer_cert_for_identity() {
6128
+ let chain = context.remote_cert_chain;
6129
+ if (!chain || chain.length === 0) return null;
6130
+ return certificateObject(chain[0].cert || chain[0]);
6131
+ }
6132
+
6133
+ /**
6134
+ * RFC 6125 identity check, or the caller's replacement for it.
6135
+ *
6136
+ * Node semantics: `checkServerIdentity` REPLACES the built-in check rather
6137
+ * than adding to it — returning undefined accepts, returning an Error
6138
+ * rejects. That is what lets an app pin a fingerprint, or deliberately
6139
+ * accept a name the default rules would refuse.
6140
+ *
6141
+ * The option was documented and exported but never read, so a caller who
6142
+ * passed one got no error and no call: their check silently never ran while
6143
+ * the connection reported itself verified.
6144
+ *
6145
+ * Returns true to continue, false when the caller should stop — the failure
6146
+ * state is already recorded on context by then.
6147
+ */
6148
+ function checkIdentity(x509) {
6149
+ // ECH rejected: the server answered as the config's public_name, and
6150
+ // must prove that name (RFC 9849 §6.1.6), not the one in the inner hello.
6151
+ let expectName = context.ech.verify_name || context.local_sni;
6152
+ if (context.isServer || !expectName) return true;
6153
+
6154
+ if (typeof context.checkServerIdentity === 'function') {
6155
+ let err;
6156
+ try {
6157
+ err = context.checkServerIdentity(expectName, peer_cert_for_identity());
6158
+ } catch (e) {
6159
+ // A throwing check is a failed check. Swallowing it would authorize
6160
+ // the peer on a bug in the caller's code.
6161
+ err = e instanceof Error ? e : new Error(String(e));
6162
+ }
6163
+ if (err) {
6164
+ context.authorizationError = err.message || 'ERR_TLS_CERT_ALTNAME_INVALID';
6165
+ context.peerAuthorized = false;
6166
+ return false;
6167
+ }
6168
+ return true;
6169
+ }
6170
+
6171
+ // checkHost matches DNS names and ignores IP SANs entirely, so a
6172
+ // connection to a literal address whose certificate carries the matching
6173
+ // IP SAN was rejected. RFC 6066 says SNI carries host names only, but this
6174
+ // field is populated from whatever the caller connected to, so an address
6175
+ // does land here. X509Certificate exposes checkIP for exactly this.
6176
+ let host = String(expectName);
6177
+ let ok = is_ip_literal(host)
6178
+ ? !!x509.checkIP(host.replace(/^\[|\]$/g, ''))
6179
+ : !!x509.checkHost(host);
6180
+
6181
+ if (!ok) {
6182
+ context.authorizationError = 'ERR_TLS_CERT_ALTNAME_INVALID';
6183
+ context.peerAuthorized = false;
6184
+ return false;
6185
+ }
6186
+ return true;
6187
+ }
4053
6188
 
4054
6189
  function validatePeerCertificate() {
4055
6190
  if (!context.remote_cert_chain || context.remote_cert_chain.length === 0) {
@@ -4076,35 +6211,64 @@ function TLSSession(options){
4076
6211
  return;
4077
6212
  }
4078
6213
 
4079
- // Check hostname (client-side only, when SNI is set)
4080
- if (!context.isServer && context.local_sni) {
4081
- if (!x509.checkHost(context.local_sni)) {
4082
- context.authorizationError = 'ERR_TLS_CERT_ALTNAME_INVALID';
4083
- context.peerAuthorized = false;
4084
- return;
4085
- }
6214
+
6215
+ // Verify the full chain against the configured trust anchors.
6216
+ //
6217
+ // NOTE the `context.ca == null` branch. Previously the entire check was
6218
+ // wrapped in `if (context.ca)`, so "no CA configured" meant "authorize
6219
+ // everything" — while rejectUnauthorized defaulted to true, which reads
6220
+ // as though verification were on. That is a MITM hole for any plain-TLS
6221
+ // consumer of this library. It is preserved here ONLY because callers
6222
+ // (notably the WebRTC/DTLS path, which authenticates by SDP fingerprint
6223
+ // and sets rejectUnauthorized:false) may rely on it — but it is now
6224
+ // explicit, recorded in authorizationError, and trivial to flip.
6225
+ // See LEMON_TLS_INSECURE_NO_CA below.
6226
+ // Resolve trust anchors: explicit `ca` first, then the platform store.
6227
+ //
6228
+ // This is the fix for the branch below. Previously an absent `ca` meant
6229
+ // "authorize everything" while rejectUnauthorized defaulted to true, so
6230
+ // a plain-TLS caller who configured nothing got a connection that
6231
+ // reported itself verified and was not. Node has always fallen back to
6232
+ // its bundled roots; now so do we.
6233
+ let anchors = context.ca;
6234
+ if ((anchors == null || (Array.isArray(anchors) && anchors.length === 0)) &&
6235
+ context.useDefaultCA && context.defaultCA) {
6236
+ let roots = context.defaultCA();
6237
+ if (roots && roots.length > 0) anchors = roots;
4086
6238
  }
4087
6239
 
4088
- // Verify against CA if provided
4089
- if (context.ca) {
4090
- let cas = Array.isArray(context.ca) ? context.ca : [context.ca];
4091
- let verified = false;
4092
- for (let i = 0; i < cas.length; i++) {
4093
- try {
4094
- let caX509 = new crypto.X509Certificate(cas[i]);
4095
- if (x509.checkIssued(caX509) && x509.verify(caX509.publicKey)) {
4096
- verified = true;
4097
- break;
4098
- }
4099
- } catch(e) { /* try next CA */ }
4100
- }
4101
- if (!verified) {
4102
- context.authorizationError = 'UNABLE_TO_VERIFY_LEAF_SIGNATURE';
4103
- context.peerAuthorized = false;
4104
- return;
4105
- }
6240
+ if (anchors == null ||
6241
+ (Array.isArray(anchors) && anchors.length === 0)) {
6242
+ // The identity check still runs here. It is the only check left, and
6243
+ // skipping it would make "no CA configured" mean "no verification at
6244
+ // all" — including any fingerprint pin the caller installed precisely
6245
+ // because there is no CA to chain to.
6246
+ if (!checkIdentity(x509)) return;
6247
+ context.peerAuthorized = true;
6248
+ context.authorizationError = null;
6249
+ context.certChainUnverified = true; // no trust anchors were available
6250
+ return;
6251
+ }
6252
+
6253
+ let cas = Array.isArray(anchors) ? anchors : [anchors];
6254
+ let chainResult = verify_cert_chain(context.remote_cert_chain, cas, now);
6255
+ if (!chainResult.ok) {
6256
+ context.authorizationError = chainResult.error;
6257
+ context.peerAuthorized = false;
6258
+ return;
4106
6259
  }
4107
6260
 
6261
+ // Identity comes LAST, after the chain verifies.
6262
+ //
6263
+ // Node documents checkServerIdentity as "only called if the certificate
6264
+ // passed all other checks, such as being issued by a trusted CA". The
6265
+ // order is not cosmetic: a hook that only compares a fingerprint and
6266
+ // returns undefined would, if run first, authorize a peer whose chain
6267
+ // never verified — the hook author reasonably assumes the CA check
6268
+ // already happened. Running identity before the chain quietly turns
6269
+ // every such pin into the ONLY check.
6270
+ if (!checkIdentity(x509)) return;
6271
+
4108
6272
  // All checks passed
4109
6273
  context.peerAuthorized = true;
4110
6274
  context.authorizationError = null;
@@ -4243,6 +6407,7 @@ function TLSSession(options){
4243
6407
  for (let gi = 0; gi < context.local_supported_groups.length; gi++) {
4244
6408
  let g = context.local_supported_groups[gi];
4245
6409
  if (!is_supported_group(g)) continue;
6410
+ if (!offers13 && is_kem_group(g)) continue; // not in a 1.2-only hello (see above)
4246
6411
  let kp = ecdh_generate_keypair(g);
4247
6412
  if (!kp) continue;
4248
6413
  initialGroup = g;
@@ -4263,6 +6428,25 @@ function TLSSession(options){
4263
6428
  private_key: initialKp.private_key
4264
6429
  };
4265
6430
 
6431
+ // A post-quantum group first gets a classical share beside it — the
6432
+ // next implemented non-KEM group in the list — as browsers do. A server
6433
+ // without the hybrid group then uses the classical share at once; with
6434
+ // only the hybrid share it would have to ask again (HelloRetryRequest),
6435
+ // a round trip on every connection to a classical server. Each share's
6436
+ // key stays in local_key_groups, so whichever the server picks is there.
6437
+ let extraKeyShares = null;
6438
+ if (is_kem_group(initialGroup)) {
6439
+ for (let gi = 0; gi < context.local_supported_groups.length; gi++) {
6440
+ let g2 = context.local_supported_groups[gi];
6441
+ if (g2 === initialGroup || is_kem_group(g2) || !is_supported_group(g2)) continue;
6442
+ let kp2 = ecdh_generate_keypair(g2);
6443
+ if (!kp2) continue;
6444
+ context.local_key_groups[g2] = { public_key: kp2.public_key, private_key: kp2.private_key };
6445
+ extraKeyShares = [{ group: g2, key_exchange: kp2.public_key }];
6446
+ break;
6447
+ }
6448
+ }
6449
+
4266
6450
  // Honor the configured signature algorithms; the hardcoded list is only
4267
6451
  // the default when none were set (previously the configured list was
4268
6452
  // ignored here — while the HRR CH2 hardcoded its own, so CH1 and CH2
@@ -4290,9 +6474,19 @@ function TLSSession(options){
4290
6474
  let extensions = build_client_hello_extensions({
4291
6475
  keyShareGroup: initialGroup,
4292
6476
  keySharePublic: public_key,
6477
+ extraKeyShares: extraKeyShares,
4293
6478
  cookie: null
4294
6479
  });
4295
6480
 
6481
+ // ECH: this list is the inner hello's; the outer one is made from it
6482
+ // once the inner hello — its PSK binder included — is complete.
6483
+ let echSplit = null;
6484
+ if (ech_on()) {
6485
+ context.ech.inner_random = context.local_random;
6486
+ context.ech.outer_random = new Uint8Array(crypto.randomBytes(32));
6487
+ echSplit = ech_split(extensions);
6488
+ }
6489
+
4296
6490
  // Resumption: check if session was provided (opaque Buffer) — decode to structured data
4297
6491
  let sessionData = null;
4298
6492
  if (options.session) {
@@ -4302,6 +6496,9 @@ function TLSSession(options){
4302
6496
  } else if (typeof options.session === 'object') {
4303
6497
  sessionData = options.session; // legacy: already-structured object
4304
6498
  }
6499
+ // A saved session: only where it may be (client_session_usable). An
6500
+ // external PSK (options.psk) authenticates the server by itself.
6501
+ if (sessionData && !client_session_usable(sessionData)) sessionData = null;
4305
6502
  } else if (options.psk) {
4306
6503
  sessionData = options.psk; // legacy path
4307
6504
  }
@@ -4310,8 +6507,8 @@ function TLSSession(options){
4310
6507
 
4311
6508
  // TLS 1.3 PSK resumption (sessionData contains psk)
4312
6509
  if (sessionData && sessionData.psk && sessionData.ticket && sessionData.cipher) {
4313
- // Add PSK key exchange modes (psk_dhe_ke = 1)
4314
- extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
6510
+ // psk_key_exchange_modes (psk_dhe_ke) is already in the hello: the
6511
+ // assembler adds it to every hello that offers 1.3.
4315
6512
 
4316
6513
  // Save PSK for later verification
4317
6514
  context.psk_offered = {
@@ -4319,6 +6516,8 @@ function TLSSession(options){
4319
6516
  psk: sessionData.psk instanceof Uint8Array ? sessionData.psk : new Uint8Array(sessionData.psk),
4320
6517
  cipher: sessionData.cipher,
4321
6518
  age_add: sessionData.age_add || 0,
6519
+ created: sessionData.created, lifetime: sessionData.lifetime, // the age again, in a second hello
6520
+ session: options.session ? sessionData : null, // the server it was made with (restore_client_session_peer)
4322
6521
  };
4323
6522
 
4324
6523
  // Compute obfuscated ticket age
@@ -4337,8 +6536,41 @@ function TLSSession(options){
4337
6536
  binders: [placeholderBinder]
4338
6537
  }
4339
6538
  };
6539
+ // 0-RTT (RFC 8446 §4.2.10): offered with a ticket that allows it,
6540
+ // when the ALPN it was made with is one offered now (the server must
6541
+ // pick it again). With ECH too (RFC 9849 §6.1): the early data is
6542
+ // the inner hello's — its keys from the inner transcript, which a
6543
+ // server that decrypts it uses — and early_data goes in the outer
6544
+ // hello as well, so a server that does not decrypt it knows to skip
6545
+ // what follows (RFC 8446 §4.2.10). Early data was never offered with
6546
+ // ECH: a client using ECH paid a round trip on every resumption.
6547
+ if (context.earlyData && (sessionData.max_early_data >>> 0) > 0 &&
6548
+ (!sessionData.alpn || (context.local_supported_alpns || []).indexOf(sessionData.alpn) >= 0)) {
6549
+ let edExt = { type: wire.TLS_EXT.EARLY_DATA, data: new Uint8Array(0) };
6550
+ extensions.push(edExt);
6551
+ note_offered_extensions([edExt]);
6552
+ if (echSplit) echSplit.outer.push(edExt);
6553
+ context.early.offered = true;
6554
+ context.early.alpn = sessionData.alpn || null;
6555
+ context.early.maxEarly = sessionData.max_early_data >>> 0;
6556
+ context.early.appData = sessionData.qa != null ? sessionData.qa : null;
6557
+ }
4340
6558
  extensions.push(pskExt); // MUST be last
4341
6559
  note_offered_extensions([pskExt]);
6560
+ // ECH: the ticket is the inner hello's alone; the outer one carries
6561
+ // a pre_shared_key of the same shape with random contents (RFC 9849
6562
+ // §10.12.3), so it does not show resumption was attempted — and a
6563
+ // server that does not decrypt finds no ticket there, and does a
6564
+ // full handshake. Last in the outer hello too.
6565
+ if (echSplit) {
6566
+ echSplit.outerPsk = {
6567
+ type: 'PRE_SHARED_KEY',
6568
+ value: {
6569
+ identities: [{ identity: new Uint8Array(crypto.randomBytes(sessionData.ticket.length)), age: crypto.randomBytes(4).readUInt32BE(0) }],
6570
+ binders: [new Uint8Array(crypto.randomBytes(hashLen))]
6571
+ }
6572
+ };
6573
+ }
4342
6574
 
4343
6575
  // Build the full message with placeholder
4344
6576
  let build_message_params = {
@@ -4351,7 +6583,7 @@ function TLSSession(options){
4351
6583
  // builder actually reads; the singular key was silently ignored and
4352
6584
  // wire.js substituted its own hardcoded fallback list — the
4353
6585
  // configured cipher suites never reached the wire. Both names passed.
4354
- cipher_suites: context.local_supported_cipher_suites,
6586
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
4355
6587
  cipher_suite: context.local_supported_cipher_suites,
4356
6588
  extensions: extensions
4357
6589
  };
@@ -4381,7 +6613,7 @@ function TLSSession(options){
4381
6613
  pskExt.value.binders = [binder];
4382
6614
  message_data = build_tls_message(build_message_params);
4383
6615
 
4384
- } else if (sessionData && sessionData.v === 12 && sessionData.master_secret) {
6616
+ } else if (!echSplit && sessionData && sessionData.v === 12 && sessionData.master_secret) {
4385
6617
  // TLS 1.2 resumption: session ID and/or SessionTicket
4386
6618
  // Save for later verification when ServerHello arrives
4387
6619
  context.tls12_client_session = sessionData;
@@ -4421,7 +6653,7 @@ function TLSSession(options){
4421
6653
  // builder actually reads; the singular key was silently ignored and
4422
6654
  // wire.js substituted its own hardcoded fallback list — the
4423
6655
  // configured cipher suites never reached the wire. Both names passed.
4424
- cipher_suites: context.local_supported_cipher_suites,
6656
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
4425
6657
  cipher_suite: context.local_supported_cipher_suites,
4426
6658
  extensions: extensions
4427
6659
  };
@@ -4442,18 +6674,39 @@ function TLSSession(options){
4442
6674
  // builder actually reads; the singular key was silently ignored and
4443
6675
  // wire.js substituted its own hardcoded fallback list — the
4444
6676
  // configured cipher suites never reached the wire. Both names passed.
4445
- cipher_suites: context.local_supported_cipher_suites,
6677
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
4446
6678
  cipher_suite: context.local_supported_cipher_suites,
4447
6679
  extensions: extensions
4448
6680
  };
4449
6681
  message_data = build_tls_message(build_message_params);
4450
6682
  }
4451
6683
 
6684
+ // ECH: what goes on the wire is the outer hello; the transcript has the
6685
+ // inner one until the ServerHello says which (ech_decide).
6686
+ let onWire = message_data;
6687
+ if (echSplit) {
6688
+ onWire = ech_outer_hello(message_data, echSplit, false);
6689
+ context.ech.outer_ch1 = onWire;
6690
+ }
6691
+
6692
+ // Client side: our own ClientHello is what a server fingerprints us by.
6693
+ // First one wins — a post-HRR retry must not replace the original offer.
6694
+ if (context.rawClientHello === null) context.rawClientHello = onWire;
6695
+
4452
6696
  pushTranscript(message_data);
4453
6697
 
4454
6698
  context.hello_sent=true;
4455
6699
 
4456
- ev.emit('message',0,context.message_sent_seq,'hello',message_data);
6700
+ // 0-RTT offered: its secret, from the PSK and this ClientHello — before
6701
+ // the hello goes, so the layer above has the key for what follows it.
6702
+ if (context.early.offered && context.psk_offered) {
6703
+ let edHash = TLS_CIPHER_SUITES[context.psk_offered.cipher] ? TLS_CIPHER_SUITES[context.psk_offered.cipher].hash : 'sha256';
6704
+ let edSecret = derive_client_early_traffic_secret(edHash, context.psk_offered.psk, get_transcript_hash(edHash), label_prefix());
6705
+ _emitKeylogSingle('CLIENT_EARLY_TRAFFIC_SECRET', edSecret);
6706
+ ev.emit('earlySecret', edSecret, { cipher: context.psk_offered.cipher, alpn: context.early.alpn, appData: context.early.appData, maxEarlyData: context.early.maxEarly });
6707
+ }
6708
+
6709
+ ev.emit('message',0,context.message_sent_seq,'hello',onWire);
4457
6710
 
4458
6711
  context.message_sent_seq++;
4459
6712
 
@@ -4462,6 +6715,375 @@ function TLSSession(options){
4462
6715
 
4463
6716
 
4464
6717
 
6718
+ /* ======================= ClientHello fingerprinting ======================= */
6719
+
6720
+ /**
6721
+ * GREASE (RFC 8701) reserved values: both bytes equal, low nibble of each
6722
+ * is 0xA — 0x0A0A, 0x1A1A ... 0xFAFA. Clients sprinkle these into ciphers,
6723
+ * extensions, groups and signature schemes specifically so that peers who
6724
+ * hardcode value lists break loudly. Every fingerprint below drops them:
6725
+ * they are random per connection, so leaving one in makes the fingerprint
6726
+ * change on every handshake from the same client.
6727
+ *
6728
+ * The narrower `(v & 0x0f0f) === 0x0a0a` test used before also matched
6729
+ * non-GREASE values such as 0x1a2a, so it is not a safe stand-in.
6730
+ */
6731
+ function fp_is_grease(v) {
6732
+ return (v & 0x0f0f) === 0x0a0a && ((v >>> 8) & 0xff) === (v & 0xff);
6733
+ }
6734
+
6735
+ function fp_no_grease(list) {
6736
+ let out = [];
6737
+ if (!list) return out;
6738
+ for (let i = 0; i < list.length; i++) {
6739
+ if (!fp_is_grease(list[i])) out.push(list[i]);
6740
+ }
6741
+ return out;
6742
+ }
6743
+
6744
+ function fp_hex4(v) {
6745
+ let s = (v >>> 0).toString(16);
6746
+ while (s.length < 4) s = '0' + s;
6747
+ return s;
6748
+ }
6749
+
6750
+ /**
6751
+ * Parse a stored hello at most once. Failure is cached as `false` so a
6752
+ * malformed message does not re-throw through the parser on every call.
6753
+ * `which` is 'client' or 'server'.
6754
+ */
6755
+ function fp_parse(which) {
6756
+ let rawKey = which === 'server' ? 'rawServerHello' : 'rawClientHello';
6757
+ let cacheKey = which === 'server' ? 'parsedServerHello' : 'parsedClientHello';
6758
+ if (context[cacheKey] === false) return null;
6759
+ if (context[cacheKey] !== null) return context[cacheKey];
6760
+ if (!context[rawKey]) return null;
6761
+ try {
6762
+ context[cacheKey] = parse_tls_message(context[rawKey]);
6763
+ } catch (e) {
6764
+ context[cacheKey] = false;
6765
+ return null;
6766
+ }
6767
+ return context[cacheKey];
6768
+ }
6769
+
6770
+ function fp_hello() { return fp_parse('client'); }
6771
+ function fp_shello() { return fp_parse('server'); }
6772
+
6773
+ /** Signature schemes in WIRE ORDER (JA4 deliberately does not sort these). */
6774
+ function fp_sig_algs(hello) {
6775
+ return fp_no_grease(hello.signature_algorithms || []);
6776
+ }
6777
+
6778
+ /**
6779
+ * JA4 version character pair. Read from supported_versions when the peer
6780
+ * offered it, else from the ClientHello's legacy_version.
6781
+ *
6782
+ * DTLS versions are 1's complement (DTLS 1.0 = 0xFEFF, 1.2 = 0xFEFD,
6783
+ * 1.3 = 0xFEFC), so "newest" is the numerically SMALLEST value — taking a
6784
+ * max over a DTLS supported_versions list picks the oldest version offered.
6785
+ */
6786
+ function fp_version_string(hello, isDTLS) {
6787
+ let ver = hello.legacy_version || 0x0303;
6788
+ let offered = fp_no_grease(hello.supported_versions || []);
6789
+ if (offered.length > 0) {
6790
+ ver = offered[0];
6791
+ for (let i = 1; i < offered.length; i++) {
6792
+ if (isDTLS ? offered[i] < ver : offered[i] > ver) ver = offered[i];
6793
+ }
6794
+ }
6795
+ let map = {
6796
+ 0x0304: '13', 0x0303: '12', 0x0302: '11', 0x0301: '10',
6797
+ 0x0300: 's3', 0x0200: 's2', 0x0100: 's1',
6798
+ 0xFEFF: 'd1', 0xFEFD: 'd2', 0xFEFC: 'd3'
6799
+ };
6800
+ return map[ver] || '00';
6801
+ }
6802
+
6803
+ /**
6804
+ * Raw bytes of the FIRST offered ALPN protocol, or null.
6805
+ *
6806
+ * Read off the wire rather than from hello.alpn, which the extension decoder
6807
+ * produced with a UTF-8 TextDecoder: any protocol name carrying a byte >=0x80
6808
+ * comes back with U+FFFD replacement characters, and the byte values JA4
6809
+ * needs are gone by then.
6810
+ *
6811
+ * ProtocolNameList: uint16 list_len, then { uint8 name_len, name bytes }*
6812
+ */
6813
+ function fp_first_alpn_bytes(hello) {
6814
+ let exts = hello.extensions || [];
6815
+ for (let i = 0; i < exts.length; i++) {
6816
+ if (exts[i].type !== 0x0010) continue;
6817
+ let d = exts[i].data;
6818
+ if (!d || d.length < 3) return null;
6819
+ let listLen = (d[0] << 8) | d[1];
6820
+ if (listLen < 1 || 2 + listLen > d.length) return null;
6821
+ let n = d[2];
6822
+ if (n < 1 || 3 + n > d.length) return null;
6823
+ return d.subarray(3, 3 + n);
6824
+ }
6825
+ return null;
6826
+ }
6827
+
6828
+ /**
6829
+ * JA4 ALPN pair: first and last byte of the first offered protocol.
6830
+ *
6831
+ * The FoxIO prose says an end byte outside ASCII alphanumeric falls back to
6832
+ * the first and last characters of the value's HEX form. Both reference
6833
+ * implementations — and the published test vectors — instead emit "99", and
6834
+ * accept the whole printable ASCII range (0x20-0x7E) rather than just
6835
+ * alphanumerics. Interop is the entire point of this fingerprint, so the
6836
+ * behaviour below follows the implementations, not the prose.
6837
+ *
6838
+ * A one-byte protocol repeats that byte when it is alphanumeric, else "99".
6839
+ * Absent or empty ALPN is "00".
6840
+ */
6841
+ function fp_alpn_pair(hello) {
6842
+ let bytes = fp_first_alpn_bytes(hello);
6843
+
6844
+ if (!bytes || bytes.length === 0) {
6845
+ // No ALPN extension on the wire — but a caller may have handed us a
6846
+ // pre-parsed hello with only the decoded strings. Fall back to those.
6847
+ let list = hello.alpn || [];
6848
+ if (list.length === 0 || typeof list[0] !== 'string' || list[0].length === 0) return '00';
6849
+ bytes = Buffer.from(list[0], 'latin1');
6850
+ }
6851
+
6852
+ let isAlnum = function (b) {
6853
+ return (b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a);
6854
+ };
6855
+ let isPrintable = function (b) { return b >= 0x20 && b <= 0x7e; };
6856
+
6857
+ let f = bytes[0];
6858
+ let l = bytes[bytes.length - 1];
6859
+
6860
+ if (bytes.length === 1) {
6861
+ return isAlnum(f) ? (String.fromCharCode(f) + String.fromCharCode(f)) : '99';
6862
+ }
6863
+ if (isPrintable(f) && isPrintable(l)) {
6864
+ return String.fromCharCode(f) + String.fromCharCode(l);
6865
+ }
6866
+ return '99';
6867
+ }
6868
+
6869
+ /** Truncated SHA-256 over a comma-joined list. An empty list hashes to zeros
6870
+ * by convention rather than to the digest of the empty string. */
6871
+ function fp_trunc_sha256(input) {
6872
+ if (input === '') return '000000000000';
6873
+ return crypto.createHash('sha256').update(input).digest('hex').substring(0, 12);
6874
+ }
6875
+
6876
+ /* ----------------------------- the computations ------------------------- */
6877
+
6878
+ /**
6879
+ * JA4 — the client fingerprint:
6880
+ *
6881
+ * t13d1516h2_8daaf6152771_e5627ece308c
6882
+ * │ │ │ │ │ │ │ └ sha256(sorted extensions [_ sigalgs])[:12]
6883
+ * │ │ │ │ │ │ └────────────── sha256(sorted ciphers)[:12]
6884
+ * │ │ │ │ │ └───────────────── first+last char of first ALPN ("00" if none)
6885
+ * │ │ │ │ └─────────────────── extension count (GREASE excluded, cap 99)
6886
+ * │ │ │ └───────────────────── cipher count (GREASE excluded, cap 99)
6887
+ * │ │ └─────────────────────── "d" if SNI present, "i" if not
6888
+ * │ └───────────────────────── TLS version (13/12/11/10/s3, d1/d2/d3)
6889
+ * └─────────────────────────── transport: t=TCP, d=DTLS, q=QUIC
6890
+ *
6891
+ * Ciphers and extensions are sorted before hashing, so a client that
6892
+ * shuffles extension order per connection still produces a stable
6893
+ * fingerprint — the main reason to prefer this over JA3. Signature
6894
+ * algorithms are appended in WIRE ORDER, unsorted, which is what preserves
6895
+ * uniqueness after that sort.
6896
+ *
6897
+ * SNI and ALPN are counted in the extension count but excluded from the
6898
+ * hash, so the same client hitting different hostnames fingerprints alike.
6899
+ */
6900
+ function fp_compute_ja4(opts) {
6901
+ let hello = fp_hello();
6902
+ if (!hello) return null;
6903
+ try {
6904
+ let isDTLS = ((hello.legacy_version & 0xFF00) === 0xFE00) ||
6905
+ ((context.selected_version & 0xFF00) === 0xFE00);
6906
+
6907
+ let proto = (opts && opts.protocol) ? String(opts.protocol).charAt(0)
6908
+ : (isDTLS ? 'd' : 't');
6909
+
6910
+ let verStr = fp_version_string(hello, isDTLS);
6911
+
6912
+ let allExts = (hello.extensions || []).map(function(e){ return e.type; });
6913
+ let hasSNI = allExts.indexOf(0x0000) >= 0;
6914
+
6915
+ let ciphers = fp_no_grease(hello.cipher_suites || []);
6916
+ let exts = fp_no_grease(allExts);
6917
+
6918
+ let pad2 = function(n){ n = Math.min(n, 99); return (n < 10 ? '0' : '') + n; };
6919
+
6920
+ let ja4_a = proto + verStr + (hasSNI ? 'd' : 'i') +
6921
+ pad2(ciphers.length) + pad2(exts.length) + fp_alpn_pair(hello);
6922
+
6923
+ // --- b: ciphers, sorted ---
6924
+ let cipherList = ciphers.map(fp_hex4).sort().join(',');
6925
+
6926
+ // --- c: extensions minus SNI(0) and ALPN(16), sorted, then sigalgs ---
6927
+ let extList = exts
6928
+ .filter(function(t){ return t !== 0x0000 && t !== 0x0010; })
6929
+ .map(fp_hex4).sort().join(',');
6930
+
6931
+ let sigList = fp_sig_algs(hello).map(fp_hex4).join(',');
6932
+ let cInput = sigList ? (extList + '_' + sigList) : extList;
6933
+
6934
+ // The raw form carries JA4_c as it was hashed: with no signature
6935
+ // algorithms, the extensions alone — no "_" after them (FoxIO's ja4_r;
6936
+ // the hash already followed this, the raw form ended in "_").
6937
+ return {
6938
+ hash: ja4_a + '_' + fp_trunc_sha256(cipherList) + '_' + fp_trunc_sha256(cInput),
6939
+ raw: ja4_a + '_' + cipherList + '_' + cInput
6940
+ };
6941
+ } catch (e) { return null; }
6942
+ }
6943
+
6944
+ /**
6945
+ * JA4S — the server-side counterpart of JA4:
6946
+ *
6947
+ * t130200_1301_a56c5b993250
6948
+ * │ │ │ │ │ └ sha256(extensions in WIRE ORDER)[:12]
6949
+ * │ │ │ │ └────── the chosen cipher, printed raw — one value, nothing to hash
6950
+ * │ │ │ └───────── ALPN pair of the selected protocol
6951
+ * │ │ └─────────── extension count
6952
+ * │ └───────────── TLS version
6953
+ * └─────────────── transport
6954
+ *
6955
+ * Two deliberate differences from JA4. There is no SNI character, because a
6956
+ * server never sends one. And extensions are NOT sorted: sorting exists in
6957
+ * JA4 to defeat clients that shuffle extension order per connection, which
6958
+ * servers do not do — so here the order is signal worth keeping rather than
6959
+ * noise worth removing.
6960
+ */
6961
+ function fp_compute_ja4s(opts) {
6962
+ let hello = fp_shello();
6963
+ if (!hello) return null;
6964
+ try {
6965
+ let isDTLS = ((hello.legacy_version & 0xFF00) === 0xFE00) ||
6966
+ ((context.selected_version & 0xFF00) === 0xFE00);
6967
+ let proto = (opts && opts.protocol) ? String(opts.protocol).charAt(0)
6968
+ : (isDTLS ? 'd' : 't');
6969
+
6970
+ // Extensions here are NOT GREASE-filtered, unlike every list in JA4.
6971
+ // The reference implementations filter GREASE only out of the server's
6972
+ // supported_versions, and pass the extension list through untouched;
6973
+ // filtering it produced a different hash from every other tool on the
6974
+ // one vector where a server echoed a GREASE extension. Servers do not
6975
+ // normally emit GREASE at all, so this is a rare path — but a wrong
6976
+ // fingerprint on a rare path is exactly what breaks a threat hunt.
6977
+ let exts = (hello.extensions || []).map(function(e){ return e.type; });
6978
+ let pad2 = function(n){ n = Math.min(n, 99); return (n < 10 ? '0' : '') + n; };
6979
+
6980
+ let cs = hello.cipher_suites || [];
6981
+ let cipher = cs.length > 0 ? fp_hex4(cs[0]) : '0000';
6982
+
6983
+ let a = proto + fp_version_string(hello, isDTLS) + pad2(exts.length) + fp_alpn_pair(hello);
6984
+ let extList = exts.map(fp_hex4).join(',');
6985
+
6986
+ return {
6987
+ hash: a + '_' + cipher + '_' + fp_trunc_sha256(extList),
6988
+ raw: a + '_' + cipher + '_' + extList
6989
+ };
6990
+ } catch (e) { return null; }
6991
+ }
6992
+
6993
+ /**
6994
+ * JA5 — ntop's successor to JA4 (draft, CC BY 4.0; the specification is
6995
+ * README.ja5.md in the nDPI repository).
6996
+ *
6997
+ * t13d1514h2_acb858a92679_7dc829385eb9_0a47a2b05960
6998
+ * │ │ │ └ sha256(sorted supported_groups)[:12]
6999
+ * │ │ └───────────── as JA4_c, without the ephemeral
7000
+ * │ │ extensions
7001
+ * │ └────────────────────────── as JA4_b: sorted ciphers
7002
+ * └───────────────────────────────────── as JA4_a, the extension count
7003
+ * without the ephemeral extensions
7004
+ *
7005
+ * The one thing it fixes: JA4 counts and hashes every extension, and some
7006
+ * of them say nothing about the client — only about THIS connection. A
7007
+ * browser coming back with a session ticket adds pre_shared_key, and its JA4
7008
+ * changes (t13d1516 → t13d1517 for Chrome 149) though nothing about the
7009
+ * browser did. JA5 leaves those out, so the same client build has one JA5
7010
+ * whether it resumed or not. The list is the specification's registry, v1:
7011
+ *
7012
+ * 0x0015 padding 0x002a early_data
7013
+ * 0x0023 session_ticket 0x002c cookie
7014
+ * 0x0029 pre_shared_key 0x002d psk_key_exchange_modes
7015
+ *
7016
+ * The one thing it adds: the supported_groups a client offers, which JA4
7017
+ * does not look at — a client that copies a browser's cipher list and not
7018
+ * its groups is caught, and whether it offers a post-quantum group
7019
+ * (X25519MLKEM768) is part of what it is. No supported_groups at all leaves
7020
+ * the field EMPTY, not zeros, so "absent" stays distinct from anything
7021
+ * present.
7022
+ *
7023
+ * GREASE is filtered with the same test as JA4 (fp_is_grease). The draft
7024
+ * writes it as (v & 0x0F0F) == 0x0A0A, which also takes in non-GREASE
7025
+ * values such as 0x1a2a; no client sends one, and one test for both
7026
+ * fingerprints keeps them from ever disagreeing on what GREASE is.
7027
+ *
7028
+ * `raw` is this library's, not the draft's (it defines none): JA4's raw
7029
+ * form with the groups appended — a_ciphers_c_groups, c as JA4 hashes it
7030
+ * (the extensions, and "_" and the signature algorithms if there are any).
7031
+ */
7032
+ var JA5_EPHEMERAL = { 0x0015: 1, 0x0023: 1, 0x0029: 1, 0x002a: 1, 0x002c: 1, 0x002d: 1 };
7033
+
7034
+ function fp_compute_ja5(opts) {
7035
+ let hello = fp_hello();
7036
+ if (!hello) return null;
7037
+ try {
7038
+ let isDTLS = ((hello.legacy_version & 0xFF00) === 0xFE00) ||
7039
+ ((context.selected_version & 0xFF00) === 0xFE00);
7040
+ let proto = (opts && opts.protocol) ? String(opts.protocol).charAt(0)
7041
+ : (isDTLS ? 'd' : 't');
7042
+ let verStr = fp_version_string(hello, isDTLS);
7043
+
7044
+ let allExts = (hello.extensions || []).map(function(e){ return e.type; });
7045
+ let hasSNI = allExts.indexOf(0x0000) >= 0;
7046
+ let exts = fp_no_grease(allExts).filter(function(t){ return !JA5_EPHEMERAL[t]; });
7047
+ let ciphers = fp_no_grease(hello.cipher_suites || []);
7048
+
7049
+ let pad2 = function(n){ n = Math.min(n, 99); return (n < 10 ? '0' : '') + n; };
7050
+ let a = proto + verStr + (hasSNI ? 'd' : 'i') +
7051
+ pad2(ciphers.length) + pad2(exts.length) + fp_alpn_pair(hello);
7052
+
7053
+ let cipherList = ciphers.map(fp_hex4).sort().join(',');
7054
+ let extList = exts
7055
+ .filter(function(t){ return t !== 0x0000 && t !== 0x0010; })
7056
+ .map(fp_hex4).sort().join(',');
7057
+ let sigList = fp_sig_algs(hello).map(fp_hex4).join(',');
7058
+ let cInput = sigList ? (extList + '_' + sigList) : extList;
7059
+
7060
+ let groups = fp_no_grease(hello.supported_groups || []).map(fp_hex4).sort().join(',');
7061
+ let d = groups ? crypto.createHash('sha256').update(groups).digest('hex').substring(0, 12) : '';
7062
+
7063
+ return {
7064
+ hash: a + '_' + fp_trunc_sha256(cipherList) + '_' + fp_trunc_sha256(cInput) + '_' + d,
7065
+ raw: a + '_' + cipherList + '_' + cInput + '_' + groups
7066
+ };
7067
+ } catch (e) { return null; }
7068
+ }
7069
+
7070
+ /**
7071
+ * Cached fingerprint accessor. Each value is computed the first time it is
7072
+ * asked for AND its underlying hello exists; until then the slot stays
7073
+ * `undefined` and is retried on the next call. That is what lets getJA4S()
7074
+ * be called from the 'clienthello' handler — before any ServerHello exists
7075
+ * — without freezing it at null forever.
7076
+ */
7077
+ function fp_get(name, opts) {
7078
+ if (context.fpCache[name] !== undefined) return context.fpCache[name];
7079
+ let v = null;
7080
+ if (name === 'ja4') v = fp_compute_ja4(opts);
7081
+ else if (name === 'ja5') v = fp_compute_ja5(opts);
7082
+ else if (name === 'ja4s') v = fp_compute_ja4s(opts);
7083
+ if (v !== null) context.fpCache[name] = v;
7084
+ return v;
7085
+ }
7086
+
4465
7087
  let api = {
4466
7088
  /**
4467
7089
  * Raw context object. Advanced users (QUIC, DTLS) can read/write
@@ -4525,6 +7147,114 @@ function TLSSession(options){
4525
7147
  return context.selected_cipher_suite;
4526
7148
  },
4527
7149
 
7150
+
7151
+ /**
7152
+ * Server: this connection's ticket, from the first connection on.
7153
+ *
7154
+ * data what this connection's ticket carries — on a resumed
7155
+ * connection what it came back with, on a full handshake what
7156
+ * ticketData made for the ticket issued to it. The same value
7157
+ * the ticket holds, so the connection that got the ticket and
7158
+ * the ones that resume with it later share it. null without a
7159
+ * ticketData option; on a full handshake, null until the
7160
+ * server's Finished is sent — whether the connection resumes
7161
+ * is settled by then, and ticketData is asked then, once. QUIC
7162
+ * reports a server connection ready at that very point, before
7163
+ * the client's Finished arrives.
7164
+ * resumed resumed with a ticket (isSessionReused()).
7165
+ * offered the client presented a ticket — resumed, or turned away:
7166
+ * expired, issued before a restart (other ticketKeys), no
7167
+ * compatible cipher. offered && !resumed: a client that has
7168
+ * been here before and cannot show who it was.
7169
+ *
7170
+ * On a full handshake `data` is what the server has just made up, not
7171
+ * something the client proved it holds; whether to trust it is the
7172
+ * application's call, by `resumed`.
7173
+ */
7174
+ /**
7175
+ * The protocol layer's state for the tickets this server issues from now
7176
+ * on — JSON-safe and small (at most 128 bytes serialized; larger, or not
7177
+ * serializable: refused, and the tickets carry none). Separate from the
7178
+ * application's ticketData, which it neither sees nor limits. Comes back
7179
+ * in getTicketInfo().state on the connection that resumes with one of
7180
+ * those tickets. TLS 1.3. Returns whether it was taken.
7181
+ */
7182
+ /** Server: the context the tickets issued from now on are for, and the
7183
+ * only one a presented ticket is accepted under (see ticketContext). */
7184
+ setTicketContext: function(ctx){
7185
+ context.ticketContext = typeof ctx === 'string' ? ctx : '';
7186
+ },
7187
+
7188
+ setTicketState: function(state){
7189
+ if (state == null) { context.ticket_state = null; return true; }
7190
+ let size;
7191
+ try { size = Buffer.byteLength(JSON.stringify(state)); } catch (e) { return false; }
7192
+ if (!(size <= TICKET_STATE_MAX)) return false;
7193
+ context.ticket_state = state;
7194
+ return true;
7195
+ },
7196
+
7197
+ /**
7198
+ * Issue another NewSessionTicket now (server, TLS 1.3, after the
7199
+ * handshake), as OpenSSL's SSL_new_session_ticket(): the first went out
7200
+ * with the handshake, before anything was known that a later one can
7201
+ * carry (setTicketState). Clients keep the newest. Returns whether one
7202
+ * was sent.
7203
+ */
7204
+ sendTicket: function(){
7205
+ if (!context.isServer || context.state !== 'connected' || !is13() ||
7206
+ !context.sessionTickets || !context.resumption_master_secret) return false;
7207
+ if (context.ticket_nonce_counter > 255) return false; // a one-byte nonce
7208
+ issue_ticket_13();
7209
+ return true;
7210
+ },
7211
+
7212
+ getTicketInfo: function(){
7213
+ let data = null;
7214
+ if (context.isServer) {
7215
+ // Settled once our Finished is out (TLS 1.3: the application
7216
+ // secrets exist), or the handshake is done (TLS 1.2).
7217
+ if (context.state === 'connected' || context.local_app_traffic_secret) data = ticket_app_data();
7218
+ else if (context.isResumed) data = context.resumed_ticket_data;
7219
+ }
7220
+ return { data: data, resumed: !!context.isResumed, offered: !!context.ticket_offered,
7221
+ state: context.isResumed ? context.resumed_ticket_state : null };
7222
+ },
7223
+
7224
+ /**
7225
+ * Encrypted Client Hello on this connection. Server: null when the
7226
+ * client did not offer it, 'accepted' when its real ClientHello was
7227
+ * decrypted and used, 'rejected' when it could not be (the handshake
7228
+ * then ran as the public name, and retry_configs went out). Client:
7229
+ * null (none, or not yet decided), 'grease', 'accepted', 'rejected'.
7230
+ */
7231
+ getEchStatus: function(){
7232
+ return context.ech.status;
7233
+ },
7234
+
7235
+ /** 0-RTT on this connection: null (not offered), 'accepted', 'rejected'
7236
+ * — the server's verdict, once given (EncryptedExtensions). */
7237
+ // Server: whether the ClientHello offered early data at all — taken or
7238
+ // not; not taken, the record layer skips it (RFC 8446 §4.2.10).
7239
+ earlyDataOfferedByPeer: function(){ return context.early.clientOffered === true; },
7240
+
7241
+ getEarlyDataStatus: function(){
7242
+ if (!context.early.offered) return null;
7243
+ return context.early.status;
7244
+ },
7245
+
7246
+ /** Client: the data to keep with the sessions this connection saves
7247
+ * (sessionAppData) — set once it is known, before a ticket arrives. */
7248
+ setSessionAppData: function(v){
7249
+ context.sessionAppData = v != null ? v : null;
7250
+ },
7251
+
7252
+ /** Server: what 0-RTT is accepted under (earlyDataContext) — set before
7253
+ * the ClientHello is read, as setTicketContext. */
7254
+ setEarlyDataContext: function(ctx){
7255
+ context.earlyDataContext = typeof ctx === 'string' ? ctx : '';
7256
+ },
7257
+
4528
7258
  /** Returns the negotiated ALPN protocol string (e.g. 'h2'), or null. */
4529
7259
  getALPN: function(){
4530
7260
  return context.selected_alpn || null;
@@ -4532,7 +7262,44 @@ function TLSSession(options){
4532
7262
 
4533
7263
  /** Returns the remote certificate chain, or null. */
4534
7264
  getPeerCertificate: function(){
4535
- return context.remote_cert_chain || null;
7265
+ return context.remote_cert_chain || context.resumed_peer_chain || null;
7266
+ },
7267
+
7268
+ /** Returns our own certificate chain — the one this connection presents
7269
+ * (on a server, the one the SNI lookup chose) — or null. */
7270
+ getCertificate: function(){
7271
+ return context.local_cert_chain || null;
7272
+ },
7273
+
7274
+ /**
7275
+ * Replace the certificate and private key this connection will present.
7276
+ *
7277
+ * Node parity with tlsSocket.setKeyCert(context), whose stated purpose is
7278
+ * selecting a server certificate from inside ALPNCallback: the chosen
7279
+ * protocol is known there, but SNICallback has already run, so this is the
7280
+ * only place left to swap credentials for that one connection.
7281
+ *
7282
+ * Accepts either a context from createSecureContext() or a plain
7283
+ * { key, cert, passphrase } that will be compiled here.
7284
+ *
7285
+ * Assigns directly rather than through set_context, matching the
7286
+ * SNICallback path above: both seed inputs ahead of the single set_context
7287
+ * that actually runs the reactive loop, and routing through it here would
7288
+ * run that loop a second time on a half-populated hello.
7289
+ *
7290
+ * Only meaningful before our Certificate message goes out; afterwards the
7291
+ * peer already has the old chain and swapping is a no-op on the wire.
7292
+ */
7293
+ setKeyCert: function(ctxOrOpts){
7294
+ if (!ctxOrOpts) return;
7295
+ let sc = ctxOrOpts;
7296
+ if (!sc.certificateChain || !sc.privateKey) {
7297
+ if (!ctxOrOpts.key || !ctxOrOpts.cert) return;
7298
+ sc = createSecureContext(ctxOrOpts);
7299
+ }
7300
+ if (sc.certificateChain) context.local_cert_chain = sc.certificateChain;
7301
+ if (sc.privateKey) context.cert_private_key = sc.privateKey;
7302
+ context.local_cert_alternates = sc.alternates || null;
4536
7303
  },
4537
7304
 
4538
7305
  /** Whether the peer certificate passed validation. */
@@ -4691,36 +7458,48 @@ function TLSSession(options){
4691
7458
  cipher: context.selected_cipher_suite,
4692
7459
  cipherName: cipherInfo ? cipherInfo.name : null,
4693
7460
  group: context.selected_group,
4694
- groupName: context.selected_group === 0x001d ? 'X25519' : context.selected_group === 0x0017 ? 'P-256' : context.selected_group === 0x0018 ? 'P-384' : null,
4695
- signatureAlgorithm: context.selected_signature_algorithm,
7461
+ groupName: context.selected_group != null ? ecdh_group_name(context.selected_group) : null,
7462
+ // The scheme of the server's CertificateVerify — what authenticated
7463
+ // the handshake (ML-DSA for a post-quantum certificate): the one we
7464
+ // signed with, or, on a client, the one we verified. null on a
7465
+ // resumed handshake, which has none.
7466
+ signatureAlgorithm: context.isServer ? context.selected_signature_algorithm : (context.peerSignatureScheme != null ? context.peerSignatureScheme : null),
7467
+ signatureAlgorithmName: scheme_name(context.isServer ? context.selected_signature_algorithm : context.peerSignatureScheme),
4696
7468
  alpn: context.selected_alpn,
4697
7469
  sni: context.selected_sni || context.local_sni,
4698
7470
  resumed: context.isResumed,
4699
7471
  helloRetried: context.helloRetried,
4700
7472
  handshakeDuration: context.handshakeEndTime && context.handshakeStartTime ? context.handshakeEndTime - context.handshakeStartTime : null,
7473
+ // RFC 8879: the algorithm each side's Certificate travelled in
7474
+ // ('brotli', 'zlib', 'zstd'), null where it went plain or was not
7475
+ // sent (a resumed handshake, no client certificate). What happened on
7476
+ // the wire, not what was configured.
7477
+ certificateCompression: {
7478
+ sent: context.cert_compression_sent != null ? cert_compression_name(context.cert_compression_sent) : null,
7479
+ received: context.cert_compression_received != null ? cert_compression_name(context.cert_compression_received) : null,
7480
+ },
4701
7481
  };
4702
7482
  },
4703
7483
 
4704
- /** Compute JA3 fingerprint from the ClientHello (server-side only).
4705
- * Returns { hash, raw } or null if no ClientHello available.
4706
- * JA3 = md5(SSLVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats)
7484
+ /**
7485
+ * TLS fingerprints, one method each, each computed the first time it is
7486
+ * asked for and kept. Each is { hash, raw } once its hello has been seen,
7487
+ * null before that. Both hellos of a connection are visible to both
7488
+ * peers, so all three answer in either role; which one describes you
7489
+ * depends on `isServer`.
7490
+ *
7491
+ * getJA4() the client, from its ClientHello — the published format,
7492
+ * what fingerprint databases and other tools match against
7493
+ * getJA5() the client too, stable across session resumption (see
7494
+ * fp_compute_ja5) — for recognising a client here
7495
+ * getJA4S() the server, from its ServerHello
7496
+ *
7497
+ * `opts.protocol` overrides the transport character for a caller driving
7498
+ * this session from a transport it cannot observe (QUIC -> 'q').
4707
7499
  */
4708
- getJA3: function(){
4709
- if (!context.rawClientHello) return null;
4710
- try {
4711
- let hello = parse_tls_message(context.rawClientHello);
4712
-
4713
- let version = hello.client_version || 0x0303;
4714
- let ciphers = (hello.cipher_suites || []).filter(c => (c & 0x0F0F) !== 0x0A0A).join('-');
4715
- let extensions = (hello.extensions || []).map(e => e.type).filter(t => t !== 0x0A0A).join('-');
4716
- let curves = (hello.supported_groups || []).filter(g => (g & 0x0F0F) !== 0x0A0A).join('-');
4717
- let pointFormats = (hello.ec_point_formats || [0]).join('-');
4718
-
4719
- let raw = [version, ciphers, extensions, curves, pointFormats].join(',');
4720
- let hash = crypto.createHash('md5').update(raw).digest('hex');
4721
- return { hash, raw };
4722
- } catch(e) { return null; }
4723
- },
7500
+ getJA4: function(opts){ return fp_get('ja4', opts); },
7501
+ getJA5: function(opts){ return fp_get('ja5', opts); },
7502
+ getJA4S: function(opts){ return fp_get('ja4s', opts); },
4724
7503
 
4725
7504
  /** Request a TLS 1.3 Key Update. requestPeer=true means ask the other side to update too. */
4726
7505
  requestKeyUpdate: function(requestPeer){
@@ -4729,7 +7508,7 @@ function TLSSession(options){
4729
7508
  let hashLen = getHashLen(hashName);
4730
7509
 
4731
7510
  // Derive new local traffic secret
4732
- let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0, label_prefix()), hashLen);
7511
+ let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0), hashLen, label_prefix());
4733
7512
  context.local_app_traffic_secret = newLocalSecret;
4734
7513
 
4735
7514
  // Send KeyUpdate message
@@ -4741,7 +7520,14 @@ function TLSSession(options){
4741
7520
  },
4742
7521
  };
4743
7522
 
4744
- for (let k in api) if (Object.prototype.hasOwnProperty.call(api,k)) this[k] = api[k];
7523
+ // Copied as property DESCRIPTORS, not values: `this[k] = api[k]` runs each
7524
+ // getter once, here, and stores what it returned — so `authorized` and
7525
+ // `authorizationError` were frozen at false / null for the life of the
7526
+ // session, and TLSSocket reported every verified peer as unauthorized and
7527
+ // every failure without its reason. handshakeDuration froze the same way.
7528
+ for (let k in api) if (Object.prototype.hasOwnProperty.call(api,k)) {
7529
+ Object.defineProperty(this, k, Object.getOwnPropertyDescriptor(api, k));
7530
+ }
4745
7531
  // Re-define dynamic getters (the for-in loop flattens them to values)
4746
7532
  Object.defineProperty(this, 'isResumed', { get: function() { return context.isResumed; }, configurable: true, enumerable: true });
4747
7533
  return this;