lemon-tls 0.7.0 → 0.10.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,6 +20,7 @@ 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,
25
26
  getHashLen,
@@ -36,18 +37,24 @@ import {
36
37
  concatUint8Arrays,
37
38
  arraysEqual,
38
39
  uint8Equal,
39
- timingSafeEqualU8
40
+ timingSafeEqualU8,
41
+ certificateObject,
40
42
  } from './utils.js';
41
43
 
42
44
  import * as wire from './wire.js';
43
45
 
44
46
  // Extracted modules
45
- 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';
46
48
  import createSecureContext from './secure_context.js';
47
- 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
48
51
  } from './session/ecdh.js';
49
52
  import { build_tls_message, parse_tls_message } from './session/message.js';
50
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';
51
58
 
52
59
  // RFC 8446 §4.1.3 downgrade-protection sentinels. A TLS 1.3-capable server that
53
60
  // negotiates a lower version stamps one of these into the last 8 bytes of
@@ -59,6 +66,16 @@ const DOWNGRADE_SENTINEL_TLS12 = new Uint8Array([0x44,0x4F,0x57,0x4E,0x47,0x52,0
59
66
  const DOWNGRADE_SENTINEL_TLS11 = new Uint8Array([0x44,0x4F,0x57,0x4E,0x47,0x52,0x44,0x00]); // "DOWNGRD\x00" (TLS 1.1 and below)
60
67
 
61
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
+
62
79
  const LEMON_DEBUG = typeof process !== 'undefined' && process.env && process.env.LEMON_DEBUG === '1';
63
80
  function dbg(tag, ...args) { if (LEMON_DEBUG) console.error('[LEMON ' + tag + ']', ...args); }
64
81
  function hexPreview(buf, max) {
@@ -71,14 +88,82 @@ function hexPreview(buf, max) {
71
88
 
72
89
  const MAX_CHAIN_DEPTH = 10;
73
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
+
74
121
  function _x509(x) {
75
122
  try { return new crypto.X509Certificate(Buffer.isBuffer(x) ? x : Buffer.from(x)); }
76
123
  catch (e) { return null; }
77
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
+ }
78
156
  function _sameCert(a, b) { return a.raw.length === b.raw.length && a.raw.equals(b.raw); }
79
157
  function _validNow(x509, now) {
80
158
  return !(now < new Date(x509.validFrom) || now > new Date(x509.validTo));
81
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
+ }
82
167
 
83
168
  /**
84
169
  * Build and verify a certificate chain from `chainDer` (leaf first) up to one
@@ -99,7 +184,16 @@ function _validNow(x509, now) {
99
184
  * window. Intermediates come from the peer and are never trusted on their own;
100
185
  * only reaching an anchor ends the walk.
101
186
  *
102
- * Returns { ok: true, depth } or { ok: false, error }.
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
103
197
  */
104
198
  function verify_cert_chain(chainDer, anchors, now) {
105
199
  if (!chainDer || chainDer.length === 0) return { ok: false, error: 'NO_PEER_CERTIFICATE' };
@@ -116,7 +210,7 @@ function verify_cert_chain(chainDer, anchors, now) {
116
210
 
117
211
  const trusted = [];
118
212
  for (let i = 0; i < (anchors || []).length; i++) {
119
- const x = _x509(anchors[i]);
213
+ const x = _anchorX509(anchors[i]);
120
214
  if (x) trusted.push(x);
121
215
  }
122
216
  if (trusted.length === 0) return { ok: false, error: 'NO_TRUST_ANCHORS' };
@@ -132,7 +226,7 @@ function verify_cert_chain(chainDer, anchors, now) {
132
226
  for (const a of trusted) {
133
227
  try {
134
228
  if (current.checkIssued(a) && current.verify(a.publicKey)) {
135
- if (!_validNow(a, now)) return { ok: false, error: 'CERT_CHAIN_ANCHOR_EXPIRED' };
229
+ if (!_validNow(a, now)) return { ok: false, error: _validityError(a, now) };
136
230
  return { ok: true, depth: depth + 1 };
137
231
  }
138
232
  } catch (e) { /* try next anchor */ }
@@ -145,13 +239,18 @@ function verify_cert_chain(chainDer, anchors, now) {
145
239
  try {
146
240
  if (!current.checkIssued(cand)) continue;
147
241
  if (!current.verify(cand.publicKey)) continue;
148
- if (cand.ca !== true) return { ok: false, error: 'INVALID_CA_BASIC_CONSTRAINT' };
149
- if (!_validNow(cand, now)) return { ok: false, error: 'CERT_CHAIN_INTERMEDIATE_EXPIRED' };
242
+ if (cand.ca !== true) return { ok: false, error: 'INVALID_CA' };
243
+ if (!_validNow(cand, now)) return { ok: false, error: _validityError(cand, now) };
150
244
  used.add(i); next = cand; break;
151
245
  } catch (e) { /* try next candidate */ }
152
246
  }
153
247
 
154
- if (!next) return { ok: false, error: 'UNABLE_TO_GET_ISSUER_CERT' };
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
+ }
155
254
  current = next;
156
255
  }
157
256
 
@@ -159,6 +258,15 @@ function verify_cert_chain(chainDer, anchors, now) {
159
258
  }
160
259
 
161
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
+
162
270
  function TLSSession(options){
163
271
  if (!(this instanceof TLSSession)) return new TLSSession(options);
164
272
  options = options || {};
@@ -169,20 +277,152 @@ function TLSSession(options){
169
277
  state: 'new', //new | negotiating | ...
170
278
  isServer: !!options.isServer,
171
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,
172
295
  ca: options.ca || null, // CA certificates (PEM strings or Buffers)
173
296
 
174
297
  SNICallback: options.SNICallback || null,
175
298
  ticketKeys: options.ticketKeys || null, // 48 bytes: [0:16]=key_name, [16:48]=AES-256-GCM key
176
- 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 },
177
381
  sessionTickets: options.sessionTickets !== false, // default true (was noTickets inverted)
178
382
 
179
383
  // Advanced options
180
384
  maxHandshakeSize: options.maxHandshakeSize || 0, // 0 = no limit
181
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,
182
404
  handshakeBytes: 0,
183
405
  handshakeStartTime: null,
184
406
  handshakeEndTime: null,
185
- 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: {},
186
426
 
187
427
  //local stuff...
188
428
  local_sni: options.servername || null,
@@ -315,11 +555,20 @@ function TLSSession(options){
315
555
  selected_cert: null,
316
556
 
317
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
318
563
 
319
564
  // Client certificate authentication
320
565
  requestCert: !!options.requestCert, // server: send CertificateRequest?
321
566
  clientCert: options.cert || null, // client: cert to send if requested
322
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,
323
572
  certificateRequested: false, // client: server sent CertificateRequest?
324
573
  certificateRequestSent: false, // server: we sent CertificateRequest?
325
574
  certificateRequestContext: null,
@@ -328,6 +577,7 @@ function TLSSession(options){
328
577
 
329
578
  // HelloRetryRequest
330
579
  helloRetried: false, // true if HRR was sent/received
580
+ client_hellos_received: 0, // server: one, or two after a HelloRetryRequest
331
581
  // What a received HelloRetryRequest committed the server to; the second
332
582
  // ServerHello is validated against these (RFC 8446 §4.1.4).
333
583
  hrr_version: null,
@@ -344,8 +594,26 @@ function TLSSession(options){
344
594
  ticket_nonce_counter: 0,
345
595
  session_ticket_sent: false,
346
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 },
347
603
  psk_accepted: false, // server accepted PSK → abbreviated handshake
348
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)
349
617
 
350
618
  // TLS 1.2 resumption
351
619
  tls12_abbreviated: false, // doing abbreviated handshake (server side)
@@ -357,6 +625,19 @@ function TLSSession(options){
357
625
  tls12_session_id_emitted: false, // 'newSession' event already fired
358
626
  tls12_client_session_emitted: false, // client-side 'session' event fired (TLS 1.2 Session ID or ticket)
359
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,
360
641
  tls12_client_session: null, // client: saved session to resume with (parsed sessionData)
361
642
  };
362
643
 
@@ -467,6 +748,8 @@ function TLSSession(options){
467
748
  * @param {object} opts
468
749
  * keyShareGroup — NamedGroup for the key_share entry (null = omit)
469
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)
470
753
  * cookie — HRR cookie to echo (RFC 8446 §4.2.2), or null
471
754
  */
472
755
  function build_client_hello_extensions(opts) {
@@ -485,7 +768,11 @@ function TLSSession(options){
485
768
  let extensions = [];
486
769
 
487
770
  if (offers13) {
488
- 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 });
489
776
  }
490
777
  // supported_groups is a CAPABILITY claim, not a copy of configuration.
491
778
  //
@@ -499,14 +786,17 @@ function TLSSession(options){
499
786
  let advertisable = [];
500
787
  for (let gi = 0; gi < context.local_supported_groups.length; gi++) {
501
788
  let g = context.local_supported_groups[gi];
502
- 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);
503
794
  }
504
795
  extensions.push({ type: 'SUPPORTED_GROUPS', value: advertisable });
505
796
  if (offers13 && opts.keySharePublic) {
506
- extensions.push({
507
- type: 'KEY_SHARE',
508
- value: [{ group: opts.keyShareGroup, key_exchange: opts.keySharePublic }]
509
- });
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 });
510
800
  }
511
801
  extensions.push({
512
802
  type: 'SIGNATURE_ALGORITHMS',
@@ -518,14 +808,35 @@ function TLSSession(options){
518
808
  // in a 1.3-only hello.
519
809
  extensions.push({ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) });
520
810
  extensions.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
521
- } 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) {
522
823
  // psk_key_exchange_modes: a server MUST NOT send NewSessionTicket to a
523
- // 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.
524
831
  extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
525
832
  }
526
833
 
527
- // SNI must come first.
528
- 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)) {
529
840
  extensions.unshift({ type: 'SERVER_NAME', value: context.local_sni });
530
841
  }
531
842
 
@@ -574,10 +885,220 @@ function TLSSession(options){
574
885
  extensions.push(context.local_extensions[i]);
575
886
  }
576
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
+
577
904
  note_offered_extensions(extensions);
578
905
  return extensions;
579
906
  }
580
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
+
581
1102
  /**
582
1103
  * Remember which extension types we actually offered.
583
1104
  *
@@ -622,6 +1143,16 @@ function TLSSession(options){
622
1143
  for (let i = 0; i < message.extensions.length; i++) {
623
1144
  let et = message.extensions[i] && message.extensions[i].type;
624
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
+
625
1156
  if (context.offered_extension_types.indexOf(et) < 0) {
626
1157
  fatalAlert(wire.TLS_ALERT.UNSUPPORTED_EXTENSION,
627
1158
  where + ' carried extension ' + et + ' which we did not offer');
@@ -681,6 +1212,156 @@ function TLSSession(options){
681
1212
  return null;
682
1213
  }
683
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
+
684
1365
  /**
685
1366
  * Build and send the second ClientHello after a HelloRetryRequest.
686
1367
  *
@@ -702,20 +1383,78 @@ function TLSSession(options){
702
1383
  keySharePublic: keySharePublic,
703
1384
  cookie: cookie
704
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
+ }
705
1427
 
706
- let ch2 = build_tls_message({
1428
+ let ch2Params = {
707
1429
  type: 'client_hello',
708
1430
  version: 0x0303,
709
- 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,
710
1434
  session_id: context.local_session_id,
711
1435
  cookie: context.dtls_cookie,
712
- cipher_suites: context.local_supported_cipher_suites,
1436
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
713
1437
  cipher_suite: context.local_supported_cipher_suites,
714
1438
  extensions: extensions,
715
- });
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
+ }
716
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
+ }
717
1456
  pushTranscript(ch2);
718
- ev.emit('message', 0, context.message_sent_seq, 'hello', ch2);
1457
+ ev.emit('message', 0, context.message_sent_seq, 'hello', onWire);
719
1458
  context.message_sent_seq++;
720
1459
  }
721
1460
 
@@ -765,12 +1504,20 @@ function TLSSession(options){
765
1504
  * and nowhere else.
766
1505
  */
767
1506
  function local_private_key() {
768
- if (!context.cert_private_key) return null;
769
- return crypto.createPrivateKey({
770
- key: Buffer.from(context.cert_private_key),
771
- format: 'der',
772
- type: 'pkcs8',
773
- });
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;
774
1521
  }
775
1522
 
776
1523
  /**
@@ -782,10 +1529,454 @@ function TLSSession(options){
782
1529
  * handshake where every message parses and every transcript matches, and only
783
1530
  * the keys differ — which surfaces as "bad record MAC" with no other clue.
784
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
+ if (LEMON_DEBUG) dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
1870
+ 'age_add:', ticket_age_add,
1871
+ 'lifetime:', ticket_lifetime);
1872
+ if (LEMON_DEBUG) 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
+
785
1972
  function label_prefix() {
786
- return context.selected_version === wire.DTLS_VERSION.DTLS1_3
787
- ? LABEL_PREFIX_DTLS13
788
- : 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;
789
1980
  }
790
1981
 
791
1982
  /**
@@ -827,6 +2018,7 @@ function TLSSession(options){
827
2018
  }
828
2019
 
829
2020
  function pushTranscript(data) {
2021
+
830
2022
  if (context.transcriptHook) {
831
2023
  data = context.transcriptHook(data);
832
2024
  }
@@ -852,6 +2044,7 @@ function TLSSession(options){
852
2044
  * re-hashes the same bytes 4 fewer times.
853
2045
  */
854
2046
  function get_transcript_hash(hashName) {
2047
+
855
2048
  if (context.transcriptHash !== null && context.transcriptHashName === hashName) {
856
2049
  return new Uint8Array(context.transcriptHash.copy().digest());
857
2050
  }
@@ -933,17 +2126,99 @@ function TLSSession(options){
933
2126
  let t = message.type;
934
2127
  let serverOnly = t === 'server_hello' || t === 'encrypted_extensions' ||
935
2128
  t === 'server_hello_done' || t === 'certificate_request' ||
936
- t === 'server_key_exchange' || t === 'new_session_ticket';
937
- 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';
938
2132
  if ((context.isServer && serverOnly) || (!context.isServer && clientOnly)) {
939
2133
  fatalAlert(wire.TLS_ALERT.UNEXPECTED_MESSAGE, 'Unexpected ' + t + ' from ' + (context.isServer ? 'client' : 'server'));
940
2134
  return;
941
2135
  }
942
2136
  }
943
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
+
944
2180
  // Emit 'handshakeMessage' hook for every message
945
2181
  ev.emit('handshakeMessage', message.type, data, message);
946
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
+
947
2222
  if((context.isServer==false && message.type=='server_hello') || (context.isServer==true && message.type=='client_hello')){
948
2223
 
949
2224
  // Server-side ClientHello policy checks — BEFORE the transcript push,
@@ -983,11 +2258,26 @@ function TLSSession(options){
983
2258
  }
984
2259
  }
985
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
+
986
2267
  pushTranscript(data);
987
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
+
988
2279
  // Save raw ClientHello + emit event (server side)
989
2280
  if (context.isServer && message.type === 'client_hello') {
990
- context.rawClientHello = data;
991
2281
  // Make the peer's extensions readable INSIDE the 'clienthello'
992
2282
  // handler (getRemoteExtension/getRemoteExtensions) — the main
993
2283
  // set_context that stores them runs after this emit, and the
@@ -1012,24 +2302,47 @@ function TLSSession(options){
1012
2302
  }
1013
2303
 
1014
2304
  // Server: detect and validate PSK BEFORE set_context (so reactive loop sees psk_accepted)
1015
- 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) {
1016
2318
  let pskIdentity = message.pre_shared_key.identities[0];
2319
+ context.ticket_offered = true;
1017
2320
  let pskBinder = message.pre_shared_key.binders ? message.pre_shared_key.binders[0] : null;
1018
2321
 
1019
- dbg('SRV-PSK', 'received identity:', hexPreview(pskIdentity.identity, 24),
2322
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'received identity:', hexPreview(pskIdentity.identity, 24),
1020
2323
  'age:', (pskIdentity.age || 0) >>> 0,
1021
2324
  'received binder:', hexPreview(pskBinder, 16));
1022
2325
 
1023
- 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;
1024
2332
  ev.emit('psk', {
1025
2333
  identity: pskIdentity.identity,
1026
2334
  obfuscatedAge: (pskIdentity.age || 0) >>> 0
1027
2335
  }, function(result) {
1028
2336
  pskResult = result;
1029
2337
  });
2338
+ if (pskResult === undefined) pskResult = open_ticket_13(pskIdentity.identity);
1030
2339
 
1031
- dbg('SRV-PSK', 'pskResult:', pskResult ? `psk=${hexPreview(pskResult.psk, 8)} cipher=0x${pskResult.cipher?.toString(16)}` : 'null (decrypt failed)');
2340
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'pskResult:', pskResult ? `psk=${hexPreview(pskResult.psk, 8)} cipher=0x${pskResult.cipher?.toString(16)}` : 'null (decrypt failed)');
1032
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
+ }
1033
2346
  if (pskResult && pskResult.psk) {
1034
2347
  let pskCipher = pskResult.cipher || 0x1301;
1035
2348
  let hashName = TLS_CIPHER_SUITES[pskCipher] ? TLS_CIPHER_SUITES[pskCipher].hash : 'sha256';
@@ -1037,14 +2350,19 @@ function TLSSession(options){
1037
2350
 
1038
2351
  let hashLen = getHashFn(hashName).outputLen;
1039
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.
1040
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
+ }
1041
2359
  let expectedBinder = compute_psk_binder(hashName, binder_key, truncatedCH, label_prefix());
1042
2360
 
1043
2361
  dbg('SRV-PSK', 'hash:', hashName, 'hashLen:', hashLen,
1044
2362
  'truncatedCH len:', truncatedCH.length,
1045
2363
  'full CH len:', data.length);
1046
- dbg('SRV-PSK', 'expected binder:', hexPreview(expectedBinder, 16));
1047
- dbg('SRV-PSK', 'received binder:', hexPreview(pskBinder, 16));
2364
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'expected binder:', hexPreview(expectedBinder, 16));
2365
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'received binder:', hexPreview(pskBinder, 16));
1048
2366
 
1049
2367
  // Constant-time binder comparison. A byte-by-byte early-exit compare
1050
2368
  // would let an attacker recover a valid binder via timing, one byte at
@@ -1056,6 +2374,9 @@ function TLSSession(options){
1056
2374
  if (binderOk) {
1057
2375
  context.psk_accepted = true;
1058
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);
1059
2380
  context.psk_offered = {
1060
2381
  psk: pskResult.psk instanceof Uint8Array ? pskResult.psk : new Uint8Array(pskResult.psk),
1061
2382
  cipher: pskCipher,
@@ -1064,17 +2385,32 @@ function TLSSession(options){
1064
2385
  // under. Stored here so selection does not have to re-derive it.
1065
2386
  hash: hashName,
1066
2387
  };
2388
+ if ('early_data' in message) early_consider(pskResult, pskIdentity, hashName);
1067
2389
  }
1068
2390
  }
1069
2391
  }
1070
2392
 
1071
2393
  // Client: detect if server accepted PSK from ServerHello (BEFORE set_context)
1072
2394
  if (!context.isServer && message.pre_shared_key && typeof message.pre_shared_key.selected === 'number') {
1073
- if (context.psk_offered) {
1074
- dbg('CLI-PSK', '✓ server accepted PSK, selected_identity:', message.pre_shared_key.selected);
1075
- context.psk_accepted = true;
1076
- 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;
1077
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);
1078
2414
  } else if (!context.isServer && context.psk_offered && message.type === 'server_hello') {
1079
2415
  dbg('CLI-PSK', '✗ server did NOT include pre_shared_key in SH — full handshake');
1080
2416
  }
@@ -1082,6 +2418,7 @@ function TLSSession(options){
1082
2418
  // Client: detect HelloRetryRequest (ServerHello with magic random)
1083
2419
  if (!context.isServer && message.random && uint8Equal(message.random, wire.TLS13_HRR_RANDOM)) {
1084
2420
  context.helloRetried = true;
2421
+ early_client_result(false); // §4.2.10: no early data after a HelloRetryRequest
1085
2422
 
1086
2423
  // Get cipher from HRR to determine hash
1087
2424
  let hrrCipher = null;
@@ -1089,6 +2426,7 @@ function TLSSession(options){
1089
2426
  else if (message.cipher_suite) hrrCipher = message.cipher_suite;
1090
2427
  if (!hrrCipher) hrrCipher = 0x1301;
1091
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
1092
2430
 
1093
2431
  // Replace transcript: CH1 → message_hash (RFC 8446 §4.4.1)
1094
2432
  // BUG FIX: The HRR was already pushed to transcript at the top of this block (line 195).
@@ -1406,6 +2744,41 @@ function TLSSession(options){
1406
2744
  }
1407
2745
  }
1408
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
+
1409
2782
  // Resolve our certificate BEFORE the set_context below.
1410
2783
  //
1411
2784
  // The reactive loop derives the cipher suite from the inputs present when
@@ -1415,18 +2788,63 @@ function TLSSession(options){
1415
2788
  // ran with no key in hand, so a server holding an ECDSA certificate could
1416
2789
  // pick an ECDHE_RSA suite and then present a certificate the peer rejects
1417
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.
1418
2796
  if (context.isServer === true && message.type === 'client_hello' &&
1419
- 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
+
1420
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
+
1421
2817
  if (!err && creds) {
1422
- // Direct assignment, not set_context: we are deliberately seeding
1423
- // inputs ahead of the single set_context below, which is what
1424
- // actually runs the loop. Going through set_context here would run
1425
- // 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.
1426
2820
  context.local_cert_chain = creds.certificateChain;
1427
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;
1428
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) : ''));
1429
2845
  });
2846
+
2847
+ sniSync = false;
1430
2848
  }
