tunnelfetch 1.2.0 → 1.4.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 +93 -28
- package/README.zh-CN.md +54 -11
- package/package.json +5 -1
- package/src/client/header-order.js +159 -0
- package/src/client.js +79 -18
- package/src/http2/connection.js +36 -4
- package/src/index.js +1 -0
- package/src/profile/chrome.js +45 -0
- package/src/profile/vendor/chacha20poly1305.js +65 -0
- package/src/profile/vendor/mlkem768.js +67 -0
- package/src/profiles.js +184 -0
- package/src/tls/aead.js +51 -16
- package/src/tls/connect.js +49 -8
- package/src/tls/constants.js +60 -6
- package/src/tls/extensions.js +10 -0
- package/src/tls/grease.js +109 -0
- package/src/tls/handshake-messages.js +78 -9
- package/src/tls/handshake.js +2 -1
- package/src/tls/hybrid.js +166 -0
- package/src/tls/record.js +24 -3
- package/src/warmup-fixture.js +46 -45
- package/types/client/header-order.d.ts +56 -0
- package/types/client.d.ts +68 -0
- package/types/http2/connection.d.ts +16 -1
- package/types/http2/hpack.d.ts +1 -1
- package/types/index.d.ts +1 -0
- package/types/profile/chrome.d.ts +16 -0
- package/types/profile/vendor/chacha20poly1305.d.ts +7 -0
- package/types/profile/vendor/mlkem768.d.ts +23 -0
- package/types/profiles.d.ts +100 -0
- package/types/tls/aead.d.ts +17 -1
- package/types/tls/connect.d.ts +45 -6
- package/types/tls/constants.d.ts +31 -3
- package/types/tls/extensions.d.ts +7 -0
- package/types/tls/grease.d.ts +46 -0
- package/types/tls/handshake-messages.d.ts +12 -5
- package/types/tls/hybrid.d.ts +63 -0
- package/types/tls/record.d.ts +23 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold a profile into a Client's options, and refuse an identity that cannot be honoured.
|
|
3
|
+
*
|
|
4
|
+
* Explicit options WIN over the profile: a caller who names a field meant to name it, and silently
|
|
5
|
+
* overriding them would make the profile impossible to adjust. The profile fills what was not said.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} options as given to the Client
|
|
8
|
+
* @returns {object} options with the profile folded in
|
|
9
|
+
*/
|
|
10
|
+
export function applyProfile(options: object): object;
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} FingerprintProfile
|
|
13
|
+
* @property {string} name
|
|
14
|
+
* @property {object} [tls] merged into `tls`
|
|
15
|
+
* @property {readonly string[]} [headerOrder]
|
|
16
|
+
* @property {Array<[number, number]>} [http2Settings]
|
|
17
|
+
* @property {string[]} [http2PseudoHeaderOrder]
|
|
18
|
+
* @property {Record<string, string>} [http2HpackIndexing]
|
|
19
|
+
* @property {Array<[string, string]>} [headers] default request headers, in order
|
|
20
|
+
* @property {string[]} [requires] capabilities the caller must inject for this identity to be
|
|
21
|
+
* honest: `'cipher:chacha20'`, `'group:x25519mlkem768'`, `'decoder:br'`, `'decoder:zstd'`
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* curl 8.21.0 / OpenSSL 3.6.3. Complete: every layer was captured, and everything it offers is
|
|
25
|
+
* something this package can actually perform. This is the default identity.
|
|
26
|
+
*/
|
|
27
|
+
export const curl: Readonly<{
|
|
28
|
+
name: "curl/8.21.0";
|
|
29
|
+
tls: Readonly<{
|
|
30
|
+
alpn: string[];
|
|
31
|
+
extensionOrder: readonly number[];
|
|
32
|
+
grease: false;
|
|
33
|
+
}>;
|
|
34
|
+
headerOrder: readonly string[];
|
|
35
|
+
headers: readonly string[][];
|
|
36
|
+
http2Settings: readonly number[][];
|
|
37
|
+
http2PseudoHeaderOrder: readonly string[];
|
|
38
|
+
http2HpackIndexing: Readonly<{
|
|
39
|
+
':path': "without";
|
|
40
|
+
}>;
|
|
41
|
+
requires: readonly never[];
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Chromium, TLS layer captured off the wire.
|
|
45
|
+
*
|
|
46
|
+
* INCOMPLETE ON PURPOSE, and it refuses to be used as though it were not. Two things are missing
|
|
47
|
+
* and neither can be papered over:
|
|
48
|
+
*
|
|
49
|
+
* * Chromium offers TLS_CHACHA20_POLY1305_SHA256 and the X25519MLKEM768 group, and this package
|
|
50
|
+
* implements neither. A ClientHello is an OFFER: a server may take either, and a client that
|
|
51
|
+
* then cannot complete the handshake has traded a fingerprint mismatch for a dead connection.
|
|
52
|
+
* Both are reachable by injection, which is why they are listed in `requires` rather than
|
|
53
|
+
* silently dropped.
|
|
54
|
+
* * Chromium's HTTP/2 preface was not captured — capturing it needs a TLS server the browser
|
|
55
|
+
* will trust, which is a different exercise. So this profile carries no h2 layer, and using it
|
|
56
|
+
* with HTTP/2 enabled would produce a Chromium ClientHello above a curl h2 preface: precisely
|
|
57
|
+
* the split identity a profile exists to prevent.
|
|
58
|
+
*
|
|
59
|
+
* `applyProfile` refuses both cases with a message naming what is missing.
|
|
60
|
+
*/
|
|
61
|
+
export const chrome: Readonly<{
|
|
62
|
+
name: "chrome/150";
|
|
63
|
+
tls: Readonly<{
|
|
64
|
+
ciphers: readonly number[];
|
|
65
|
+
groups: readonly number[];
|
|
66
|
+
offerGroups: readonly number[];
|
|
67
|
+
sigSchemes: readonly number[];
|
|
68
|
+
alpn: string[];
|
|
69
|
+
extensionOrder: "shuffle";
|
|
70
|
+
grease: true;
|
|
71
|
+
}>;
|
|
72
|
+
http2Settings: readonly number[][];
|
|
73
|
+
http2ConnectionWindow: number;
|
|
74
|
+
http2PseudoHeaderOrder: readonly string[];
|
|
75
|
+
headerOrder: readonly string[];
|
|
76
|
+
headers: readonly string[][];
|
|
77
|
+
requires: readonly string[];
|
|
78
|
+
}>;
|
|
79
|
+
/** @type {Record<string, FingerprintProfile>} */
|
|
80
|
+
export const profiles: Record<string, FingerprintProfile>;
|
|
81
|
+
export type FingerprintProfile = {
|
|
82
|
+
name: string;
|
|
83
|
+
/**
|
|
84
|
+
* merged into `tls`
|
|
85
|
+
*/
|
|
86
|
+
tls?: object | undefined;
|
|
87
|
+
headerOrder?: readonly string[] | undefined;
|
|
88
|
+
http2Settings?: [number, number][] | undefined;
|
|
89
|
+
http2PseudoHeaderOrder?: string[] | undefined;
|
|
90
|
+
http2HpackIndexing?: Record<string, string> | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* default request headers, in order
|
|
93
|
+
*/
|
|
94
|
+
headers?: [string, string][] | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* capabilities the caller must inject for this identity to be
|
|
97
|
+
* honest: `'cipher:chacha20'`, `'group:x25519mlkem768'`, `'decoder:br'`, `'decoder:zstd'`
|
|
98
|
+
*/
|
|
99
|
+
requires?: string[] | undefined;
|
|
100
|
+
};
|
package/types/tls/aead.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export function buildNonce(iv: Uint8Array, seq: number | bigint): Uint8Array;
|
|
|
25
25
|
* @property {Uint8Array} key
|
|
26
26
|
* @property {Uint8Array} iv the 12-byte static IV for TLS 1.3, the 4-byte implicit salt for
|
|
27
27
|
* TLS 1.2
|
|
28
|
+
* @property {{seal: (k: Uint8Array, n: Uint8Array, p: Uint8Array, aad: Uint8Array) => Uint8Array,
|
|
29
|
+
* open: (k: Uint8Array, n: Uint8Array, c: Uint8Array, aad: Uint8Array) => Uint8Array | null}}
|
|
30
|
+
* [impl] caller-supplied AEAD, required for ChaCha20-Poly1305 and unused otherwise. This runtime
|
|
31
|
+
* has no WebCrypto ChaCha20 — its only native path is node:crypto, and taking it would cost the
|
|
32
|
+
* package its "web platform only" property — so the implementation is injected. `open` returns
|
|
33
|
+
* null when authentication fails.
|
|
28
34
|
*/
|
|
29
35
|
/**
|
|
30
36
|
* Create record protection for one direction under one key. A new key (handshake -> application,
|
|
@@ -33,7 +39,7 @@ export function buildNonce(iv: Uint8Array, seq: number | bigint): Uint8Array;
|
|
|
33
39
|
* @param {AeadOptions} opts
|
|
34
40
|
* @returns {Promise<Aead>}
|
|
35
41
|
*/
|
|
36
|
-
export function createAead({ version, cipher, key, iv }: AeadOptions): Promise<Aead>;
|
|
42
|
+
export function createAead({ version, cipher, key, iv, impl }: AeadOptions): Promise<Aead>;
|
|
37
43
|
/**
|
|
38
44
|
* Record protection for one direction under one key. `encrypt` returns the encrypted record
|
|
39
45
|
* body ready for framing; `decrypt` either returns authenticated plaintext (with the inner
|
|
@@ -64,4 +70,14 @@ export type AeadOptions = {
|
|
|
64
70
|
* TLS 1.2
|
|
65
71
|
*/
|
|
66
72
|
iv: Uint8Array;
|
|
73
|
+
/**
|
|
74
|
+
* caller-supplied AEAD, required for ChaCha20-Poly1305 and unused otherwise. This runtime
|
|
75
|
+
* has no WebCrypto ChaCha20 — its only native path is node:crypto, and taking it would cost the
|
|
76
|
+
* package its "web platform only" property — so the implementation is injected. `open` returns
|
|
77
|
+
* null when authentication fails.
|
|
78
|
+
*/
|
|
79
|
+
impl?: {
|
|
80
|
+
seal: (k: Uint8Array, n: Uint8Array, p: Uint8Array, aad: Uint8Array) => Uint8Array;
|
|
81
|
+
open: (k: Uint8Array, n: Uint8Array, c: Uint8Array, aad: Uint8Array) => Uint8Array | null;
|
|
82
|
+
} | undefined;
|
|
67
83
|
};
|
package/types/tls/connect.d.ts
CHANGED
|
@@ -15,7 +15,12 @@
|
|
|
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
17
|
* @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
|
|
18
|
-
* @property {number
|
|
18
|
+
* @property {boolean | number} [grease] send GREASE (RFC 8701) reserved values in the cipher list,
|
|
19
|
+
* the extension list (one at each end), supported_groups, supported_versions and key_share.
|
|
20
|
+
* Default false, because curl does not GREASE — Chromium does. A number is a seed, which makes
|
|
21
|
+
* the hello reproducible; `true` draws one from `deps.randomBytes`. A server that negotiates a
|
|
22
|
+
* GREASE value is refused with a typed error naming it.
|
|
23
|
+
* @property {number[] | 'shuffle'} [extensionOrder] ClientHello extension types, in the order to emit them.
|
|
19
24
|
* JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
|
|
20
25
|
* reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
|
|
21
26
|
* natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
|
|
@@ -66,11 +71,19 @@
|
|
|
66
71
|
* @property {object} peer
|
|
67
72
|
*/
|
|
68
73
|
/**
|
|
69
|
-
* Injectable nondeterminism
|
|
70
|
-
*
|
|
74
|
+
* Injectable nondeterminism and crypto primitives the platform does not provide.
|
|
75
|
+
*
|
|
76
|
+
* `randomBytes` and `generateKeyPair` supply reproducibility (a recorded session replayed in an
|
|
77
|
+
* offline test). `aead` and `kem` supply capabilities this runtime lacks entirely: ChaCha20 and
|
|
78
|
+
* ML-KEM are absent from WebCrypto here, so an implementation must be injected before the suite or
|
|
79
|
+
* group they back can be offered — a ClientHello being an offer a server may take.
|
|
71
80
|
* @typedef {object} TlsDeps
|
|
72
81
|
* @property {(n: number) => Uint8Array} [randomBytes]
|
|
73
82
|
* @property {(algorithm: object, group: number) => Promise<CryptoKeyPair>} [generateKeyPair]
|
|
83
|
+
* @property {{ chacha20?: import('./aead.js').AeadOptions['impl'] }} [aead] injected AEAD
|
|
84
|
+
* implementations by name; `chacha20` gates and performs TLS_CHACHA20_POLY1305_SHA256
|
|
85
|
+
* @property {{ x25519mlkem768?: import('./hybrid.js').MlKem768 }} [kem] injected KEM
|
|
86
|
+
* implementations by name; `x25519mlkem768` gates and performs the X25519MLKEM768 hybrid group
|
|
74
87
|
*/
|
|
75
88
|
/**
|
|
76
89
|
* What a completed handshake reports about itself.
|
|
@@ -154,6 +167,14 @@ export type TlsOptions = {
|
|
|
154
167
|
* signature_algorithms to offer, in preference order.
|
|
155
168
|
*/
|
|
156
169
|
sigSchemes?: number[] | undefined;
|
|
170
|
+
/**
|
|
171
|
+
* send GREASE (RFC 8701) reserved values in the cipher list,
|
|
172
|
+
* the extension list (one at each end), supported_groups, supported_versions and key_share.
|
|
173
|
+
* Default false, because curl does not GREASE — Chromium does. A number is a seed, which makes
|
|
174
|
+
* the hello reproducible; `true` draws one from `deps.randomBytes`. A server that negotiates a
|
|
175
|
+
* GREASE value is refused with a typed error naming it.
|
|
176
|
+
*/
|
|
177
|
+
grease?: number | boolean | undefined;
|
|
157
178
|
/**
|
|
158
179
|
* ClientHello extension types, in the order to emit them.
|
|
159
180
|
* JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
|
|
@@ -161,7 +182,7 @@ export type TlsOptions = {
|
|
|
161
182
|
* natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
|
|
162
183
|
* 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
|
|
163
184
|
*/
|
|
164
|
-
extensionOrder?: number[] | undefined;
|
|
185
|
+
extensionOrder?: number[] | "shuffle" | undefined;
|
|
165
186
|
/**
|
|
166
187
|
* fixed ClientHello.random, for reproducible handshakes.
|
|
167
188
|
*/
|
|
@@ -243,12 +264,30 @@ export type CapturedTicket = {
|
|
|
243
264
|
peer: object;
|
|
244
265
|
};
|
|
245
266
|
/**
|
|
246
|
-
* Injectable nondeterminism
|
|
247
|
-
*
|
|
267
|
+
* Injectable nondeterminism and crypto primitives the platform does not provide.
|
|
268
|
+
*
|
|
269
|
+
* `randomBytes` and `generateKeyPair` supply reproducibility (a recorded session replayed in an
|
|
270
|
+
* offline test). `aead` and `kem` supply capabilities this runtime lacks entirely: ChaCha20 and
|
|
271
|
+
* ML-KEM are absent from WebCrypto here, so an implementation must be injected before the suite or
|
|
272
|
+
* group they back can be offered — a ClientHello being an offer a server may take.
|
|
248
273
|
*/
|
|
249
274
|
export type TlsDeps = {
|
|
250
275
|
randomBytes?: ((n: number) => Uint8Array) | undefined;
|
|
251
276
|
generateKeyPair?: ((algorithm: object, group: number) => Promise<CryptoKeyPair>) | undefined;
|
|
277
|
+
/**
|
|
278
|
+
* injected AEAD
|
|
279
|
+
* implementations by name; `chacha20` gates and performs TLS_CHACHA20_POLY1305_SHA256
|
|
280
|
+
*/
|
|
281
|
+
aead?: {
|
|
282
|
+
chacha20?: import("./aead.js").AeadOptions["impl"];
|
|
283
|
+
} | undefined;
|
|
284
|
+
/**
|
|
285
|
+
* injected KEM
|
|
286
|
+
* implementations by name; `x25519mlkem768` gates and performs the X25519MLKEM768 hybrid group
|
|
287
|
+
*/
|
|
288
|
+
kem?: {
|
|
289
|
+
x25519mlkem768?: import("./hybrid.js").MlKem768;
|
|
290
|
+
} | undefined;
|
|
252
291
|
};
|
|
253
292
|
/**
|
|
254
293
|
* What a completed handshake reports about itself.
|
package/types/tls/constants.d.ts
CHANGED
|
@@ -67,6 +67,18 @@ export const CIPHER_NAME: {
|
|
|
67
67
|
};
|
|
68
68
|
/** Offered in ClientHello, in preference order. */
|
|
69
69
|
export const TLS13_CIPHERS: number[];
|
|
70
|
+
/**
|
|
71
|
+
* curl 8.21.0 / OpenSSL 3.6.3 offers its TLS 1.3 suites in this exact order — AES-256-GCM,
|
|
72
|
+
* ChaCha20-Poly1305, AES-128-GCM — captured off the wire 2026-08-01 (`0x1302 0x1303 0x1301`).
|
|
73
|
+
* ChaCha20 is SECOND, right after AES-256-GCM; that is "curl's position" for it.
|
|
74
|
+
*
|
|
75
|
+
* TLS13_CIPHERS above leads with AES-128, which is the order this package has always offered and
|
|
76
|
+
* which the offline test server keys its default suite selection off; reordering it would change
|
|
77
|
+
* the negotiated suite across the whole suite. So this curl-faithful order is used ONLY when a
|
|
78
|
+
* ChaCha20 implementation has been injected — i.e. when the caller has opted into being able to
|
|
79
|
+
* perform every suite curl offers — and never otherwise. See connect.js.
|
|
80
|
+
*/
|
|
81
|
+
export const TLS13_CIPHERS_WITH_CHACHA: number[];
|
|
70
82
|
export const TLS12_CIPHERS: number[];
|
|
71
83
|
/**
|
|
72
84
|
* Per-suite parameters. `hash` drives the whole key schedule; `keyLen` the AEAD key size.
|
|
@@ -90,6 +102,7 @@ export namespace GROUP {
|
|
|
90
102
|
let x25519: number;
|
|
91
103
|
let x448: number;
|
|
92
104
|
let ffdhe2048: number;
|
|
105
|
+
let x25519mlkem768: number;
|
|
93
106
|
}
|
|
94
107
|
export const GROUP_NAME: {
|
|
95
108
|
[k: string]: string;
|
|
@@ -102,10 +115,14 @@ export const GROUP_NAME: {
|
|
|
102
115
|
export const SUPPORTED_GROUPS: number[];
|
|
103
116
|
/**
|
|
104
117
|
* WebCrypto parameters per group, discriminated on `kind` because X25519 sizes its shared
|
|
105
|
-
* secret in bytes while ECDH sizes it in bits
|
|
118
|
+
* secret in bytes while ECDH sizes it in bits, and the ML-KEM hybrid is not a WebCrypto
|
|
119
|
+
* primitive at all — it carries wire sizes only, and hybrid.js owns the crypto.
|
|
106
120
|
* @typedef {{ kind: 'x25519', algorithm: { name: string }, publicLen: number, secretLen: number }
|
|
107
121
|
* | { kind: 'ec', algorithm: { name: string, namedCurve: string }, publicLen: number,
|
|
108
|
-
* secretBits: number }
|
|
122
|
+
* secretBits: number }
|
|
123
|
+
* | { kind: 'hybrid', clientShareLen: number, serverShareLen: number, secretLen: number,
|
|
124
|
+
* mlkemPublicLen: number, mlkemSecretKeyLen: number, mlkemCiphertextLen: number,
|
|
125
|
+
* classicalPublicLen: number, classicalSecretLen: number }} GroupParams
|
|
109
126
|
*/
|
|
110
127
|
/**
|
|
111
128
|
* WebCrypto parameters per group. x448 and the finite-field groups are absent by design.
|
|
@@ -228,7 +245,8 @@ export type CipherParams = {
|
|
|
228
245
|
};
|
|
229
246
|
/**
|
|
230
247
|
* WebCrypto parameters per group, discriminated on `kind` because X25519 sizes its shared
|
|
231
|
-
* secret in bytes while ECDH sizes it in bits
|
|
248
|
+
* secret in bytes while ECDH sizes it in bits, and the ML-KEM hybrid is not a WebCrypto
|
|
249
|
+
* primitive at all — it carries wire sizes only, and hybrid.js owns the crypto.
|
|
232
250
|
*/
|
|
233
251
|
export type GroupParams = {
|
|
234
252
|
kind: "x25519";
|
|
@@ -245,6 +263,16 @@ export type GroupParams = {
|
|
|
245
263
|
};
|
|
246
264
|
publicLen: number;
|
|
247
265
|
secretBits: number;
|
|
266
|
+
} | {
|
|
267
|
+
kind: "hybrid";
|
|
268
|
+
clientShareLen: number;
|
|
269
|
+
serverShareLen: number;
|
|
270
|
+
secretLen: number;
|
|
271
|
+
mlkemPublicLen: number;
|
|
272
|
+
mlkemSecretKeyLen: number;
|
|
273
|
+
mlkemCiphertextLen: number;
|
|
274
|
+
classicalPublicLen: number;
|
|
275
|
+
classicalSecretLen: number;
|
|
248
276
|
};
|
|
249
277
|
/**
|
|
250
278
|
* How to verify one signature scheme with WebCrypto. `format: 'ecdsa-der'` marks the schemes
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An extension of an arbitrary type with an arbitrary body. Exists for GREASE (RFC 8701), whose
|
|
3
|
+
* whole point is to carry a reserved type this package assigns no meaning to.
|
|
4
|
+
* @param {number} type
|
|
5
|
+
* @param {Uint8Array} body
|
|
6
|
+
*/
|
|
7
|
+
export function encodeRawExtension(type: number, body: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
1
8
|
/**
|
|
2
9
|
* server_name (RFC 6066). Only host_name (type 0) exists in practice.
|
|
3
10
|
* An IP literal must NOT be sent as SNI — RFC 6066 s3 forbids it, and servers that do virtual
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @param {number} v @returns {boolean} */
|
|
2
|
+
export function isGrease(v: number): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* A deterministic-from-seed source of GREASE values and shuffles.
|
|
5
|
+
*
|
|
6
|
+
* Seeded rather than ad-hoc `Math.random` for two reasons: this package forbids ambient randomness
|
|
7
|
+
* in `src/` (repo-hygiene enforces it, so that every byte on the wire is reproducible in a test),
|
|
8
|
+
* and a fingerprint that cannot be reproduced cannot be asserted byte-for-byte.
|
|
9
|
+
*
|
|
10
|
+
* @param {number} seed
|
|
11
|
+
*/
|
|
12
|
+
export function greaseSource(seed: number): {
|
|
13
|
+
/** A GREASE value not yet handed out in this hello, since Chromium never repeats one. */
|
|
14
|
+
take(): number;
|
|
15
|
+
next: () => number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Fisher-Yates over the middle of the extension list, leaving the ends alone.
|
|
19
|
+
*
|
|
20
|
+
* The first and last positions are not free: Chromium pins a GREASE extension to each, and
|
|
21
|
+
* `pre_shared_key` MUST be last of all (RFC 8446 s4.2.11 — the binder transcript is the hello
|
|
22
|
+
* truncated just before the binders, a range that only exists if nothing follows them). So the
|
|
23
|
+
* shuffle covers everything between the fixed ends and nothing else.
|
|
24
|
+
*
|
|
25
|
+
* @param {Array<Uint8Array>} parts encoded extensions, already ordered
|
|
26
|
+
* @param {{next: () => number}} rng
|
|
27
|
+
* @param {(e: Uint8Array) => number} typeOf
|
|
28
|
+
* @param {number} pskType
|
|
29
|
+
* @returns {Array<Uint8Array>}
|
|
30
|
+
*/
|
|
31
|
+
export function shuffleExtensions(parts: Array<Uint8Array>, rng: {
|
|
32
|
+
next: () => number;
|
|
33
|
+
}, typeOf: (e: Uint8Array) => number, pskType: number): Array<Uint8Array>;
|
|
34
|
+
/**
|
|
35
|
+
* A GREASE key_share entry: a reserved group with a single-byte key, which is what Chromium sends.
|
|
36
|
+
* The byte is fixed rather than random — it is never used for anything, and a value that varies
|
|
37
|
+
* would only make the hello harder to assert on.
|
|
38
|
+
*
|
|
39
|
+
* @param {number} group
|
|
40
|
+
*/
|
|
41
|
+
export function greaseKeyShare(group: number): {
|
|
42
|
+
group: number;
|
|
43
|
+
keyExchange: Uint8Array<ArrayBuffer>;
|
|
44
|
+
};
|
|
45
|
+
/** The sixteen reserved values (RFC 8701 s2): 0x0A0A, 0x1A1A, ... 0xFAFA. */
|
|
46
|
+
export const GREASE_VALUES: readonly number[];
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* Generate an ephemeral key share for one group.
|
|
11
11
|
* `generateKeyPair` is injectable so a recorded handshake can be replayed with the exact private
|
|
12
|
-
* key that produced it.
|
|
12
|
+
* key that produced it. X25519MLKEM768 dispatches to hybrid.js, using the injected ML-KEM
|
|
13
|
+
* implementation from `deps.kem`.
|
|
13
14
|
*
|
|
14
15
|
* @param {number} group
|
|
15
16
|
* @param {import('./connect.js').TlsDeps} [deps]
|
|
16
17
|
* @returns {Promise<KeyShare>}
|
|
17
18
|
*/
|
|
18
|
-
export function generateKeyShare(group: number,
|
|
19
|
+
export function generateKeyShare(group: number, deps?: import("./connect.js").TlsDeps): Promise<KeyShare>;
|
|
19
20
|
/**
|
|
20
21
|
* ECDH/X25519 shared secret. The peer's key is imported in raw form, which is where a malformed
|
|
21
22
|
* point is caught: WebCrypto rejects a point that is not on the curve, so we do not have to
|
|
@@ -23,12 +24,15 @@ export function generateKeyShare(group: number, { generateKeyPair }?: import("./
|
|
|
23
24
|
* so it is checked here first.
|
|
24
25
|
*
|
|
25
26
|
* @param {number} group
|
|
26
|
-
* @param {CryptoKey} privateKey our ephemeral private key
|
|
27
|
+
* @param {CryptoKey | import('./hybrid.js').HybridPrivate} privateKey our ephemeral private key
|
|
28
|
+
* for the group (a compound value for the ML-KEM hybrid)
|
|
27
29
|
* @param {Uint8Array} peerKey the server's raw public key from its key_share
|
|
30
|
+
* @param {import('./connect.js').TlsDeps} [deps] carries the injected ML-KEM implementation the
|
|
31
|
+
* hybrid group needs; unused by the classical groups
|
|
28
32
|
* @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
|
|
29
33
|
*/
|
|
30
|
-
export function deriveSharedSecret(group: number, privateKey: CryptoKey, peerKey: Uint8Array): Promise<Uint8Array>;
|
|
31
|
-
export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, psk, randomBytes, }: {
|
|
34
|
+
export function deriveSharedSecret(group: number, privateKey: CryptoKey | import("./hybrid.js").HybridPrivate, peerKey: Uint8Array, deps?: import("./connect.js").TlsDeps): Promise<Uint8Array>;
|
|
35
|
+
export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, psk, grease, randomBytes, }: {
|
|
32
36
|
hostname: any;
|
|
33
37
|
keyShares: any;
|
|
34
38
|
random: any;
|
|
@@ -41,6 +45,7 @@ export function buildClientHello({ hostname, keyShares, random, legacySessionId,
|
|
|
41
45
|
extensionOrder?: readonly number[] | undefined;
|
|
42
46
|
extraExtensions?: never[] | undefined;
|
|
43
47
|
psk?: null | undefined;
|
|
48
|
+
grease?: boolean | undefined;
|
|
44
49
|
randomBytes?: ((n: any) => Uint8Array<any>) | undefined;
|
|
45
50
|
}): {
|
|
46
51
|
message: Uint8Array<ArrayBufferLike>;
|
|
@@ -351,6 +356,8 @@ export function checkAlpn(extensions: Map<number, Uint8Array>, offeredAlpn: stri
|
|
|
351
356
|
* whatever the caller asks for, because RFC 8446 s4.2.11 defines the binder transcript as the hello
|
|
352
357
|
* truncated just before the binders — a range that only exists if nothing follows them.
|
|
353
358
|
*/
|
|
359
|
+
/** `extensionOrder: SHUFFLE_EXTENSIONS` reproduces what Chromium does — see grease.js. */
|
|
360
|
+
export const SHUFFLE_EXTENSIONS: "shuffle";
|
|
354
361
|
export const CURL_EXTENSION_ORDER: readonly number[];
|
|
355
362
|
export { GROUP_PARAMS };
|
|
356
363
|
/**
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a client key share for X25519MLKEM768.
|
|
3
|
+
*
|
|
4
|
+
* @param {MlKem768} kem the injected ML-KEM-768 implementation
|
|
5
|
+
* @param {import('./connect.js').TlsDeps} [deps] `generateKeyPair` is honoured for the X25519
|
|
6
|
+
* half so a recorded handshake can be replayed with a fixed classical key
|
|
7
|
+
* @returns {Promise<{ group: number, keyExchange: Uint8Array, privateKey: HybridPrivate }>}
|
|
8
|
+
* `keyExchange` is the 1216-byte wire share (ML-KEM encapsulation key || X25519 public key)
|
|
9
|
+
*/
|
|
10
|
+
export function generateHybridKeyShare(kem: MlKem768, { generateKeyPair }?: import("./connect.js").TlsDeps): Promise<{
|
|
11
|
+
group: number;
|
|
12
|
+
keyExchange: Uint8Array;
|
|
13
|
+
privateKey: HybridPrivate;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Derive the 64-byte hybrid shared secret from the server's X25519MLKEM768 key share.
|
|
17
|
+
*
|
|
18
|
+
* @param {MlKem768} kem the injected ML-KEM-768 implementation
|
|
19
|
+
* @param {HybridPrivate} privateKey what generateHybridKeyShare kept
|
|
20
|
+
* @param {Uint8Array} serverShare the server's 1120-byte key_exchange (ciphertext || X25519 pub)
|
|
21
|
+
* @returns {Promise<Uint8Array>} ML-KEM shared secret (32) || X25519 shared secret (32) = 64
|
|
22
|
+
*/
|
|
23
|
+
export function deriveHybridSecret(kem: MlKem768, privateKey: HybridPrivate, serverShare: Uint8Array): Promise<Uint8Array>;
|
|
24
|
+
export const HYBRID_GROUP: number;
|
|
25
|
+
/**
|
|
26
|
+
* An injected ML-KEM-768 implementation (FIPS 203). Shapes match `wasmcrypto`'s `mlkem768`:
|
|
27
|
+
*/
|
|
28
|
+
export type MlKem768 = {
|
|
29
|
+
/**
|
|
30
|
+
* ML-KEM.KeyGen; `publicKey` is the 1184-byte encapsulation key, `secretKey` the 2400-byte
|
|
31
|
+
* decapsulation key.
|
|
32
|
+
*/
|
|
33
|
+
keygen: (seed?: Uint8Array) => {
|
|
34
|
+
publicKey: Uint8Array;
|
|
35
|
+
secretKey: Uint8Array;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* ML-KEM.Encaps; used by a server, exercised by the offline test server.
|
|
39
|
+
*/
|
|
40
|
+
encapsulate: (publicKey: Uint8Array) => {
|
|
41
|
+
cipherText: Uint8Array;
|
|
42
|
+
sharedSecret: Uint8Array;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* ML-KEM.Decaps; returns the 32-byte shared secret (implicit rejection on a bad ciphertext).
|
|
46
|
+
*/
|
|
47
|
+
decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The private half kept between generateHybridKeyShare and deriveHybridSecret: the ML-KEM
|
|
51
|
+
* decapsulation key and the X25519 private key. Not a CryptoKey, so callers treat KeyShare's
|
|
52
|
+
* private component opaquely (they already do — it only ever flows back into derivation).
|
|
53
|
+
*/
|
|
54
|
+
export type HybridPrivate = {
|
|
55
|
+
/**
|
|
56
|
+
* the ML-KEM decapsulation key
|
|
57
|
+
*/
|
|
58
|
+
mlkemSecretKey: Uint8Array;
|
|
59
|
+
/**
|
|
60
|
+
* the X25519 private key
|
|
61
|
+
*/
|
|
62
|
+
classicalPrivateKey: CryptoKey;
|
|
63
|
+
};
|
package/types/tls/record.d.ts
CHANGED
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
* record, TLS 1.3 only
|
|
33
33
|
* @property {null | ((msg: HandshakeMessage) => void | Promise<void>)} [onPostHandshake]
|
|
34
34
|
* NewSessionTicket consumer; default is to discard
|
|
35
|
+
* @property {null | { chacha20?: import('./aead.js').AeadOptions['impl'] }} [aeadImpls] injected
|
|
36
|
+
* AEAD implementations by name. ChaCha20-Poly1305 has no WebCrypto path on this runtime, so its
|
|
37
|
+
* seal/open are supplied here and threaded into every createAead for the ChaCha20 suite (initial
|
|
38
|
+
* keys and each KeyUpdate rotation). Absent for the AES-GCM-only default, which needs nothing.
|
|
35
39
|
*/
|
|
36
40
|
export class RecordLayer {
|
|
37
41
|
/**
|
|
@@ -46,6 +50,9 @@ export class RecordLayer {
|
|
|
46
50
|
_shutdownGraceMs: number;
|
|
47
51
|
_padding: ((type: number, length: number) => number) | null;
|
|
48
52
|
_onPostHandshake: ((msg: HandshakeMessage) => void | Promise<void>) | null;
|
|
53
|
+
_aeadImpls: {
|
|
54
|
+
chacha20?: import("./aead.js").AeadOptions["impl"];
|
|
55
|
+
} | null;
|
|
49
56
|
_version: number;
|
|
50
57
|
/** @type {DirectionState} */
|
|
51
58
|
_send: DirectionState;
|
|
@@ -124,6 +131,13 @@ export class RecordLayer {
|
|
|
124
131
|
key?: Uint8Array;
|
|
125
132
|
iv?: Uint8Array;
|
|
126
133
|
}): Promise<NonNullable<DirectionState>>;
|
|
134
|
+
/**
|
|
135
|
+
* The injected AEAD implementation a suite needs, or null. Only ChaCha20-Poly1305 needs one on
|
|
136
|
+
* this runtime; every AES-GCM suite goes through WebCrypto and passes null.
|
|
137
|
+
* @param {number} cipher
|
|
138
|
+
* @returns {import('./aead.js').AeadOptions['impl'] | null}
|
|
139
|
+
*/
|
|
140
|
+
_implFor(cipher: number): import("./aead.js").AeadOptions["impl"] | null;
|
|
127
141
|
/**
|
|
128
142
|
* Next handshake message during the handshake phase.
|
|
129
143
|
* Returns `{ type, body, raw }` (raw includes the 4-byte header, ready for the transcript),
|
|
@@ -363,6 +377,15 @@ export type RecordLayerOptions = {
|
|
|
363
377
|
* NewSessionTicket consumer; default is to discard
|
|
364
378
|
*/
|
|
365
379
|
onPostHandshake?: ((msg: HandshakeMessage) => void | Promise<void>) | null | undefined;
|
|
380
|
+
/**
|
|
381
|
+
* injected
|
|
382
|
+
* AEAD implementations by name. ChaCha20-Poly1305 has no WebCrypto path on this runtime, so its
|
|
383
|
+
* seal/open are supplied here and threaded into every createAead for the ChaCha20 suite (initial
|
|
384
|
+
* keys and each KeyUpdate rotation). Absent for the AES-GCM-only default, which needs nothing.
|
|
385
|
+
*/
|
|
386
|
+
aeadImpls?: {
|
|
387
|
+
chacha20?: import("./aead.js").AeadOptions["impl"];
|
|
388
|
+
} | null | undefined;
|
|
366
389
|
};
|
|
367
390
|
import { ByteReader } from '../util/bytes.js';
|
|
368
391
|
import { ByteWriter } from '../util/bytes.js';
|