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.
@@ -0,0 +1,715 @@
1
+ /**
2
+ * TLS Encrypted Client Hello (RFC 9849) — the client-facing server, shared
3
+ * mode: this server decrypts the ClientHello and is also the backend that
4
+ * answers it.
5
+ *
6
+ * Without ECH the first message of every TLS connection — the ClientHello,
7
+ * with the name of the site (SNI) and the ALPN list — crosses the network in
8
+ * the clear; HTTPS hides everything after it. With ECH the client sends two
9
+ * hellos in one: an outer ClientHello in the clear naming a shared
10
+ * "public_name", and inside it the real one (ClientHelloInner), encrypted
11
+ * with HPKE to a public key the server publishes in DNS (the `ech` key of
12
+ * an HTTPS record, RFC 9460). The server decrypts the inner hello and the
13
+ * handshake continues with it; it tells the client so with 8 bytes of the
14
+ * ServerHello random that only a holder of the inner hello can compute.
15
+ *
16
+ * When the server cannot decrypt (a key it no longer holds, or a client
17
+ * sending GREASE ECH) it completes the handshake with the outer hello, as
18
+ * the public_name — its certificate must cover that name — and sends
19
+ * `retry_configs`, its current keys, which the client takes and reconnects
20
+ * with (§6.1.6).
21
+ *
22
+ * The key is not authenticated by any CA: its trust comes from DNS. A
23
+ * substituted key lets an attacker read which name the client asked for,
24
+ * never impersonate the server — the certificate is still checked against
25
+ * that name.
26
+ *
27
+ * TLS only: the DTLS variant (other hello layout, other header) is not here.
28
+ */
29
+
30
+ import crypto from 'node:crypto';
31
+ import { hkdf_extract, hkdf_expand_label } from '../crypto.js';
32
+ import { hpke_suite, is_supported_hpke_suite, setup_recipient, setup_sender,
33
+ hpke_generate_key_pair, hpke_public_key } from './hpke.js';
34
+
35
+ var ECH_VERSION = 0xfe0d; // the extension, and the ECHConfig version
36
+ var EXT_ECH = 0xfe0d;
37
+ var EXT_ECH_OUTER_EXTENSIONS = 0xfd00;
38
+ var EXT_SERVER_NAME = 0;
39
+ var EXT_SUPPORTED_VERSIONS = 43;
40
+
41
+ var ALERT_ILLEGAL_PARAMETER = 47;
42
+ var ALERT_DECODE_ERROR = 50;
43
+ var ALERT_DECRYPT_ERROR = 51;
44
+ var ALERT_MISSING_EXTENSION = 109;
45
+
46
+ function eccError(msg, alert) {
47
+ var e = new Error('ECH: ' + msg);
48
+ e.alertDesc = alert || ALERT_ILLEGAL_PARAMETER;
49
+ return e;
50
+ }
51
+
52
+ var te = new TextEncoder(), td = new TextDecoder();
53
+
54
+ function u16(b, o) { return (b[o] << 8) | b[o + 1]; }
55
+ function put16(b, o, v) { b[o] = (v >>> 8) & 0xff; b[o + 1] = v & 0xff; }
56
+ function concat(list) {
57
+ var len = 0, i;
58
+ for (i = 0; i < list.length; i++) len += list[i].length;
59
+ var out = new Uint8Array(len), off = 0;
60
+ for (i = 0; i < list.length; i++) { out.set(list[i], off); off += list[i].length; }
61
+ return out;
62
+ }
63
+ function toU8(v, what) {
64
+ if (v instanceof Uint8Array) return v;
65
+ if (typeof v === 'string') return new Uint8Array(Buffer.from(v, 'base64'));
66
+ if (v && typeof v.length === 'number') return new Uint8Array(v);
67
+ throw new TypeError(what + ' must be bytes (Uint8Array / Buffer) or base64');
68
+ }
69
+
70
+ // A bounds-checked reader: every overrun is a decode_error, never a throw
71
+ // from deep inside a typed array.
72
+ function Reader(buf, off, end) { this.b = buf; this.o = off || 0; this.end = end == null ? buf.length : end; }
73
+ Reader.prototype.need = function (n) { if (this.o + n > this.end) throw eccError('truncated structure', ALERT_DECODE_ERROR); };
74
+ Reader.prototype.u8 = function () { this.need(1); return this.b[this.o++]; };
75
+ Reader.prototype.u16 = function () { this.need(2); var v = u16(this.b, this.o); this.o += 2; return v; };
76
+ Reader.prototype.bytes = function (n) { this.need(n); var v = this.b.subarray(this.o, this.o + n); this.o += n; return v; };
77
+ Reader.prototype.vec8 = function () { return this.bytes(this.u8()); };
78
+ Reader.prototype.vec16 = function () { return this.bytes(this.u16()); };
79
+
80
+
81
+ // ============================================================
82
+ // ECHConfig / ECHConfigList (§4)
83
+ // ============================================================
84
+
85
+ /**
86
+ * An ECHConfigList → the configs of the version this implements, each with
87
+ * its exact bytes (`raw`: what HPKE's info binds to). Others are skipped,
88
+ * as §4 requires of a client. Throws on a malformed list.
89
+ */
90
+ function parse_ech_config_list(bytes) {
91
+ bytes = toU8(bytes, 'ECHConfigList');
92
+ var r = new Reader(bytes);
93
+ var list = r.vec16();
94
+ if (r.o !== bytes.length) throw eccError('trailing bytes after ECHConfigList', ALERT_DECODE_ERROR);
95
+ if (list.length < 4) throw eccError('empty ECHConfigList', ALERT_DECODE_ERROR);
96
+ var out = [], lr = new Reader(list);
97
+ while (lr.o < list.length) {
98
+ var start = lr.o;
99
+ var version = lr.u16();
100
+ var contents = lr.vec16();
101
+ var raw = list.subarray(start, lr.o);
102
+ if (version !== ECH_VERSION) continue;
103
+ out.push(parse_ech_config_contents(contents, raw));
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /** One ECHConfig (version, length, contents) → its fields. */
109
+ function parse_ech_config(bytes) {
110
+ bytes = toU8(bytes, 'ECHConfig');
111
+ var r = new Reader(bytes);
112
+ var version = r.u16();
113
+ var contents = r.vec16();
114
+ if (r.o !== bytes.length) throw eccError('trailing bytes after ECHConfig', ALERT_DECODE_ERROR);
115
+ if (version !== ECH_VERSION) throw eccError('ECHConfig version 0x' + version.toString(16) + ' (this implements 0xfe0d)');
116
+ return parse_ech_config_contents(contents, bytes);
117
+ }
118
+
119
+ function parse_ech_config_contents(contents, raw) {
120
+ var r = new Reader(contents);
121
+ var config_id = r.u8();
122
+ var kem_id = r.u16();
123
+ var public_key = r.vec16();
124
+ var suitesBytes = r.vec16();
125
+ if (suitesBytes.length < 4 || suitesBytes.length % 4) throw eccError('ECHConfig cipher_suites malformed', ALERT_DECODE_ERROR);
126
+ var cipher_suites = [];
127
+ for (var i = 0; i < suitesBytes.length; i += 4) cipher_suites.push({ kdf_id: u16(suitesBytes, i), aead_id: u16(suitesBytes, i + 2) });
128
+ var maximum_name_length = r.u8();
129
+ var name = r.vec8();
130
+ if (name.length < 1) throw eccError('ECHConfig public_name empty', ALERT_DECODE_ERROR);
131
+ var extBytes = r.vec16();
132
+ if (r.o !== contents.length) throw eccError('trailing bytes in ECHConfigContents', ALERT_DECODE_ERROR);
133
+ var extensions = [], er = new Reader(extBytes);
134
+ while (er.o < extBytes.length) extensions.push({ type: er.u16(), data: er.vec16() });
135
+ return {
136
+ version: ECH_VERSION, raw: raw,
137
+ config_id: config_id, kem_id: kem_id, public_key: public_key, cipher_suites: cipher_suites,
138
+ maximum_name_length: maximum_name_length, public_name: td.decode(name), extensions: extensions
139
+ };
140
+ }
141
+
142
+ function encode_ech_config(c) {
143
+ var name = te.encode(c.public_name);
144
+ if (name.length < 1 || name.length > 255) throw new TypeError('publicName must be 1-255 bytes');
145
+ var suites = new Uint8Array(c.cipher_suites.length * 4);
146
+ c.cipher_suites.forEach(function (s, i) { put16(suites, i * 4, s.kdf_id); put16(suites, i * 4 + 2, s.aead_id); });
147
+ var exts = concat((c.extensions || []).map(function (e) {
148
+ var h = new Uint8Array(4); put16(h, 0, e.type); put16(h, 2, e.data.length); return concat([h, e.data]);
149
+ }));
150
+ var pk = c.public_key;
151
+ var contents = concat([
152
+ new Uint8Array([c.config_id & 0xff, (c.kem_id >>> 8) & 0xff, c.kem_id & 0xff, (pk.length >>> 8) & 0xff, pk.length & 0xff]), pk,
153
+ new Uint8Array([(suites.length >>> 8) & 0xff, suites.length & 0xff]), suites,
154
+ new Uint8Array([c.maximum_name_length & 0xff, name.length]), name,
155
+ new Uint8Array([(exts.length >>> 8) & 0xff, exts.length & 0xff]), exts
156
+ ]);
157
+ var head = new Uint8Array(4); put16(head, 0, ECH_VERSION); put16(head, 2, contents.length);
158
+ return concat([head, contents]);
159
+ }
160
+
161
+ /** ECHConfig values → the ECHConfigList that goes in DNS and retry_configs. */
162
+ function build_ech_config_list(configs) {
163
+ var body = concat(configs);
164
+ var out = new Uint8Array(2 + body.length);
165
+ put16(out, 0, body.length); out.set(body, 2);
166
+ return out;
167
+ }
168
+
169
+ var KEM_BY_NAME = { 'x25519': 0x0020, 'p-256': 0x0010, 'p256': 0x0010, 'p-384': 0x0011, 'p384': 0x0011,
170
+ 'p-521': 0x0012, 'p521': 0x0012, 'x448': 0x0021 };
171
+
172
+ /**
173
+ * A new ECH key: `{ config, privateKey, configList, configListBase64 }`.
174
+ * `configListBase64` is the value of `ech=` in the HTTPS record.
175
+ *
176
+ * opts: `publicName` (required — the name the outer hello carries, which
177
+ * the server must hold a certificate for), `configId` (0-255; random by
178
+ * default), `maxNameLength` (the longest name behind it, for client padding;
179
+ * 0 = unknown), `kem` ('x25519' default, 'p-256', …).
180
+ *
181
+ * Cipher suites offered: HKDF-SHA256 with AES-128-GCM and ChaCha20-Poly1305
182
+ * — what Chrome, Firefox, Cloudflare and Go use.
183
+ */
184
+ function generate_ech_key(opts) {
185
+ opts = opts || {};
186
+ if (typeof opts.publicName !== 'string' || !opts.publicName) throw new TypeError('generateEchKey: publicName is required');
187
+ var publicName = opts.publicName.toLowerCase().replace(/\.$/, '');
188
+ // RFC 9849 §6.1.7: clients ignore a config whose public_name is not a
189
+ // host name — LDH labels, not an IPv4 address (a numeric last label).
190
+ var labels = publicName.split('.');
191
+ var ldh = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
192
+ if (publicName.length > 253 || labels.some(function (l) { return !ldh.test(l); }) || /^[0-9]+$/.test(labels[labels.length - 1]) || /^0x[0-9a-f]+$/.test(labels[labels.length - 1])) {
193
+ throw new TypeError('generateEchKey: publicName must be a DNS host name (letters, digits, hyphens; not an IP address): ' + JSON.stringify(opts.publicName));
194
+ }
195
+ var kem_id = 0x0020;
196
+ if (opts.kem != null) {
197
+ kem_id = typeof opts.kem === 'number' ? opts.kem : KEM_BY_NAME[String(opts.kem).toLowerCase()];
198
+ if (!kem_id || !is_supported_hpke_suite(kem_id, 1, 1)) throw new TypeError('generateEchKey: unsupported kem ' + JSON.stringify(opts.kem));
199
+ }
200
+ var configId = opts.configId == null ? crypto.randomBytes(1)[0] : opts.configId;
201
+ if (!(configId >= 0 && configId <= 255 && (configId | 0) === configId)) throw new TypeError('generateEchKey: configId must be 0-255');
202
+ var maxNameLength = opts.maxNameLength == null ? 0 : opts.maxNameLength;
203
+ if (!(maxNameLength >= 0 && maxNameLength <= 255)) throw new TypeError('generateEchKey: maxNameLength must be 0-255');
204
+
205
+ var kp = hpke_generate_key_pair(kem_id);
206
+ var config = encode_ech_config({
207
+ config_id: configId, kem_id: kem_id, public_key: kp.public_key,
208
+ cipher_suites: [{ kdf_id: 0x0001, aead_id: 0x0001 }, { kdf_id: 0x0001, aead_id: 0x0003 }],
209
+ maximum_name_length: maxNameLength, public_name: publicName, extensions: []
210
+ });
211
+ var configList = build_ech_config_list([config]);
212
+ return {
213
+ config: config,
214
+ privateKey: kp.private_key,
215
+ configList: configList,
216
+ configListBase64: Buffer.from(configList).toString('base64')
217
+ };
218
+ }
219
+
220
+ /**
221
+ * The server option — `{ keys: [{ config, privateKey, retry }] }` — checked
222
+ * and prepared: each key's config parsed, its private key matched to its
223
+ * public key (a mismatch would only show as every client failing), and the
224
+ * retry_configs list built once. null when absent or with no keys.
225
+ */
226
+ // Prepared once per option object — a server hands the same one to every
227
+ // connection, and checking a key costs a scalar multiplication. The entry
228
+ // remembers what it was made from: an application that changes the object
229
+ // in place (getEchKeys(), add a key, setEchKeys()) gets it prepared again,
230
+ // not the old result.
231
+ var prepared = new WeakMap();
232
+
233
+ function ech_option_snapshot(option) {
234
+ var keys = option && option.keys, snap = [keys];
235
+ if (Array.isArray(keys)) {
236
+ for (var i = 0; i < keys.length; i++) {
237
+ var k = keys[i] || {};
238
+ snap.push(k, k.config, k.privateKey, k.retry);
239
+ }
240
+ }
241
+ return snap;
242
+ }
243
+
244
+ function same_snapshot(a, b) {
245
+ if (a.length !== b.length) return false;
246
+ for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
247
+ return true;
248
+ }
249
+
250
+ function prepare_ech_server(option) {
251
+ if (option == null || option === false) return null;
252
+ var snap = ech_option_snapshot(option);
253
+ if (typeof option === 'object') {
254
+ var hit = prepared.get(option);
255
+ if (hit && same_snapshot(hit.snap, snap)) return hit.out;
256
+ }
257
+ var out = prepare_ech_server_uncached(option);
258
+ if (typeof option === 'object') prepared.set(option, { snap: snap, out: out });
259
+ return out;
260
+ }
261
+
262
+ function prepare_ech_server_uncached(option) {
263
+ if (typeof option !== 'object' || !Array.isArray(option.keys)) throw new TypeError('ech must be { keys: [{ config, privateKey, retry }] }');
264
+ if (option.keys.length === 0) return null;
265
+ var keys = [], retry = [];
266
+ for (var i = 0; i < option.keys.length; i++) {
267
+ var k = option.keys[i] || {};
268
+ var cfg = parse_ech_config(k.config);
269
+ var sk = toU8(k.privateKey, 'ech.keys[' + i + '].privateKey');
270
+ if (!is_supported_hpke_suite(cfg.kem_id, 1, 1)) throw new TypeError('ech.keys[' + i + ']: unsupported KEM 0x' + cfg.kem_id.toString(16));
271
+ var pk;
272
+ try { pk = hpke_public_key(cfg.kem_id, sk); } catch (e) { throw new TypeError('ech.keys[' + i + '].privateKey: ' + e.message); }
273
+ if (!Buffer.from(pk).equals(Buffer.from(cfg.public_key))) throw new TypeError('ech.keys[' + i + ']: privateKey does not match the public key in its config');
274
+ keys.push({ config: cfg, private_key: sk });
275
+ if (k.retry !== false) retry.push(cfg.raw);
276
+ }
277
+ return { keys: keys, retry_config_list: retry.length ? build_ech_config_list(retry) : null };
278
+ }
279
+
280
+
281
+ // ============================================================
282
+ // ClientHello bytes (RFC 8446 §4.1.2), without the handshake header
283
+ // ============================================================
284
+
285
+ /**
286
+ * The fields of a ClientHello body with their positions — what rebuilding
287
+ * the inner hello and zeroing the outer payload need, and what a parsed
288
+ * message object no longer has.
289
+ */
290
+ function scan_client_hello(body) {
291
+ var r = new Reader(body);
292
+ r.u16(); // legacy_version
293
+ var random = r.bytes(32);
294
+ var sidStart = r.o; var session_id = r.vec8();
295
+ var afterSid = r.o;
296
+ r.vec16(); // cipher_suites
297
+ r.vec8(); // legacy_compression_methods
298
+ var extsStart = r.o;
299
+ var exts = [];
300
+ if (r.o < body.length) {
301
+ var extBlock = r.vec16();
302
+ if (r.o !== body.length) throw eccError('trailing bytes after ClientHello', ALERT_DECODE_ERROR);
303
+ var base = extsStart + 2, er = new Reader(extBlock);
304
+ while (er.o < extBlock.length) {
305
+ var at = er.o, type = er.u16(), data = er.vec16();
306
+ exts.push({ type: type, start: base + at, end: base + er.o, data: data, dataStart: base + at + 4 });
307
+ }
308
+ }
309
+ return { random: random, session_id: session_id, sidStart: sidStart, afterSid: afterSid, extsStart: extsStart, exts: exts };
310
+ }
311
+
312
+ function find_ext(scan, type) {
313
+ for (var i = 0; i < scan.exts.length; i++) if (scan.exts[i].type === type) return scan.exts[i];
314
+ return null;
315
+ }
316
+
317
+ /** The encrypted_client_hello extension's contents (§5). */
318
+ function parse_ech_ext(data) {
319
+ var r = new Reader(data);
320
+ var type = r.u8();
321
+ if (type === 1) {
322
+ if (r.o !== data.length) throw eccError('inner ECH extension is not empty', ALERT_DECODE_ERROR);
323
+ return { type: 1 };
324
+ }
325
+ if (type !== 0) throw eccError('unknown ECHClientHello type ' + type);
326
+ var kdf_id = r.u16(), aead_id = r.u16(), config_id = r.u8();
327
+ var enc = r.vec16();
328
+ var payloadOff = r.o + 2;
329
+ var payload = r.vec16();
330
+ if (payload.length < 1) throw eccError('empty ECH payload', ALERT_DECODE_ERROR);
331
+ if (r.o !== data.length) throw eccError('trailing bytes in ECH extension', ALERT_DECODE_ERROR);
332
+ return { type: 0, kdf_id: kdf_id, aead_id: aead_id, config_id: config_id, enc: enc, payload: payload, payloadOff: payloadOff };
333
+ }
334
+
335
+ /** §5.2: the outer hello with the ECH payload replaced by zeros. */
336
+ function outer_aad(body, ext, ech) {
337
+ // A copy: on a Buffer, .slice() is a view, and zeroing it would zero the
338
+ // hello the transcript is about to hash.
339
+ var aad = new Uint8Array(body);
340
+ aad.fill(0, ext.dataStart + ech.payloadOff, ext.dataStart + ech.payloadOff + ech.payload.length);
341
+ return aad;
342
+ }
343
+
344
+ /**
345
+ * §5.1: EncodedClientHelloInner → ClientHelloInner body. The session id
346
+ * comes back from the outer hello; ech_outer_extensions is replaced by the
347
+ * outer extensions it names. In linear time, as §5.1 asks: one forward walk
348
+ * over the outer extensions for the whole reference list.
349
+ */
350
+ function decode_inner(encoded, outerBody, outerScan) {
351
+ var r = new Reader(encoded);
352
+ r.u16(); r.bytes(32);
353
+ var sidLen = r.u8();
354
+ if (sidLen !== 0) throw eccError('EncodedClientHelloInner session id not empty');
355
+ var afterSid = r.o;
356
+ r.vec16(); r.vec8();
357
+ var extsStart = r.o;
358
+ var extBlock = r.vec16();
359
+ // Everything after the ClientHello is padding, and must be zeros.
360
+ for (var p = r.o; p < encoded.length; p++) {
361
+ if (encoded[p] !== 0) throw eccError('non-zero padding in EncodedClientHelloInner');
362
+ }
363
+
364
+ var parts = [], er = new Reader(extBlock), sawOuter = false;
365
+ while (er.o < extBlock.length) {
366
+ var at = er.o, type = er.u16(), data = er.vec16();
367
+ if (type !== EXT_ECH_OUTER_EXTENSIONS) { parts.push(extBlock.subarray(at, er.o)); continue; }
368
+ if (sawOuter) throw eccError('ech_outer_extensions twice');
369
+ sawOuter = true;
370
+ var lr = new Reader(data);
371
+ var list = lr.vec8();
372
+ if (lr.o !== data.length || list.length < 2 || list.length % 2) throw eccError('ech_outer_extensions malformed', ALERT_DECODE_ERROR);
373
+ var cursor = 0; // into outerScan.exts: order and uniqueness in one pass
374
+ for (var i = 0; i < list.length; i += 2) {
375
+ var want = u16(list, i);
376
+ if (want === EXT_ECH) throw eccError('ech_outer_extensions references encrypted_client_hello');
377
+ while (cursor < outerScan.exts.length && outerScan.exts[cursor].type !== want) cursor++;
378
+ if (cursor >= outerScan.exts.length) throw eccError('ech_outer_extensions references 0x' + want.toString(16) + ', missing or out of order in ClientHelloOuter');
379
+ var oe = outerScan.exts[cursor++];
380
+ parts.push(outerBody.subarray(oe.start, oe.end));
381
+ }
382
+ }
383
+
384
+ var newExts = concat(parts);
385
+ var sid = outerScan.session_id;
386
+ var head = encoded.subarray(0, afterSid - 1); // legacy_version + random
387
+ var mid = encoded.subarray(afterSid, extsStart); // cipher_suites + compression
388
+ var extLen = new Uint8Array(2); put16(extLen, 0, newExts.length);
389
+ return concat([head, new Uint8Array([sid.length]), sid, mid, extLen, newExts]);
390
+ }
391
+
392
+ /** A handshake message (type ClientHello) around a body. */
393
+ function wrap_client_hello(body) {
394
+ var h = new Uint8Array(4 + body.length);
395
+ h[0] = 1; h[1] = (body.length >>> 16) & 0xff; h[2] = (body.length >>> 8) & 0xff; h[3] = body.length & 0xff;
396
+ h.set(body, 4);
397
+ return h;
398
+ }
399
+
400
+ /**
401
+ * §7.1: the inner hello must carry the inner ECH extension and offer
402
+ * nothing below TLS 1.3.
403
+ */
404
+ function check_inner(innerBody) {
405
+ var scan = scan_client_hello(innerBody);
406
+ var e = find_ext(scan, EXT_ECH);
407
+ if (!e || parse_ech_ext(e.data).type !== 1) throw eccError('ClientHelloInner without the inner encrypted_client_hello extension');
408
+ var sv = find_ext(scan, EXT_SUPPORTED_VERSIONS);
409
+ if (!sv) throw eccError('ClientHelloInner offers TLS 1.2');
410
+ var r = new Reader(sv.data), list = r.vec8();
411
+ if (list.length === 0 || list.length % 2) throw eccError('ClientHelloInner supported_versions malformed', ALERT_DECODE_ERROR);
412
+ for (var i = 0; i < list.length; i += 2) {
413
+ var v = u16(list, i);
414
+ // 0x?a?a is GREASE (RFC 8701). TLS 1.3 and up: 0x0304 to 0xfdff — the
415
+ // 0xfexx codes are DTLS, never a TLS version.
416
+ if ((v & 0x0f0f) === 0x0a0a) continue;
417
+ if (v < 0x0304 || v >= 0xfe00) throw eccError('ClientHelloInner offers a version other than TLS 1.3 and up (0x' + v.toString(16) + ')');
418
+ }
419
+ return scan;
420
+ }
421
+
422
+
423
+ // ============================================================
424
+ // The server's side (§7.1, §7.1.1)
425
+ // ============================================================
426
+
427
+ /**
428
+ * A ClientHello as it arrived (handshake header included) → what the
429
+ * handshake should continue with. `state` is per connection, `{}` at
430
+ * first; it carries the HPKE context from the first ClientHello to the
431
+ * second one after a HelloRetryRequest.
432
+ *
433
+ * Returns `{ data }` with the ClientHelloInner when it decrypted, null to
434
+ * continue with the hello as it came (no ECH, or ECH not decryptable — then
435
+ * `state.status` is 'rejected' and retry_configs are due). Throws, with
436
+ * `.alertDesc`, where §5.1 / §7.1 / §7.1.1 require the connection to end.
437
+ */
438
+ function server_process_client_hello(server, state, data, isSecond) {
439
+ var body = data.subarray(4);
440
+ var scan = scan_client_hello(body);
441
+ var ext = find_ext(scan, EXT_ECH);
442
+
443
+ if (isSecond) {
444
+ if (state.status !== 'accepted') return null; // §7.1.1: not decrypted
445
+ if (!ext) throw eccError('second ClientHello without encrypted_client_hello', ALERT_MISSING_EXTENSION);
446
+ var ech2 = parse_ech_ext(ext.data);
447
+ if (ech2.type !== 0) throw eccError('second ClientHello carries the inner ECH extension');
448
+ if (ech2.kdf_id !== state.kdf_id || ech2.aead_id !== state.aead_id || ech2.config_id !== state.config_id || ech2.enc.length !== 0) {
449
+ throw eccError('second ClientHelloOuter changed the ECH cipher suite, config_id or enc');
450
+ }
451
+ var pt2 = state.context.open(outer_aad(body, ext, ech2), ech2.payload);
452
+ if (!pt2) throw eccError('second ClientHelloInner does not decrypt', ALERT_DECRYPT_ERROR);
453
+ var inner2 = decode_inner(pt2, body, scan);
454
+ var scan2 = check_inner(inner2);
455
+ // RFC 8446 §4.1.2: the second hello repeats the first one's random; the
456
+ // ServerHello's confirmation (§7.2) is computed over it.
457
+ if (!Buffer.from(scan2.random).equals(Buffer.from(state.inner_random))) {
458
+ throw eccError('second ClientHelloInner changed its random');
459
+ }
460
+ return { data: wrap_client_hello(inner2) };
461
+ }
462
+
463
+ if (!ext) { state.status = null; return null; }
464
+ var ech = parse_ech_ext(ext.data);
465
+ if (ech.type !== 0) throw eccError('ClientHello from the network carries the inner ECH extension');
466
+
467
+ // §7.1: candidates by config_id; each must offer the suite the client used.
468
+ var aad = null;
469
+ for (var i = 0; i < server.keys.length; i++) {
470
+ var key = server.keys[i], cfg = key.config;
471
+ if (cfg.config_id !== ech.config_id) continue;
472
+ var offered = false;
473
+ for (var j = 0; j < cfg.cipher_suites.length; j++) {
474
+ if (cfg.cipher_suites[j].kdf_id === ech.kdf_id && cfg.cipher_suites[j].aead_id === ech.aead_id) offered = true;
475
+ }
476
+ // 0xFFFF is HPKE's export-only AEAD: it cannot carry a ClientHello.
477
+ if (!offered || ech.aead_id === 0xffff || !is_supported_hpke_suite(cfg.kem_id, ech.kdf_id, ech.aead_id)) continue;
478
+
479
+ var ctx;
480
+ try {
481
+ ctx = setup_recipient(hpke_suite(cfg.kem_id, ech.kdf_id, ech.aead_id), ech.enc, key.private_key,
482
+ concat([te.encode('tls ech'), new Uint8Array([0]), cfg.raw]),
483
+ { recipient_public_key: cfg.public_key });
484
+ } catch (e) { continue; } // enc not a valid key: not this config
485
+ if (!aad) aad = outer_aad(body, ext, ech);
486
+ var pt = ctx.open(aad, ech.payload);
487
+ if (!pt) continue;
488
+
489
+ // Decrypted: from here a malformed inner hello is fatal (§5.1).
490
+ var inner = decode_inner(pt, body, scan);
491
+ var innerScan = check_inner(inner);
492
+ state.status = 'accepted';
493
+ state.context = ctx;
494
+ state.kdf_id = ech.kdf_id; state.aead_id = ech.aead_id; state.config_id = ech.config_id;
495
+ state.inner_random = new Uint8Array(innerScan.random);
496
+ state.public_name = cfg.public_name;
497
+ return { data: wrap_client_hello(inner) };
498
+ }
499
+
500
+ state.status = 'rejected';
501
+ return null;
502
+ }
503
+
504
+ // ============================================================
505
+ // The client's side (§6.1, §6.2)
506
+ //
507
+ // The client sends two hellos in one. The real one — ClientHelloInner,
508
+ // with the name it wants — goes encrypted, with HPKE, to the key of an
509
+ // ECHConfig the server published (DNS). Around it, in the clear,
510
+ // ClientHelloOuter names the config's public_name. Extensions the two share
511
+ // are not sent twice: the inner hello names them (ech_outer_extensions,
512
+ // §5.1) and the server copies them back from the outer one — the key_share
513
+ // above all, the largest.
514
+ //
515
+ // The server answers either as the inner hello's server — and says so with
516
+ // 8 bytes of its random that only a holder of the inner hello can compute
517
+ // (§7.2) — or, unable to decrypt, as the public_name, with its current keys
518
+ // in retry_configs. Which one is known only at the ServerHello, so until
519
+ // then the client keeps both transcripts (tls_session).
520
+ // ============================================================
521
+
522
+ // RFC 9849 §6.1.7: a public_name the client uses — a DNS host name, not an
523
+ // IPv4 address in any notation (a last label that is all digits, or 0x…).
524
+ function valid_public_name(name) {
525
+ if (!name || name.length > 253) return false;
526
+ var labels = name.toLowerCase().replace(/\.$/, '').split('.');
527
+ var ldh = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
528
+ for (var i = 0; i < labels.length; i++) if (!ldh.test(labels[i])) return false;
529
+ var last = labels[labels.length - 1];
530
+ return !/^[0-9]+$/.test(last) && !/^0x[0-9a-f]*$/.test(last);
531
+ }
532
+
533
+ // The cipher suites a client offers, in preference: what every deployment
534
+ // publishes first.
535
+ var CLIENT_SUITE_PREF = [[0x0001, 0x0001], [0x0001, 0x0003], [0x0001, 0x0002], [0x0002, 0x0002], [0x0003, 0x0002], [0x0002, 0x0001], [0x0003, 0x0001], [0x0002, 0x0003], [0x0003, 0x0003]];
536
+
537
+ /**
538
+ * The client option — an ECHConfigList (bytes, or base64: the `ech=` value
539
+ * of an HTTPS record) — → the config to use: the first in the list this
540
+ * client can (§6.1: a KEM and a suite it has, a valid public_name, no
541
+ * mandatory extension it does not know). null when absent. Throws on a list
542
+ * that is malformed, or that has no usable config.
543
+ */
544
+ function prepare_ech_client(option) {
545
+ if (option == null || option === false) return null;
546
+ var list;
547
+ try { list = parse_ech_config_list(option); }
548
+ catch (e) { throw new TypeError('ech: not an ECHConfigList — ' + e.message); }
549
+ for (var i = 0; i < list.length; i++) {
550
+ var c = list[i];
551
+ if (!is_supported_hpke_suite(c.kem_id, 1, 1)) continue;
552
+ if (!valid_public_name(c.public_name)) continue;
553
+ var mandatory = false;
554
+ for (var x = 0; x < c.extensions.length; x++) if (c.extensions[x].type & 0x8000) mandatory = true;
555
+ if (mandatory) continue;
556
+ var pick = null;
557
+ for (var p = 0; p < CLIENT_SUITE_PREF.length && !pick; p++) {
558
+ for (var j = 0; j < c.cipher_suites.length; j++) {
559
+ var cs = c.cipher_suites[j];
560
+ if (cs.kdf_id === CLIENT_SUITE_PREF[p][0] && cs.aead_id === CLIENT_SUITE_PREF[p][1] && is_supported_hpke_suite(c.kem_id, cs.kdf_id, cs.aead_id)) { pick = cs; break; }
561
+ }
562
+ }
563
+ if (!pick) continue;
564
+ return { config: c, kdf_id: pick.kdf_id, aead_id: pick.aead_id, suite: hpke_suite(c.kem_id, pick.kdf_id, pick.aead_id) };
565
+ }
566
+ throw new TypeError('ech: no ECHConfig in the list this client can use (version 0xfe0d, a supported KEM and cipher suite, a host-name public_name)');
567
+ }
568
+
569
+ /** §6.1: the HPKE sender context for this connection: `{ enc, context }`. */
570
+ function client_hpke_setup(prep) {
571
+ var info = concat([te.encode('tls ech'), new Uint8Array([0]), prep.config.raw]);
572
+ return setup_sender(prep.suite, prep.config.public_key, info);
573
+ }
574
+
575
+ /** The outer encrypted_client_hello extension (§5), its payload zero. */
576
+ function client_outer_ext_data(kdf_id, aead_id, config_id, enc, payloadLen) {
577
+ var out = new Uint8Array(1 + 2 + 2 + 1 + 2 + enc.length + 2 + payloadLen);
578
+ var o = 0;
579
+ out[o++] = 0;
580
+ put16(out, o, kdf_id); o += 2;
581
+ put16(out, o, aead_id); o += 2;
582
+ out[o++] = config_id & 0xff;
583
+ put16(out, o, enc.length); o += 2;
584
+ out.set(enc, o); o += enc.length;
585
+ put16(out, o, payloadLen);
586
+ return out;
587
+ }
588
+
589
+ /**
590
+ * §5.1, §6.1.3: ClientHelloInner (a handshake message, as the transcript
591
+ * has it) → EncodedClientHelloInner: no session id (it comes back from the
592
+ * outer hello); the extensions of type in `compressed` — consecutive in the
593
+ * inner hello, in the order the outer one has them — replaced by one
594
+ * ech_outer_extensions naming them; then zeros, so the length gives away
595
+ * little of the name: the server_name padded to maximum_name_length, and
596
+ * the whole to a multiple of 32.
597
+ */
598
+ function client_encode_inner(innerMsg, compressed, maxNameLength, sniLength) {
599
+ var body = innerMsg.subarray(4);
600
+ var scan = scan_client_hello(body);
601
+ var parts = [], placed = false;
602
+ for (var i = 0; i < scan.exts.length; i++) {
603
+ var e = scan.exts[i];
604
+ if (compressed.indexOf(e.type) >= 0) {
605
+ if (placed) continue;
606
+ placed = true;
607
+ var list = new Uint8Array(1 + compressed.length * 2);
608
+ list[0] = compressed.length * 2;
609
+ for (var k = 0; k < compressed.length; k++) put16(list, 1 + k * 2, compressed[k]);
610
+ var h = new Uint8Array(4); put16(h, 0, EXT_ECH_OUTER_EXTENSIONS); put16(h, 2, list.length);
611
+ parts.push(h, list);
612
+ continue;
613
+ }
614
+ parts.push(body.subarray(e.start, e.end));
615
+ }
616
+ var exts = concat(parts);
617
+ var head = body.subarray(0, scan.sidStart); // legacy_version + random
618
+ var mid = body.subarray(scan.afterSid, scan.extsStart); // cipher_suites + compression
619
+ var extLen = new Uint8Array(2); put16(extLen, 0, exts.length);
620
+ var encoded = concat([head, new Uint8Array([0]), mid, extLen, exts]);
621
+ var pad = sniLength != null ? Math.max(0, maxNameLength - sniLength) : 9 + maxNameLength;
622
+ pad += 31 - ((encoded.length + pad - 1) % 32);
623
+ return concat([encoded, new Uint8Array(pad)]);
624
+ }
625
+
626
+ /**
627
+ * ClientHelloOuter (a handshake message, its ECH payload zero) → the same
628
+ * with the payload: `encoded` sealed with AAD = the outer hello itself, as
629
+ * it is now (§5.2).
630
+ */
631
+ function client_seal_outer(outerMsg, hpkeContext, encoded) {
632
+ var body = outerMsg.subarray(4);
633
+ var scan = scan_client_hello(body);
634
+ var ext = find_ext(scan, EXT_ECH);
635
+ var ech = parse_ech_ext(ext.data);
636
+ var ct = hpkeContext.seal(new Uint8Array(body), encoded);
637
+ if (ct.length !== ech.payload.length) throw new Error('ECH: sealed payload length differs from the space left for it');
638
+ var out = new Uint8Array(outerMsg);
639
+ out.set(ct, 4 + ext.dataStart + ech.payloadOff);
640
+ return out;
641
+ }
642
+
643
+ /**
644
+ * §6.2: GREASE ECH — an encrypted_client_hello extension that decrypts
645
+ * under no key, from a client with no config, so that one with a config
646
+ * does not stand out. `g` is fixed per connection (config_id, payload
647
+ * length); after a HelloRetryRequest, `enc` is empty and the payload new
648
+ * random bytes of the same length.
649
+ */
650
+ function grease_ech_state() {
651
+ var lens = [144, 176, 208, 240];
652
+ return { config_id: crypto.randomBytes(1)[0], payload_len: lens[crypto.randomBytes(1)[0] & 3] + 16 };
653
+ }
654
+ function grease_ech_ext_data(g, second) {
655
+ var enc = second ? new Uint8Array(0) : new Uint8Array(crypto.randomBytes(32));
656
+ var d = client_outer_ext_data(0x0001, 0x0001, g.config_id, enc, g.payload_len);
657
+ d.set(crypto.randomBytes(g.payload_len), d.length - g.payload_len);
658
+ return d;
659
+ }
660
+
661
+ /**
662
+ * A HelloRetryRequest (a handshake message): where its encrypted_client_hello
663
+ * extension's 8 bytes start, or -1 when it has none (§7.2.1: the server did
664
+ * not decrypt). decode_error on a malformed one.
665
+ */
666
+ function hrr_ech_offset(hrrMsg) {
667
+ var r = new Reader(hrrMsg, 4);
668
+ r.u16(); r.bytes(32); r.vec8(); r.u16(); r.u8();
669
+ if (r.o >= hrrMsg.length) return -1;
670
+ var extEnd = r.u16(); extEnd += r.o;
671
+ if (extEnd !== hrrMsg.length) throw eccError('HelloRetryRequest extensions malformed', ALERT_DECODE_ERROR);
672
+ while (r.o < extEnd) {
673
+ var type = r.u16(), len = r.u16(), at = r.o;
674
+ r.bytes(len);
675
+ if (type === EXT_ECH) {
676
+ if (len !== 8) throw eccError('HelloRetryRequest encrypted_client_hello is not 8 bytes', ALERT_DECODE_ERROR);
677
+ return at;
678
+ }
679
+ }
680
+ return -1;
681
+ }
682
+
683
+
684
+ /**
685
+ * §7.2 / §7.2.1: the 8 confirmation bytes, over the transcript hash that
686
+ * ends with the ServerHello (its last 8 random bytes zero) or with the
687
+ * HelloRetryRequest (its ECH extension's 8 bytes zero).
688
+ */
689
+ function accept_confirmation(hashName, innerRandom, transcriptHash, isHrr, labelPrefix) {
690
+ var prk = hkdf_extract(hashName, new Uint8Array(0), innerRandom); // salt: Hash.length zeros
691
+ return hkdf_expand_label(hashName, prk, isHrr ? 'hrr ech accept confirmation' : 'ech accept confirmation',
692
+ transcriptHash, 8, labelPrefix);
693
+ }
694
+
695
+ export {
696
+ ECH_VERSION, EXT_ECH,
697
+ parse_ech_config_list,
698
+ prepare_ech_client,
699
+ client_hpke_setup,
700
+ client_outer_ext_data,
701
+ client_encode_inner,
702
+ client_seal_outer,
703
+ grease_ech_state,
704
+ grease_ech_ext_data,
705
+ hrr_ech_offset,
706
+ parse_ech_config,
707
+ encode_ech_config,
708
+ build_ech_config_list,
709
+ generate_ech_key,
710
+ prepare_ech_server,
711
+ server_process_client_hello,
712
+ accept_confirmation,
713
+ scan_client_hello,
714
+ parse_ech_ext,
715
+ };