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.
@@ -71,11 +71,19 @@
71
71
  * @property {object} peer
72
72
  */
73
73
  /**
74
- * Injectable nondeterminism. Supplying these makes a handshake byte-for-byte reproducible, which
75
- * is what allows a recorded session to be replayed in an offline test.
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. Supplying these makes a handshake byte-for-byte reproducible, which
260
- * is what allows a recorded session to be replayed in an offline test.
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.
@@ -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 }} GroupParams
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, { generateKeyPair }?: import("./connect.js").TlsDeps): Promise<KeyShare>;
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 for the group
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
+ };
@@ -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';