1431
2849
 
1432
2850
  set_context({
@@ -1482,10 +2900,13 @@ function TLSSession(options){
1482
2900
 
1483
2901
  // 1) Try ticket-based resumption (RFC 5077) — stateless, preferred over session_id
1484
2902
  if (message.session_ticket && message.session_ticket.length > 0 && context.ticketKeys && context.sessionTickets) {
2903
+ context.ticket_offered = true;
1485
2904
  let state = decrypt_session_blob(message.session_ticket, context.ticketKeys);
1486
- 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)) {
1487
2906
  // Honor ticket-based resumption: we'll proceed as abbreviated handshake once TLS 1.2 is selected.
1488
2907
  context.tls12_resume_state = state;
2908
+ restore_ticket_peer(state);
2909
+ context.resumed_ticket_data = state.app != null ? state.app : null;
1489
2910
  context.tls12_session_ticket_offered = message.session_ticket;
1490
2911
  }
1491
2912
  }
@@ -1514,7 +2935,11 @@ function TLSSession(options){
1514
2935
  } else if (typeof sessionData === 'object' && sessionData.master_secret) {
1515
2936
  state = sessionData;
1516
2937
  }
1517
- 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);
1518
2943
  set_context({
1519
2944
  tls12_resume_state: state,
1520
2945
  });
