tunnelfetch 1.3.0 → 1.4.1
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/decode.js +24 -3
- package/src/client.js +31 -4
- 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 +47 -13
- package/src/tls/aead.js +51 -16
- package/src/tls/connect.js +42 -7
- package/src/tls/constants.js +60 -6
- package/src/tls/handshake-messages.js +18 -5
- package/src/tls/handshake.js +1 -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/decode.d.ts +3 -1
- package/types/client.d.ts +34 -2
- 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 +6 -2
- package/types/tls/aead.d.ts +17 -1
- package/types/tls/connect.d.ts +30 -4
- package/types/tls/constants.d.ts +31 -3
- package/types/tls/handshake-messages.d.ts +8 -4
- package/types/tls/hybrid.d.ts +63 -0
- package/types/tls/record.d.ts +23 -0
package/types/tls/connect.d.ts
CHANGED
|
@@ -71,11 +71,19 @@
|
|
|
71
71
|
* @property {object} peer
|
|
72
72
|
*/
|
|
73
73
|
/**
|
|
74
|
-
* Injectable nondeterminism
|
|
75
|
-
*
|
|
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.
|
|
76
80
|
* @typedef {object} TlsDeps
|
|
77
81
|
* @property {(n: number) => Uint8Array} [randomBytes]
|
|
78
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
|
|
79
87
|
*/
|
|
80
88
|
/**
|
|
81
89
|
* What a completed handshake reports about itself.
|
|
@@ -256,12 +264,30 @@ export type CapturedTicket = {
|
|
|
256
264
|
peer: object;
|
|
257
265
|
};
|
|
258
266
|
/**
|
|
259
|
-
* Injectable nondeterminism
|
|
260
|
-
*
|
|
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.
|
|
261
273
|
*/
|
|
262
274
|
export type TlsDeps = {
|
|
263
275
|
randomBytes?: ((n: number) => Uint8Array) | undefined;
|
|
264
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;
|
|
265
291
|
};
|
|
266
292
|
/**
|
|
267
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
|
|
@@ -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,11 +24,14 @@ 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>;
|
|
34
|
+
export function deriveSharedSecret(group: number, privateKey: CryptoKey | import("./hybrid.js").HybridPrivate, peerKey: Uint8Array, deps?: import("./connect.js").TlsDeps): Promise<Uint8Array>;
|
|
31
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;
|
|
@@ -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';
|