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