@@ -1547,7 +2972,7 @@ function TLSSession(options){
1547
2972
  let sentSid = context.local_session_id;
1548
2973
  let hasTicket = context.tls12_client_session.ticket && context.tls12_client_session.ticket.length > 0;
1549
2974
 
1550
- dbg('CLI-12RESUME', 'saved sid:', hexPreview(savedSid, 16),
2975
+ if (LEMON_DEBUG) dbg('CLI-12RESUME', 'saved sid:', hexPreview(savedSid, 16),
1551
2976
  'sent sid:', hexPreview(sentSid, 16),
1552
2977
  'received sid:', hexPreview(message.session_id, 16),
1553
2978
  'hasTicket:', hasTicket);
@@ -1572,6 +2997,7 @@ function TLSSession(options){
1572
2997
  if (abbreviatedDetected) {
1573
2998
  context.tls12_abbreviated = true;
1574
2999
  context.isResumed = true;
3000
+ restore_client_session_peer(context.tls12_client_session);
1575
3001
  // Load master_secret and EMS flag from saved session
1576
3002
  set_context({
1577
3003
  base_secret: context.tls12_client_session.master_secret,
@@ -1592,6 +3018,7 @@ function TLSSession(options){
1592
3018
 
1593
3019
  }else if(message.type=='client_key_exchange' || message.type=='server_key_exchange'){
1594
3020
 
3021
+ if (message.type === 'server_key_exchange') context.ske_received = true; // CertificateStatus comes before it
1595
3022
  pushTranscript(data);
1596
3023
 
1597
3024
  // TLS 1.2 ECDHE: the ServerKeyExchange is signed over
@@ -1641,7 +3068,7 @@ function TLSSession(options){
1641
3068
  context.peerSignatureScheme = message.sig_alg;
1642
3069
  }
1643
3070
 
1644
- 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
1645
3072
 
1646
3073
  // ServerKeyExchange carries the group; ClientKeyExchange does not (server already chose it)
1647
3074
  let kex_group = message.group || context.selected_group;
@@ -1675,6 +3102,7 @@ function TLSSession(options){
1675
3102
 
1676
3103
  }else if(message.type=='server_hello_done'){
1677
3104
 
3105
+ context.shd_received = true; // CertificateStatus comes before it
1678
3106
  pushTranscript(data);
1679
3107
 
1680
3108
 
@@ -1716,8 +3144,42 @@ function TLSSession(options){
1716
3144
  }
1717
3145
  }
1718
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
+
1719
3165
  pushTranscript(data);
1720
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
+
1721
3183
  // TLS 1.3: most server extensions arrive HERE, not in the
1722
3184
  // ServerHello (RFC 8446 §4.3.1) — including application-protocol
1723
3185
  // ones like use_srtp (RFC 8842 requires it in EncryptedExtensions).
@@ -1738,9 +3200,54 @@ function TLSSession(options){
1738
3200
  context.remote_extensions = _mergedExts;
1739
3201
  }
1740
3202
 
1741
- set_context({
1742
- remote_supported_groups: message.supported_groups || [],
1743
- });
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 });
1744
3251
 
1745
3252
  }else if(message.type=='certificate'){
1746
3253
 
@@ -1748,6 +3255,25 @@ function TLSSession(options){
1748
3255
 
1749
3256
  let entries = message.entries || [];
1750
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
+
1751
3277
  // RFC 8446 §4.4.2.4 / RFC 5246 §7.4.6.
1752
3278
  // - From a SERVER an empty certificate_list is always illegal: the
1753
3279
  // server has nothing else to authenticate with, so this is fatal here
@@ -1863,6 +3389,12 @@ function TLSSession(options){
1863
3389
 
1864
3390
  }else if(message.type=='finished'){
1865
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
+
1866
3398
  // ── Order enforcement: Finished may not follow a bare Certificate ──
1867
3399
  //
1868
3400
  // RFC 8446 §4.4.3: a peer that sends a non-empty Certificate MUST follow
@@ -1903,13 +3435,21 @@ function TLSSession(options){
1903
3435
  dbg('CLI-NST', 'received TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
1904
3436
  'hash:', hashName,
1905
3437
  'transcript len:', concatUint8Arrays(context.transcript).length);
1906
- dbg('CLI-NST', 'ticket_nonce:', hexPreview(message.ticket_nonce, 4),
3438
+ if (LEMON_DEBUG) dbg('CLI-NST', 'ticket_nonce:', hexPreview(message.ticket_nonce, 4),
1907
3439
  'age_add:', message.ticket_age_add,
1908
3440
  'lifetime:', message.ticket_lifetime);
1909
- dbg('CLI-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
3441
+ if (LEMON_DEBUG) dbg('CLI-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
1910
3442
  'derived psk:', hexPreview(psk, 8));
1911
3443
 
1912
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
+ }
1913
3453
  let session_blob = encode_client_session({
1914
3454
  v: 13, // blob kind: TLS 1.3
1915
3455
  version: context.selected_version,
@@ -1921,6 +3461,9 @@ function TLSSession(options){
1921
3461
  sni: context.local_sni || null,
1922
3462
  alpn: context.selected_alpn || null,
1923
3463
  created: Date.now(),
3464
+ max_early_data: maxEarly || undefined,
3465
+ qa: context.sessionAppData != null ? context.sessionAppData : undefined,
3466
+ ...client_session_peer_fields(),
1924
3467
  });
1925
3468
 
1926
3469
  ev.emit('session', session_blob);
@@ -1945,6 +3488,7 @@ function TLSSession(options){
1945
3488
  sni: context.local_sni || null,
1946
3489
  alpn: context.selected_alpn || null,
1947
3490
  created: Date.now(),
3491
+ ...client_session_peer_fields(),
1948
3492
  });
1949
3493
 
1950
3494
  ev.emit('session', session_blob);
@@ -1952,6 +3496,19 @@ function TLSSession(options){
1952
3496
  }
1953
3497
  }
1954
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
+
1955
3512
  }else if(message.type=='key_update'){
1956
3513
 
1957
3514
  // Peer is updating their traffic secret (we update our read key)
@@ -2018,6 +3575,14 @@ function TLSSession(options){
2018
3575
  if (options && typeof options === 'object'){
2019
3576
 
2020
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
+
2021
3586
  if('local_supported_versions' in options){
2022
3587
  if(arraysEqual(options.local_supported_versions,context.local_supported_versions)==false){
2023
3588
  context.local_supported_versions=options.local_supported_versions;
@@ -2491,6 +4056,39 @@ function TLSSession(options){
2491
4056
  if('selected_version' in params_to_set==false || params_to_set.selected_version==null){
2492
4057
  }
2493
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
+
2494
4092
  // TLS 1.2: clear key_share groups from ClientHello.
2495
4093
  // key_share is a TLS 1.3 extension; in TLS 1.2, keys come from CKE/SKE.
2496
4094
  // Without this, the server would compute the shared secret too early
@@ -2502,6 +4100,35 @@ function TLSSession(options){
2502
4100
  }
2503
4101
  }
2504
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
+
2505
4132
  //select selected_cipher...
2506
4133
  if (context.selected_cipher_suite == null && context.local_supported_cipher_suites.length > 0 && context.remote_supported_cipher_suites.length > 0) {
2507
4134
 
@@ -2610,8 +4237,19 @@ function TLSSession(options){
2610
4237
  return true;
2611
4238
  };
2612
4239
 
2613
- for (let i2 = 0; i2 < context.local_supported_cipher_suites.length; i2++) {
2614
- 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;
2615
4253
  if (!suiteAuthOk(cs)) continue;
2616
4254
  // A suite belongs to exactly one protocol generation (RFC 8446
2617
4255
  // §B.4). Selecting a 1.3 suite while negotiating 1.2 — which a
@@ -2666,17 +4304,68 @@ function TLSSession(options){
2666
4304
  }
2667
4305
 
2668
4306
  //select alpn...
2669
- if (context.selected_alpn == null && context.local_supported_alpns && context.remote_supported_alpns) {
2670
- // iterate local list by preference order
2671
- for (let a = 0; a < context.local_supported_alpns.length; a++) {
2672
- let cand = context.local_supported_alpns[a];
2673
- for (let b = 0; b < context.remote_supported_alpns.length; b++) {
2674
- if (context.remote_supported_alpns[b] === cand) {
2675
- params_to_set['selected_alpn'] = cand;
2676
- 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; }
2677
4355
  }
2678
4356
  }
2679
- 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;
2680
4369
  }
2681
4370
  }
2682
4371
 
@@ -2694,8 +4383,58 @@ function TLSSession(options){
2694
4383
  //select group...
2695
4384
  if (context.selected_group == null){
2696
4385
  if(context.local_supported_groups.length > 0 && context.remote_supported_groups.length > 0) {
2697
- 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++) {
4426
+ let g = context.local_supported_groups[i];
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++) {
2698
4436
  let g = context.local_supported_groups[i];
4437
+ if (is_kem_group(g) && !selIs13) continue;
2699
4438
  if (context.remote_supported_groups.indexOf(g) < 0) continue;
2700
4439
 
2701
4440
  // Mutual support is not enough: we must be able to actually PRODUCE
@@ -2789,7 +4528,32 @@ function TLSSession(options){
2789
4528
  // Curve-specific keygen lives in ecdh.js (single source of truth for
2790
4529
  // which groups exist and how their keys are formed); the loop only
2791
4530
  // decides WHEN a key is needed.
2792
- 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){
2793
4557
 
2794
4558
  let kp = ecdh_generate_keypair(context.selected_group);
2795
4559
  if (kp) {
@@ -2882,17 +4646,45 @@ function TLSSession(options){
2882
4646
  // Build and send HRR (it's a ServerHello with magic random)
2883
4647
  let hrr_body = wire.build_hello_retry_request({
2884
4648
  cipher_suite: hrrCipherNow,
2885
- 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,
2886
4657
  selected_group: hrrGroupNow,
2887
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,
2888
4665
  });
2889
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
+ }
2890
4671
  pushTranscript(hrr_data);
2891
4672
 
2892
4673
  ev.emit('message', 0, context.message_sent_seq, 'hello_retry_request', hrr_data);
2893
4674
  context.message_sent_seq++;
2894
4675
 
2895
- // 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; }
2896
4688
  context.remote_random = null;
2897
4689
  context.remote_extensions = [];
2898
4690
  context.remote_supported_versions = [];
@@ -2904,7 +4696,21 @@ function TLSSession(options){
2904
4696
 
2905
4697
  let can_send_hello=false;
2906
4698
 
2907
- 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){
2908
4714
 
2909
4715
  if(context.selected_version!==null && context.selected_cipher_suite!==null && context.selected_session_id!==null){
2910
4716
  if((is13())){
@@ -2923,6 +4729,18 @@ function TLSSession(options){
2923
4729
  }
2924
4730
  }
2925
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
+
2926
4744
  if(can_send_hello==true){
2927
4745
 
2928
4746
  if(context.local_random==null){
@@ -3008,11 +4826,44 @@ function TLSSession(options){
3008
4826
  { type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) }
3009
4827
  ];
3010
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
+
3011
4835
  // Only echo extended_master_secret if client sent it
3012
4836
  if (context.use_extended_master_secret) {
3013
4837
  ext_list.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
3014
4838
  }
3015
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
+
3016
4867
  if (context.selected_alpn) {
3017
4868
  // RFC 7301: ServerHello echoes a single selected protocol
3018
4869
  ext_list.push({ type: 'ALPN', value: [ String(context.selected_alpn) ] });
@@ -3058,6 +4909,8 @@ function TLSSession(options){
3058
4909
  sid_to_send = context.remote_session_id || new Uint8Array(0);
3059
4910
  } else if (context.selected_version === wire.DTLS_VERSION.DTLS1_2) {
3060
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
3061
4914
  } else {
3062
4915
  if (!context.tls12_session_id_for_store) {
3063
4916
  context.tls12_session_id_for_store = new Uint8Array(crypto.randomBytes(32));
@@ -3083,8 +4936,33 @@ function TLSSession(options){
3083
4936
  if(build_message_params!==null){
3084
4937
 
3085
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
+
3086
4945
  let message_data = build_tls_message(build_message_params);
3087
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
+
3088
4966
  pushTranscript(message_data);
3089
4967
 
3090
4968
  context.hello_sent=true;
@@ -3104,7 +4982,24 @@ function TLSSession(options){
3104
4982
 
3105
4983
 
3106
4984
  //get base_secret
3107
- 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){
3108
5003
  if((is13()) && (context.ecdhe_shared_secret !== null)){
3109
5004
 
3110
5005
  let hashName = negotiated_hash();
@@ -3184,6 +5079,8 @@ function TLSSession(options){
3184
5079
  if(context.selected_alpn!==null){
3185
5080
  extensions.push({ type: 'ALPN', value: [context.selected_alpn] });
3186
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) });
3187
5084
 
3188
5085
  // server_name acknowledgement (RFC 6066 §3, RFC 8446 §4.4.2).
3189
5086
  // When a client sent SNI and we accepted it, the server confirms by
@@ -3203,6 +5100,21 @@ function TLSSession(options){
3203
5100
  }
3204
5101
 
3205
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
+
3206
5118
  // DTLS-SRTP answer (RFC 5764 §4.1). One profile, empty MKI.
3207
5119
  let srtpSel = negotiate_srtp_profile();
3208
5120
  if (srtpSel !== null) {
@@ -3211,6 +5123,13 @@ function TLSSession(options){
3211
5123
  value: { profiles: [srtpSel], mki: new Uint8Array(0) } });
3212
5124
  }
3213
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
+
3214
5133
  for(let i = 0; i < context.local_extensions.length; i++){
3215
5134
  extensions.push(context.local_extensions[i]);
3216
5135
  }
@@ -3239,6 +5158,8 @@ function TLSSession(options){
3239
5158
  type: 'certificate_request',
3240
5159
  certificate_request_context: new Uint8Array(0),
3241
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,
3242
5163
  });
3243
5164
  pushTranscript(cr_data);
3244
5165
  context.certificateRequestSent = true;
@@ -3257,15 +5178,18 @@ function TLSSession(options){
3257
5178
 
3258
5179
  if(certDue13 || certDue12){
3259
5180
 
3260
- let message_data = build_tls_message({
3261
- type: 'certificate',
3262
- version: context.selected_version,
3263
- entries: context.local_cert_chain
3264
- });
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));
3265
5191
  pushTranscript(message_data);
3266
5192
 
3267
- context.cert_sent=true;
3268
-
3269
5193
  if ((is13())){
3270
5194
  ev.emit('message',1,context.message_sent_seq,'certificate',message_data);
3271
5195
  }else{
@@ -3273,6 +5197,18 @@ function TLSSession(options){
3273
5197
  }
3274
5198
 
3275
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;
3276
5212
 
3277
5213
 
3278
5214
  }
@@ -3303,7 +5239,8 @@ function TLSSession(options){
3303
5239
  let selected_scheme = pick_scheme(
3304
5240
  wire.TLS_VERSION.TLS1_3,
3305
5241
  cert_private_key_obj,
3306
- context.remote_supported_signature_algorithms || []
5242
+ context.remote_supported_signature_algorithms || [],
5243
+ context.local_supported_signature_algorithms
3307
5244
  );
3308
5245
 
3309
5246
  let sig_data = null;
@@ -3323,6 +5260,7 @@ function TLSSession(options){
3323
5260
  }
3324
5261
 
3325
5262
  if(sig_data){
5263
+ context.selected_signature_algorithm = selected_scheme; // reported (getNegotiationResult)
3326
5264
 
3327
5265
  let message_data = build_tls_message({
3328
5266
  type: 'certificate_verify',
@@ -3420,7 +5358,7 @@ function TLSSession(options){
3420
5358
  // Pick scheme matching our cert + server's requested algorithms
3421
5359
  let cert_key_obj = local_private_key();
3422
5360
  let reqAlgs = peer_requested_sig_algs();
3423
- 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);
3424
5362
  let signature = sign_with_scheme(wire.TLS_VERSION.TLS1_2, scheme, transcript_data, cert_key_obj);
3425
5363
 
3426
5364
  // Build CertificateVerify: scheme(2) + sig_length(2) + sig
@@ -3452,7 +5390,7 @@ function TLSSession(options){
3452
5390
 
3453
5391
  let cert_private_key_obj = local_private_key();
3454
5392
 
3455
- 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);
3456
5394
 
3457
5395
  let sig_data = sign_with_scheme(wire.TLS_VERSION.TLS1_2, scheme12, tbs_data, cert_private_key_obj);
3458
5396
 
@@ -3566,6 +5504,10 @@ function TLSSession(options){
3566
5504
  sni: context.selected_sni || context.remote_sni || null,
3567
5505
  alpn: context.selected_alpn || null,
3568
5506
  created: Date.now(),
5507
+ lifetime: context.ticketLifetime,
5508
+ app: ticket_app_data(),
5509
+ cx: context.ticketContext || undefined,
5510
+ ...ticket_peer_fields(),
3569
5511
  }, context.ticketKeys);
3570
5512
 
3571
5513
  let nst_data = build_tls_message({
@@ -3597,6 +5539,26 @@ function TLSSession(options){
3597
5539
  }
3598
5540
  }
3599
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
+
3600
5562
  //send finished...
3601
5563
  // Client: send Certificate + CertificateVerify before Finished (if server requested)
3602
5564
  if(context.isServer==false && context.certificateRequested && !context.clientCertSent &&
@@ -3605,13 +5567,12 @@ function TLSSession(options){
3605
5567
 
3606
5568
  if(context.clientCert && context.clientKey){
3607
5569
  // Send client certificate
3608
- let certCtx = createSecureContext({ key: context.clientKey, cert: context.clientCert });
3609
- let cert_data = build_tls_message({
3610
- type: 'certificate',
3611
- version: wire.TLS_VERSION.TLS1_3,
3612
- entries: certCtx.certificateChain,
3613
- certificate_request_context: context.certificateRequestContext || new Uint8Array(0),
3614
- });
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));
3615
5576
  pushTranscript(cert_data);
3616
5577
  ev.emit('message', 1, context.message_sent_seq, 'certificate', cert_data);
3617
5578
  context.message_sent_seq++;
@@ -3631,7 +5592,7 @@ function TLSSession(options){
3631
5592
  });
3632
5593
  let peerSigAlgs = peer_requested_sig_algs();
3633
5594
 
3634
- 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);
3635
5596
  if (scheme === null) {
3636
5597
  fatalAlert(wire.TLS_ALERT.HANDSHAKE_FAILURE, 'No signature scheme shared with server for client certificate');
3637
5598
  return;
@@ -4052,8 +6013,13 @@ function TLSSession(options){
4052
6013
 
4053
6014
  context.state='connected';
4054
6015
  context.handshakeEndTime = Date.now();
4055
- ev.emit('secureConnect');
4056
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.
4057
6023
  // TLS 1.3: compute resumption_master_secret (both client and server need it)
4058
6024
  if ((is13()) && context.tls13_master_secret && !context.resumption_master_secret) {
4059
6025
  let hashName = negotiated_hash();
@@ -4063,73 +6029,19 @@ function TLSSession(options){
4063
6029
  }
4064
6030
 
4065
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.
4066
6037
  if ((is13()) && context.isServer && !context.session_ticket_sent && context.sessionTickets && context.resumption_master_secret) {
4067
6038
  context.session_ticket_sent = true;
4068
-
4069
- let hashName = negotiated_hash();
4070
- let ticket_nonce = new Uint8Array([context.ticket_nonce_counter++]);
4071
- let psk = derive_psk(hashName, context.resumption_master_secret, ticket_nonce, label_prefix());
4072
- let ticket_age_add = crypto.randomBytes(4).readUInt32BE(0);
4073
- let ticket_lifetime = context.ticketLifetime;
4074
-
4075
- dbg('SRV-NST', 'issuing TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
4076
- 'hash:', hashName,
4077
- 'transcript len:', concatUint8Arrays(context.transcript).length);
4078
- dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
4079
- 'age_add:', ticket_age_add,
4080
- 'lifetime:', ticket_lifetime);
4081
- dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
4082
- 'derived psk:', hexPreview(psk, 8));
4083
-
4084
- // Ensure ticketKeys is 48 bytes (key_name + aes_key)
4085
- if (!context.ticketKeys || context.ticketKeys.length !== 48) {
4086
- context.ticketKeys = crypto.randomBytes(48);
4087
- }
4088
-
4089
- // Encrypt session state into opaque ticket (unified format: key_name(16) | IV(12) | CT | Tag(16))
4090
- let ticket = encrypt_session_blob({
4091
- v: 13, // blob kind: TLS 1.3 PSK
4092
- version: context.selected_version,
4093
- cipher: context.selected_cipher_suite,
4094
- psk: psk,
4095
- age_add: ticket_age_add,
4096
- sni: context.selected_sni || context.remote_sni || null,
4097
- alpn: context.selected_alpn || null,
4098
- created: Date.now(),
4099
- }, context.ticketKeys);
4100
-
4101
- let nst_data = wire.build_message(wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET,
4102
- wire.build_new_session_ticket({
4103
- ticket_lifetime: ticket_lifetime,
4104
- ticket_age_add: ticket_age_add,
4105
- ticket_nonce: ticket_nonce,
4106
- ticket: ticket,
4107
- extensions: []
4108
- })
4109
- );
4110
-
4111
- ev.emit('message', 2, context.message_sent_seq, 'new_session_ticket', nst_data);
4112
- context.message_sent_seq++;
4113
-
4114
- // Emit 'session' event on server side too — lets users track when tickets are
4115
- // issued (e.g. for monitoring or metrics). Not part of Node.js API but useful.
4116
- // Emits the same Buffer the client would receive via their 'session' event,
4117
- // so server-side apps could also persist it if they want.
4118
- let server_session_blob = encode_client_session({
4119
- v: 13,
4120
- version: context.selected_version,
4121
- cipher: context.selected_cipher_suite,
4122
- ticket: ticket,
4123
- psk: psk,
4124
- age_add: ticket_age_add,
4125
- lifetime: ticket_lifetime,
4126
- sni: context.selected_sni || context.remote_sni || null,
4127
- alpn: context.selected_alpn || null,
4128
- created: Date.now(),
4129
- });
4130
- ev.emit('session', server_session_blob);
6039
+ issue_ticket_13();
6040
+ if (!context.psk_accepted) issue_ticket_13();
4131
6041
  }
4132
6042
 
6043
+ ev.emit('secureConnect');
6044
+
4133
6045
  // TLS 1.2 server: emit 'newSession' for Session ID-based resumption.
4134
6046
  // Fires whenever we generated a session_id for this connection AND didn't issue
4135
6047
  // a NewSessionTicket (so the client can only resume via Session ID — we need the
@@ -4155,6 +6067,10 @@ function TLSSession(options){
4155
6067
  sni: context.selected_sni || context.remote_sni || null,
4156
6068
  alpn: context.selected_alpn || null,
4157
6069
  created: Date.now(),
6070
+ lifetime: context.ticketLifetime,
6071
+ app: ticket_app_data(),
6072
+ cx: context.ticketContext || undefined,
6073
+ ...ticket_peer_fields(),
4158
6074
  }, context.ticketKeys);
4159
6075
 
4160
6076
  // User stores this; returns it on next handshake via 'resumeSession' callback.
@@ -4188,6 +6104,7 @@ function TLSSession(options){
4188
6104
  sni: context.local_sni || null,
4189
6105
  alpn: context.selected_alpn || null,
4190
6106
  created: Date.now(),
6107
+ ...client_session_peer_fields(),
4191
6108
  });
4192
6109
  ev.emit('session', session_blob);
4193
6110
  }
@@ -4199,6 +6116,75 @@ function TLSSession(options){
4199
6116
  }
4200
6117
  }
4201
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
+ }
4202
6188
 
4203
6189
  function validatePeerCertificate() {
4204
6190
  if (!context.remote_cert_chain || context.remote_cert_chain.length === 0) {
@@ -4225,14 +6211,6 @@ function TLSSession(options){
4225
6211
  return;
4226
6212
  }
4227
6213
 
4228
- // Check hostname (client-side only, when SNI is set)
4229
- if (!context.isServer && context.local_sni) {
4230
- if (!x509.checkHost(context.local_sni)) {
4231
- context.authorizationError = 'ERR_TLS_CERT_ALTNAME_INVALID';
4232
- context.peerAuthorized = false;
4233
- return;
4234
- }
4235
- }
4236
6214
 
4237
6215
  // Verify the full chain against the configured trust anchors.
4238
6216
  //
@@ -4245,15 +6223,34 @@ function TLSSession(options){
4245
6223
  // and sets rejectUnauthorized:false) may rely on it — but it is now
4246
6224
  // explicit, recorded in authorizationError, and trivial to flip.
4247
6225
  // See LEMON_TLS_INSECURE_NO_CA below.
4248
- if (context.ca == null ||
4249
- (Array.isArray(context.ca) && context.ca.length === 0)) {
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;
6238
+ }
6239
+
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;
4250
6247
  context.peerAuthorized = true;
4251
6248
  context.authorizationError = null;
4252
6249
  context.certChainUnverified = true; // no trust anchors were available
4253
6250
  return;
4254
6251
  }
4255
6252
 
4256
- let cas = Array.isArray(context.ca) ? context.ca : [context.ca];
6253
+ let cas = Array.isArray(anchors) ? anchors : [anchors];
4257
6254
  let chainResult = verify_cert_chain(context.remote_cert_chain, cas, now);
4258
6255
  if (!chainResult.ok) {
4259
6256
  context.authorizationError = chainResult.error;
@@ -4261,6 +6258,17 @@ function TLSSession(options){
4261
6258
  return;
4262
6259
  }
4263
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
+
4264
6272
  // All checks passed
4265
6273
  context.peerAuthorized = true;
4266
6274
  context.authorizationError = null;
@@ -4399,6 +6407,7 @@ function TLSSession(options){
4399
6407
  for (let gi = 0; gi < context.local_supported_groups.length; gi++) {
4400
6408
  let g = context.local_supported_groups[gi];
4401
6409
  if (!is_supported_group(g)) continue;
6410
+ if (!offers13 && is_kem_group(g)) continue; // not in a 1.2-only hello (see above)
4402
6411
  let kp = ecdh_generate_keypair(g);
4403
6412
  if (!kp) continue;
4404
6413
  initialGroup = g;
@@ -4419,6 +6428,25 @@ function TLSSession(options){
4419
6428
  private_key: initialKp.private_key
4420
6429
  };
4421
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
+
4422
6450
  // Honor the configured signature algorithms; the hardcoded list is only
4423
6451
  // the default when none were set (previously the configured list was
4424
6452
  // ignored here — while the HRR CH2 hardcoded its own, so CH1 and CH2
@@ -4446,9 +6474,19 @@ function TLSSession(options){
4446
6474
  let extensions = build_client_hello_extensions({
4447
6475
  keyShareGroup: initialGroup,
4448
6476
  keySharePublic: public_key,
6477
+ extraKeyShares: extraKeyShares,
4449
6478
  cookie: null
4450
6479
  });
4451
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
+
4452
6490
  // Resumption: check if session was provided (opaque Buffer) — decode to structured data
4453
6491
  let sessionData = null;
4454
6492
  if (options.session) {
@@ -4458,6 +6496,9 @@ function TLSSession(options){
4458
6496
  } else if (typeof options.session === 'object') {
4459
6497
  sessionData = options.session; // legacy: already-structured object
4460
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;
4461
6502
  } else if (options.psk) {
4462
6503
  sessionData = options.psk; // legacy path
4463
6504
  }
@@ -4466,8 +6507,8 @@ function TLSSession(options){
4466
6507
 
4467
6508
  // TLS 1.3 PSK resumption (sessionData contains psk)
4468
6509
  if (sessionData && sessionData.psk && sessionData.ticket && sessionData.cipher) {
4469
- // Add PSK key exchange modes (psk_dhe_ke = 1)
4470
- 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.
4471
6512
 
4472
6513
  // Save PSK for later verification
4473
6514
  context.psk_offered = {
@@ -4475,6 +6516,8 @@ function TLSSession(options){
4475
6516
  psk: sessionData.psk instanceof Uint8Array ? sessionData.psk : new Uint8Array(sessionData.psk),
4476
6517
  cipher: sessionData.cipher,
4477
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)
4478
6521
  };
4479
6522
 
4480
6523
  // Compute obfuscated ticket age
@@ -4493,8 +6536,41 @@ function TLSSession(options){
4493
6536
  binders: [placeholderBinder]
4494
6537
  }
4495
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
+ }
4496
6558
  extensions.push(pskExt); // MUST be last
4497
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
+ }
4498
6574
 
4499
6575
  // Build the full message with placeholder
4500
6576
  let build_message_params = {
@@ -4507,7 +6583,7 @@ function TLSSession(options){
4507
6583
  // builder actually reads; the singular key was silently ignored and
4508
6584
  // wire.js substituted its own hardcoded fallback list — the
4509
6585
  // configured cipher suites never reached the wire. Both names passed.
4510
- cipher_suites: context.local_supported_cipher_suites,
6586
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
4511
6587
  cipher_suite: context.local_supported_cipher_suites,
4512
6588
  extensions: extensions
4513
6589
  };
@@ -4521,23 +6597,23 @@ function TLSSession(options){
4521
6597
  let binder_key = derive_binder_key(hashName, context.psk_offered.psk, false, label_prefix());
4522
6598
  let binder = compute_psk_binder(hashName, binder_key, truncatedMessage, label_prefix());
4523
6599
 
4524
- dbg('CLI-PSK', 'ticket:', hexPreview(sessionData.ticket, 24),
6600
+ if (LEMON_DEBUG) dbg('CLI-PSK', 'ticket:', hexPreview(sessionData.ticket, 24),
4525
6601
  'cipher:', '0x' + sessionData.cipher.toString(16),
4526
6602
  'hash:', hashName);
4527
- dbg('CLI-PSK', 'psk:', hexPreview(sessionData.psk, 8),
6603
+ if (LEMON_DEBUG) dbg('CLI-PSK', 'psk:', hexPreview(sessionData.psk, 8),
4528
6604
  'age_add:', sessionData.age_add,
4529
6605
  'lifetime:', sessionData.lifetime,
4530
6606
  'ticketAge (ms):', ticketAge,
4531
6607
  'obfuscatedAge:', obfuscatedAge);
4532
6608
  dbg('CLI-PSK', 'truncatedMessage len:', truncatedMessage.length,
4533
6609
  'full CH len (after real binder):', 'see next');
4534
- dbg('CLI-PSK', 'sent binder:', hexPreview(binder, 16));
6610
+ if (LEMON_DEBUG) dbg('CLI-PSK', 'sent binder:', hexPreview(binder, 16));
4535
6611
 
4536
6612
  // Rebuild with real binder
4537
6613
  pskExt.value.binders = [binder];
4538
6614
  message_data = build_tls_message(build_message_params);
4539
6615
 
4540
- } else if (sessionData && sessionData.v === 12 && sessionData.master_secret) {
6616
+ } else if (!echSplit && sessionData && sessionData.v === 12 && sessionData.master_secret) {
4541
6617
  // TLS 1.2 resumption: session ID and/or SessionTicket
4542
6618
  // Save for later verification when ServerHello arrives
4543
6619
  context.tls12_client_session = sessionData;
@@ -4577,7 +6653,7 @@ function TLSSession(options){
4577
6653
  // builder actually reads; the singular key was silently ignored and
4578
6654
  // wire.js substituted its own hardcoded fallback list — the
4579
6655
  // configured cipher suites never reached the wire. Both names passed.
4580
- cipher_suites: context.local_supported_cipher_suites,
6656
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
4581
6657
  cipher_suite: context.local_supported_cipher_suites,
4582
6658
  extensions: extensions
4583
6659
  };
@@ -4598,18 +6674,39 @@ function TLSSession(options){
4598
6674
  // builder actually reads; the singular key was silently ignored and
4599
6675
  // wire.js substituted its own hardcoded fallback list — the
4600
6676
  // configured cipher suites never reached the wire. Both names passed.
4601
- cipher_suites: context.local_supported_cipher_suites,
6677
+ cipher_suites: grease_cipher_suites(context.local_supported_cipher_suites),
4602
6678
  cipher_suite: context.local_supported_cipher_suites,
4603
6679
  extensions: extensions
4604
6680
  };
4605
6681
  message_data = build_tls_message(build_message_params);
4606
6682
  }
4607
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
+
4608
6696
  pushTranscript(message_data);
4609
6697
 
4610
6698
  context.hello_sent=true;
4611
6699
 
4612
- 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);
4613
6710
 
4614
6711
  context.message_sent_seq++;
4615
6712
 
@@ -4618,6 +6715,375 @@ function TLSSession(options){
4618
6715
 
4619
6716
 
4620
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
+
4621
7087
  let api = {
4622
7088
  /**
4623
7089
  * Raw context object. Advanced users (QUIC, DTLS) can read/write
@@ -4681,6 +7147,114 @@ function TLSSession(options){
4681
7147
  return context.selected_cipher_suite;
4682
7148
  },
4683
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
+
4684
7258
  /** Returns the negotiated ALPN protocol string (e.g. 'h2'), or null. */
4685
7259
  getALPN: function(){
4686
7260
  return context.selected_alpn || null;
@@ -4688,7 +7262,44 @@ function TLSSession(options){
4688
7262
 
4689
7263
  /** Returns the remote certificate chain, or null. */
4690
7264
  getPeerCertificate: function(){
4691
- 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;
4692
7303
  },
4693
7304
 
4694
7305
  /** Whether the peer certificate passed validation. */
@@ -4847,36 +7458,48 @@ function TLSSession(options){
4847
7458
  cipher: context.selected_cipher_suite,
4848
7459
  cipherName: cipherInfo ? cipherInfo.name : null,
4849
7460
  group: context.selected_group,
4850
- groupName: context.selected_group === 0x001d ? 'X25519' : context.selected_group === 0x0017 ? 'P-256' : context.selected_group === 0x0018 ? 'P-384' : null,
4851
- 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),
4852
7468
  alpn: context.selected_alpn,
4853
7469
  sni: context.selected_sni || context.local_sni,
4854
7470
  resumed: context.isResumed,
4855
7471
  helloRetried: context.helloRetried,
4856
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
+ },
4857
7481
  };
4858
7482
  },
4859
7483
 
4860
- /** Compute JA3 fingerprint from the ClientHello (server-side only).
4861
- * Returns { hash, raw } or null if no ClientHello available.
4862
- * 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').
4863
7499
  */
4864
- getJA3: function(){
4865
- if (!context.rawClientHello) return null;
4866
- try {
4867
- let hello = parse_tls_message(context.rawClientHello);
4868
-
4869
- let version = hello.client_version || 0x0303;
4870
- let ciphers = (hello.cipher_suites || []).filter(c => (c & 0x0F0F) !== 0x0A0A).join('-');
4871
- let extensions = (hello.extensions || []).map(e => e.type).filter(t => t !== 0x0A0A).join('-');
4872
- let curves = (hello.supported_groups || []).filter(g => (g & 0x0F0F) !== 0x0A0A).join('-');
4873
- let pointFormats = (hello.ec_point_formats || [0]).join('-');
4874
-
4875
- let raw = [version, ciphers, extensions, curves, pointFormats].join(',');
4876
- let hash = crypto.createHash('md5').update(raw).digest('hex');
4877
- return { hash, raw };
4878
- } catch(e) { return null; }
4879
- },
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); },
4880
7503
 
4881
7504
  /** Request a TLS 1.3 Key Update. requestPeer=true means ask the other side to update too. */
4882
7505
  requestKeyUpdate: function(requestPeer){
@@ -4897,7 +7520,14 @@ function TLSSession(options){
4897
7520
  },
4898
7521
  };
4899
7522
 
4900
- 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
+ }
4901
7531
  // Re-define dynamic getters (the for-in loop flattens them to values)
4902
7532
  Object.defineProperty(this, 'isResumed', { get: function() { return context.isResumed; }, configurable: true, enumerable: true });
4903
7533
  return this;