tunnelfetch 1.0.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.
Files changed (96) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +617 -0
  3. package/README.zh-CN.md +470 -0
  4. package/package.json +74 -0
  5. package/src/client/cookies.js +429 -0
  6. package/src/client/decode.js +346 -0
  7. package/src/client/redirect.js +249 -0
  8. package/src/client.js +704 -0
  9. package/src/errors.js +181 -0
  10. package/src/http1/chunked.js +289 -0
  11. package/src/http1/index.js +10 -0
  12. package/src/http1/request.js +143 -0
  13. package/src/http1/response.js +493 -0
  14. package/src/http2/connection.js +1170 -0
  15. package/src/http2/constants.js +129 -0
  16. package/src/http2/frames.js +291 -0
  17. package/src/http2/hpack.js +420 -0
  18. package/src/http2/huffman.js +203 -0
  19. package/src/http2/index.js +21 -0
  20. package/src/index.js +46 -0
  21. package/src/pool.js +256 -0
  22. package/src/proxy/direct.js +62 -0
  23. package/src/proxy/http-connect.js +206 -0
  24. package/src/proxy/index.js +197 -0
  25. package/src/proxy/socks5.js +344 -0
  26. package/src/tls/aead.js +263 -0
  27. package/src/tls/connect.js +407 -0
  28. package/src/tls/constants.js +334 -0
  29. package/src/tls/extensions.js +376 -0
  30. package/src/tls/handshake-messages.js +901 -0
  31. package/src/tls/handshake.js +568 -0
  32. package/src/tls/handshake12.js +507 -0
  33. package/src/tls/index.js +44 -0
  34. package/src/tls/keyschedule.js +473 -0
  35. package/src/tls/record.js +872 -0
  36. package/src/tls/tickets.js +145 -0
  37. package/src/tls/transcript.js +101 -0
  38. package/src/tls/wire.js +224 -0
  39. package/src/transport.js +296 -0
  40. package/src/trust/der.js +551 -0
  41. package/src/trust/index.js +375 -0
  42. package/src/trust/name.js +235 -0
  43. package/src/trust/ocsp.js +759 -0
  44. package/src/trust/path.js +595 -0
  45. package/src/trust/roots.js +454 -0
  46. package/src/trust/x509.js +902 -0
  47. package/src/util/bytes.js +470 -0
  48. package/src/util/deadline.js +266 -0
  49. package/src/warmup-fixture.js +85 -0
  50. package/src/warmup.js +243 -0
  51. package/types/client/cookies.d.ts +159 -0
  52. package/types/client/decode.d.ts +54 -0
  53. package/types/client/redirect.d.ts +96 -0
  54. package/types/client.d.ts +323 -0
  55. package/types/errors.d.ts +141 -0
  56. package/types/http1/chunked.d.ts +48 -0
  57. package/types/http1/index.d.ts +3 -0
  58. package/types/http1/request.d.ts +44 -0
  59. package/types/http1/response.d.ts +183 -0
  60. package/types/http2/connection.d.ts +282 -0
  61. package/types/http2/constants.d.ts +95 -0
  62. package/types/http2/frames.d.ts +116 -0
  63. package/types/http2/hpack.d.ts +99 -0
  64. package/types/http2/huffman.d.ts +21 -0
  65. package/types/http2/index.d.ts +5 -0
  66. package/types/index.d.ts +17 -0
  67. package/types/pool.d.ts +135 -0
  68. package/types/proxy/direct.d.ts +26 -0
  69. package/types/proxy/http-connect.d.ts +37 -0
  70. package/types/proxy/index.d.ts +62 -0
  71. package/types/proxy/socks5.d.ts +47 -0
  72. package/types/tls/aead.d.ts +67 -0
  73. package/types/tls/connect.d.ts +280 -0
  74. package/types/tls/constants.d.ts +275 -0
  75. package/types/tls/extensions.d.ts +195 -0
  76. package/types/tls/handshake-messages.d.ts +430 -0
  77. package/types/tls/handshake.d.ts +90 -0
  78. package/types/tls/handshake12.d.ts +35 -0
  79. package/types/tls/index.d.ts +9 -0
  80. package/types/tls/keyschedule.d.ts +272 -0
  81. package/types/tls/record.d.ts +361 -0
  82. package/types/tls/tickets.d.ts +66 -0
  83. package/types/tls/transcript.d.ts +52 -0
  84. package/types/tls/wire.d.ts +106 -0
  85. package/types/transport.d.ts +222 -0
  86. package/types/trust/der.d.ts +239 -0
  87. package/types/trust/index.d.ts +194 -0
  88. package/types/trust/name.d.ts +33 -0
  89. package/types/trust/ocsp.d.ts +138 -0
  90. package/types/trust/path.d.ts +139 -0
  91. package/types/trust/roots.d.ts +36 -0
  92. package/types/trust/x509.d.ts +401 -0
  93. package/types/util/bytes.d.ts +183 -0
  94. package/types/util/deadline.d.ts +133 -0
  95. package/types/warmup-fixture.d.ts +11 -0
  96. package/types/warmup.d.ts +45 -0
