lemon-tls 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -20
- package/index.d.ts +94 -6
- package/index.js +126 -0
- package/package.json +1 -1
- package/src/compat.js +316 -45
- package/src/crypto.js +19 -0
- package/src/dtls_session.js +62 -6
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +72 -5
- package/src/secure_context.js +49 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +39 -4
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +38 -25
- package/src/tls_session.js +2825 -195
- package/src/tls_socket.js +839 -148
- package/src/utils.js +68 -1
- package/src/wire.js +148 -9
package/src/tls_session.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
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: '
|
|
149
|
-
if (!_validNow(cand, now)) return { ok: false, error:
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
1428
|
+
let ch2Params = {
|
|
707
1429
|
type: 'client_hello',
|
|
708
1430
|
version: 0x0303,
|
|
709
|
-
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',
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
+
dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
|
|
1870
|
+
'age_add:', ticket_age_add,
|
|
1871
|
+
'lifetime:', ticket_lifetime);
|
|
1872
|
+
dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
|
|
1873
|
+
'derived psk:', hexPreview(psk, 8));
|
|
1874
|
+
|
|
1875
|
+
// Ensure ticketKeys is 48 bytes (key_name + aes_key)
|
|
1876
|
+
if (!context.ticketKeys || context.ticketKeys.length !== 48) {
|
|
1877
|
+
context.ticketKeys = crypto.randomBytes(48);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Encrypt session state into opaque ticket (unified format: key_name(16) | IV(12) | CT | Tag(16))
|
|
1881
|
+
let ticket = encrypt_session_blob({
|
|
1882
|
+
v: 13, // blob kind: TLS 1.3 PSK
|
|
1883
|
+
version: context.selected_version,
|
|
1884
|
+
cipher: context.selected_cipher_suite,
|
|
1885
|
+
psk: psk,
|
|
1886
|
+
age_add: ticket_age_add,
|
|
1887
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
1888
|
+
alpn: context.selected_alpn || null,
|
|
1889
|
+
created: Date.now(),
|
|
1890
|
+
lifetime: ticket_lifetime,
|
|
1891
|
+
app: ticket_app_data(),
|
|
1892
|
+
st: context.ticket_state != null ? context.ticket_state : undefined,
|
|
1893
|
+
cx: context.ticketContext || undefined,
|
|
1894
|
+
// 0-RTT allowed with it, and under what (open_ticket_13, early_eligible).
|
|
1895
|
+
ed: context.earlyData ? 1 : undefined,
|
|
1896
|
+
edc: context.earlyData ? context.earlyDataContext : undefined,
|
|
1897
|
+
...ticket_peer_fields(),
|
|
1898
|
+
}, context.ticketKeys);
|
|
1899
|
+
|
|
1900
|
+
// early_data in the ticket (RFC 8446 §4.2.10): max_early_data_size.
|
|
1901
|
+
let nstExts = [];
|
|
1902
|
+
if (context.earlyData) {
|
|
1903
|
+
let m = context.maxEarlyData >>> 0;
|
|
1904
|
+
nstExts.push({ type: wire.TLS_EXT.EARLY_DATA, data: new Uint8Array([m >>> 24 & 255, m >>> 16 & 255, m >>> 8 & 255, m & 255]) });
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
let nst_data = wire.build_message(wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET,
|
|
1908
|
+
wire.build_new_session_ticket({
|
|
1909
|
+
ticket_lifetime: ticket_lifetime,
|
|
1910
|
+
ticket_age_add: ticket_age_add,
|
|
1911
|
+
ticket_nonce: ticket_nonce,
|
|
1912
|
+
ticket: ticket,
|
|
1913
|
+
extensions: nstExts
|
|
1914
|
+
})
|
|
1915
|
+
);
|
|
1916
|
+
|
|
1917
|
+
ev.emit('message', 2, context.message_sent_seq, 'new_session_ticket', nst_data);
|
|
1918
|
+
context.message_sent_seq++;
|
|
1919
|
+
|
|
1920
|
+
// Emit 'session' event on server side too — lets users track when tickets are
|
|
1921
|
+
// issued (e.g. for monitoring or metrics). Not part of Node.js API but useful.
|
|
1922
|
+
// Emits the same Buffer the client would receive via their 'session' event,
|
|
1923
|
+
// so server-side apps could also persist it if they want.
|
|
1924
|
+
let server_session_blob = encode_client_session({
|
|
1925
|
+
v: 13,
|
|
1926
|
+
version: context.selected_version,
|
|
1927
|
+
cipher: context.selected_cipher_suite,
|
|
1928
|
+
ticket: ticket,
|
|
1929
|
+
psk: psk,
|
|
1930
|
+
age_add: ticket_age_add,
|
|
1931
|
+
lifetime: ticket_lifetime,
|
|
1932
|
+
sni: context.selected_sni || context.remote_sni || null,
|
|
1933
|
+
alpn: context.selected_alpn || null,
|
|
1934
|
+
created: Date.now(),
|
|
1935
|
+
});
|
|
1936
|
+
ev.emit('session', server_session_blob);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// What the application wants in the ticket being issued (ticketData).
|
|
1940
|
+
// Never allowed to break a handshake: a throw, something that does not
|
|
1941
|
+
// serialize, or anything too big means a ticket without application data.
|
|
1942
|
+
// Too big because a ticket travels in every ClientHello that presents it —
|
|
1943
|
+
// over QUIC that ClientHello should fit the first packet.
|
|
1944
|
+
const TICKET_DATA_MAX = 256; // bytes, serialized
|
|
1945
|
+
const TICKET_STATE_MAX = 128; // setTicketState's, apart from it
|
|
1946
|
+
//
|
|
1947
|
+
// Asked once per connection, and the answer kept: the ticket issued and
|
|
1948
|
+
// getTicketInfo() — asked by the application for this very connection —
|
|
1949
|
+
// must carry the same value. Asked twice, ticketData minting a new id on a
|
|
1950
|
+
// full handshake would give the connection one id and its ticket another,
|
|
1951
|
+
// and the first connection would never be tied to the ones resuming it.
|
|
1952
|
+
function ticket_app_data() {
|
|
1953
|
+
if (context.app_ticket_data !== undefined) return context.app_ticket_data;
|
|
1954
|
+
context.app_ticket_data = ticket_app_data_ask();
|
|
1955
|
+
return context.app_ticket_data;
|
|
1956
|
+
}
|
|
1957
|
+
function ticket_app_data_ask() {
|
|
1958
|
+
if (!context.ticketData) return null;
|
|
1959
|
+
let data;
|
|
1960
|
+
try { data = context.ticketData(context.isResumed ? context.resumed_ticket_data : null); }
|
|
1961
|
+
catch (e) { dbg('SRV-NST', 'ticketData threw:', e && e.message); return null; }
|
|
1962
|
+
if (data == null) return null;
|
|
1963
|
+
let size;
|
|
1964
|
+
try { size = Buffer.byteLength(JSON.stringify(data)); } catch (e) { return null; }
|
|
1965
|
+
if (!(size <= TICKET_DATA_MAX)) {
|
|
1966
|
+
dbg('SRV-NST', 'ticketData is ' + size + ' bytes, over ' + TICKET_DATA_MAX + ' — left out');
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
return data;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
785
1972
|
function label_prefix() {
|
|
786
|
-
return context.selected_version
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
2322
|
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
|
-
|
|
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
2340
|
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,7 +2350,12 @@ 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,
|
|
@@ -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
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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:
|
|
1423
|
-
//
|
|
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
|
});
|
|
@@ -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 (
|
|
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
|
-
|
|
1742
|
-
|
|
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
|
|
@@ -1910,6 +3442,14 @@ function TLSSession(options){
|
|
|
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
|
-
|
|
2614
|
-
|
|
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
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
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
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4249
|
-
|
|
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(
|
|
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
|
-
//
|
|
4470
|
-
|
|
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
|
};
|
|
@@ -4537,7 +6613,7 @@ function TLSSession(options){
|
|
|
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
|
-
|
|
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
|
|
4851
|
-
|
|
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
|
-
/**
|
|
4861
|
-
*
|
|
4862
|
-
*
|
|
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
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
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
|
-
|
|
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;
|