tunnelfetch 1.8.4 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/package.json +1 -1
- package/src/tls/connect.js +43 -1
- package/src/tls/handshake.js +8 -1
- package/types/tls/connect.d.ts +23 -2
package/README.md
CHANGED
|
@@ -453,6 +453,36 @@ Until 1.6.2 the profile carried curl **8.7.1's** window while presenting curl **
|
|
|
453
453
|
ClientHello: one named client, two source versions, and a split identity that only a capture could
|
|
454
454
|
find, because each half was individually true of some curl.
|
|
455
455
|
|
|
456
|
+
**`extensionOrder` arranges extensions; it cannot add them.** The builder filters to the extensions
|
|
457
|
+
it actually generated and sorts those, so an identity needing one this package does not build is not
|
|
458
|
+
reachable by ordering. Chromium sends five that it does not:
|
|
459
|
+
|
|
460
|
+
| | |
|
|
461
|
+
| --- | --- |
|
|
462
|
+
| `signed_certificate_timestamp` | 18 |
|
|
463
|
+
| `compress_certificate` | 27 |
|
|
464
|
+
| `session_ticket` | 35 |
|
|
465
|
+
| `application_settings` | 17613 |
|
|
466
|
+
| `encrypted_client_hello` | 65037 |
|
|
467
|
+
|
|
468
|
+
So `profiles.chrome` presents a Chromium cipher list, group list and GREASE placement over an
|
|
469
|
+
extension **set** that is curl's. A test reads that gap straight out of the committed Chromium
|
|
470
|
+
capture and fails if it changes, so it cannot drift quietly — but it is a real difference and a
|
|
471
|
+
JA3/JA4 hash sees it.
|
|
472
|
+
|
|
473
|
+
`tls.extraExtensions` takes pre-encoded extensions and is the only way to close it. They are ordered
|
|
474
|
+
like any other and reproduced on a HelloRetryRequest retry, because a second hello that changed its
|
|
475
|
+
extension set would be both malformed (RFC 8446 §4.1.2) and a signal in itself. Encoding them
|
|
476
|
+
correctly is the caller's job; this package does not parse what it did not build.
|
|
477
|
+
|
|
478
|
+
**Every offered cipher must be one this package can perform.** `tls.ciphers` used to be taken
|
|
479
|
+
verbatim, so a list containing a CBC or RSA-key-exchange suite put a number on the wire that a
|
|
480
|
+
server could select — after which the AEAD layer had nothing to build and the connection died
|
|
481
|
+
mid-handshake. Such a list is now refused at configuration time. An explicit `TLS_CHACHA20_POLY1305_SHA256`
|
|
482
|
+
without an injected implementation is refused too, rather than silently dropped: quietly presenting
|
|
483
|
+
a different fingerprint from the one you asked for is the worst outcome available to a package like
|
|
484
|
+
this one.
|
|
485
|
+
|
|
456
486
|
Extension order matters because JA3 and JA4 hash the extension list **in wire order**, so it is most
|
|
457
487
|
of what a fingerprinter reads. `pre_shared_key` is forced last whatever you ask for: RFC 8446
|
|
458
488
|
§4.2.11 defines the binder transcript as the hello truncated just before the binders, which is a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "A fetch-shaped HTTP client that can route through HTTP CONNECT / HTTPS / SOCKS5 proxies on runtimes with only raw TCP, such as Cloudflare Workers. Implements TLS in userland because the runtime cannot verify a tunnelled peer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
package/src/tls/connect.js
CHANGED
|
@@ -137,7 +137,16 @@ function expectServerHello(msg, offers12) {
|
|
|
137
137
|
* @property {number[]} [groups] supported_groups, in preference order.
|
|
138
138
|
* @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
|
|
139
139
|
* supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
|
|
140
|
-
* @property {number[]} [ciphers] cipher suites to offer, in preference order.
|
|
140
|
+
* @property {number[]} [ciphers] cipher suites to offer, in preference order. Every suite must be
|
|
141
|
+
* one this package can perform; an offer it cannot honour is a dead connection the moment a
|
|
142
|
+
* server selects it, so an unknown suite is refused here rather than on the wire.
|
|
143
|
+
* @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
|
|
144
|
+
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
145
|
+
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
146
|
+
* generate. Chromium sends several that it does not: `signed_certificate_timestamp` (18),
|
|
147
|
+
* `compress_certificate` (27), `session_ticket` (35), `application_settings` (17613) and ECH
|
|
148
|
+
* (65037). Supplying them here is the only way to close that gap, and it is the caller's job to
|
|
149
|
+
* encode them correctly — this package does not parse what it did not build.
|
|
141
150
|
* @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
|
|
142
151
|
* @property {boolean | number} [grease] send GREASE (RFC 8701) reserved values in the cipher list,
|
|
143
152
|
* the extension list (one at each end), supported_groups, supported_versions and key_share.
|
|
@@ -328,6 +337,38 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
|
|
|
328
337
|
];
|
|
329
338
|
if (!chacha) ciphers = ciphers.filter((c) => c !== CIPHER.TLS_CHACHA20_POLY1305_SHA256);
|
|
330
339
|
|
|
340
|
+
// A caller-supplied list was previously taken verbatim. That let a hello offer suites this
|
|
341
|
+
// package cannot perform — the CBC and RSA-key-exchange suites, most obviously — and the failure
|
|
342
|
+
// did not surface at configuration time. It surfaced after the ClientHello was on the wire, when
|
|
343
|
+
// a server took the offer up and the AEAD layer had nothing to build: a dead connection rather
|
|
344
|
+
// than a rejected config.
|
|
345
|
+
//
|
|
346
|
+
// profiles.js already states the principle for the profile system: "a fingerprint field this
|
|
347
|
+
// package cannot perform is an offer a server may take and then find unhonoured, which fails the
|
|
348
|
+
// connection rather than merely looking wrong". It applies just as much to `tls.ciphers`, and
|
|
349
|
+
// now does. CIPHER_PARAMS is the set this package knows how to key and seal.
|
|
350
|
+
if (options.ciphers) {
|
|
351
|
+
const unperformable = ciphers.filter((c) => !CIPHER_PARAMS[c]);
|
|
352
|
+
if (unperformable.length) {
|
|
353
|
+
throw new TlsError(
|
|
354
|
+
codes.CONFIG_INVALID,
|
|
355
|
+
`tls.ciphers offers ${unperformable.map((c) => `0x${c.toString(16).padStart(4, '0')}`).join(', ')}, ` +
|
|
356
|
+
'which this package cannot perform. Offering a suite it cannot complete trades a ' +
|
|
357
|
+
'fingerprint match for a dead connection the moment a server selects one — see the ' +
|
|
358
|
+
'chrome profile, which drops the CBC and RSA-key-exchange suites for exactly this reason.',
|
|
359
|
+
{ unperformable },
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (!chacha && options.ciphers.includes(CIPHER.TLS_CHACHA20_POLY1305_SHA256)) {
|
|
363
|
+
throw new TlsError(
|
|
364
|
+
codes.CONFIG_INVALID,
|
|
365
|
+
'tls.ciphers offers TLS_CHACHA20_POLY1305_SHA256 but no ChaCha20 implementation was ' +
|
|
366
|
+
'supplied. Pass one through `ciphers: { chacha20 }`, or drop the suite from the list — ' +
|
|
367
|
+
'silently removing it would change the fingerprint you asked for without saying so.',
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
331
372
|
// --- resumption offer ----------------------------------------------------------------------
|
|
332
373
|
// Everything about the offered PSK that later steps need is derived once, up front: the Early
|
|
333
374
|
// Secret and binder key here (they depend only on the PSK), the binder itself per hello (it
|
|
@@ -372,6 +413,7 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
|
|
|
372
413
|
groups,
|
|
373
414
|
alpn,
|
|
374
415
|
ciphers,
|
|
416
|
+
extraExtensions: options.extraExtensions ?? [],
|
|
375
417
|
versions,
|
|
376
418
|
extensionOrder: options.extensionOrder,
|
|
377
419
|
sigSchemes: options.sigSchemes,
|
package/src/tls/handshake.js
CHANGED
|
@@ -246,7 +246,14 @@ export async function continueTls13(ctx) {
|
|
|
246
246
|
grease: options.grease ?? false,
|
|
247
247
|
random: hello.clientRandom,
|
|
248
248
|
legacySessionId: hello.legacySessionId,
|
|
249
|
-
|
|
249
|
+
// The caller's extras must be reproduced too, for the same reason the order is: s4.1.2 does
|
|
250
|
+
// not permit a second hello to change its extension SET, and a retry that quietly dropped
|
|
251
|
+
// them would present one fingerprint on the first flight and a different one on the second —
|
|
252
|
+
// a difference that is itself a signal.
|
|
253
|
+
extraExtensions: [
|
|
254
|
+
...(options.extraExtensions ?? []),
|
|
255
|
+
...(cookie ? [cookieExtension(cookie)] : []),
|
|
256
|
+
],
|
|
250
257
|
psk: offeredPsk && {
|
|
251
258
|
identity: offeredPsk.identity,
|
|
252
259
|
obfuscatedTicketAge: offeredPsk.obfuscatedTicketAge(),
|
package/types/tls/connect.d.ts
CHANGED
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
* @property {number[]} [groups] supported_groups, in preference order.
|
|
14
14
|
* @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
|
|
15
15
|
* supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
|
|
16
|
-
* @property {number[]} [ciphers] cipher suites to offer, in preference order.
|
|
16
|
+
* @property {number[]} [ciphers] cipher suites to offer, in preference order. Every suite must be
|
|
17
|
+
* one this package can perform; an offer it cannot honour is a dead connection the moment a
|
|
18
|
+
* server selects it, so an unknown suite is refused here rather than on the wire.
|
|
19
|
+
* @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
|
|
20
|
+
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
21
|
+
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
22
|
+
* generate. Chromium sends several that it does not: `signed_certificate_timestamp` (18),
|
|
23
|
+
* `compress_certificate` (27), `session_ticket` (35), `application_settings` (17613) and ECH
|
|
24
|
+
* (65037). Supplying them here is the only way to close that gap, and it is the caller's job to
|
|
25
|
+
* encode them correctly — this package does not parse what it did not build.
|
|
17
26
|
* @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
|
|
18
27
|
* @property {boolean | number} [grease] send GREASE (RFC 8701) reserved values in the cipher list,
|
|
19
28
|
* the extension list (one at each end), supported_groups, supported_versions and key_share.
|
|
@@ -160,9 +169,21 @@ export type TlsOptions = {
|
|
|
160
169
|
*/
|
|
161
170
|
offerGroups?: number[] | undefined;
|
|
162
171
|
/**
|
|
163
|
-
* cipher suites to offer, in preference order.
|
|
172
|
+
* cipher suites to offer, in preference order. Every suite must be
|
|
173
|
+
* one this package can perform; an offer it cannot honour is a dead connection the moment a
|
|
174
|
+
* server selects it, so an unknown suite is refused here rather than on the wire.
|
|
164
175
|
*/
|
|
165
176
|
ciphers?: number[] | undefined;
|
|
177
|
+
/**
|
|
178
|
+
* pre-encoded ClientHello extensions, appended before
|
|
179
|
+
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
180
|
+
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
181
|
+
* generate. Chromium sends several that it does not: `signed_certificate_timestamp` (18),
|
|
182
|
+
* `compress_certificate` (27), `session_ticket` (35), `application_settings` (17613) and ECH
|
|
183
|
+
* (65037). Supplying them here is the only way to close that gap, and it is the caller's job to
|
|
184
|
+
* encode them correctly — this package does not parse what it did not build.
|
|
185
|
+
*/
|
|
186
|
+
extraExtensions?: Uint8Array<ArrayBufferLike>[] | undefined;
|
|
166
187
|
/**
|
|
167
188
|
* signature_algorithms to offer, in preference order.
|
|
168
189
|
*/
|