@@ -0,0 +1,901 @@
1
+ // Handshake message encoding, decoding, and the negotiation decisions that follow from them.
2
+ //
3
+ // Deliberately free of any dependency on the record layer: these are byte-level functions over
4
+ // complete handshake message bodies, which makes the whole of negotiation — including every
5
+ // downgrade guard — testable from fixed vectors with no I/O.
6
+ //
7
+ // All randomness and key generation is injectable. That is not a testing nicety: without it a
8
+ // TLS handshake cannot be replayed, and a transport that cannot be replayed cannot be tested
9
+ // offline at all.
10
+
11
+ import { TlsError, TlsUnsupportedError, CertificateError, codes, hex16 } from '../errors.js';
12
+ import { concat, equal, timingSafeEqual, utf8 } from '../util/bytes.js';
13
+ import { Builder, Cursor, vector, handshakeMessage } from './wire.js';
14
+ // der.js is a strict ASN.1 reader with no trust policy in it; the signature-format conversion
15
+ // lives there so the certificate path builder and this file cannot drift apart.
16
+ import { ecdsaDerToRaw } from '../trust/der.js';
17
+ import {
18
+ CIPHER_NAME,
19
+ CIPHER_PARAMS,
20
+ DOWNGRADE_SENTINEL_11,
21
+ DOWNGRADE_SENTINEL_12,
22
+ EXTENSION,
23
+ GROUP_PARAMS,
24
+ HANDSHAKE_TYPE,
25
+ HELLO_RETRY_REQUEST_RANDOM,
26
+ LEGACY_VERSION,
27
+ SIG_SCHEME_PARAMS,
28
+ TLS12,
29
+ TLS12_CIPHERS,
30
+ TLS13,
31
+ TLS13_CIPHERS,
32
+ SUPPORTED_GROUPS,
33
+ SUPPORTED_SIG_SCHEMES,
34
+ ALPN_HTTP11,
35
+ } from './constants.js';
36
+ import {
37
+ decodeAlpn,
38
+ decodeExtensionBlock,
39
+ decodeKeyShareEntry,
40
+ decodeKeyShareHrr,
41
+ decodeSelectedVersion,
42
+ describeSigScheme,
43
+ describeVersion,
44
+ encodeAlpn,
45
+ encodeEcPointFormats,
46
+ encodeExtendedMasterSecret,
47
+ encodeExtensionBlock,
48
+ encodeKeyShare,
49
+ encodePreSharedKey,
50
+ encodePskKeyExchangeModes,
51
+ encodeRenegotiationInfo,
52
+ encodeServerName,
53
+ encodeSignatureAlgorithms,
54
+ encodeStatusRequest,
55
+ encodeSupportedGroups,
56
+ encodeSupportedVersions,
57
+ pskBinderTrailerLength,
58
+ rejectUnofferedExtensions,
59
+ requireSupportedGroup,
60
+ } from './extensions.js';
61
+
62
+ const defaultRandom = (n) => crypto.getRandomValues(new Uint8Array(n));
63
+
64
+ // ------------------------------------------------------------------ key shares
65
+
66
+ /**
67
+ * An ephemeral key share: the public half as sent in key_share, plus the private key the
68
+ * eventual ServerHello selection will feed into deriveSharedSecret.
69
+ * @typedef {object} KeyShare
70
+ * @property {number} group
71
+ * @property {Uint8Array} keyExchange raw public key, the exact bytes on the wire
72
+ * @property {CryptoKey} privateKey non-extractable
73
+ */
74
+
75
+ /**
76
+ * Generate an ephemeral key share for one group.
77
+ * `generateKeyPair` is injectable so a recorded handshake can be replayed with the exact private
78
+ * key that produced it.
79
+ *
80
+ * @param {number} group
81
+ * @param {import('./connect.js').TlsDeps} [deps]
82
+ * @returns {Promise<KeyShare>}
83
+ */
84
+ export async function generateKeyShare(group, { generateKeyPair } = {}) {
85
+ const params = requireSupportedGroup(group, 'ClientHello');
86
+ const gen =
87
+ generateKeyPair ??
88
+ ((algorithm) => crypto.subtle.generateKey(algorithm, false, ['deriveBits']));
89
+ const pair = await gen(params.algorithm, group);
90
+ const raw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
91
+ if (raw.byteLength !== params.publicLen) {
92
+ throw new TlsError(
93
+ codes.TLS_HANDSHAKE,
94
+ `generated a ${raw.byteLength}-byte public key for group ${hex16(group)}, expected ${params.publicLen}`,
95
+ );
96
+ }
97
+ return { group, keyExchange: raw, privateKey: pair.privateKey };
98
+ }
99
+
100
+ /**
101
+ * ECDH/X25519 shared secret. The peer's key is imported in raw form, which is where a malformed
102
+ * point is caught: WebCrypto rejects a point that is not on the curve, so we do not have to
103
+ * implement that check ourselves — but a wrong LENGTH would be accepted by some implementations,
104
+ * so it is checked here first.
105
+ *
106
+ * @param {number} group
107
+ * @param {CryptoKey} privateKey our ephemeral private key for the group
108
+ * @param {Uint8Array} peerKey the server's raw public key from its key_share
109
+ * @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
110
+ */
111
+ export async function deriveSharedSecret(group, privateKey, peerKey) {
112
+ const params = requireSupportedGroup(group, 'ServerHello');
113
+ if (peerKey.byteLength !== params.publicLen) {
114
+ throw new TlsError(
115
+ codes.TLS_HANDSHAKE,
116
+ `server key_share for group ${hex16(group)} is ${peerKey.byteLength} bytes, expected ${params.publicLen}`,
117
+ { group, got: peerKey.byteLength, expected: params.publicLen },
118
+ );
119
+ }
120
+ if (params.kind === 'ec' && peerKey[0] !== 0x04) {
121
+ throw new TlsUnsupportedError(
122
+ codes.TLS_GROUP_UNSUPPORTED,
123
+ `server sent a compressed or invalid EC point (first byte 0x${peerKey[0].toString(16)}) ` +
124
+ `for group ${hex16(group)}; only uncompressed points are supported`,
125
+ { group, firstByte: peerKey[0] },
126
+ );
127
+ }
128
+ let peer;
129
+ try {
130
+ peer = await crypto.subtle.importKey('raw', peerKey, params.algorithm, false, []);
131
+ } catch (cause) {
132
+ throw new TlsError(
133
+ codes.TLS_HANDSHAKE,
134
+ `server key_share for group ${hex16(group)} is not a valid public key: ${cause?.message}`,
135
+ { group },
136
+ );
137
+ }
138
+ const bits = params.kind === 'x25519' ? params.secretLen * 8 : params.secretBits;
139
+ let shared;
140
+ try {
141
+ shared = new Uint8Array(
142
+ await crypto.subtle.deriveBits({ ...params.algorithm, public: peer }, privateKey, bits),
143
+ );
144
+ } catch (cause) {
145
+ // WebCrypto rejects small-order X25519 peers itself, with an OperationError carrying no
146
+ // useful text. Letting that escape untyped would put a bare DOMException in front of a
147
+ // caller who is catching our error taxonomy, so it is translated here.
148
+ throw new TlsError(
149
+ codes.TLS_HANDSHAKE,
150
+ `key agreement failed for group ${hex16(group)}: ${cause?.message ?? cause}. ` +
151
+ 'This usually means the server sent a degenerate or small-order public key.',
152
+ { group },
153
+ );
154
+ }
155
+ // RFC 7748 s6.1: an all-zero X25519 output means a small-order peer key. Reject.
156
+ if (params.kind === 'x25519' && shared.every((b) => b === 0)) {
157
+ throw new TlsError(
158
+ codes.TLS_HANDSHAKE,
159
+ 'X25519 shared secret is all zeroes, indicating a small-order server key_share',
160
+ );
161
+ }
162
+ return shared;
163
+ }
164
+
165
+ // ------------------------------------------------------------------ ClientHello
166
+
167
+ /**
168
+ * @typedef {object} ClientHelloOptions
169
+ * @property {string} hostname SNI, unless it is an IP literal (then no SNI is sent)
170
+ * @property {Array<{ group: number, keyExchange: Uint8Array }>} keyShares public halves to
171
+ * offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
172
+ * @property {Uint8Array} [random] fixed ClientHello.random, for reproducible handshakes
173
+ * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise
174
+ * @property {number[]} [ciphers] default: the union for the offered versions, 1.3 first
175
+ * @property {number[]} [groups] supported_groups, default SUPPORTED_GROUPS
176
+ * @property {number[]} [sigSchemes] default SUPPORTED_SIG_SCHEMES
177
+ * @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
178
+ * @property {number[]} [versions] default [TLS13, TLS12]
179
+ * @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
180
+ * @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
181
+ * offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
182
+ * the real binder over `message.subarray(0, truncatedLength)` and patch it in at
183
+ * `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
184
+ * every honest server must reject.
185
+ * @property {(n: number) => Uint8Array} [randomBytes] injectable randomness
186
+ */
187
+
188
+ /**
189
+ * The built hello plus everything later steps need to police the server's answer against what
190
+ * was actually offered — negotiation checks must run against this record, never against the
191
+ * defaults they might have come from.
192
+ * @typedef {object} ClientHello
193
+ * @property {Uint8Array} message framed handshake message, ready for the record layer
194
+ * @property {Uint8Array} clientRandom
195
+ * @property {Uint8Array} legacySessionId
196
+ * @property {number[]} offeredCiphers
197
+ * @property {number[]} offeredGroups
198
+ * @property {number[]} offeredSigSchemes
199
+ * @property {Set<number>} offeredExtensions extension types present in the hello
200
+ * @property {string[]} offeredAlpn
201
+ * @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
202
+ * @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
203
+ * transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
204
+ */
205
+
206
+ /**
207
+ * Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
208
+ * handshake needs to police the server's answer.
209
+ *
210
+ * @param {ClientHelloOptions} opts
211
+ * @returns {ClientHello}
212
+ */
213
+ export function buildClientHello({
214
+ hostname,
215
+ keyShares,
216
+ random,
217
+ legacySessionId,
218
+ ciphers,
219
+ groups = SUPPORTED_GROUPS,
220
+ sigSchemes = SUPPORTED_SIG_SCHEMES,
221
+ alpn = [ALPN_HTTP11],
222
+ versions = [TLS13, TLS12],
223
+ extraExtensions = [],
224
+ psk = null,
225
+ randomBytes = defaultRandom,
226
+ }) {
227
+ const clientRandom = random ?? randomBytes(32);
228
+ if (clientRandom.byteLength !== 32) {
229
+ throw new TlsError(codes.CONFIG_INVALID, `ClientHello.random must be 32 bytes, got ${clientRandom.byteLength}`);
230
+ }
231
+ // RFC 8446 s4.1.2: a non-empty legacy_session_id makes middleboxes treat the flow as a
232
+ // resumed TLS 1.2 session and leave it alone. 32 random bytes is what every browser sends.
233
+ const sessionId = legacySessionId ?? randomBytes(32);
234
+
235
+ const offersTls13 = versions.includes(TLS13);
236
+ const offersTls12 = versions.includes(TLS12);
237
+ const suites = ciphers ?? [
238
+ ...(offersTls13 ? TLS13_CIPHERS : []),
239
+ ...(offersTls12 ? TLS12_CIPHERS : []),
240
+ ];
241
+ if (suites.length === 0) {
242
+ throw new TlsError(codes.CONFIG_INVALID, 'ClientHello would offer no cipher suites');
243
+ }
244
+
245
+ const suiteBytes = new Builder();
246
+ for (const s of suites) suiteBytes.u16(s);
247
+
248
+ if (psk && !offersTls13) {
249
+ // A pre_shared_key of this kind exists only in TLS 1.3 (RFC 5077 tickets are a different,
250
+ // unimplemented mechanism), so offering one from a hello that cannot negotiate 1.3 is a
251
+ // wiring bug that must not pass silently as "the server just declined".
252
+ throw new TlsError(codes.CONFIG_INVALID,
253
+ 'a resumption PSK was supplied but TLS 1.3 is not among the offered versions');
254
+ }
255
+
256
+ const extensionParts = [
257
+ encodeServerName(hostname),
258
+ // Always offered, for either version: without it a server may not staple (RFC 6066 s8), and
259
+ // a stapled OCSP response is the only revocation signal this package can consume.
260
+ encodeStatusRequest(),
261
+ encodeSupportedGroups(groups),
262
+ encodeSignatureAlgorithms(sigSchemes),
263
+ alpn.length ? encodeAlpn(alpn) : null,
264
+ offersTls13 ? encodeSupportedVersions(versions) : null,
265
+ offersTls13 ? encodeKeyShare(keyShares.map(({ group, keyExchange }) => ({ group, keyExchange }))) : null,
266
+ offersTls13 ? encodePskKeyExchangeModes() : null,
267
+ offersTls12 ? encodeExtendedMasterSecret() : null,
268
+ offersTls12 ? encodeEcPointFormats() : null,
269
+ offersTls12 ? encodeRenegotiationInfo() : null,
270
+ // A HelloRetryRequest cookie arrives here, already encoded, and must go out verbatim.
271
+ ...extraExtensions,
272
+ // pre_shared_key MUST be the last extension in the hello (RFC 8446 s4.2.11) — the binder
273
+ // transcript is "the hello truncated just before the binders", which only names a
274
+ // well-defined byte range if nothing follows them. Servers are required to check.
275
+ psk ? encodePreSharedKey(psk) : null,
276
+ ];
277
+ const offered = new Set();
278
+ for (const part of extensionParts) {
279
+ if (part) offered.add((part[0] << 8) | part[1]);
280
+ }
281
+
282
+ const body = new Builder()
283
+ .u16(LEGACY_VERSION)
284
+ .push(clientRandom)
285
+ .vector(1, sessionId)
286
+ .vector(2, suiteBytes.build())
287
+ .vector(1, Uint8Array.from([0])) // legacy_compression_methods: null only
288
+ .push(encodeExtensionBlock(extensionParts))
289
+ .build();
290
+
291
+ const message = handshakeMessage(HANDSHAKE_TYPE.client_hello, body);
292
+ const out = {
293
+ message,
294
+ clientRandom,
295
+ legacySessionId: sessionId,
296
+ offeredCiphers: suites,
297
+ offeredGroups: groups,
298
+ offeredSigSchemes: sigSchemes,
299
+ offeredExtensions: offered,
300
+ offeredAlpn: alpn,
301
+ };
302
+ if (psk) {
303
+ // The two numbers that make the binder computable — and the two easiest to get wrong.
304
+ // Derived from the one formula in pskBinderTrailerLength, against the FINAL framed message,
305
+ // so "hello minus binders list" cannot drift from what was actually encoded.
306
+ out.binderOffset = message.byteLength - psk.binderLen;
307
+ out.truncatedLength = message.byteLength - pskBinderTrailerLength(psk.binderLen);
308
+ }
309
+ return out;
310
+ }
311
+
312
+ /**
313
+ * Patch the real binder over the placeholder `buildClientHello` emitted. Separate from the
314
+ * builder because the binder is derived FROM the built message (truncated), so there is no
315
+ * ordering in which one function could do both.
316
+ * @param {ClientHello} hello a hello built with a psk offer
317
+ * @param {Uint8Array} binder
318
+ */
319
+ export function setPskBinder(hello, binder) {
320
+ if (hello.binderOffset === undefined ||
321
+ hello.binderOffset + binder.byteLength !== hello.message.byteLength) {
322
+ throw new TlsError(codes.CONFIG_INVALID,
323
+ `cannot patch a ${binder.byteLength}-byte PSK binder: the hello reserved ` +
324
+ `${hello.binderOffset === undefined ? 'no placeholder'
325
+ : `${hello.message.byteLength - hello.binderOffset} bytes`}`);
326
+ }
327
+ hello.message.set(binder, hello.binderOffset);
328
+ }
329
+
330
+ // ------------------------------------------------------------------ ServerHello
331
+
332
+ /**
333
+ * A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
334
+ * everything else is exactly what the wire carried, judged later by the negotiate* functions.
335
+ * @typedef {object} ServerHello
336
+ * @property {number} legacyVersion
337
+ * @property {Uint8Array} random
338
+ * @property {Uint8Array} legacySessionIdEcho
339
+ * @property {number} cipherSuite
340
+ * @property {Map<number, Uint8Array>} extensions
341
+ * @property {boolean} isHelloRetryRequest
342
+ */
343
+
344
+ /**
345
+ * @param {Uint8Array} body
346
+ * @returns {ServerHello} throws on malformed encoding or a compression method other than null
347
+ */
348
+ export function parseServerHello(body) {
349
+ const c = new Cursor(body, 'ServerHello');
350
+ const legacyVersion = c.u16('legacy_version');
351
+ const random = c.take(32, 'random');
352
+ const legacySessionIdEcho = c.vector(1, 'legacy_session_id_echo');
353
+ const cipherSuite = c.u16('cipher_suite');
354
+ const compressionMethod = c.u8('legacy_compression_method');
355
+ // RFC 5246 s7.4.1.4 makes the extension block optional, and TLS 1.2 servers with nothing to say
356
+ // do omit it entirely. (TLS 1.3 always has one, since supported_versions lives there — a 1.3
357
+ // ServerHello without extensions cannot pass negotiateVersion anyway.)
358
+ const extensions = c.done
359
+ ? new Map()
360
+ : decodeExtensionBlock(c.vector(2, 'extensions'), 'ServerHello');
361
+ c.end('ServerHello');
362
+
363
+ if (compressionMethod !== 0) {
364
+ throw new TlsError(
365
+ codes.TLS_HANDSHAKE,
366
+ `server selected compression method ${compressionMethod}; TLS compression is not implemented and is unsafe (CRIME)`,
367
+ { compressionMethod },
368
+ );
369
+ }
370
+ return {
371
+ legacyVersion,
372
+ random,
373
+ legacySessionIdEcho,
374
+ cipherSuite,
375
+ extensions,
376
+ isHelloRetryRequest: equal(random, HELLO_RETRY_REQUEST_RANDOM),
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Decide the negotiated version, and refuse every shape of downgrade.
382
+ *
383
+ * The subtle one is the sentinel check (RFC 8446 s4.1.3): a server that supports TLS 1.3 but was
384
+ * pushed down to 1.2 by an attacker stripping our supported_versions plants a known value in the
385
+ * last 8 bytes of its random. A 1.3-capable client that ignores it is exactly the client the
386
+ * attack targets.
387
+ *
388
+ * @param {ServerHello} serverHello
389
+ * @param {{ offeredVersions: number[] }} offer
390
+ * @returns {number} the negotiated version; every downgrade shape throws instead
391
+ */
392
+ export function negotiateVersion(serverHello, { offeredVersions }) {
393
+ const ext = serverHello.extensions.get(EXTENSION.supported_versions);
394
+ let selected;
395
+ if (ext) {
396
+ selected = decodeSelectedVersion(ext);
397
+ if (selected !== TLS13) {
398
+ throw new TlsError(
399
+ codes.TLS_VERSION_UNSUPPORTED,
400
+ `server sent supported_versions selecting ${describeVersion(selected)}; ` +
401
+ 'that extension may only select TLS 1.3 in a ServerHello',
402
+ { selected },
403
+ );
404
+ }
405
+ if (serverHello.legacyVersion !== LEGACY_VERSION) {
406
+ throw new TlsError(
407
+ codes.TLS_HANDSHAKE,
408
+ `server negotiated TLS 1.3 but set legacy_version to ${describeVersion(serverHello.legacyVersion)}, expected 0x0303`,
409
+ { legacyVersion: serverHello.legacyVersion },
410
+ );
411
+ }
412
+ } else {
413
+ selected = serverHello.legacyVersion;
414
+ }
415
+
416
+ if (!offeredVersions.includes(selected)) {
417
+ throw new TlsUnsupportedError(
418
+ codes.TLS_VERSION_UNSUPPORTED,
419
+ `server selected ${describeVersion(selected)}, which was not offered ` +
420
+ `(offered ${offeredVersions.map(describeVersion).join(', ')}). ` +
421
+ 'TLS 1.0 and 1.1 are not implemented: their only cipher suites are RC4 and CBC ' +
422
+ 'MAC-then-encrypt, whose padding check cannot be made constant-time in JavaScript.',
423
+ { selected, offeredVersions },
424
+ );
425
+ }
426
+
427
+ if (selected === TLS12 && offeredVersions.includes(TLS13)) {
428
+ const tail = serverHello.random.subarray(24, 32);
429
+ if (timingSafeEqual(tail, DOWNGRADE_SENTINEL_12) || timingSafeEqual(tail, DOWNGRADE_SENTINEL_11)) {
430
+ throw new TlsError(
431
+ codes.TLS_VERSION_UNSUPPORTED,
432
+ 'server planted the RFC 8446 downgrade sentinel in ServerHello.random while negotiating ' +
433
+ 'TLS 1.2, which means a TLS 1.3 capable server saw a tampered ClientHello',
434
+ { sentinel: true },
435
+ );
436
+ }
437
+ }
438
+ return selected;
439
+ }
440
+
441
+ /**
442
+ * @param {ServerHello} serverHello
443
+ * @param {{ offeredCiphers: number[], version: number }} offer the negotiated version re-checks
444
+ * the suite's family, so a union offer cannot run a 1.3 suite under 1.2 or the reverse
445
+ * @returns {{ suite: number, params: import('./constants.js').CipherParams }}
446
+ */
447
+ export function negotiateCipher(serverHello, { offeredCiphers, version }) {
448
+ const suite = serverHello.cipherSuite;
449
+ if (!offeredCiphers.includes(suite)) {
450
+ throw new TlsUnsupportedError(
451
+ codes.TLS_CIPHER_UNSUPPORTED,
452
+ `server selected cipher suite ${hex16(suite)}` +
453
+ `${CIPHER_NAME[suite] ? ` (${CIPHER_NAME[suite]})` : ''} under ${describeVersion(version)}, ` +
454
+ 'which was not offered. This package negotiates AEAD suites only: CBC suites are ' +
455
+ 'MAC-then-encrypt and cannot be implemented without a Lucky13 padding oracle in JavaScript.',
456
+ { cipherSuite: suite, version },
457
+ );
458
+ }
459
+ const params = CIPHER_PARAMS[suite];
460
+ if (!params) {
461
+ throw new TlsUnsupportedError(
462
+ codes.TLS_CIPHER_UNSUPPORTED,
463
+ `cipher suite ${hex16(suite)} has no parameters; this is a bug in the offer list`,
464
+ { cipherSuite: suite },
465
+ );
466
+ }
467
+ const isTls13Suite = TLS13_CIPHERS.includes(suite) || suite === 0x1303;
468
+ if (version === TLS13 && !isTls13Suite) {
469
+ throw new TlsError(
470
+ codes.TLS_CIPHER_UNSUPPORTED,
471
+ `server selected TLS 1.2 cipher suite ${hex16(suite)} under TLS 1.3`,
472
+ { cipherSuite: suite },
473
+ );
474
+ }
475
+ if (version === TLS12 && isTls13Suite) {
476
+ throw new TlsError(
477
+ codes.TLS_CIPHER_UNSUPPORTED,
478
+ `server selected TLS 1.3 cipher suite ${hex16(suite)} under TLS 1.2`,
479
+ { cipherSuite: suite },
480
+ );
481
+ }
482
+ return { suite, params };
483
+ }
484
+
485
+ /**
486
+ * RFC 8446 s4.1.3: the server must echo legacy_session_id verbatim. A mismatch means the
487
+ * ServerHello does not belong to our ClientHello.
488
+ * @param {ServerHello} serverHello
489
+ * @param {Uint8Array} legacySessionId
490
+ * @returns {void} throws TlsError on mismatch
491
+ */
492
+ export function checkSessionIdEcho(serverHello, legacySessionId) {
493
+ if (!equal(serverHello.legacySessionIdEcho, legacySessionId)) {
494
+ throw new TlsError(
495
+ codes.TLS_HANDSHAKE,
496
+ 'server did not echo legacy_session_id; the ServerHello does not match our ClientHello',
497
+ );
498
+ }
499
+ }
500
+
501
+ /**
502
+ * The server's chosen key share, validated against what we actually offered.
503
+ * @param {ServerHello} serverHello
504
+ * @param {KeyShare[]} keyShares the shares we generated for the hello
505
+ * @returns {{ group: number, keyExchange: Uint8Array, privateKey: CryptoKey }} the server's
506
+ * group and public key, paired with OUR private key for it
507
+ */
508
+ export function selectServerKeyShare(serverHello, keyShares) {
509
+ const ext = serverHello.extensions.get(EXTENSION.key_share);
510
+ if (!ext) {
511
+ throw new TlsError(
512
+ codes.TLS_HANDSHAKE,
513
+ 'TLS 1.3 ServerHello has no key_share extension; PSK-only resumption is not supported',
514
+ );
515
+ }
516
+ const { group, keyExchange } = decodeKeyShareEntry(ext, 'ServerHello');
517
+ const mine = keyShares.find((k) => k.group === group);
518
+ if (!mine) {
519
+ throw new TlsError(
520
+ codes.TLS_HANDSHAKE,
521
+ `server chose key_share group ${hex16(group)} for which no share was offered`,
522
+ { group },
523
+ );
524
+ }
525
+ return { group, keyExchange, privateKey: mine.privateKey };
526
+ }
527
+
528
+ /**
529
+ * HelloRetryRequest: the server names one group and expects a fresh ClientHello.
530
+ * @param {ServerHello} serverHello
531
+ * @param {{ offeredGroups: number[] }} offer
532
+ * @returns {{ group: number, cookie: Uint8Array | null }}
533
+ */
534
+ export function parseHelloRetryRequest(serverHello, { offeredGroups }) {
535
+ const ext = serverHello.extensions.get(EXTENSION.key_share);
536
+ if (!ext) {
537
+ throw new TlsError(codes.TLS_HANDSHAKE, 'HelloRetryRequest has no key_share extension');
538
+ }
539
+ const group = decodeKeyShareHrr(ext);
540
+ if (!offeredGroups.includes(group)) {
541
+ throw new TlsError(
542
+ codes.TLS_HANDSHAKE,
543
+ `HelloRetryRequest asked for group ${hex16(group)}, which was not in supported_groups`,
544
+ { group },
545
+ );
546
+ }
547
+ requireSupportedGroup(group, 'HelloRetryRequest');
548
+ const cookie = serverHello.extensions.get(EXTENSION.cookie) ?? null;
549
+ return { group, cookie };
550
+ }
551
+
552
+ // ------------------------------------------------------------------ NewSessionTicket
553
+
554
+ /**
555
+ * NewSessionTicket (RFC 8446 s4.6.1), the post-handshake message a resumption PSK is minted
556
+ * from. Strict on structure — a truncated field, trailing bytes, or a zero-length ticket ends
557
+ * the connection, because a peer whose post-handshake messages do not parse cannot be trusted
558
+ * to frame the application data either — but faithful to s4.6.1 on extensions: early_data is
559
+ * validated in shape and its value RECORDED but never acted on (0-RTT is deliberately not
560
+ * implemented; see the driver), and unrecognized extensions are ignored, which s4.6.1 makes
561
+ * mandatory ("Clients MUST ignore unrecognized extensions").
562
+ *
563
+ * Lifetime semantics (zero means discard, 604800 s is the cap a client may honour) are POLICY,
564
+ * applied by the ticket store; this function reports what the wire said.
565
+ *
566
+ * @param {Uint8Array} body
567
+ * @returns {{ lifetimeSec: number, ageAdd: number, nonce: Uint8Array, ticket: Uint8Array,
568
+ * maxEarlyDataSize: number | null }}
569
+ */
570
+ export function parseNewSessionTicket(body) {
571
+ const c = new Cursor(body, 'NewSessionTicket');
572
+ const lifetimeSec = c.u32('ticket_lifetime');
573
+ const ageAdd = c.u32('ticket_age_add');
574
+ const nonce = c.vector(1, 'ticket_nonce');
575
+ const ticket = c.vector(2, 'ticket');
576
+ const exts = decodeExtensionBlock(c.vector(2, 'extensions'), 'NewSessionTicket');
577
+ c.end('NewSessionTicket');
578
+ if (ticket.byteLength === 0) {
579
+ // opaque ticket<1..2^16-1>: zero is outside the vector's floor, and an empty identity could
580
+ // never be offered back (encodePreSharedKey refuses it), so it is dead on arrival.
581
+ throw new TlsError(codes.TLS_TICKET, 'NewSessionTicket carries a zero-length ticket');
582
+ }
583
+ let maxEarlyDataSize = null;
584
+ for (const [type, data] of exts) {
585
+ if (type === EXTENSION.early_data) {
586
+ if (data.byteLength !== 4) {
587
+ throw new TlsError(codes.TLS_TICKET,
588
+ `NewSessionTicket early_data extension is ${data.byteLength} bytes; ` +
589
+ 'max_early_data_size is a uint32 (RFC 8446 s4.2.10)',
590
+ { length: data.byteLength });
591
+ }
592
+ maxEarlyDataSize = new Cursor(data, 'early_data').u32('max_early_data_size');
593
+ }
594
+ // Anything else: ignored by requirement of s4.6.1. Duplicates were already fatal above.
595
+ }
596
+ return { lifetimeSec, ageAdd, nonce, ticket, maxEarlyDataSize };
597
+ }
598
+
599
+ // ------------------------------------------------------------------ Certificate
600
+
601
+ /**
602
+ * The CertificateStatus body of RFC 6066 s8: `status_type(1) || opaque OCSPResponse<1..2^24-1>`.
603
+ * Two carriers share this exact shape — the TLS 1.2 CertificateStatus handshake message, and the
604
+ * extension_data of a TLS 1.3 status_request CertificateEntry extension (RFC 8446 s4.4.2.1) —
605
+ * which is why it is one parser and not two.
606
+ *
607
+ * @param {Uint8Array} body
608
+ * @param {string} where named in errors
609
+ * @returns {Uint8Array} the DER OCSPResponse, exactly as sent; its meaning is the trust layer's
610
+ * problem, not this layer's
611
+ */
612
+ export function parseCertificateStatus(body, where) {
613
+ const c = new Cursor(body, where);
614
+ const statusType = c.u8('status_type');
615
+ if (statusType !== 1) {
616
+ throw new TlsError(
617
+ codes.TLS_HANDSHAKE,
618
+ `${where} carries status_type ${statusType}; only ocsp(1) is defined (RFC 6066 s8) and ` +
619
+ 'nothing else can be consumed',
620
+ { statusType },
621
+ );
622
+ }
623
+ const response = c.vector(3, 'OCSPResponse');
624
+ c.end(where);
625
+ if (response.byteLength === 0) {
626
+ // opaque OCSPResponse<1..2^24-1>: the zero length is outside the vector's floor, and an
627
+ // empty "response" pretending to be a staple must not read as one.
628
+ throw new TlsError(codes.TLS_HANDSHAKE, `${where} carries a zero-length OCSPResponse`);
629
+ }
630
+ return response;
631
+ }
632
+
633
+ /**
634
+ * TLS 1.3 Certificate (RFC 8446 s4.4.2): context, then entries carrying per-cert extensions.
635
+ *
636
+ * Entry extensions are policed, not skipped: RFC 8446 s4.4.2 allows a server to send only
637
+ * extensions the ClientHello offered, and s4.2 confines each type to specific messages — for
638
+ * CertificateEntry that is status_request and signed_certificate_timestamp. An extension we
639
+ * cannot attribute to our own offer is either a server confusion or a smuggling attempt, and
640
+ * both end the handshake.
641
+ *
642
+ * Only the LEAF's stapled OCSP response is returned. A server may staple for intermediates too;
643
+ * those staples are validated structurally (they must still be well-formed CertificateStatus)
644
+ * but not consumed — this package checks revocation of the identity it is authenticating, and
645
+ * inventing partial intermediate coverage would imply a guarantee it does not give.
646
+ *
647
+ * @param {Uint8Array} body
648
+ * @param {{ offeredExtensions?: Set<number> }} [opts] extension types our ClientHello offered.
649
+ * Omitting it means "nothing was offered", the fail-closed reading.
650
+ * @returns {{ chain: Uint8Array[], ocspResponse: Uint8Array | null }} DER certificates in wire
651
+ * order (leaf first), plus the leaf's stapled DER OCSPResponse if the server sent one
652
+ */
653
+ export function parseCertificate13(body, { offeredExtensions = new Set() } = {}) {
654
+ const c = new Cursor(body, 'Certificate');
655
+ const context = c.vector(1, 'certificate_request_context');
656
+ if (context.byteLength !== 0) {
657
+ throw new TlsError(
658
+ codes.TLS_HANDSHAKE,
659
+ `server Certificate has a ${context.byteLength}-byte certificate_request_context; must be empty`,
660
+ );
661
+ }
662
+ const list = c.sub(3, 'certificate_list');
663
+ const chain = [];
664
+ let ocspResponse = null;
665
+ while (!list.done) {
666
+ const der = list.vector(3, 'cert_data');
667
+ if (der.byteLength === 0) {
668
+ throw new TlsError(codes.TLS_HANDSHAKE, 'Certificate entry has zero-length cert_data');
669
+ }
670
+ const entryLabel = `CertificateEntry ${chain.length}`;
671
+ const exts = decodeExtensionBlock(list.vector(2, 'certificate extensions'), entryLabel);
672
+ rejectUnofferedExtensions(exts, offeredExtensions, entryLabel);
673
+ for (const [type, data] of exts) {
674
+ if (type === EXTENSION.status_request) {
675
+ const staple = parseCertificateStatus(data, `${entryLabel} status_request`);
676
+ if (chain.length === 0) ocspResponse = staple;
677
+ } else if (type !== EXTENSION.signed_certificate_timestamp) {
678
+ // Offered in the hello, but RFC 8446 s4.2 does not admit it in a Certificate message:
679
+ // the server answered a question in the wrong room, which s4.2 makes fatal
680
+ // (illegal_parameter), not ignorable.
681
+ throw new TlsError(
682
+ codes.TLS_HANDSHAKE,
683
+ `${entryLabel} carries extension ${hex16(type)}, which does not belong in a ` +
684
+ 'Certificate message (RFC 8446 s4.2)',
685
+ { extension: type },
686
+ );
687
+ }
688
+ }
689
+ chain.push(der);
690
+ }
691
+ c.end('Certificate');
692
+ if (chain.length === 0) {
693
+ throw new CertificateError(codes.CERT_CHAIN_INCOMPLETE, 'server sent an empty certificate_list');
694
+ }
695
+ return { chain, ocspResponse };
696
+ }
697
+
698
+ /**
699
+ * TLS 1.2 Certificate (RFC 5246 s7.4.2): a bare list, no context and no per-cert extensions.
700
+ * @param {Uint8Array} body
701
+ * @returns {Uint8Array[]} DER certificates in wire order, leaf first
702
+ */
703
+ export function parseCertificate12(body) {
704
+ const c = new Cursor(body, 'Certificate');
705
+ const list = c.sub(3, 'certificate_list');
706
+ const chain = [];
707
+ while (!list.done) {
708
+ const der = list.vector(3, 'certificate');
709
+ if (der.byteLength === 0) {
710
+ throw new TlsError(codes.TLS_HANDSHAKE, 'Certificate entry has zero length');
711
+ }
712
+ chain.push(der);
713
+ }
714
+ c.end('Certificate');
715
+ if (chain.length === 0) {
716
+ throw new CertificateError(codes.CERT_CHAIN_INCOMPLETE, 'server sent an empty certificate_list');
717
+ }
718
+ return chain;
719
+ }
720
+
721
+ /**
722
+ * @param {Uint8Array} body
723
+ * @returns {{ algorithm: number, signature: Uint8Array }}
724
+ */
725
+ export function parseCertificateVerify(body) {
726
+ const c = new Cursor(body, 'CertificateVerify');
727
+ const algorithm = c.u16('signature algorithm');
728
+ const signature = c.vector(2, 'signature');
729
+ c.end('CertificateVerify');
730
+ return { algorithm, signature };
731
+ }
732
+
733
+ /**
734
+ * RFC 8446 s4.4.3: 64 spaces, a context string, a zero byte, then the transcript hash.
735
+ * @param {Uint8Array} transcriptHash
736
+ * @param {boolean} [isServer]
737
+ * @returns {Uint8Array}
738
+ */
739
+ export function certificateVerifyContent(transcriptHash, isServer = true) {
740
+ const label = isServer ? 'TLS 1.3, server CertificateVerify' : 'TLS 1.3, client CertificateVerify';
741
+ return concat([new Uint8Array(64).fill(0x20), utf8(label), Uint8Array.from([0]), transcriptHash]);
742
+ }
743
+
744
+ /**
745
+ * Verify a handshake signature with WebCrypto.
746
+ * `spki` is the DER SubjectPublicKeyInfo lifted straight out of the leaf certificate, so the key
747
+ * used to check the signature is provably the key the trust layer validated.
748
+ *
749
+ * @param {object} args
750
+ * @param {number} args.scheme signature scheme id from the wire
751
+ * @param {Uint8Array} args.spki DER SubjectPublicKeyInfo of the validated leaf
752
+ * @param {Uint8Array} args.signature as received: DER for ECDSA, raw otherwise
753
+ * @param {Uint8Array} args.content the exact bytes the signature must cover
754
+ * @returns {Promise<true>} every failure throws; there is no false
755
+ */
756
+ export async function verifyHandshakeSignature({ scheme, spki, signature, content }) {
757
+ const params = SIG_SCHEME_PARAMS[scheme];
758
+ if (!params) {
759
+ throw new TlsUnsupportedError(
760
+ codes.TLS_SIGALG_UNSUPPORTED,
761
+ `server signed the handshake with ${describeSigScheme(scheme)}, which is not implemented; ` +
762
+ `offered ${SUPPORTED_SIG_SCHEMES.map(describeSigScheme).join(', ')}`,
763
+ { scheme },
764
+ );
765
+ }
766
+ let key;
767
+ try {
768
+ key = await crypto.subtle.importKey('spki', spki, params.import, false, ['verify']);
769
+ } catch (cause) {
770
+ throw new TlsError(
771
+ codes.TLS_HANDSHAKE,
772
+ `certificate public key cannot be used with ${describeSigScheme(scheme)}: ${cause?.message}`,
773
+ { scheme },
774
+ );
775
+ }
776
+
777
+ // TLS carries ECDSA signatures as a DER ECDSA-Sig-Value; WebCrypto accepts only the fixed-width
778
+ // r||s concatenation. Omitting this conversion produces a client that verifies its own
779
+ // signatures happily and fails against every real ECDSA server — which is most of them.
780
+ const sigBytes =
781
+ params.format === 'ecdsa-der'
782
+ ? ecdsaDerToRaw(signature, params.curveOrderLen, (why) =>
783
+ new TlsError(
784
+ codes.TLS_HANDSHAKE,
785
+ `handshake signature (${describeSigScheme(scheme)}) is not a well-formed ` +
786
+ `ECDSA-Sig-Value: ${why}`,
787
+ { scheme },
788
+ ))
789
+ : signature;
790
+
791
+ const ok = await crypto.subtle.verify(params.verify, key, sigBytes, content);
792
+ if (!ok) {
793
+ throw new TlsError(
794
+ codes.TLS_HANDSHAKE,
795
+ `handshake signature (${describeSigScheme(scheme)}) does not verify against the certificate public key`,
796
+ { scheme },
797
+ );
798
+ }
799
+ return true;
800
+ }
801
+
802
+ // ------------------------------------------------------------------ TLS 1.2 ServerKeyExchange
803
+
804
+ /**
805
+ * RFC 8422 s5.4. Only named-curve ECDHE is accepted: explicit curves are a decade-dead feature
806
+ * and finite-field DHE would need bignum arithmetic WebCrypto does not expose.
807
+ *
808
+ * @param {Uint8Array} body
809
+ * @returns {{ group: number, publicKey: Uint8Array, signatureAlgorithm: number,
810
+ * signature: Uint8Array, signedParams: Uint8Array }} `signedParams` is the exact byte range
811
+ * the server's signature covers (curve_type through the public key)
812
+ */
813
+ export function parseServerKeyExchangeEcdhe(body) {
814
+ const c = new Cursor(body, 'ServerKeyExchange');
815
+ const curveType = c.u8('curve_type');
816
+ if (curveType !== 3) {
817
+ throw new TlsUnsupportedError(
818
+ codes.TLS_GROUP_UNSUPPORTED,
819
+ `server used ECCurveType ${curveType} in ServerKeyExchange; only named_curve (3) is implemented`,
820
+ { curveType },
821
+ );
822
+ }
823
+ const group = c.u16('named_curve');
824
+ const publicKey = c.vector(1, 'ECPoint');
825
+ const signatureAlgorithm = c.u16('SignatureAndHashAlgorithm');
826
+ const signature = c.vector(2, 'signature');
827
+ c.end('ServerKeyExchange');
828
+ // The signed blob is everything from curve_type through the public key.
829
+ const signedParams = body.subarray(0, 4 + publicKey.byteLength);
830
+ return { group, publicKey, signatureAlgorithm, signature, signedParams };
831
+ }
832
+
833
+ /**
834
+ * TLS 1.2 signs client_random || server_random || ServerECDHParams.
835
+ * @param {Uint8Array} clientRandom
836
+ * @param {Uint8Array} serverRandom
837
+ * @param {Uint8Array} signedParams
838
+ * @returns {Uint8Array}
839
+ */
840
+ export function serverKeyExchangeContent(clientRandom, serverRandom, signedParams) {
841
+ return concat([clientRandom, serverRandom, signedParams]);
842
+ }
843
+
844
+ /**
845
+ * @param {Uint8Array} publicKey
846
+ * @returns {Uint8Array} framed ClientKeyExchange message
847
+ */
848
+ export function buildClientKeyExchange(publicKey) {
849
+ return handshakeMessage(HANDSHAKE_TYPE.client_key_exchange, vector(1, publicKey));
850
+ }
851
+
852
+ /**
853
+ * @param {Uint8Array} verifyData
854
+ * @returns {Uint8Array} framed Finished message
855
+ */
856
+ export function buildFinished(verifyData) {
857
+ return handshakeMessage(HANDSHAKE_TYPE.finished, verifyData);
858
+ }
859
+
860
+ /**
861
+ * Compare a peer Finished against ours. Constant-time in intent: verify_data is derived from
862
+ * secrets the peer must already know, so a timing leak is not a decryption oracle, but there is
863
+ * no reason to leak the prefix length either.
864
+ * @param {Uint8Array} received
865
+ * @param {Uint8Array} expected
866
+ * @returns {true} a mismatch throws; there is no false
867
+ */
868
+ export function checkFinished(received, expected) {
869
+ if (!timingSafeEqual(received, expected)) {
870
+ throw new TlsError(
871
+ codes.TLS_HANDSHAKE,
872
+ 'peer Finished verify_data does not match; the handshake transcript differs, which means ' +
873
+ 'the connection was tampered with or the peer is not speaking the negotiated parameters',
874
+ );
875
+ }
876
+ return true;
877
+ }
878
+
879
+ /**
880
+ * The negotiated ALPN protocol, refusing anything we did not offer.
881
+ * @param {Map<number, Uint8Array>} extensions
882
+ * @param {string[]} offeredAlpn
883
+ * @param {string} where
884
+ * @returns {string | null} null when the server declined ALPN entirely
885
+ */
886
+ export function checkAlpn(extensions, offeredAlpn, where) {
887
+ const ext = extensions.get(EXTENSION.alpn);
888
+ if (!ext) return null; // absent means the server declined; HTTP/1.1 is the default anyway
889
+ const selected = decodeAlpn(ext);
890
+ if (!offeredAlpn.includes(selected)) {
891
+ throw new TlsError(
892
+ codes.TLS_ALPN,
893
+ `server selected ALPN protocol "${selected}" in ${where}, which was not offered ` +
894
+ `(offered ${offeredAlpn.map((p) => `"${p}"`).join(', ')})`,
895
+ { selected, offeredAlpn },
896
+ );
897
+ }
898
+ return selected;
899
+ }
900
+
901
+ export { GROUP_PARAMS };