lemon-tls 0.7.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -20
- package/index.cjs +9 -7
- package/index.d.ts +323 -23
- package/index.js +126 -0
- package/package.json +39 -6
- package/src/compat.js +325 -46
- package/src/crypto.js +19 -0
- package/src/dtls_session.js +141 -17
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +192 -57
- package/src/secure_context.js +63 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +39 -4
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +38 -25
- package/src/tls_session.js +2835 -205
- package/src/tls_socket.js +839 -148
- package/src/utils.js +68 -1
- package/src/wire.js +148 -9
- package/types/crypto.d.ts +40 -0
- package/types/record.d.ts +40 -0
- package/types/session.d.ts +3 -0
- package/types/wire.d.ts +62 -0
|
@@ -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
|
+
};
|