tunnelfetch 1.1.2 → 1.2.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 +44 -1
- package/README.zh-CN.md +32 -0
- package/package.json +1 -1
- package/src/client.js +10 -0
- package/src/http2/connection.js +19 -5
- package/src/tls/connect.js +8 -0
- package/src/tls/handshake-messages.js +61 -1
- package/src/tls/handshake.js +4 -0
- package/src/warmup-fixture.js +44 -44
- package/types/client.d.ts +13 -0
- package/types/http2/connection.d.ts +9 -0
- package/types/tls/connect.d.ts +18 -0
- package/types/tls/handshake-messages.d.ts +101 -57
package/README.md
CHANGED
|
@@ -311,10 +311,53 @@ behaviour — measure your own targets, and expect the answer to change. Our TLS
|
|
|
311
311
|
identical outcomes on every reachable host in that sample, so JA3-style TLS shaping is not what
|
|
312
312
|
gates access here — but curl's **HTTP/2** fingerprint passed where HTTP/1.1 was challenged. So the
|
|
313
313
|
`SETTINGS` frame values, the initial window sizes, the connection `WINDOW_UPDATE`, and the
|
|
314
|
-
pseudo-header order are matched byte-for-byte to curl (
|
|
314
|
+
pseudo-header order are matched byte-for-byte to curl (nghttp2 1.69.0), captured off the wire.
|
|
315
315
|
This is empirical: a naïve h2 fingerprint can fail exactly where curl's succeeds, which would waste
|
|
316
316
|
the whole exercise.
|
|
317
317
|
|
|
318
|
+
### Fingerprints
|
|
319
|
+
|
|
320
|
+
Both halves are matched to **curl 8.21.0 / OpenSSL 3.6.3** and both are fully configurable. The TLS
|
|
321
|
+
backend matters more than the curl version — the same curl built against SecureTransport produces a
|
|
322
|
+
completely different ClientHello — so the reference is captured off the wire, not recalled, and
|
|
323
|
+
pinned in `test/tls/fingerprint.test.js` and `test/http2/fingerprint.test.js`.
|
|
324
|
+
|
|
325
|
+
| Layer | Default | Configure with |
|
|
326
|
+
|---|---|---|
|
|
327
|
+
| ClientHello extension **order** | curl's, exactly, for every extension both send | `tls.extensionOrder` |
|
|
328
|
+
| Cipher suites | the AEAD suites this package implements, in curl's relative order | `tls.ciphers` |
|
|
329
|
+
| Supported groups | `x25519, secp256r1, secp384r1, secp521r1` | `tls.groups` |
|
|
330
|
+
| Signature algorithms | ECDSA and RSA-PSS/PKCS#1 over SHA-256/384/512 | `tls.sigSchemes` |
|
|
331
|
+
| ALPN | `h2, http/1.1` | `tls.alpn` |
|
|
332
|
+
| HTTP/2 `SETTINGS` ids **and order** | curl's: `MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH` | `http2Settings` |
|
|
333
|
+
| h2 preface, `WINDOW_UPDATE`, pseudo-header order, HPACK representation | curl's, byte-for-byte | fixed |
|
|
334
|
+
| `Accept-Encoding` | `gzip, deflate` — curl's | `decoders` appends |
|
|
335
|
+
|
|
336
|
+
Extension order matters because JA3 and JA4 hash the extension list **in wire order**, so it is most
|
|
337
|
+
of what a fingerprinter reads. `pre_shared_key` is forced last whatever you ask for: RFC 8446
|
|
338
|
+
§4.2.11 defines the binder transcript as the hello truncated just before the binders, which is a
|
|
339
|
+
well-defined byte range only if nothing follows them.
|
|
340
|
+
|
|
341
|
+
**Where the default deliberately differs from curl**, and why it cannot simply be copied: a
|
|
342
|
+
ClientHello is an *offer*, and a server may take you up on any of it. Advertising what you cannot do
|
|
343
|
+
trades a fingerprint mismatch for a broken handshake, which is worse and fails silently.
|
|
344
|
+
|
|
345
|
+
| curl sends | This package | Why |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| 30 cipher suites, incl. RSA key exchange and CBC | 6 AEAD suites | Not implemented, by design. A server selecting `TLS_RSA_WITH_AES_256_CBC_SHA` would get a dead connection |
|
|
348
|
+
| `X25519MLKEM768` group and a 1216-byte key share | not offered | ML-KEM is not implemented |
|
|
349
|
+
| SHA-1 signature schemes | not offered | Refused deliberately |
|
|
350
|
+
| `encrypt_then_mac` | not sent | Applies only to CBC suites, which are not offered |
|
|
351
|
+
| `post_handshake_auth` | not sent | Invites a post-handshake `CertificateRequest`, which is not implemented |
|
|
352
|
+
| — | `status_request` | curl does not ask for a stapled OCSP response; this package must, because a staple is its only revocation signal |
|
|
353
|
+
|
|
354
|
+
Closing that gap means implementing ML-KEM, RSA key exchange and CBC suites — a different project,
|
|
355
|
+
and the last two are things this package refuses on purpose. `tls.ciphers` and `tls.groups` will let
|
|
356
|
+
you offer them anyway; the handshake will then fail if a server picks one, and that is yours to own.
|
|
357
|
+
|
|
358
|
+
A test asserts this delta is exactly the list above, so gaining one of these capabilities without
|
|
359
|
+
updating the table fails the build.
|
|
360
|
+
|
|
318
361
|
Everything an HTTP/1.1 body has, an HTTP/2 body keeps: streaming (SSE works unchanged), trailers,
|
|
319
362
|
gzip decoding, and the idle deadline wrapping the raw body before any decode. The one thing that is
|
|
320
363
|
structurally different is under the hood — a single h2 connection multiplexes every concurrent
|
package/README.zh-CN.md
CHANGED
|
@@ -251,6 +251,38 @@ TLS 指纹塑形不是这里的门槛——但 curl 的 **HTTP/2** 指纹能过
|
|
|
251
251
|
curl(8.7.1 / nghttp2),照线上抓包原样复刻。这么做是实证需要,不是讲究:一个想当然的 h2
|
|
252
252
|
指纹,可能恰好败在 curl 能过的地方——那这整件事就白做了。
|
|
253
253
|
|
|
254
|
+
### 指纹
|
|
255
|
+
|
|
256
|
+
两侧都对齐 **curl 8.21.0 / OpenSSL 3.6.3**,且全部可自定义。TLS 后端比 curl 版本更要紧——同一个 curl 用 SecureTransport 编译出来的 ClientHello 完全不同——所以参照物是从线上抓下来的,不是凭记忆写的,并且钉死在 `test/tls/fingerprint.test.js` 与 `test/http2/fingerprint.test.js` 里。
|
|
257
|
+
|
|
258
|
+
| 层 | 默认 | 配置项 |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| ClientHello 扩展**顺序** | 与 curl 完全一致(就双方都发的那些而言) | `tls.extensionOrder` |
|
|
261
|
+
| 密码套件 | 本包实现的 AEAD 套件,按 curl 的相对顺序 | `tls.ciphers` |
|
|
262
|
+
| supported_groups | `x25519, secp256r1, secp384r1, secp521r1` | `tls.groups` |
|
|
263
|
+
| 签名算法 | SHA-256/384/512 上的 ECDSA 与 RSA-PSS/PKCS#1 | `tls.sigSchemes` |
|
|
264
|
+
| ALPN | `h2, http/1.1` | `tls.alpn` |
|
|
265
|
+
| HTTP/2 `SETTINGS` 的 id **与顺序** | curl 的:`MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH` | `http2Settings` |
|
|
266
|
+
| h2 前导、`WINDOW_UPDATE`、伪头顺序、HPACK 表示 | curl 的,逐字节一致 | 固定 |
|
|
267
|
+
| `Accept-Encoding` | `gzip, deflate`——curl 的 | `decoders` 会追加 |
|
|
268
|
+
|
|
269
|
+
扩展顺序之所以要紧,是因为 JA3 和 JA4 哈希的正是**线上顺序**的扩展列表,那是指纹识别读到的主要内容。`pre_shared_key` 无论你怎么配都强制排最后:RFC 8446 §4.2.11 把 binder 的转录定义为"截到 binder 之前的那段 hello",只有后面不跟东西时这个范围才成立。
|
|
270
|
+
|
|
271
|
+
**默认值刻意与 curl 不同的地方**,以及为什么不能照抄:ClientHello 是一份**要约**,服务器可以接受其中任何一项。声明你做不到的事,等于拿指纹不一致换一次握手失败——后者更糟,而且是静默的。
|
|
272
|
+
|
|
273
|
+
| curl 发送 | 本包 | 原因 |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| 30 个套件,含 RSA 密钥交换与 CBC | 6 个 AEAD 套件 | 有意不实现。服务器选中 `TLS_RSA_WITH_AES_256_CBC_SHA` 会得到一条死连接 |
|
|
276
|
+
| `X25519MLKEM768` 群及 1216 字节 key share | 不提供 | 未实现 ML-KEM |
|
|
277
|
+
| SHA-1 签名方案 | 不提供 | 明确拒绝 |
|
|
278
|
+
| `encrypt_then_mac` | 不发 | 只对 CBC 套件有意义,而 CBC 不提供 |
|
|
279
|
+
| `post_handshake_auth` | 不发 | 会招来握手后的 `CertificateRequest`,未实现 |
|
|
280
|
+
| — | `status_request` | curl 不索要 OCSP staple;本包必须要,因为 staple 是它唯一的吊销信号 |
|
|
281
|
+
|
|
282
|
+
要抹平这个差距,得先实现 ML-KEM、RSA 密钥交换和 CBC 套件——那是另一个项目,而且后两者是本包**故意**不做的。`tls.ciphers` 和 `tls.groups` 允许你照样把它们报出去;服务器一旦选中握手就会失败,后果由你自己承担。
|
|
283
|
+
|
|
284
|
+
有一个测试断言这张差异表**恰好**就是上面这些,所以哪天获得了其中某项能力却没更新这张表,构建会直接失败。
|
|
285
|
+
|
|
254
286
|
HTTP/1.1 的 body 有的,HTTP/2 的 body 全都保留:流式(SSE 原样可用)、trailer、gzip 解码、
|
|
255
287
|
以及在任何解码之前先包住原始 body 的 idle 截止线。结构上唯一不同的在水面之下——一条 h2 连接把
|
|
256
288
|
发往同一源站的所有并发请求复用在一起,而不是一次签出、一次只服务一个请求。`install()`、重定向、
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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/client.js
CHANGED
|
@@ -82,6 +82,11 @@ const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
|
|
|
82
82
|
* the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
|
|
83
83
|
* matching a browser's Accept-Encoding, not saving CPU.
|
|
84
84
|
* @property {boolean} [keepAlive] default true.
|
|
85
|
+
* @property {Array<[number, number]>} [http2Settings] the HTTP/2 SETTINGS flight, as [id, value]
|
|
86
|
+
* pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
|
|
87
|
+
* are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
|
|
88
|
+
* TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
|
|
89
|
+
* `alpn`, `versions`, `extensionOrder`).
|
|
85
90
|
* @property {boolean} [http2] offer HTTP/2 via ALPN and speak it when the server selects it.
|
|
86
91
|
* Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
|
|
87
92
|
* on a CPU-billed runtime h2 costs MORE than h1 (HPACK is extra work). Set false to offer only
|
|
@@ -436,6 +441,11 @@ function registerHttp2(client, key, conn) {
|
|
|
436
441
|
{ readable: conn.readable, writable: conn.writable, close: conn.close },
|
|
437
442
|
{
|
|
438
443
|
info: conn.info,
|
|
444
|
+
// The h2 half of the fingerprint. Passed through so a caller can present some client other
|
|
445
|
+
// than curl without reaching past the Client for it — the TLS half is configurable through
|
|
446
|
+
// `tls`, and one being reachable while the other was not made "the fingerprint is
|
|
447
|
+
// configurable" only half true.
|
|
448
|
+
...(client.options.http2Settings ? { settings: client.options.http2Settings } : {}),
|
|
439
449
|
onClose: () => {
|
|
440
450
|
client._h2conns.delete(h2);
|
|
441
451
|
// Only drop the keyed entry if it is still this connection; a newer one may have replaced it.
|
package/src/http2/connection.js
CHANGED
|
@@ -117,6 +117,10 @@ export class Http2Retryable extends Http2Error {}
|
|
|
117
117
|
* @property {number} [maxConcurrentStreams] our advertised SETTINGS_MAX_CONCURRENT_STREAMS.
|
|
118
118
|
* @property {number} [maxHeaderTableSize] our advertised SETTINGS_HEADER_TABLE_SIZE.
|
|
119
119
|
* @property {number} [maxHeaderListSize] self-protection cap on a decoded response header list.
|
|
120
|
+
* @property {Array<[number, number]>} [settings] the SETTINGS flight sent in the connection
|
|
121
|
+
* preface, as [id, value] pairs. Order is significant — an Akamai-style HTTP/2 fingerprint reads
|
|
122
|
+
* the ids in the order they are sent — so this replaces the flight entirely rather than merging.
|
|
123
|
+
* Defaults to curl's: MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH.
|
|
120
124
|
* @property {number} [maxHeaderBlockBytes] cap on the RAW bytes of one HEADERS+CONTINUATION run,
|
|
121
125
|
* before HPACK decoding. Default 262144, matching the decoded cap. This is the bound that stops
|
|
122
126
|
* a CONTINUATION flood; `maxHeaderListSize` cannot, because it is only reachable once the whole
|
|
@@ -196,6 +200,8 @@ export class Http2Connection {
|
|
|
196
200
|
// reference makes a small input decode LARGER, never the reverse — so a block whose raw size
|
|
197
201
|
// exceeds the decoded cap could not have produced an acceptable header list anyway.
|
|
198
202
|
this._maxHeaderBlockBytes = opts.maxHeaderBlockBytes ?? 262144;
|
|
203
|
+
/** @type {Array<[number, number]> | null} the SETTINGS flight, ids and order included */
|
|
204
|
+
this._settingsFlight = opts.settings ?? null;
|
|
199
205
|
this._expectFirstSettings = true;
|
|
200
206
|
|
|
201
207
|
this._fatal = null; // set once; rejects every stream and every future request
|
|
@@ -230,11 +236,19 @@ export class Http2Connection {
|
|
|
230
236
|
_sendPreface() {
|
|
231
237
|
// Exactly curl's flight and order: the 24-byte magic, then SETTINGS (ids 3,4,2), then a
|
|
232
238
|
// connection-level WINDOW_UPDATE that raises the receive window to 1000 MiB. See constants.js.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
//
|
|
240
|
+
// The ORDER of the settings, not just their values, is part of the fingerprint an Akamai-style
|
|
241
|
+
// h2 hash reads, so a caller matching some other client needs to be able to set both. Supplying
|
|
242
|
+
// `settings` replaces the flight wholesale; the values still drive this connection's own
|
|
243
|
+
// behaviour, so a caller who advertises a window it will not honour has misconfigured the
|
|
244
|
+
// connection rather than merely disguised it.
|
|
245
|
+
const settings = settingsFrame(
|
|
246
|
+
this._settingsFlight ?? [
|
|
247
|
+
[SETTINGS.MAX_CONCURRENT_STREAMS, this._ourMaxConcurrent],
|
|
248
|
+
[SETTINGS.INITIAL_WINDOW_SIZE, this._ourInitialWindow],
|
|
249
|
+
[SETTINGS.ENABLE_PUSH, 0],
|
|
250
|
+
],
|
|
251
|
+
);
|
|
238
252
|
const inc = this._ourConnWindow - DEFAULT_INITIAL_WINDOW;
|
|
239
253
|
const flight =
|
|
240
254
|
inc > 0
|
package/src/tls/connect.js
CHANGED
|
@@ -135,6 +135,12 @@ function expectServerHello(msg, offers12) {
|
|
|
135
135
|
* @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
|
|
136
136
|
* supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
|
|
137
137
|
* @property {number[]} [ciphers] cipher suites to offer, in preference order.
|
|
138
|
+
* @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
|
|
139
|
+
* @property {number[]} [extensionOrder] ClientHello extension types, in the order to emit them.
|
|
140
|
+
* JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
|
|
141
|
+
* reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
|
|
142
|
+
* natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
|
|
143
|
+
* 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
|
|
138
144
|
* @property {Uint8Array} [clientRandom] fixed ClientHello.random, for reproducible handshakes.
|
|
139
145
|
* @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise.
|
|
140
146
|
* @property {boolean} [compatibilityCcs] send the middlebox-compatibility ChangeCipherSpec.
|
|
@@ -326,6 +332,8 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
|
|
|
326
332
|
alpn,
|
|
327
333
|
ciphers,
|
|
328
334
|
versions,
|
|
335
|
+
extensionOrder: options.extensionOrder,
|
|
336
|
+
sigSchemes: options.sigSchemes,
|
|
329
337
|
random: options.clientRandom,
|
|
330
338
|
legacySessionId: options.legacySessionId,
|
|
331
339
|
psk: pskOffer && {
|
|
@@ -210,6 +210,65 @@ export async function deriveSharedSecret(group, privateKey, peerKey) {
|
|
|
210
210
|
* @param {ClientHelloOptions} opts
|
|
211
211
|
* @returns {ClientHello}
|
|
212
212
|
*/
|
|
213
|
+
/**
|
|
214
|
+
* Extension emission order, by type. This is not cosmetic: JA3 and JA4 hash the extension list in
|
|
215
|
+
* WIRE ORDER, so the order alone is a large part of what a fingerprinter reads.
|
|
216
|
+
*
|
|
217
|
+
* Captured from curl 8.21.0 / OpenSSL 3.6.3, which sends:
|
|
218
|
+
* renegotiation_info, server_name, ec_point_formats, supported_groups, ALPN, encrypt_then_mac,
|
|
219
|
+
* extended_master_secret, post_handshake_auth, signature_algorithms, supported_versions,
|
|
220
|
+
* psk_key_exchange_modes, key_share
|
|
221
|
+
*
|
|
222
|
+
* Two of those this package does not send, and the reason is the same in both cases — an extension
|
|
223
|
+
* is a claim about what we can do. encrypt_then_mac only applies to CBC suites, which are not
|
|
224
|
+
* offered; post_handshake_auth invites a CertificateRequest after the handshake, which is not
|
|
225
|
+
* implemented. status_request goes the other way: curl does not send it, this package does,
|
|
226
|
+
* because a stapled OCSP response is its only revocation signal. It is placed where OpenSSL puts
|
|
227
|
+
* it when it does send one, right after server_name.
|
|
228
|
+
*
|
|
229
|
+
* Anything not named here keeps its natural position at the end, and pre_shared_key is forced last
|
|
230
|
+
* whatever the caller asks for, because RFC 8446 s4.2.11 defines the binder transcript as the hello
|
|
231
|
+
* truncated just before the binders — a range that only exists if nothing follows them.
|
|
232
|
+
*/
|
|
233
|
+
export const CURL_EXTENSION_ORDER = Object.freeze([
|
|
234
|
+
EXTENSION.renegotiation_info,
|
|
235
|
+
EXTENSION.server_name,
|
|
236
|
+
EXTENSION.status_request,
|
|
237
|
+
EXTENSION.ec_point_formats,
|
|
238
|
+
EXTENSION.supported_groups,
|
|
239
|
+
EXTENSION.alpn,
|
|
240
|
+
EXTENSION.extended_master_secret,
|
|
241
|
+
EXTENSION.signature_algorithms,
|
|
242
|
+
EXTENSION.supported_versions,
|
|
243
|
+
EXTENSION.psk_key_exchange_modes,
|
|
244
|
+
EXTENSION.key_share,
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Put the encoded extensions into the requested order.
|
|
249
|
+
*
|
|
250
|
+
* @param {Array<Uint8Array|null>} parts encoded extensions, nulls for the ones not offered
|
|
251
|
+
* @param {number[]} order extension types, most significant first
|
|
252
|
+
* @returns {Array<Uint8Array>}
|
|
253
|
+
*/
|
|
254
|
+
function orderExtensions(parts, order) {
|
|
255
|
+
const present = parts.filter(Boolean);
|
|
256
|
+
const typeOf = (e) => (e[0] << 8) | e[1];
|
|
257
|
+
// pre_shared_key is not the caller's to place.
|
|
258
|
+
const psk = present.filter((e) => typeOf(e) === EXTENSION.pre_shared_key);
|
|
259
|
+
const rest = present.filter((e) => typeOf(e) !== EXTENSION.pre_shared_key);
|
|
260
|
+
const rank = new Map(order.map((t, i) => [t, i]));
|
|
261
|
+
// A stable sort on rank, with unranked extensions after every ranked one in the order they were
|
|
262
|
+
// built. Array.prototype.sort is required to be stable, so equal ranks keep their relative order.
|
|
263
|
+
return [
|
|
264
|
+
...rest
|
|
265
|
+
.map((e, i) => ({ e, i, r: rank.get(typeOf(e)) ?? Number.MAX_SAFE_INTEGER }))
|
|
266
|
+
.sort((x, y) => x.r - y.r || x.i - y.i)
|
|
267
|
+
.map((x) => x.e),
|
|
268
|
+
...psk,
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
|
|
213
272
|
export function buildClientHello({
|
|
214
273
|
hostname,
|
|
215
274
|
keyShares,
|
|
@@ -220,6 +279,7 @@ export function buildClientHello({
|
|
|
220
279
|
sigSchemes = SUPPORTED_SIG_SCHEMES,
|
|
221
280
|
alpn = [ALPN_HTTP11],
|
|
222
281
|
versions = [TLS13, TLS12],
|
|
282
|
+
extensionOrder = CURL_EXTENSION_ORDER,
|
|
223
283
|
extraExtensions = [],
|
|
224
284
|
psk = null,
|
|
225
285
|
randomBytes = defaultRandom,
|
|
@@ -285,7 +345,7 @@ export function buildClientHello({
|
|
|
285
345
|
.vector(1, sessionId)
|
|
286
346
|
.vector(2, suiteBytes.build())
|
|
287
347
|
.vector(1, Uint8Array.from([0])) // legacy_compression_methods: null only
|
|
288
|
-
.push(encodeExtensionBlock(extensionParts))
|
|
348
|
+
.push(encodeExtensionBlock(orderExtensions(extensionParts, extensionOrder)))
|
|
289
349
|
.build();
|
|
290
350
|
|
|
291
351
|
const message = handshakeMessage(HANDSHAKE_TYPE.client_hello, body);
|
package/src/tls/handshake.js
CHANGED
|
@@ -239,6 +239,10 @@ export async function continueTls13(ctx) {
|
|
|
239
239
|
alpn,
|
|
240
240
|
ciphers,
|
|
241
241
|
versions,
|
|
242
|
+
// The retry must reproduce the first hello's extension set AND order: s4.1.2 does not list
|
|
243
|
+
// either among the modifications a second ClientHello may make, and a strict server checks.
|
|
244
|
+
extensionOrder: options.extensionOrder,
|
|
245
|
+
sigSchemes: options.sigSchemes,
|
|
242
246
|
random: hello.clientRandom,
|
|
243
247
|
legacySessionId: hello.legacySessionId,
|
|
244
248
|
extraExtensions: cookie ? [cookieExtension(cookie)] : [],
|
package/src/warmup-fixture.js
CHANGED
|
@@ -18,61 +18,61 @@ export const WARMUP_HOSTNAME = "warmup.invalid";
|
|
|
18
18
|
export const WARMUP_NOW = 1893456000000; // fixed epoch ms inside the chain's validity window
|
|
19
19
|
|
|
20
20
|
const CLIENT_PRIV =
|
|
21
|
-
"
|
|
21
|
+
"MC4CAQAwBQYDK2VuBCIEINivgNQO5n8jwnzWfEWjdR14q7Km+UhPnqyL3RtkDbF7";
|
|
22
22
|
const CLIENT_PUB =
|
|
23
|
-
"
|
|
23
|
+
"o5wNrq9yxfTLf8U3jLhUsXB52Kmf2q3zodyy/4jELSM=";
|
|
24
24
|
const CLIENT_RANDOM =
|
|
25
25
|
"AwoRGB8mLTQ7QklQV15lbHN6gYiPlp2kq7K5wMfO1dw=";
|
|
26
26
|
const SESSION_ID =
|
|
27
27
|
"BRAbJjE8R1JdaHN+iZSfqrXAy9bh7PcCDRgjLjlET1o=";
|
|
28
28
|
const CLIENT_HELLO =
|
|
29
29
|
"AQAA9AMDAwoRGB8mLTQ7QklQV15lbHN6gYiPlp2kq7K5wMfO1dwgBRAbJjE8R1JdaHN+iZSfqrXAy9bh7PcCDRgjLjlET1oADBMB" +
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
30
|
+
"EwLAK8AvwCzAMAEAAJ//AQABAAAAABMAEQAADndhcm11cC5pbnZhbGlkAAUABQEAAAAAAAsAAgEAAAoACgAIAB0AFwAYABkAEAAL" +
|
|
31
|
+
"AAkIaHR0cC8xLjEAFwAAAA0AFgAUBAMFAwYDCAQIBQgGCAcEAQUBBgEAKwAFBAMEAwMALQACAQEAMwAmACQAHQAgo5wNrq9yxfTL" +
|
|
32
|
+
"f8U3jLhUsXB52Kmf2q3zodyy/4jELSM=";
|
|
33
33
|
const SERVER_BYTES =
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"+
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"/
|
|
57
|
-
"
|
|
58
|
-
"/
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
34
|
+
"FgMBAHoCAAB2AwMC2KMpeoduVRsS/IdMsSXpe01e8s3wE/rytik1l91rgiAFEBsmMTxHUl1oc36JlJ+qtcDL1uHs9wINGCMuOURP" +
|
|
35
|
+
"WhMBAAAuACsAAgMEADMAJAAdACAr+v3w+m4O5oxHFSy624+fpkWPq277w7+v3Ke3hrKpOxcDAwdY/NxJTmM9YwAtwCbLgQJoioff" +
|
|
36
|
+
"HlNuzTL1ruUzSLqTb4yMSTiKyiOITsAVKStDY8NsguJ1/CKx/s93Oy4wgtcmJudrxMQK5tRTYfwCrZDgR5yfSVD25wIX5VjuTuJ4" +
|
|
37
|
+
"OsQFJ0qX5cqXv4042XcyR06iRqN7iOKY8h2aH5opqQylx5+51GyKjFWB+qIwTceJ8+r/sl/wkxh4hwq7AfmZuRE8suxcZlojgrH6" +
|
|
38
|
+
"ka1eJ81f/HbalU90Xjs3L9A4UnxB/e3CtoA12a8TkvD6bQCb3sCZUwgt0GSKajpw8/n11XWEyW5AAojiopI+pxwur5wMEkdNrABX" +
|
|
39
|
+
"FpDt+pBW2hfWgUgeHO0JLn2XkXQVnipgdd6e6mtePKtIFqT4tW7dW5Rzp1X8QwqSA+oGpwUTleL4qROziaY9cdALrVHFQ3P5L4IA" +
|
|
40
|
+
"fWPF7FmGj+PBOqo1V4Xlw9suTnGJmiIve9XOY+J0GndLb3b/DULmo0wNG4VdvyO0keFqg2Ov3cKAFHfrbPCRSJwdQfpaA2abuC9H" +
|
|
41
|
+
"MkKgK5JAtvWbAopxu281UMA3tJpWSxiI8LjpbLnNqhUFseBukdRFVVPwE4ckqwqk5NcTb66Eqyyw0fd+TioDTqyTyyGJJwvBHm3l" +
|
|
42
|
+
"KXkU4x4crwFsyDWDZpH8KbYbImVlpCtK+uSPEouREgTeimbeR3elxccpfGkm47yYnERBAfCcM18OcZkMb3AqFsgnaN+Cy345lBJs" +
|
|
43
|
+
"uJfRkRfAmdhlCz5KWmqSNY+3fT6iWyJ9dPBvmcsCnxn6FUZfNxAoYSi7wNzsSv7CRMDWPhdEIWMoDVjf8ZKubz0R/OddtVr1Il20" +
|
|
44
|
+
"xiIcevpEJ9pRntZjCkWtykA+z48Vw3Ia8HoMsKDMQagJ472FhNggwMipgA1uM6+nEe+/scUC3HDck6YGwgpb9k1LWVxYsCIdoz0t" +
|
|
45
|
+
"pbVoCV+QCvqyJGNWpIwRQLYjxHV84WzH/tWbGyyFXkFrA/7XgjE4bydjs88lcC7rAHKXLRrpF1CMwQg5XOkI8ftYRbYIKK9OYx/7" +
|
|
46
|
+
"uUPGBPdPvYkrOwe2q1VgHQd+vK87hlN+aNDOlbxSdcy9U84OSUPwDQs7TYqu7R9Hxsc8BTnRo1Gn6XeVrIdVZmrhQQWdCX9MFSww" +
|
|
47
|
+
"GCs8IxY5CM/2/thifsZN85K0AxgBz45XphZS9YNAjCIex8L4ky5mruYESLcHErLPkMZCDUuvgapmtD8SKsxTz/sLhZuotN6Xmbmb" +
|
|
48
|
+
"5WjTm3Rw7W95mI41mHRCLBEHK2KhqSLdpH2vO43D/g4LKAh21M3wxKJAmElkRyUa8zb+E6GcqtTnL6Zldlm+cRp8KopVLBAJFpFw" +
|
|
49
|
+
"T3riPPiJ6QxWcZjM9aXCHPE+TL1W+KzyTwLChHzPjBopJ+PMjXos7T7DTgQLmO9jsdILAl6xNQgPPukoOAy9Wn3yCWcsETEelZbV" +
|
|
50
|
+
"rgZ4aEwXkurJ5RrsOjGEN+uODmJ2lbW2//B+lKlDGILtisLEg8CzzrSN6BZ420Z50PkGZoP6ML7+JC0rtC75gyNrWXYIBzxwr2/S" +
|
|
51
|
+
"2F30780r8cqU5yvq7VI+nDbUzNoauhWgdVBrCQ/4blaupU+yxHbBvYjRBMBuKwcHTJO2R/s+xTqK5j41Sukm3x99+vEqwrTmN1dm" +
|
|
52
|
+
"aWYk70oKuW1Ewx9V7/ExKwGbsDBxYMjKS8h/aAc0CZz7Fxk30V05LA5rUPsG7zeUyKsvBnnivtCcY7GN1b6mEsLhkkHg6hxaDjBf" +
|
|
53
|
+
"GfjodpXTHsbiZExkL+CYQnE6LQGtOhHrt+ojtQb7iSCvdjIZ/i1Mw2N3UFjkW6qM1ISASQBCSrG8WZqvjZQ+wsngaeo1iL5Zg5qv" +
|
|
54
|
+
"Rc+KoaEackS0NVraKSFrc+xHfyofDskV/FsS9ovfYnouP3pV5SdIcBWMOvIUovGLB2m1nW1k1mySRfm4VHqeag/cSmXSNQX6aVSh" +
|
|
55
|
+
"AsrHAyPrwtBBF5Ox892Ok5p91yDahZhDWfTBTTkY76Ru12FaVafiPyG6lFxbx88tIpiVqfnhqvlj2qvLJFe24MmvjWiXdO00axAy" +
|
|
56
|
+
"M9JTe0IvA5hvyVaTn0w1MzzaNBkDDPpgLlt0yEVoblcrqaQjP0H9J+SjxAUgT52sDDCLjFxFY/zh5guI9RhDYZ2Pyg+vdudJKfja" +
|
|
57
|
+
"dr4L4CpUTKFO+DEJf/Efc7nOlTJWD4Dh3SavGfu7gplzh3FMBFegkUkCQJC2hzatNNSpkE8REhC73Rm9me2iysJMv/4Sy5a6Er5Y" +
|
|
58
|
+
"ffE0aAsJ/gXCvE7fQbeZhogp3iBQm911LBwtUn5Wwh8ALGELJxSsQloxFJwPYSzhUGJ01XKNYo6GIWjrpWtcKIKAe1tlB4Ni/Ag7" +
|
|
59
|
+
"bQV7J67dyHvMti/Jgq7VODUJIb8E+u6H7FVQ/8jzE1mm4Dmmtfd9LqB7pal3YbzzxwfN6Qw84jN7lgcksByuZ7eTV2Q4T0IK/toG" +
|
|
60
|
+
"oroTtRRz7vMwg6xLnNIhtdeQaQMVnveuVkWZ29pzwNTxaKVITIIfsJjxTmHqph7wED0rBpR7vSs9WOsMSeoXAwMBGfgD+3KLo6SI" +
|
|
61
|
+
"9AOwKrhRqXO7Xgn2i58peIpNbvy4DVJ3AOjAect4osO+zRcr5gBp/2JFJgL5l5hgdUpApp9U50/SOOPwCvAZIpfPDlWlixJjUF/K" +
|
|
62
|
+
"8NxWHgx62Wuq6mX7CW7BEqwNzb1yYNSzfZYR0qhmiJ04vj4nnxXcmKum9fj5eO4ptiyLUAqrDpYwUAKCAqlLVZCJbS1dVdwEWSQs" +
|
|
63
|
+
"X/lq6RoYYBaRde8fJT2EXzQnfkn5eV4dzI1gseEMQUJ85kshKa847TwI/4rNRhdhXtDCHi8lMfoRgcobGwYmSImTtt6pxJPj07Og" +
|
|
64
|
+
"tyDBW2lyhFOLT2hMSJSdK/tQ/PezQDviGzEaV/5oKHDReaTKWOsjbDkftJiufdJ+FwMDABOfvj5nYBpetvkAsU02oXftLJYe";
|
|
65
65
|
const ROOT_DER =
|
|
66
66
|
"MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNXYXJtdXAgRml4dHVyZSBSb290MB4XDTI1MDEwMTAw" +
|
|
67
67
|
"MDAwMFoXDTM1MDEwMTAwMDAwMFowHjEcMBoGA1UEAwwTV2FybXVwIEZpeHR1cmUgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEP" +
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
68
|
+
"ADCCAQoCggEBAMEAjrxVUntvHweCVz7mjYZTWZiTOW3mabXocCrFXKs8CcJVYNGQkpvhC/1VIr4t7hdrSPG31j6PvR4x0OAZTJZR" +
|
|
69
|
+
"k++Zo1jPUWIG3YomFJWhVjoJqoZTcO8uMDydvO75OTxto6Mc5gpfzHgfc52aAqemSqnfzryjupS6o1KOwbE1byLjM/Mmpp4CrRXx" +
|
|
70
|
+
"Fy/Jd8g2w1dSTTZo7QDIYBuZcNW4UYna9Mpac7otnv8JdlRSKonbbxXbx1E2GUm0tVM+4B5ARtBOFggnZYNW+KvQwcl4NRLTrFZS" +
|
|
71
|
+
"KNaRAW78DxclmSBsMm0lV+lsnV9miyuR2vfsj1FwSHKe1gzf12yRMpECAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B" +
|
|
72
|
+
"Af8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAGmB1ws+kKHGtO5D8UnCCyXI/q5LtHsMDO+lDwp/6HAKJ6xVnJCNVP4d1/nTwMVm" +
|
|
73
|
+
"KeJTfEseakqaVSiUcowlPEgSM1OVIRU6aRqBog4cjyBt9F74cmdy+VOewkd4jwHUDnkuj8sNx6BdU8FN83w2nIlQ35g/exzQDP8q" +
|
|
74
|
+
"mB/GtQlnGbmi6mRMKzFn5stSwQS5tcTwz8h5dkDG2FtsBG4f8YDs8r2G0ur7FQD9aIhlttcUUdSE1mZHm4itgqgyE+hreL0x0wqz" +
|
|
75
|
+
"7V7NQK7fMH8OLTr5JrjVMyrpX8EQqyntEkAS8IaaQ42h3GNo1+HRVeJ9M53noaveacxhtTZDnI5u8wA=";
|
|
76
76
|
|
|
77
77
|
export const WARMUP_FIXTURE = {
|
|
78
78
|
clientPrivPkcs8: () => B(CLIENT_PRIV),
|
package/types/client.d.ts
CHANGED
|
@@ -65,6 +65,11 @@ export function install(options?: ClientOptions): () => void;
|
|
|
65
65
|
* the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
|
|
66
66
|
* matching a browser's Accept-Encoding, not saving CPU.
|
|
67
67
|
* @property {boolean} [keepAlive] default true.
|
|
68
|
+
* @property {Array<[number, number]>} [http2Settings] the HTTP/2 SETTINGS flight, as [id, value]
|
|
69
|
+
* pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
|
|
70
|
+
* are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
|
|
71
|
+
* TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
|
|
72
|
+
* `alpn`, `versions`, `extensionOrder`).
|
|
68
73
|
* @property {boolean} [http2] offer HTTP/2 via ALPN and speak it when the server selects it.
|
|
69
74
|
* Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
|
|
70
75
|
* on a CPU-billed runtime h2 costs MORE than h1 (HPACK is extra work). Set false to offer only
|
|
@@ -222,6 +227,14 @@ export type ClientOptions = {
|
|
|
222
227
|
* default true.
|
|
223
228
|
*/
|
|
224
229
|
keepAlive?: boolean | undefined;
|
|
230
|
+
/**
|
|
231
|
+
* the HTTP/2 SETTINGS flight, as [id, value]
|
|
232
|
+
* pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
|
|
233
|
+
* are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
|
|
234
|
+
* TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
|
|
235
|
+
* `alpn`, `versions`, `extensionOrder`).
|
|
236
|
+
*/
|
|
237
|
+
http2Settings?: [number, number][] | undefined;
|
|
225
238
|
/**
|
|
226
239
|
* offer HTTP/2 via ALPN and speak it when the server selects it.
|
|
227
240
|
* Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
|
|
@@ -74,6 +74,8 @@ export class Http2Connection {
|
|
|
74
74
|
endStream: boolean;
|
|
75
75
|
} | null;
|
|
76
76
|
_maxHeaderBlockBytes: number;
|
|
77
|
+
/** @type {Array<[number, number]> | null} the SETTINGS flight, ids and order included */
|
|
78
|
+
_settingsFlight: Array<[number, number]> | null;
|
|
77
79
|
_expectFirstSettings: boolean;
|
|
78
80
|
_fatal: any;
|
|
79
81
|
_goaway: {
|
|
@@ -252,6 +254,13 @@ export type Http2ConnectionOptions = {
|
|
|
252
254
|
* self-protection cap on a decoded response header list.
|
|
253
255
|
*/
|
|
254
256
|
maxHeaderListSize?: number | undefined;
|
|
257
|
+
/**
|
|
258
|
+
* the SETTINGS flight sent in the connection
|
|
259
|
+
* preface, as [id, value] pairs. Order is significant — an Akamai-style HTTP/2 fingerprint reads
|
|
260
|
+
* the ids in the order they are sent — so this replaces the flight entirely rather than merging.
|
|
261
|
+
* Defaults to curl's: MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH.
|
|
262
|
+
*/
|
|
263
|
+
settings?: [number, number][] | undefined;
|
|
255
264
|
/**
|
|
256
265
|
* cap on the RAW bytes of one HEADERS+CONTINUATION run,
|
|
257
266
|
* before HPACK decoding. Default 262144, matching the decoded cap. This is the bound that stops
|
package/types/tls/connect.d.ts
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
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
16
|
* @property {number[]} [ciphers] cipher suites to offer, in preference order.
|
|
17
|
+
* @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
|
|
18
|
+
* @property {number[]} [extensionOrder] ClientHello extension types, in the order to emit them.
|
|
19
|
+
* JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
|
|
20
|
+
* reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
|
|
21
|
+
* natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
|
|
22
|
+
* 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
|
|
17
23
|
* @property {Uint8Array} [clientRandom] fixed ClientHello.random, for reproducible handshakes.
|
|
18
24
|
* @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise.
|
|
19
25
|
* @property {boolean} [compatibilityCcs] send the middlebox-compatibility ChangeCipherSpec.
|
|
@@ -144,6 +150,18 @@ export type TlsOptions = {
|
|
|
144
150
|
* cipher suites to offer, in preference order.
|
|
145
151
|
*/
|
|
146
152
|
ciphers?: number[] | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* signature_algorithms to offer, in preference order.
|
|
155
|
+
*/
|
|
156
|
+
sigSchemes?: number[] | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* ClientHello extension types, in the order to emit them.
|
|
159
|
+
* JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
|
|
160
|
+
* reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
|
|
161
|
+
* natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
|
|
162
|
+
* 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
|
|
163
|
+
*/
|
|
164
|
+
extensionOrder?: number[] | undefined;
|
|
147
165
|
/**
|
|
148
166
|
* fixed ClientHello.random, for reproducible handshakes.
|
|
149
167
|
*/
|
|
@@ -28,51 +28,30 @@ export function generateKeyShare(group: number, { generateKeyPair }?: import("./
|
|
|
28
28
|
* @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
|
|
29
29
|
*/
|
|
30
30
|
export function deriveSharedSecret(group: number, privateKey: CryptoKey, peerKey: Uint8Array): Promise<Uint8Array>;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* @typedef {object} ClientHello
|
|
56
|
-
* @property {Uint8Array} message framed handshake message, ready for the record layer
|
|
57
|
-
* @property {Uint8Array} clientRandom
|
|
58
|
-
* @property {Uint8Array} legacySessionId
|
|
59
|
-
* @property {number[]} offeredCiphers
|
|
60
|
-
* @property {number[]} offeredGroups
|
|
61
|
-
* @property {number[]} offeredSigSchemes
|
|
62
|
-
* @property {Set<number>} offeredExtensions extension types present in the hello
|
|
63
|
-
* @property {string[]} offeredAlpn
|
|
64
|
-
* @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
|
|
65
|
-
* @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
|
|
66
|
-
* transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
|
|
67
|
-
*/
|
|
68
|
-
/**
|
|
69
|
-
* Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
|
|
70
|
-
* handshake needs to police the server's answer.
|
|
71
|
-
*
|
|
72
|
-
* @param {ClientHelloOptions} opts
|
|
73
|
-
* @returns {ClientHello}
|
|
74
|
-
*/
|
|
75
|
-
export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extraExtensions, psk, randomBytes, }: ClientHelloOptions): ClientHello;
|
|
31
|
+
export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, psk, randomBytes, }: {
|
|
32
|
+
hostname: any;
|
|
33
|
+
keyShares: any;
|
|
34
|
+
random: any;
|
|
35
|
+
legacySessionId: any;
|
|
36
|
+
ciphers: any;
|
|
37
|
+
groups?: number[] | undefined;
|
|
38
|
+
sigSchemes?: number[] | undefined;
|
|
39
|
+
alpn?: string[] | undefined;
|
|
40
|
+
versions?: number[] | undefined;
|
|
41
|
+
extensionOrder?: readonly number[] | undefined;
|
|
42
|
+
extraExtensions?: never[] | undefined;
|
|
43
|
+
psk?: null | undefined;
|
|
44
|
+
randomBytes?: ((n: any) => Uint8Array<any>) | undefined;
|
|
45
|
+
}): {
|
|
46
|
+
message: Uint8Array<ArrayBufferLike>;
|
|
47
|
+
clientRandom: any;
|
|
48
|
+
legacySessionId: any;
|
|
49
|
+
offeredCiphers: any;
|
|
50
|
+
offeredGroups: number[];
|
|
51
|
+
offeredSigSchemes: number[];
|
|
52
|
+
offeredExtensions: Set<any>;
|
|
53
|
+
offeredAlpn: string[];
|
|
54
|
+
};
|
|
76
55
|
/**
|
|
77
56
|
* Patch the real binder over the placeholder `buildClientHello` emitted. Separate from the
|
|
78
57
|
* builder because the binder is derived FROM the built message (truncated), so there is no
|
|
@@ -308,6 +287,71 @@ export function checkFinished(received: Uint8Array, expected: Uint8Array): true;
|
|
|
308
287
|
* @returns {string | null} null when the server declined ALPN entirely
|
|
309
288
|
*/
|
|
310
289
|
export function checkAlpn(extensions: Map<number, Uint8Array>, offeredAlpn: string[], where: string): string | null;
|
|
290
|
+
/**
|
|
291
|
+
* @typedef {object} ClientHelloOptions
|
|
292
|
+
* @property {string} hostname SNI, unless it is an IP literal (then no SNI is sent)
|
|
293
|
+
* @property {Array<{ group: number, keyExchange: Uint8Array }>} keyShares public halves to
|
|
294
|
+
* offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
|
|
295
|
+
* @property {Uint8Array} [random] fixed ClientHello.random, for reproducible handshakes
|
|
296
|
+
* @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise
|
|
297
|
+
* @property {number[]} [ciphers] default: the union for the offered versions, 1.3 first
|
|
298
|
+
* @property {number[]} [groups] supported_groups, default SUPPORTED_GROUPS
|
|
299
|
+
* @property {number[]} [sigSchemes] default SUPPORTED_SIG_SCHEMES
|
|
300
|
+
* @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
|
|
301
|
+
* @property {number[]} [versions] default [TLS13, TLS12]
|
|
302
|
+
* @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
|
|
303
|
+
* @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
|
|
304
|
+
* offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
|
|
305
|
+
* the real binder over `message.subarray(0, truncatedLength)` and patch it in at
|
|
306
|
+
* `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
|
|
307
|
+
* every honest server must reject.
|
|
308
|
+
* @property {(n: number) => Uint8Array} [randomBytes] injectable randomness
|
|
309
|
+
*/
|
|
310
|
+
/**
|
|
311
|
+
* The built hello plus everything later steps need to police the server's answer against what
|
|
312
|
+
* was actually offered — negotiation checks must run against this record, never against the
|
|
313
|
+
* defaults they might have come from.
|
|
314
|
+
* @typedef {object} ClientHello
|
|
315
|
+
* @property {Uint8Array} message framed handshake message, ready for the record layer
|
|
316
|
+
* @property {Uint8Array} clientRandom
|
|
317
|
+
* @property {Uint8Array} legacySessionId
|
|
318
|
+
* @property {number[]} offeredCiphers
|
|
319
|
+
* @property {number[]} offeredGroups
|
|
320
|
+
* @property {number[]} offeredSigSchemes
|
|
321
|
+
* @property {Set<number>} offeredExtensions extension types present in the hello
|
|
322
|
+
* @property {string[]} offeredAlpn
|
|
323
|
+
* @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
|
|
324
|
+
* @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
|
|
325
|
+
* transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
|
|
326
|
+
*/
|
|
327
|
+
/**
|
|
328
|
+
* Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
|
|
329
|
+
* handshake needs to police the server's answer.
|
|
330
|
+
*
|
|
331
|
+
* @param {ClientHelloOptions} opts
|
|
332
|
+
* @returns {ClientHello}
|
|
333
|
+
*/
|
|
334
|
+
/**
|
|
335
|
+
* Extension emission order, by type. This is not cosmetic: JA3 and JA4 hash the extension list in
|
|
336
|
+
* WIRE ORDER, so the order alone is a large part of what a fingerprinter reads.
|
|
337
|
+
*
|
|
338
|
+
* Captured from curl 8.21.0 / OpenSSL 3.6.3, which sends:
|
|
339
|
+
* renegotiation_info, server_name, ec_point_formats, supported_groups, ALPN, encrypt_then_mac,
|
|
340
|
+
* extended_master_secret, post_handshake_auth, signature_algorithms, supported_versions,
|
|
341
|
+
* psk_key_exchange_modes, key_share
|
|
342
|
+
*
|
|
343
|
+
* Two of those this package does not send, and the reason is the same in both cases — an extension
|
|
344
|
+
* is a claim about what we can do. encrypt_then_mac only applies to CBC suites, which are not
|
|
345
|
+
* offered; post_handshake_auth invites a CertificateRequest after the handshake, which is not
|
|
346
|
+
* implemented. status_request goes the other way: curl does not send it, this package does,
|
|
347
|
+
* because a stapled OCSP response is its only revocation signal. It is placed where OpenSSL puts
|
|
348
|
+
* it when it does send one, right after server_name.
|
|
349
|
+
*
|
|
350
|
+
* Anything not named here keeps its natural position at the end, and pre_shared_key is forced last
|
|
351
|
+
* whatever the caller asks for, because RFC 8446 s4.2.11 defines the binder transcript as the hello
|
|
352
|
+
* truncated just before the binders — a range that only exists if nothing follows them.
|
|
353
|
+
*/
|
|
354
|
+
export const CURL_EXTENSION_ORDER: readonly number[];
|
|
311
355
|
export { GROUP_PARAMS };
|
|
312
356
|
/**
|
|
313
357
|
* An ephemeral key share: the public half as sent in key_share, plus the private key the
|
|
@@ -324,6 +368,18 @@ export type KeyShare = {
|
|
|
324
368
|
*/
|
|
325
369
|
privateKey: CryptoKey;
|
|
326
370
|
};
|
|
371
|
+
/**
|
|
372
|
+
* A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
|
|
373
|
+
* everything else is exactly what the wire carried, judged later by the negotiate* functions.
|
|
374
|
+
*/
|
|
375
|
+
export type ServerHello = {
|
|
376
|
+
legacyVersion: number;
|
|
377
|
+
random: Uint8Array;
|
|
378
|
+
legacySessionIdEcho: Uint8Array;
|
|
379
|
+
cipherSuite: number;
|
|
380
|
+
extensions: Map<number, Uint8Array>;
|
|
381
|
+
isHelloRetryRequest: boolean;
|
|
382
|
+
};
|
|
327
383
|
export type ClientHelloOptions = {
|
|
328
384
|
/**
|
|
329
385
|
* SNI, unless it is an IP literal (then no SNI is sent)
|
|
@@ -415,16 +471,4 @@ export type ClientHello = {
|
|
|
415
471
|
*/
|
|
416
472
|
truncatedLength?: number | undefined;
|
|
417
473
|
};
|
|
418
|
-
/**
|
|
419
|
-
* A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
|
|
420
|
-
* everything else is exactly what the wire carried, judged later by the negotiate* functions.
|
|
421
|
-
*/
|
|
422
|
-
export type ServerHello = {
|
|
423
|
-
legacyVersion: number;
|
|
424
|
-
random: Uint8Array;
|
|
425
|
-
legacySessionIdEcho: Uint8Array;
|
|
426
|
-
cipherSuite: number;
|
|
427
|
-
extensions: Map<number, Uint8Array>;
|
|
428
|
-
isHelloRetryRequest: boolean;
|
|
429
|
-
};
|
|
430
474
|
import { GROUP_PARAMS } from './constants.js';
|