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.
@@ -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
+ };
@@ -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
  };
@@ -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[]} [extensionOrder] ClientHello extension types, in the order to emit them.
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. Supplying these makes a handshake byte-for-byte reproducible, which
70
- * 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.
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. Supplying these makes a handshake byte-for-byte reproducible, which
247
- * 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.
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.
@@ -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
@@ -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, { 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,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 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>;
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
+ };
@@ -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';