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,507 @@
1
+ // TLS 1.2 client handshake driver.
2
+ //
3
+ // This driver exists for servers that never learned TLS 1.3; it is a fallback, not a peer of
4
+ // handshake.js in importance, but it must meet exactly the same bar: on this runtime nothing
5
+ // below us verifies a tunnelled peer, so every guarantee the caller gets is manufactured here.
6
+ // The negotiable surface is deliberately tiny — ECDHE key exchange and AES-GCM only (see
7
+ // constants.js for the reasoning) — and three refusals are absolute:
8
+ //
9
+ // * No renegotiation. A HelloRequest at any point is an error, never a trigger. Renegotiation
10
+ // is the mechanism behind the 2009 prefix-injection MITM (CVE-2009-3555), and a client that
11
+ // only ever dials out has no legitimate use for it.
12
+ // * No resumption. We send a throwaway session id (middlebox camouflage, same as 1.3); a
13
+ // server echoing it announces an abbreviated handshake that skips authentication, which is
14
+ // not a handshake we are willing to have. Every connection pays for the full flight.
15
+ // * No ServerKeyExchange, no connection. Without it there is no ephemeral key: the selected
16
+ // suite would degenerate into static key transport with no forward secrecy, which this
17
+ // package refuses to implement.
18
+ //
19
+ // Two ordering rules are load-bearing, mirroring the 1.3 driver:
20
+ //
21
+ // 1. The certificate chain is validated by the injected trust layer BEFORE the
22
+ // ServerKeyExchange signature is checked against the leaf's key, and both happen before any
23
+ // key material or Finished leaves this side. Signature-then-trust would only prove that
24
+ // whoever holds the socket also holds a key — no evidence at all.
25
+ // 2. TLS 1.2 signs only client_random || server_random || ServerECDHParams — the ServerHello
26
+ // and the chosen suite are NOT covered by that signature, only (eventually) by Finished. So
27
+ // the transcript must be byte-exact at both Finished computations, and when the server
28
+ // supports extended master secret (RFC 7627) the master secret itself is bound to the
29
+ // session hash, closing the triple-handshake gap that the plain randoms-based derivation
30
+ // leaves open. EMS is preferred whenever offered back; a server that declines is accepted,
31
+ // but the downgrade is reported in info.extendedMasterSecret so callers can see it.
32
+ //
33
+ // The ClientHello/ServerHello preamble lives in connect.js, shared with the 1.3 driver so that
34
+ // one hello can offer both versions; continueTls12 below is everything that happens after the
35
+ // ServerHello routed the connection here. handshakeTls12 remains the single-version entry: it is
36
+ // connectTls with the offer pinned to [TLS 1.2], which keeps the hello free of every 1.3
37
+ // extension exactly as this driver always sent it.
38
+ //
39
+ // Verification is injected rather than imported, for the same reasons as the 1.3 driver: this
40
+ // module holds no trust policy, and the handshake must be replayable offline.
41
+
42
+ import { TlsError, TlsUnsupportedError, codes, hex8, hex16 } from '../errors.js';
43
+ import {
44
+ CIPHER_NAME,
45
+ EXTENSION,
46
+ HANDSHAKE_TYPE,
47
+ SIG_SCHEME,
48
+ TLS12,
49
+ } from './constants.js';
50
+ import {
51
+ extendedMasterSecret12,
52
+ keyBlock12,
53
+ masterSecret12,
54
+ verifyData12,
55
+ } from './keyschedule.js';
56
+ import {
57
+ buildClientKeyExchange,
58
+ buildFinished,
59
+ checkAlpn,
60
+ checkFinished,
61
+ deriveSharedSecret,
62
+ generateKeyShare,
63
+ parseCertificate12,
64
+ parseCertificateStatus,
65
+ parseServerKeyExchangeEcdhe,
66
+ serverKeyExchangeContent,
67
+ verifyHandshakeSignature,
68
+ } from './handshake-messages.js';
69
+ import { describeSigScheme, rejectUnofferedExtensions } from './extensions.js';
70
+ import { equal } from '../util/bytes.js';
71
+ import { handshakeMessage, vector } from './wire.js';
72
+ import { connectTls } from './connect.js';
73
+
74
+ const HS_NAME = Object.fromEntries(Object.entries(HANDSHAKE_TYPE).map(([k, v]) => [v, k]));
75
+
76
+ /** HelloRequest has no entry in HANDSHAKE_TYPE because no other module may ever act on one. */
77
+ const HELLO_REQUEST = 0;
78
+
79
+ const describeType = (t) => `${hex8(t)} (${HS_NAME[t] ?? 'unknown'})`;
80
+ const describeSuite = (s) => `${hex16(s)}${CIPHER_NAME[s] ? ` (${CIPHER_NAME[s]})` : ''}`;
81
+
82
+ /**
83
+ * Which authentication family a signature scheme proves. The cipher suite name pins the server
84
+ * certificate's key type (ECDHE_RSA vs ECDHE_ECDSA, with EdDSA folded into the ECDSA family by
85
+ * RFC 8422 s5.10), but the ServerKeyExchange signature does not cover the ServerHello — so this
86
+ * check is the only thing tying the signature algorithm to the suite the server claims to be
87
+ * honouring until Finished lands.
88
+ */
89
+ const SIG_KIND = {
90
+ [SIG_SCHEME.ecdsa_secp256r1_sha256]: 'ecdsa',
91
+ [SIG_SCHEME.ecdsa_secp384r1_sha384]: 'ecdsa',
92
+ [SIG_SCHEME.ecdsa_secp521r1_sha512]: 'ecdsa',
93
+ [SIG_SCHEME.ed25519]: 'ecdsa',
94
+ [SIG_SCHEME.rsa_pkcs1_sha256]: 'rsa',
95
+ [SIG_SCHEME.rsa_pkcs1_sha384]: 'rsa',
96
+ [SIG_SCHEME.rsa_pkcs1_sha512]: 'rsa',
97
+ [SIG_SCHEME.rsa_pss_rsae_sha256]: 'rsa',
98
+ [SIG_SCHEME.rsa_pss_rsae_sha384]: 'rsa',
99
+ [SIG_SCHEME.rsa_pss_rsae_sha512]: 'rsa',
100
+ };
101
+
102
+ /**
103
+ * The one refusal that must fire regardless of which message was expected. Exported for
104
+ * connect.js, whose ServerHello wait is the earliest point a 1.2 server can spring one.
105
+ * @returns {never}
106
+ */
107
+ export function refuseHelloRequest() {
108
+ throw new TlsError(
109
+ codes.TLS_HANDSHAKE,
110
+ 'server sent HelloRequest; renegotiation is refused. Renegotiation lets a MITM splice an ' +
111
+ 'attacker-controlled prefix onto the authenticated stream (CVE-2009-3555), and this ' +
112
+ 'client has no use for it',
113
+ { type: HELLO_REQUEST },
114
+ );
115
+ }
116
+
117
+ /** Demand a specific handshake message, turning every other outcome into a named failure. */
118
+ function expect(msg, type, where) {
119
+ if (msg === null) {
120
+ throw new TlsError(
121
+ codes.TLS_TRUNCATED,
122
+ `server closed the connection during the handshake while ${where} was expected`,
123
+ { expected: HS_NAME[type] ?? type },
124
+ );
125
+ }
126
+ if (msg.ccs) {
127
+ throw new TlsError(codes.TLS_RECORD, `change_cipher_spec arrived where ${where} was expected`, {
128
+ expected: HS_NAME[type] ?? type,
129
+ });
130
+ }
131
+ if (msg.type === HELLO_REQUEST) refuseHelloRequest();
132
+ if (msg.type !== type) {
133
+ throw new TlsError(
134
+ codes.TLS_HANDSHAKE,
135
+ `server sent handshake type ${describeType(msg.type)} where ${where} was expected`,
136
+ { got: msg.type, expected: type },
137
+ );
138
+ }
139
+ return msg;
140
+ }
141
+
142
+ /** Demand the server's ChangeCipherSpec, which in 1.2 is a real key-change signal. */
143
+ function expectCcs(msg) {
144
+ if (msg === null) {
145
+ throw new TlsError(
146
+ codes.TLS_TRUNCATED,
147
+ 'server closed the connection while its ChangeCipherSpec was expected',
148
+ );
149
+ }
150
+ if (msg.ccs) return;
151
+ if (msg.type === HELLO_REQUEST) refuseHelloRequest();
152
+ throw new TlsError(
153
+ codes.TLS_HANDSHAKE,
154
+ `server sent handshake type ${describeType(msg.type)} where its ChangeCipherSpec was ` +
155
+ 'expected; a Finished before the key change would arrive unprotected',
156
+ { got: msg.type },
157
+ );
158
+ }
159
+
160
+
161
+ /**
162
+ * Run a TLS 1.2 handshake over a byte duplex and return the plaintext duplex above it.
163
+ *
164
+ * @param {object} args
165
+ * @param {import('./connect.js').ByteDuplex} args.transport
166
+ * @param {string} args.hostname the identity the certificate must prove, and the SNI sent
167
+ * @param {import('./handshake.js').VerifyPeer} args.verifyPeer
168
+ * Must throw to reject. Resolves with the validated leaf; its SPKI is the only key this
169
+ * handshake will accept a ServerKeyExchange signature from.
170
+ * @param {import('./connect.js').TlsOptions} [args.options] `versions` is ignored: this entry
171
+ * pins the offer to [TLS 1.2]
172
+ * @param {import('./connect.js').TlsDeps} [args.deps]
173
+ * @returns {Promise<import('./connect.js').TlsSession>}
174
+ */
175
+ export async function handshakeTls12({ transport, hostname, verifyPeer, options = {}, deps = {} }) {
176
+ if (typeof verifyPeer !== 'function') {
177
+ // Refusing to start is the only safe default. A missing verifier must never read as "skip".
178
+ throw new TlsError(
179
+ codes.CONFIG_INVALID,
180
+ 'handshakeTls12 requires a verifyPeer function; there is no unverified mode',
181
+ );
182
+ }
183
+ // The offer is pinned to [TLS 1.2]: no supported_versions, no key_share, no PSK modes. A
184
+ // 1.3-capable ClientHello answered by this driver would mean the version dispatch above us
185
+ // routed wrongly, and the 1.3 downgrade protections (sentinel, supported_versions echo) would
186
+ // silently not apply — so the offer itself must make that state unrepresentable.
187
+ return connectTls({
188
+ transport,
189
+ hostname,
190
+ verifyPeer,
191
+ deps,
192
+ options: { ...options, versions: [TLS12] },
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Continue a TLS 1.2 handshake from the ServerHello. Called by connect.js once negotiation
198
+ * routed the connection here; the record layer arrives already pinned to 1.2 semantics and the
199
+ * transcript already runs under the negotiated suite's PRF hash.
200
+ * @param {import('./handshake.js').HandshakeContext} ctx
201
+ * @returns {Promise<import('./connect.js').TlsSession>}
202
+ */
203
+ export async function continueTls12(ctx) {
204
+ const { record, transcript, hello, serverHello: sh, rawServerHello, suite, params } = ctx;
205
+ const { hostname, verifyPeer, deps } = ctx;
206
+ const { alpn, groups } = ctx.offer;
207
+ const hash = params.hash;
208
+
209
+ // RFC 5246 s7.4.1.3: a server echoing the client's session id is announcing an abbreviated
210
+ // handshake — ChangeCipherSpec and Finished next, no certificate, keys from a cached master
211
+ // secret we do not hold. Ours is a throwaway id, so an echo is either a broken cache or an
212
+ // attempt to skip authentication; the CCS guard below would catch the flight shape anyway,
213
+ // but naming the cause here beats "change_cipher_spec arrived where Certificate was expected".
214
+ if (
215
+ hello.legacySessionId.byteLength > 0 &&
216
+ equal(sh.legacySessionIdEcho, hello.legacySessionId)
217
+ ) {
218
+ throw new TlsError(
219
+ codes.TLS_HANDSHAKE,
220
+ 'server echoed our session id, which announces an abbreviated (resumed) handshake; ' +
221
+ 'session resumption is not supported and the handshake is never abbreviated',
222
+ );
223
+ }
224
+
225
+ // RFC 5246 s7.4.1.4: the ServerHello may only carry extensions the ClientHello offered. An
226
+ // unoffered one (session_ticket is the classic) would commit us to protocol behaviour we do
227
+ // not implement, so it is refused before any of its semantics can be presumed.
228
+ rejectUnofferedExtensions(sh.extensions, hello.offeredExtensions, 'ServerHello');
229
+
230
+ // When the ClientHello offered 1.3 as well, the 1.3-only extensions WERE offered, so the
231
+ // check above can no longer catch a server echoing them while negotiating 1.2. RFC 8446
232
+ // s4.1.4 permits key_share in a ServerHello only when 1.3 was selected, and
233
+ // psk_key_exchange_modes may never appear in one at all; a server that answers the 1.3 half
234
+ // of the offer while refusing the 1.3 that gives it meaning is confused at best and splicing
235
+ // two handshakes at worst. (supported_versions cannot reach this driver: negotiateVersion
236
+ // either routed it to 1.3 or refused it outright.)
237
+ for (const type of [EXTENSION.key_share, EXTENSION.psk_key_exchange_modes]) {
238
+ if (sh.extensions.has(type)) {
239
+ throw new TlsError(
240
+ codes.TLS_HANDSHAKE,
241
+ `server echoed the TLS 1.3-only extension ${hex16(type)} in a ServerHello that ` +
242
+ 'negotiates TLS 1.2',
243
+ { extension: type },
244
+ );
245
+ }
246
+ }
247
+
248
+ const reneg = sh.extensions.get(EXTENSION.renegotiation_info);
249
+ if (reneg && !(reneg.byteLength === 1 && reneg[0] === 0x00)) {
250
+ // RFC 5746 s3.4: on an initial handshake the server's renegotiated_connection must be
251
+ // empty. Anything else means the server believes this connection is a renegotiation.
252
+ throw new TlsError(
253
+ codes.TLS_HANDSHAKE,
254
+ 'server sent a non-empty renegotiation_info, treating this connection as a ' +
255
+ 'renegotiation; renegotiation is refused',
256
+ { length: reneg.byteLength },
257
+ );
258
+ }
259
+
260
+ const emsEcho = sh.extensions.get(EXTENSION.extended_master_secret);
261
+ if (emsEcho && emsEcho.byteLength !== 0) {
262
+ throw new TlsError(
263
+ codes.TLS_HANDSHAKE,
264
+ `extended_master_secret extension_data must be empty, got ${emsEcho.byteLength} bytes`,
265
+ { length: emsEcho.byteLength },
266
+ );
267
+ }
268
+ const extendedMasterSecret = emsEcho !== undefined;
269
+
270
+ // RFC 6066 s8: a server that intends to send CertificateStatus announces it by echoing
271
+ // status_request with EMPTY extension_data. The echo is permission to send the message, not a
272
+ // promise to — a server may echo and then have nothing to staple.
273
+ const statusEcho = sh.extensions.get(EXTENSION.status_request);
274
+ if (statusEcho && statusEcho.byteLength !== 0) {
275
+ throw new TlsError(
276
+ codes.TLS_HANDSHAKE,
277
+ `status_request in a ServerHello must have empty extension_data (RFC 6066 s8), got ` +
278
+ `${statusEcho.byteLength} bytes`,
279
+ { length: statusEcho.byteLength },
280
+ );
281
+ }
282
+
283
+ // In 1.2 the ALPN answer lives in the ServerHello — there is no EncryptedExtensions.
284
+ const alpnProtocol = checkAlpn(sh.extensions, alpn, 'ServerHello');
285
+ transcript.update(rawServerHello);
286
+
287
+ // --- server flight -------------------------------------------------------------------------
288
+ const certMsg = expect(
289
+ await record.nextHandshakeMessage(),
290
+ HANDSHAKE_TYPE.certificate,
291
+ 'Certificate',
292
+ );
293
+ transcript.update(certMsg.raw);
294
+ const chain = parseCertificate12(certMsg.body);
295
+
296
+ let next = await record.nextHandshakeMessage();
297
+
298
+ // CertificateStatus (RFC 6066 s8): the stapled OCSP response, arriving as its own handshake
299
+ // message immediately after Certificate — TLS 1.2's shape of what 1.3 carries inside the
300
+ // CertificateEntry. Legal only when the ServerHello echoed status_request; a staple nobody
301
+ // announced is a message from outside the negotiated protocol and ends the handshake. It is
302
+ // optional even when announced. The message must be folded into the transcript exactly here,
303
+ // like every other handshake message, or both Finished computations drift.
304
+ let ocspResponse = null;
305
+ if (next !== null && !next.ccs && next.type === HANDSHAKE_TYPE.certificate_status) {
306
+ if (statusEcho === undefined) {
307
+ throw new TlsError(
308
+ codes.TLS_HANDSHAKE,
309
+ 'server sent CertificateStatus without echoing status_request in its ServerHello ' +
310
+ '(RFC 6066 s8 requires the echo first)',
311
+ );
312
+ }
313
+ ocspResponse = parseCertificateStatus(next.body, 'CertificateStatus');
314
+ transcript.update(next.raw);
315
+ next = await record.nextHandshakeMessage();
316
+ }
317
+
318
+ if (
319
+ next !== null &&
320
+ !next.ccs &&
321
+ (next.type === HANDSHAKE_TYPE.certificate_request ||
322
+ next.type === HANDSHAKE_TYPE.server_hello_done)
323
+ ) {
324
+ // The flight skipped straight past ServerKeyExchange. For every suite this package offers
325
+ // that message carries the ephemeral key, so its absence is not a variant — it is the
326
+ // server trying to run the suite as static key transport.
327
+ throw new TlsError(
328
+ codes.TLS_HANDSHAKE,
329
+ `server omitted ServerKeyExchange under ${describeSuite(suite)}. Every suite this ` +
330
+ 'package offers is ECDHE: without ServerKeyExchange there is no ephemeral key and no ' +
331
+ 'forward secrecy, and static-RSA key transport is deliberately not implemented',
332
+ { cipherSuite: suite, got: next.type },
333
+ );
334
+ }
335
+ const skeMsg = expect(next, HANDSHAKE_TYPE.server_key_exchange, 'ServerKeyExchange');
336
+ transcript.update(skeMsg.raw);
337
+ const ske = parseServerKeyExchangeEcdhe(skeMsg.body);
338
+ if (!groups.includes(ske.group)) {
339
+ throw new TlsError(
340
+ codes.TLS_HANDSHAKE,
341
+ `server chose group ${hex16(ske.group)} in ServerKeyExchange, which was not offered in ` +
342
+ 'supported_groups',
343
+ { group: ske.group },
344
+ );
345
+ }
346
+
347
+ let certificateRequested = false;
348
+ next = await record.nextHandshakeMessage();
349
+ if (next !== null && !next.ccs && next.type === HANDSHAKE_TYPE.certificate_request) {
350
+ // We hold no client certificate, so the body's demands are irrelevant — the answer is an
351
+ // empty Certificate in the client flight either way (RFC 5246 s7.4.6) — but the message
352
+ // still has to be remembered and folded into the transcript at this exact point.
353
+ certificateRequested = true;
354
+ transcript.update(next.raw);
355
+ next = await record.nextHandshakeMessage();
356
+ }
357
+ const done = expect(next, HANDSHAKE_TYPE.server_hello_done, 'ServerHelloDone');
358
+ if (done.body.byteLength !== 0) {
359
+ throw new TlsError(
360
+ codes.TLS_HANDSHAKE,
361
+ `ServerHelloDone must have an empty body, got ${done.body.byteLength} bytes`,
362
+ { length: done.body.byteLength },
363
+ );
364
+ }
365
+ transcript.update(done.raw);
366
+
367
+ // --- trust, then the ServerKeyExchange signature -------------------------------------------
368
+ // Trust first. The signature check below is only meaningful once the key performing it has
369
+ // been tied by the trust layer to a chain we accept for this hostname; done the other way
370
+ // round it merely proves that whoever holds the socket also holds a key, which is no evidence
371
+ // at all. Nothing of ours — no key share, no Finished — has been sent yet, so a failure here
372
+ // leaks nothing but the ClientHello.
373
+ const peer = await verifyPeer(chain, hostname, { ocspResponse });
374
+ const spki = peer?.spki?.spkiDer;
375
+ if (!spki) {
376
+ throw new TlsError(
377
+ codes.CONFIG_INVALID,
378
+ 'verifyPeer must resolve with the validated leaf certificate, including spki.spkiDer',
379
+ );
380
+ }
381
+
382
+ const scheme = ske.signatureAlgorithm;
383
+ if (!hello.offeredSigSchemes.includes(scheme)) {
384
+ // RFC 5246 s7.4.3: the server must sign with an algorithm from our signature_algorithms.
385
+ throw new TlsUnsupportedError(
386
+ codes.TLS_SIGALG_UNSUPPORTED,
387
+ `server signed ServerKeyExchange with ${describeSigScheme(scheme)}, which was not ` +
388
+ 'offered in signature_algorithms',
389
+ { scheme },
390
+ );
391
+ }
392
+ if (SIG_KIND[scheme] !== params.sig) {
393
+ throw new TlsError(
394
+ codes.TLS_HANDSHAKE,
395
+ `cipher suite ${describeSuite(suite)} authenticates with ${params.sig}, but ` +
396
+ `ServerKeyExchange is signed with ${describeSigScheme(scheme)}`,
397
+ { cipherSuite: suite, scheme },
398
+ );
399
+ }
400
+ // The signature goes to verifyHandshakeSignature in the form it arrived in. That function owns
401
+ // the DER-to-P1363 conversion for ECDSA, so both handshake drivers share one implementation of
402
+ // it — converting here as well would hand it an already-converted signature to re-parse.
403
+ await verifyHandshakeSignature({
404
+ scheme,
405
+ spki,
406
+ signature: ske.signature,
407
+ content: serverKeyExchangeContent(hello.clientRandom, sh.random, ske.signedParams),
408
+ });
409
+
410
+ // --- key exchange --------------------------------------------------------------------------
411
+ // The server's point is validated (length, uncompressed form, on-curve via the WebCrypto
412
+ // import) before anything of ours goes on the wire.
413
+ const share = await generateKeyShare(ske.group, deps);
414
+ const preMaster = await deriveSharedSecret(ske.group, share.privateKey, ske.publicKey);
415
+
416
+ // --- client flight -------------------------------------------------------------------------
417
+ const clientFlight = [];
418
+ if (certificateRequested) {
419
+ // RFC 5246 s7.4.6: no credentials means a Certificate message with an empty
420
+ // certificate_list — and, because the list is empty, no CertificateVerify afterwards.
421
+ const empty = handshakeMessage(HANDSHAKE_TYPE.certificate, vector(3, new Uint8Array(0)));
422
+ clientFlight.push(empty);
423
+ transcript.update(empty);
424
+ }
425
+ const cke = buildClientKeyExchange(share.keyExchange);
426
+ clientFlight.push(cke);
427
+ transcript.update(cke);
428
+ await record.writeHandshake(clientFlight);
429
+
430
+ // RFC 7627: with EMS the master secret is bound to the session hash — the transcript through
431
+ // ClientKeyExchange, hashed right here, before Finished joins it — so it cannot be replayed
432
+ // onto a different handshake (the triple-handshake attack). Without the extension the RFC 5246
433
+ // derivation over the two randoms is all there is; that acceptance is recorded in
434
+ // info.extendedMasterSecret rather than hidden.
435
+ const master = extendedMasterSecret
436
+ ? await extendedMasterSecret12(hash, preMaster, await transcript.hash())
437
+ : await masterSecret12(hash, preMaster, hello.clientRandom, sh.random);
438
+
439
+ // RFC 5246 s6.3 puts MAC keys first in the key_block, but AEAD suites have none — a stray
440
+ // MAC-key slot would silently shift every later slice into the wrong bytes, so the layout is
441
+ // asserted rather than trusted.
442
+ const kb = await keyBlock12(hash, master, hello.clientRandom, sh.random, {
443
+ keyLen: params.keyLen,
444
+ fixedIvLen: params.fixedIvLen,
445
+ });
446
+ if (
447
+ kb.clientWriteMacKey !== undefined ||
448
+ kb.clientWriteKey.byteLength !== params.keyLen ||
449
+ kb.serverWriteKey.byteLength !== params.keyLen ||
450
+ kb.clientWriteIv.byteLength !== params.fixedIvLen ||
451
+ kb.serverWriteIv.byteLength !== params.fixedIvLen
452
+ ) {
453
+ throw new TlsError(
454
+ codes.CONFIG_INVALID,
455
+ 'key_block layout is wrong for an AEAD suite; a MAC-key slot would shift every slice',
456
+ { cipherSuite: suite },
457
+ );
458
+ }
459
+
460
+ // Our ChangeCipherSpec, then Finished under the new keys. The verify_data covers everything
461
+ // through ClientKeyExchange; the Finished message itself then joins the transcript because
462
+ // the server's Finished must cover ours (RFC 5246 s7.4.9).
463
+ await record.writeChangeCipherSpec();
464
+ await record.setSendKeys({ cipher: suite, key: kb.clientWriteKey, iv: kb.clientWriteIv });
465
+ const finished = buildFinished(
466
+ await verifyData12(hash, master, 'client finished', await transcript.hash()),
467
+ );
468
+ transcript.update(finished);
469
+ await record.writeHandshake([finished]);
470
+
471
+ // --- server ChangeCipherSpec and Finished --------------------------------------------------
472
+ // The {ccs: true} event is the record layer telling us the peer switched keys — a real
473
+ // signal in 1.2, unlike the 1.3 compatibility noise — and receive keys are installed only in
474
+ // response to it. The record layer separately guarantees no handshake message spans the
475
+ // change and that a second CCS (renegotiation's opening move) is fatal.
476
+ expectCcs(await record.nextHandshakeMessage());
477
+ await record.setReceiveKeys({ cipher: suite, key: kb.serverWriteKey, iv: kb.serverWriteIv });
478
+
479
+ const sf = expect(await record.nextHandshakeMessage(), HANDSHAKE_TYPE.finished, 'server Finished');
480
+ checkFinished(sf.body, await verifyData12(hash, master, 'server finished', await transcript.hash()));
481
+
482
+ record.markHandshakeComplete();
483
+ // Lazy for the same reasons as the 1.3 driver: direct record-layer consumers never need the
484
+ // platform-stream wrappers, and the warmup replay runs where constructing them is forbidden.
485
+ let duplex = null;
486
+ const lazyDuplex = () => (duplex ??= record.plaintextDuplex());
487
+ return {
488
+ get readable() {
489
+ return lazyDuplex().readable;
490
+ },
491
+ get writable() {
492
+ return lazyDuplex().writable;
493
+ },
494
+ record,
495
+ peer,
496
+ info: {
497
+ version: TLS12,
498
+ cipherSuite: suite,
499
+ group: ske.group,
500
+ alpnProtocol: alpnProtocol ?? null,
501
+ certificateRequested,
502
+ extendedMasterSecret,
503
+ hostname,
504
+ },
505
+ close: () => record.close(),
506
+ };
507
+ }
@@ -0,0 +1,44 @@
1
+ // The TLS layer as a standalone transport: give it a byte duplex, get a plaintext one back.
2
+ //
3
+ // Exported separately because the layering is the point — anyone with a socket-shaped thing and a
4
+ // trust policy can use this without the HTTP client above it, and every layer in this package is
5
+ // testable over an in-memory pipe for exactly that reason.
6
+
7
+ export { connectTls } from './connect.js';
8
+ export { handshakeTls13 } from './handshake.js';
9
+ export { handshakeTls12 } from './handshake12.js';
10
+ export { RecordLayer } from './record.js';
11
+ export { Transcript } from './transcript.js';
12
+ export { createAead, buildNonce } from './aead.js';
13
+ // Session resumption: the per-client ticket store, exported so a direct connectTls consumer can
14
+ // wire options.psk / options.onSessionTicket with the same lifetime and single-use policy the
15
+ // Client uses. Keying discipline is the caller's burden and is documented on the class.
16
+ export { TicketStore } from './tickets.js';
17
+
18
+ export {
19
+ ALPN_HTTP11,
20
+ CIPHER,
21
+ CIPHER_NAME,
22
+ CIPHER_PARAMS,
23
+ GROUP,
24
+ GROUP_NAME,
25
+ SIG_SCHEME,
26
+ SIG_SCHEME_NAME,
27
+ SUPPORTED_GROUPS,
28
+ SUPPORTED_SIG_SCHEMES,
29
+ TLS12,
30
+ TLS12_CIPHERS,
31
+ TLS13,
32
+ TLS13_CIPHERS,
33
+ VERSION_NAME,
34
+ } from './constants.js';
35
+
36
+ export {
37
+ buildClientHello,
38
+ deriveSharedSecret,
39
+ generateKeyShare,
40
+ negotiateCipher,
41
+ negotiateVersion,
42
+ parseServerHello,
43
+ verifyHandshakeSignature,
44
+ } from './handshake-messages.js';