tunnelfetch 1.3.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.
@@ -15,6 +15,9 @@ import { greaseSource, greaseKeyShare, shuffleExtensions, isGrease } from './gre
15
15
  // der.js is a strict ASN.1 reader with no trust policy in it; the signature-format conversion
16
16
  // lives there so the certificate path builder and this file cannot drift apart.
17
17
  import { ecdsaDerToRaw } from '../trust/der.js';
18
+ // The X25519MLKEM768 hybrid is a group like any other to the driver, but its key exchange is not
19
+ // a WebCrypto primitive, so its keygen/derive live in their own module and are dispatched to here.
20
+ import { HYBRID_GROUP, deriveHybridSecret, generateHybridKeyShare } from './hybrid.js';
18
21
  import {
19
22
  CIPHER_NAME,
20
23
  CIPHER_PARAMS,
@@ -77,16 +80,20 @@ const defaultRandom = (n) => crypto.getRandomValues(new Uint8Array(n));
77
80
  /**
78
81
  * Generate an ephemeral key share for one group.
79
82
  * `generateKeyPair` is injectable so a recorded handshake can be replayed with the exact private
80
- * key that produced it.
83
+ * key that produced it. X25519MLKEM768 dispatches to hybrid.js, using the injected ML-KEM
84
+ * implementation from `deps.kem`.
81
85
  *
82
86
  * @param {number} group
83
87
  * @param {import('./connect.js').TlsDeps} [deps]
84
88
  * @returns {Promise<KeyShare>}
85
89
  */
86
- export async function generateKeyShare(group, { generateKeyPair } = {}) {
90
+ export async function generateKeyShare(group, deps = {}) {
91
+ if (group === HYBRID_GROUP) {
92
+ return generateHybridKeyShare(deps.kem?.x25519mlkem768, deps);
93
+ }
87
94
  const params = requireSupportedGroup(group, 'ClientHello');
88
95
  const gen =
89
- generateKeyPair ??
96
+ deps.generateKeyPair ??
90
97
  ((algorithm) => crypto.subtle.generateKey(algorithm, false, ['deriveBits']));
91
98
  const pair = await gen(params.algorithm, group);
92
99
  const raw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
@@ -106,11 +113,17 @@ export async function generateKeyShare(group, { generateKeyPair } = {}) {
106
113
  * so it is checked here first.
107
114
  *
108
115
  * @param {number} group
109
- * @param {CryptoKey} privateKey our ephemeral private key for the group
116
+ * @param {CryptoKey | import('./hybrid.js').HybridPrivate} privateKey our ephemeral private key
117
+ * for the group (a compound value for the ML-KEM hybrid)
110
118
  * @param {Uint8Array} peerKey the server's raw public key from its key_share
119
+ * @param {import('./connect.js').TlsDeps} [deps] carries the injected ML-KEM implementation the
120
+ * hybrid group needs; unused by the classical groups
111
121
  * @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
112
122
  */
113
- export async function deriveSharedSecret(group, privateKey, peerKey) {
123
+ export async function deriveSharedSecret(group, privateKey, peerKey, deps = {}) {
124
+ if (group === HYBRID_GROUP) {
125
+ return deriveHybridSecret(deps.kem?.x25519mlkem768, privateKey, peerKey);
126
+ }
114
127
  const params = requireSupportedGroup(group, 'ServerHello');
115
128
  if (peerKey.byteLength !== params.publicLen) {
116
129
  throw new TlsError(
@@ -355,7 +355,7 @@ export async function continueTls13(ctx) {
355
355
  // Only psk_dhe_ke is ever offered (s4.2.9), so a key exchange happens whether or not the PSK
356
356
  // was taken; selectServerKeyShare fails closed on a ServerHello without key_share.
357
357
  const server = selectServerKeyShare(sh, keyShares);
358
- const shared = await deriveSharedSecret(server.group, server.privateKey, server.keyExchange);
358
+ const shared = await deriveSharedSecret(server.group, server.privateKey, server.keyExchange, deps);
359
359
 
360
360
  // s7.1: with a PSK in use the Early Secret is extracted from it; otherwise from zeros. The
361
361
  // accepted offer already carries that extraction (connect.js derived it for the binder), and
@@ -0,0 +1,166 @@
1
+ // X25519MLKEM768 (group 0x11EC): the post-quantum hybrid key exchange of
2
+ // draft-kwiatkowski-tls-ecdhe-mlkem.
3
+ //
4
+ // This is a HYBRID: two independent key agreements run side by side and their secrets are
5
+ // concatenated, so the session is safe as long as EITHER holds — X25519 against a classical
6
+ // attacker, ML-KEM-768 against a future quantum one. ML-KEM is not a WebCrypto primitive on this
7
+ // runtime, so its keygen/encapsulate/decapsulate are INJECTED (the same discipline ChaCha20 gets
8
+ // in aead.js): without an implementation the group is never offered, and a ClientHello is an offer
9
+ // a server may take. The X25519 half is plain WebCrypto, exactly as x25519 alone is elsewhere.
10
+ //
11
+ // The three orderings below are the whole subtlety of this file, and each is a silent trap: a
12
+ // reversed concatenation does not throw, it produces a shared secret that merely DIFFERS, so the
13
+ // handshake fails only later, at a Finished that will not verify — indistinguishable from a server
14
+ // bug. draft-kwiatkowski-tls-ecdhe-mlkem fixes them, and they were confirmed against a real
15
+ // X25519MLKEM768 server (Cloudflare) by completing a handshake, not read off the page:
16
+ //
17
+ // * client key_share = ML-KEM-768 encapsulation key (1184) || X25519 public key (32) = 1216
18
+ // * server key_share = ML-KEM-768 ciphertext (1088) || X25519 public key (32) = 1120
19
+ // * shared secret = ML-KEM-768 shared secret (32) || X25519 shared secret (32) = 64
20
+ //
21
+ // ML-KEM comes FIRST in all three. (This is the opposite of the NIST-curve hybrids such as
22
+ // SecP256r1MLKEM768, where the classical share leads — which is exactly why it must be verified
23
+ // per group rather than assumed.) ML-KEM decapsulation uses FIPS 203 implicit rejection: a bad
24
+ // ciphertext yields a pseudorandom secret rather than an error, so a mismatch here can ONLY show
25
+ // up as a bad Finished, never as an exception. That is by design and is why offline agreement is
26
+ // necessary but not sufficient — see the live test.
27
+
28
+ import { TlsError, codes, hex16 } from '../errors.js';
29
+ import { concat } from '../util/bytes.js';
30
+ import { GROUP, GROUP_PARAMS } from './constants.js';
31
+
32
+ export const HYBRID_GROUP = GROUP.x25519mlkem768;
33
+ const P = GROUP_PARAMS[HYBRID_GROUP];
34
+
35
+ const X25519_ALG = { name: 'X25519' };
36
+
37
+ /**
38
+ * An injected ML-KEM-768 implementation (FIPS 203). Shapes match `wasmcrypto`'s `mlkem768`:
39
+ * @typedef {object} MlKem768
40
+ * @property {(seed?: Uint8Array) => { publicKey: Uint8Array, secretKey: Uint8Array }} keygen
41
+ * ML-KEM.KeyGen; `publicKey` is the 1184-byte encapsulation key, `secretKey` the 2400-byte
42
+ * decapsulation key.
43
+ * @property {(publicKey: Uint8Array) => { cipherText: Uint8Array, sharedSecret: Uint8Array }}
44
+ * encapsulate ML-KEM.Encaps; used by a server, exercised by the offline test server.
45
+ * @property {(cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array} decapsulate
46
+ * ML-KEM.Decaps; returns the 32-byte shared secret (implicit rejection on a bad ciphertext).
47
+ */
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
+ * @typedef {object} HybridPrivate
54
+ * @property {Uint8Array} mlkemSecretKey the ML-KEM decapsulation key
55
+ * @property {CryptoKey} classicalPrivateKey the X25519 private key
56
+ */
57
+
58
+ function checkLen(actual, want, what) {
59
+ if (actual !== want) {
60
+ throw new TlsError(
61
+ codes.TLS_HANDSHAKE,
62
+ `X25519MLKEM768 ${what} is ${actual} bytes, expected ${want}`,
63
+ { group: HYBRID_GROUP, got: actual, expected: want },
64
+ );
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Generate a client key share for X25519MLKEM768.
70
+ *
71
+ * @param {MlKem768} kem the injected ML-KEM-768 implementation
72
+ * @param {import('./connect.js').TlsDeps} [deps] `generateKeyPair` is honoured for the X25519
73
+ * half so a recorded handshake can be replayed with a fixed classical key
74
+ * @returns {Promise<{ group: number, keyExchange: Uint8Array, privateKey: HybridPrivate }>}
75
+ * `keyExchange` is the 1216-byte wire share (ML-KEM encapsulation key || X25519 public key)
76
+ */
77
+ export async function generateHybridKeyShare(kem, { generateKeyPair } = {}) {
78
+ requireKem(kem);
79
+ // ML-KEM keypair. The encapsulation key goes on the wire; the decapsulation key is kept.
80
+ const mlkem = kem.keygen();
81
+ checkLen(mlkem.publicKey?.byteLength, P.mlkemPublicLen, 'ML-KEM encapsulation key');
82
+ checkLen(mlkem.secretKey?.byteLength, P.mlkemSecretKeyLen, 'ML-KEM decapsulation key');
83
+
84
+ // X25519 keypair, via WebCrypto exactly as the standalone x25519 group does.
85
+ const gen =
86
+ generateKeyPair ??
87
+ ((algorithm) => crypto.subtle.generateKey(algorithm, false, ['deriveBits']));
88
+ const pair = await gen(X25519_ALG, GROUP.x25519);
89
+ const classicalPublic = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
90
+ checkLen(classicalPublic.byteLength, P.classicalPublicLen, 'X25519 public key');
91
+
92
+ // ML-KEM FIRST, then X25519 — the client-share ordering. 1184 + 32 = 1216.
93
+ const keyExchange = concat([mlkem.publicKey, classicalPublic]);
94
+ return {
95
+ group: HYBRID_GROUP,
96
+ keyExchange,
97
+ privateKey: { mlkemSecretKey: mlkem.secretKey, classicalPrivateKey: pair.privateKey },
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Derive the 64-byte hybrid shared secret from the server's X25519MLKEM768 key share.
103
+ *
104
+ * @param {MlKem768} kem the injected ML-KEM-768 implementation
105
+ * @param {HybridPrivate} privateKey what generateHybridKeyShare kept
106
+ * @param {Uint8Array} serverShare the server's 1120-byte key_exchange (ciphertext || X25519 pub)
107
+ * @returns {Promise<Uint8Array>} ML-KEM shared secret (32) || X25519 shared secret (32) = 64
108
+ */
109
+ export async function deriveHybridSecret(kem, privateKey, serverShare) {
110
+ requireKem(kem);
111
+ if (!privateKey || !privateKey.mlkemSecretKey || !privateKey.classicalPrivateKey) {
112
+ throw new TlsError(codes.CONFIG_INVALID,
113
+ 'X25519MLKEM768 derivation needs the ML-KEM decapsulation key and the X25519 private key');
114
+ }
115
+ // The server share is ML-KEM ciphertext (1088) then X25519 public key (32). A wrong length here
116
+ // is the server contradicting the group it selected; fail closed rather than slice past the end.
117
+ checkLen(serverShare.byteLength, P.serverShareLen, 'server key_share');
118
+ const ciphertext = serverShare.subarray(0, P.mlkemCiphertextLen);
119
+ const classicalPublic = serverShare.subarray(P.mlkemCiphertextLen);
120
+
121
+ // ML-KEM decapsulation. Implicit rejection (FIPS 203) means a tampered ciphertext yields a
122
+ // pseudorandom secret, not an error — the mismatch can only surface at Finished, by design.
123
+ const mlkemSecret = kem.decapsulate(ciphertext, privateKey.mlkemSecretKey);
124
+ checkLen(mlkemSecret?.byteLength, P.classicalSecretLen, 'ML-KEM shared secret');
125
+
126
+ // X25519 agreement, with the same small-order guard the standalone x25519 path applies.
127
+ let classicalPeer;
128
+ try {
129
+ classicalPeer = await crypto.subtle.importKey('raw', classicalPublic, X25519_ALG, false, []);
130
+ } catch (cause) {
131
+ throw new TlsError(codes.TLS_HANDSHAKE,
132
+ `X25519MLKEM768 server X25519 share is not a valid public key: ${cause?.message}`,
133
+ { group: HYBRID_GROUP });
134
+ }
135
+ let classicalSecret;
136
+ try {
137
+ classicalSecret = new Uint8Array(
138
+ await crypto.subtle.deriveBits({ ...X25519_ALG, public: classicalPeer },
139
+ privateKey.classicalPrivateKey, P.classicalSecretLen * 8));
140
+ } catch (cause) {
141
+ throw new TlsError(codes.TLS_HANDSHAKE,
142
+ `X25519MLKEM768 X25519 agreement failed: ${cause?.message ?? cause}. This usually means a ` +
143
+ 'degenerate or small-order server key.', { group: HYBRID_GROUP });
144
+ }
145
+ // RFC 7748 s6.1: an all-zero X25519 output is a small-order peer key. Reject, as x25519 does.
146
+ if (classicalSecret.every((b) => b === 0)) {
147
+ throw new TlsError(codes.TLS_HANDSHAKE,
148
+ 'X25519MLKEM768 X25519 shared secret is all zeroes, indicating a small-order server key');
149
+ }
150
+
151
+ // ML-KEM FIRST, then X25519 — the shared-secret ordering. 32 + 32 = 64.
152
+ return concat([mlkemSecret, classicalSecret]);
153
+ }
154
+
155
+ /** @param {MlKem768} kem */
156
+ function requireKem(kem) {
157
+ if (typeof kem?.keygen !== 'function' || typeof kem?.decapsulate !== 'function') {
158
+ throw new TlsError(
159
+ codes.CONFIG_INVALID,
160
+ `${hex16(HYBRID_GROUP)} (x25519mlkem768) was reached but no ML-KEM-768 implementation was ` +
161
+ 'supplied; pass one as `groups: { x25519mlkem768: impl }` with keygen()/encapsulate()/' +
162
+ 'decapsulate()',
163
+ { group: HYBRID_GROUP },
164
+ );
165
+ }
166
+ }
package/src/tls/record.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  import { TlsError, TlsUnsupportedError, codes, hex8, hex16 } from '../errors.js';
21
21
  import {
22
22
  RECORD_TYPE, HANDSHAKE_TYPE, ALERT_DESC, ALERT_LEVEL, MAX_PLAINTEXT, MAX_CIPHERTEXT,
23
- LEGACY_VERSION, TLS12, TLS13, CIPHER_PARAMS,
23
+ LEGACY_VERSION, TLS12, TLS13, CIPHER, CIPHER_PARAMS,
24
24
  } from './constants.js';
25
25
  import { createAead } from './aead.js';
26
26
  import { trafficKeys, nextTrafficSecret } from './keyschedule.js';
@@ -106,6 +106,10 @@ async function withGrace(promise, ms) {
106
106
  * record, TLS 1.3 only
107
107
  * @property {null | ((msg: HandshakeMessage) => void | Promise<void>)} [onPostHandshake]
108
108
  * NewSessionTicket consumer; default is to discard
109
+ * @property {null | { chacha20?: import('./aead.js').AeadOptions['impl'] }} [aeadImpls] injected
110
+ * AEAD implementations by name. ChaCha20-Poly1305 has no WebCrypto path on this runtime, so its
111
+ * seal/open are supplied here and threaded into every createAead for the ChaCha20 suite (initial
112
+ * keys and each KeyUpdate rotation). Absent for the AES-GCM-only default, which needs nothing.
109
113
  */
110
114
 
111
115
  export class RecordLayer {
@@ -122,6 +126,9 @@ export class RecordLayer {
122
126
  this._shutdownGraceMs = opts.shutdownGraceMs ?? 2000;
123
127
  this._padding = opts.padding ?? null;
124
128
  this._onPostHandshake = opts.onPostHandshake ?? null;
129
+ // Injected AEAD implementations (ChaCha20-Poly1305). Threaded into every createAead so a
130
+ // KeyUpdate rotation reaches for the same implementation the initial keys used.
131
+ this._aeadImpls = opts.aeadImpls ?? null;
125
132
 
126
133
  this._version = TLS13; // semantics selector; setVersion() pins it when negotiated
127
134
  /** @type {DirectionState} */
@@ -250,10 +257,22 @@ export class RecordLayer {
250
257
  } else if (!key || !iv) {
251
258
  throw new TlsError(codes.CONFIG_INVALID, 'keys need either a traffic secret or key+iv');
252
259
  }
253
- const aead = await createAead({ version: this._version, cipher, key, iv });
260
+ const aead = await createAead({
261
+ version: this._version, cipher, key, iv, impl: this._implFor(cipher),
262
+ });
254
263
  return { aead, seq: 0n, cipher, hash: params.hash, secret: secret ?? null };
255
264
  }
256
265
 
266
+ /**
267
+ * The injected AEAD implementation a suite needs, or null. Only ChaCha20-Poly1305 needs one on
268
+ * this runtime; every AES-GCM suite goes through WebCrypto and passes null.
269
+ * @param {number} cipher
270
+ * @returns {import('./aead.js').AeadOptions['impl'] | null}
271
+ */
272
+ _implFor(cipher) {
273
+ return cipher === CIPHER.TLS_CHACHA20_POLY1305_SHA256 ? (this._aeadImpls?.chacha20 ?? null) : null;
274
+ }
275
+
257
276
  // ------------------------------------------------------------------ read side
258
277
 
259
278
  /**
@@ -840,7 +859,9 @@ export class RecordLayer {
840
859
  const secret = await nextTrafficSecret(s.hash, s.secret);
841
860
  const params = CIPHER_PARAMS[s.cipher];
842
861
  const { key, iv } = await trafficKeys(params.hash, secret, params.keyLen, params.ivLen);
843
- const aead = await createAead({ version: this._version, cipher: s.cipher, key, iv });
862
+ const aead = await createAead({
863
+ version: this._version, cipher: s.cipher, key, iv, impl: this._implFor(s.cipher),
864
+ });
844
865
  this._send = { aead, seq: 0n, cipher: s.cipher, hash: s.hash, secret };
845
866
  }
846
867
 
@@ -18,61 +18,62 @@ 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
- "MC4CAQAwBQYDK2VuBCIEINivgNQO5n8jwnzWfEWjdR14q7Km+UhPnqyL3RtkDbF7";
21
+ "MC4CAQAwBQYDK2VuBCIEIGD2kIBgmWlTqN+qzpUvC5PMClhKiudEWslvOrbnG7lq";
22
22
  const CLIENT_PUB =
23
- "o5wNrq9yxfTLf8U3jLhUsXB52Kmf2q3zodyy/4jELSM=";
23
+ "c4dk24oOgG+LLBpm0e90sFXS+UhcTLZpEIrCDhTPgCo=";
24
24
  const CLIENT_RANDOM =
25
25
  "AwoRGB8mLTQ7QklQV15lbHN6gYiPlp2kq7K5wMfO1dw=";
26
26
  const SESSION_ID =
27
27
  "BRAbJjE8R1JdaHN+iZSfqrXAy9bh7PcCDRgjLjlET1o=";
28
28
  const CLIENT_HELLO =
29
- "AQAA9AMDAwoRGB8mLTQ7QklQV15lbHN6gYiPlp2kq7K5wMfO1dwgBRAbJjE8R1JdaHN+iZSfqrXAy9bh7PcCDRgjLjlET1oADBMB" +
30
- "EwLAK8AvwCzAMAEAAJ//AQABAAAAABMAEQAADndhcm11cC5pbnZhbGlkAAUABQEAAAAAAAsAAgEAAAoACgAIAB0AFwAYABkAEAAL" +
31
- "AAkIaHR0cC8xLjEAFwAAAA0AFgAUBAMFAwYDCAQIBQgGCAcEAQUBBgEAKwAFBAMEAwMALQACAQEAMwAmACQAHQAgo5wNrq9yxfTL" +
32
- "f8U3jLhUsXB52Kmf2q3zodyy/4jELSM=";
29
+ "AQAA9AMDAwoRGB8mLTQ7QklQV15lbHN6gYiPlp2kq7K5wMfO1dwgBRAbJjE8R1JdaHN+iZSfqrXAy9bh7PcCDRgjLjlET1oADBMC" +
30
+ "EwHALMAwwCvALwEAAJ//AQABAAAAABMAEQAADndhcm11cC5pbnZhbGlkAAUABQEAAAAAAAsAAgEAAAoACgAIAB0AFwAYABkAEAAL" +
31
+ "AAkIaHR0cC8xLjEAFwAAAA0AFgAUBAMFAwYDCAQIBQgGCAcEAQUBBgEAKwAFBAMEAwMALQACAQEAMwAmACQAHQAgc4dk24oOgG+L" +
32
+ "LBpm0e90sFXS+UhcTLZpEIrCDhTPgCo=";
33
33
  const SERVER_BYTES =
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";
34
+ "FgMBAHoCAAB2AwOd1LxJZeGnlrBrfh+XURLQ0+oy/nHBoHi4f2te1UCsgiAFEBsmMTxHUl1oc36JlJ+qtcDL1uHs9wINGCMuOURP" +
35
+ "WhMCAAAuACsAAgMEADMAJAAdACA/u21n72qimI15SHG70pBa5bDie2NJ5jJ3U9mc6quvbhcDAwdo1lcruKbvqnFuYLiYAcl82akL" +
36
+ "xNhbdsBzqLb0/hxNrpwSyceH0iseW8tCkUmLDZSikWT3FIfTy5gxHhzw9jCF20wVsupTSDu1vhVDHVCae5vl4cBbCpAIGP1DfMkG" +
37
+ "fHy2EqaX9Hvhs/f0IxymKE+phAWvFtG8CWhC+jKkOemjTGeYu7Zz92+xZrjqZvItnNzogFojQFYYAiRkNHkWsCvUmwHAMKOIY3H3" +
38
+ "2ov3zLxtAoFB9GwfOOTe4q0h+JBkTzXdc45AcMVZPyjRIGr/2/XDt5F2NZZY53DZAB9Vba8k1IUUo/YgInGXHULRZaootpQzALAP" +
39
+ "2B29d4DezKxA8HaVw3QQsJ8YKGXRoXGsac1/4MwGjj7gUkzwqXHSeNreoBeGkdu6eCLYKqp7ideGeibQ8zIPNjFSUijOnf8kKmsI" +
40
+ "AjkwLoz0W8NFGaw2ZN7vSABDHAqxiP2hVunU5ppnqD9jWtC5ef1rzOw5UYg6YVWZwywR3p04z4F8FZtqNSuYwgnMXN4M54lGPz8Y" +
41
+ "Vw1NE5sCFDKutVJTAnwiae0L8JoFnfVgdV8f4SInPiH3DWLrTmZK2IL0bzUhfB1OGtXdPJ59ZHR8wh3rU1Gcbew3CkzuOa4YX9YC" +
42
+ "kcBhZP2NXGUANcwX1rhVXUCj3jz3z0zn8fIHYqhtFRkAlk0BNr/VPoCnIZZPinARp2CDi9eiUA43zpAXlbwxddmwGTmJSFax2PYs" +
43
+ "OvtpREy/sYGZrO5Yi33NQby+kYkXOxjhnC5KGLKE3O0SA7KbYBzWbWuw15Am0dtizFBam3aZHQzw9IC5MX9p7ENabhSUMjd7QZUV" +
44
+ "fjQRV+ZOndb/vdJG3et7WUa8NzeJ7Gwb+2pzDBj7eu54mtoHmjxu9svI+LEfCc22M7KNydkkNGDlcqJVETx/yS4XZLw2RviZadt3" +
45
+ "mU/9Lzs9Avz+c6+DuMcz0WPqUCuFuD6unOd5c2LikO84RWtfBobEuZwwZhLGM3VwYWdh61wXotjid6aywKl54yM+QvUonvVs9533" +
46
+ "6BG1MRqB7+MuxXBBDOGofZ0ba+3FcF/f9bbbuBBwntU3PlsOzZ5gTeBw/I28MQjOYe4ZfgM3ljTN1BSXokZSC+noYFS73r1cW+oC" +
47
+ "MunCecMP/Vy1AIGWLs87li9FbQBgzF+l1U4bOgE22vCPnC3UC7AH8sGGyPGW5+02ZhPOFOpvh4kp92PbCV5aAF88ZXVEfH5VQmWn" +
48
+ "uR8TLjP6e70NYhnQOWcEQJSCYG85TnsDiRz2/GcQJqDBC/g/Yv7JQhIBicoXhiJyswx9lxrKDsGoxOurY3sJRvgUpvj40EUrTsk8" +
49
+ "6J18td6JJQTnMPiYhSXudXZej0fkcm73DtwX6nEXq05mwwuKiZLSc2fyldVNb7RNyqjnTQLjVCQxtv9Y2Iyl/BSYN4C0dRmAoQur" +
50
+ "mpDtE626fJpP1EBzNmsDlEJoBvPOEVvBEhozdqd8bxkj5p4ePP87nS6diSARiEdmLcSbaOPi6CTckQfnjLfP+Ltws8kC8n2j6lOe" +
51
+ "6MkkRU5vnZzSILVC7pLIDawz4rUfGoVQGU0qWEIFDJ/uHriaq0G7yA3WTUU8Oko8Qxnxc+idtJ/wnTclNH78PkDLPodFb0obT8dh" +
52
+ "m6EkWttPqKty/fMHBUr1afRijHxpO1z6Affz7GMCyJgH8w8lw0R0HDAQTl2+xWXTygGVaKXO4ySqoU2FxANurjp2DpyjUmfDMHOq" +
53
+ "7Ac2rCnSrGbgE7OcPJ00CPqd/6w9EYqt0G3FZaIIIK9n9Tz6WT+8Qh4QtVzyG/6r/WgP6paI4yR9KO1AVr0/UGadjzuk0GXqm2mT" +
54
+ "XiJPpP6YzDkJ1wJCmZ3wHwy2F9SXbv+xLRcYwcTQY+YNxujdiZ+3ZljUULgrIcD3E/RzL9wEYw/fGIYnPqhyJeA3ws73H796u/MX" +
55
+ "D6fwPNL1ImUqJA5dOUMt3RwpdZl8zTmnoDy/T6mpq5agtVAfvJORj7zILPfYohXy2oDTQfuAAqsF0osSm7j4R5LRrzBoFe9FJ6hY" +
56
+ "hiWlYRzObNuDeKX1js6IFzlDBjBaoXJ6S3xO9TED0gfT6aZP109fPSFwYAzitMpSdEKTQlHAavXxppuaztd4Rw+ia6rgEa3dxB3c" +
57
+ "+tZlBeBnmozz2DMenp28CYsPyCfrt9Yv6On8AWbOezmJnpAziJhJrJD3jIKAp02txekEz3eUTNrusCzD+QEBZxZoiq3rr7bY+Wao" +
58
+ "fAbquyVSYB1QurNbLy+LqPFKzZWzrZhcivsRO1ZFjw/wcw/Oeqci570vppJ67QQ1irbEHNb7N8cWIJNAtbYpXRQf5/sp+B1kDdvM" +
59
+ "e1j9sig2JbHPp/7TI7a58ry8Z77+5ivgDSsM3uFmUwbs0/KB9foP2QC4a+pTyN1RXdLnuNkRgWTB6tG05RrCYZfTbTjzfFDWgtzu" +
60
+ "6fJgvActBuvvFjfHx/N3QG2xg5z+txNic7rDJorfBL5d8xK47T+KklNRS6CJws9hBgVjD95kZKU3+o4cNFQE9uZF88yAKSyIk0R2" +
61
+ "bgPUFwMDARkp6obVqB0nPnEgl4GA4oIJuCNzzB7pza1qKxqIqLjVs6ZhzOwPyhyIh1W1nscTrxfpJOOmn9yuOji40yBPz0UGUwqL" +
62
+ "rhWdkK21FpvKx4eoMh/2B1S3XbTA83EIhnQxdwjyutUt+Zf/LCIKC8Q7kCnxqybPek6mxyI9JU6be/I+IV+qsECppD5T73aGmUpa" +
63
+ "5vfNuso40YuTX7a6gUrUPdXluIB4godoeyS9q1AOvgf7hWVIMp/bNl5DOTW/7Lcb/mdlqGMUepXScrEDyTGNyQ/P6R3bOfmpBm5Q" +
64
+ "ONGg9gKm7psWT/U/aK9XQNW+jpqGrBIx9Ws2rbpihOFmzhiPPMhILSLj3rvMyNZoHN8Xv+IhHNqpsCcwSxvg7BcDAwATS/g1rD6Z" +
65
+ "kAgj5tcHEb0GRYv7RQ==";
65
66
  const ROOT_DER =
66
67
  "MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNXYXJtdXAgRml4dHVyZSBSb290MB4XDTI1MDEwMTAw" +
67
68
  "MDAwMFoXDTM1MDEwMTAwMDAwMFowHjEcMBoGA1UEAwwTV2FybXVwIEZpeHR1cmUgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEP" +
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=";
69
+ "ADCCAQoCggEBAIRQ0+WIza3mrmr2wDgG0u3kvBYYFp23u+AcofQDWrUKZj/6uOOq2Ur2rZF3cSSkjedqouNvtFfKTA8kUD4IHaM/" +
70
+ "rDjUNBI7ITsyRHieixi39qddXpTgb9+WInm0pyIvAgCLZlyg33AG7y29mEfPTJYAcWaSYRLQwu+/dMAshxC16i3zSU5HleZntE9F" +
71
+ "VJbYR5QGIS6Y5MzU+1hSlXxSbl5boQXTJql1OdWt4t8MU/7voXJhZ6mdGXXiPQXdSrc8txKUJLqd6UgP5AnIhbWhHkfg0MpAXm0Y" +
72
+ "5rrodnclqTNI5ibDDt+ylA9vk5AiCH+mCqPtNd7mERMsP2rpBvRM/EsCAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B" +
73
+ "Af8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAHjQ1bWdwKNQpAefj7nyNX7KHdvfNK7TRpAobtunyuEQE6Ia4d1CIOGNHAX87UM2" +
74
+ "4otia7hM4S2oysqvW+HWpMR4cCe2TDofg/nvWo9SOJCsKRcn5caeB3AL0LG7eh97tDoMiytI1xQ2RN/ZajxQ8Fc9WdUjIosiOc5l" +
75
+ "IQFg6pBvzsAhwC2hFuspk31ay/mwE6t5pP+68ZJELqwjBiN6M+vGjIoQ/29tMsgnOaPCXB5vp4gaZpaisu1QiZEG95O7owrJ49+u" +
76
+ "h6JTXWf2j7qxiPqdvqYUGjqZSwKRwZGMYWwIRub/tSdRaRiBpV7QXjarJ8IK9yTAOwe/2fHAX5VbOI0=";
76
77
 
77
78
  export const WARMUP_FIXTURE = {
78
79
  clientPrivPkcs8: () => B(CLIENT_PRIV),
package/types/client.d.ts CHANGED
@@ -64,6 +64,15 @@ export function install(options?: ClientOptions): () => void;
64
64
  * yours and visible. Measured on the edge: WASM brotli decodes at about 2x native gzip, and
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
+ * @property {{ chacha20?: import('./tls/aead.js').AeadOptions['impl'] }} [ciphers] injected AEAD
68
+ * implementations, by capability name. `chacha20` (seal/open, RFC 8439) is what lets
69
+ * TLS_CHACHA20_POLY1305_SHA256 be offered and performed — WebCrypto has no ChaCha20 on this
70
+ * runtime, and a suite offered but not performable is a dead connection if a server selects it,
71
+ * so without this the suite stays out of the ClientHello. Required by `profiles.chrome`.
72
+ * @property {{ x25519mlkem768?: import('./tls/hybrid.js').MlKem768 }} [groups] injected key-exchange
73
+ * implementations, by capability name. `x25519mlkem768` (ML-KEM-768 keygen/encapsulate/
74
+ * decapsulate) is what lets the post-quantum hybrid group be offered and performed; without it
75
+ * the group stays out of the ClientHello. Required by `profiles.chrome`.
67
76
  * @property {boolean} [keepAlive] default true.
68
77
  * @property {import('./profiles.js').FingerprintProfile} [profile] one coherent network identity
69
78
  * instead of a dozen knobs that can disagree — TLS, HTTP/2, header order and default headers
@@ -237,6 +246,25 @@ export type ClientOptions = {
237
246
  * matching a browser's Accept-Encoding, not saving CPU.
238
247
  */
239
248
  decoders?: Record<string, import("./client/decode.js").BodyDecoder> | undefined;
249
+ /**
250
+ * injected AEAD
251
+ * implementations, by capability name. `chacha20` (seal/open, RFC 8439) is what lets
252
+ * TLS_CHACHA20_POLY1305_SHA256 be offered and performed — WebCrypto has no ChaCha20 on this
253
+ * runtime, and a suite offered but not performable is a dead connection if a server selects it,
254
+ * so without this the suite stays out of the ClientHello. Required by `profiles.chrome`.
255
+ */
256
+ ciphers?: {
257
+ chacha20?: import("./tls/aead.js").AeadOptions["impl"];
258
+ } | undefined;
259
+ /**
260
+ * injected key-exchange
261
+ * implementations, by capability name. `x25519mlkem768` (ML-KEM-768 keygen/encapsulate/
262
+ * decapsulate) is what lets the post-quantum hybrid group be offered and performed; without it
263
+ * the group stays out of the ClientHello. Required by `profiles.chrome`.
264
+ */
265
+ groups?: {
266
+ x25519mlkem768?: import("./tls/hybrid.js").MlKem768;
267
+ } | undefined;
240
268
  /**
241
269
  * default true.
242
270
  */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `profiles.chrome` with the two capabilities this package cannot perform natively already wired
3
+ * in. Everything else about it — the cipher list, the groups, the shuffled extension order, GREASE,
4
+ * the HTTP/2 SETTINGS and pseudo-header order, the request header order — comes from the
5
+ * declaration unchanged, and all of it was captured off the wire from Chrome 150.
6
+ *
7
+ * @type {import('../profiles.js').FingerprintProfile & {
8
+ * ciphers: Record<string, unknown>, groups: Record<string, unknown> }}
9
+ */
10
+ export const chrome: import("../profiles.js").FingerprintProfile & {
11
+ ciphers: Record<string, unknown>;
12
+ groups: Record<string, unknown>;
13
+ };
14
+ import { chacha20poly1305 } from './vendor/chacha20poly1305.js';
15
+ import { mlkem768 } from './vendor/mlkem768.js';
16
+ export { chacha20poly1305, mlkem768 };
@@ -0,0 +1,7 @@
1
+ export const buildId: "ec15f83a678a";
2
+ export namespace chacha20poly1305 {
3
+ /** seal(key32, nonce12, plaintext, aad?) -> ciphertext||tag (RFC 8439 AEAD) */
4
+ function seal(key: any, nonce: any, plaintext: any, aad?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
5
+ /** open(key32, nonce12, ciphertextAndTag, aad?) -> plaintext; throws on auth failure */
6
+ function open(key: any, nonce: any, ciphertext: any, aad?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
7
+ }
@@ -0,0 +1,23 @@
1
+ export const buildId: "e17c38843d1e";
2
+ export namespace mlkem768 {
3
+ export { PK_B as publicKeyBytes };
4
+ export { SK_B as secretKeyBytes };
5
+ export { CT_B as cipherTextBytes };
6
+ export let sharedSecretBytes: number;
7
+ /** keygen(seed?) — seed is d||z (64 bytes) for ML-KEM.KeyGen_internal; omitted = fresh CSPRNG */
8
+ export function keygen(seed: any): {
9
+ publicKey: Uint8Array<ArrayBuffer>;
10
+ secretKey: Uint8Array<ArrayBuffer>;
11
+ };
12
+ /** encapsulate(publicKey, msg?) — msg is m (32 bytes) for ML-KEM.Encaps_internal */
13
+ export function encapsulate(publicKey: any, msg: any): {
14
+ cipherText: Uint8Array<ArrayBuffer>;
15
+ sharedSecret: Uint8Array<ArrayBuffer>;
16
+ };
17
+ /** decapsulate(cipherText, secretKey) -> sharedSecret (implicit rejection per FIPS 203) */
18
+ export function decapsulate(cipherText: any, secretKey: any): Uint8Array<ArrayBuffer>;
19
+ }
20
+ declare const PK_B: any;
21
+ declare const SK_B: any;
22
+ declare const CT_B: any;
23
+ export {};
@@ -59,17 +59,21 @@ export const curl: Readonly<{
59
59
  * `applyProfile` refuses both cases with a message naming what is missing.
60
60
  */
61
61
  export const chrome: Readonly<{
62
- name: "chromium (TLS layer only)";
62
+ name: "chrome/150";
63
63
  tls: Readonly<{
64
64
  ciphers: readonly number[];
65
65
  groups: readonly number[];
66
+ offerGroups: readonly number[];
66
67
  sigSchemes: readonly number[];
67
68
  alpn: string[];
68
69
  extensionOrder: "shuffle";
69
70
  grease: true;
70
71
  }>;
72
+ http2Settings: readonly number[][];
73
+ http2ConnectionWindow: number;
74
+ http2PseudoHeaderOrder: readonly string[];
75
+ headerOrder: readonly string[];
71
76
  headers: readonly string[][];
72
- http2Settings: null;
73
77
  requires: readonly string[];
74
78
  }>;
75
79
  /** @type {Record<string, FingerprintProfile>} */
@@ -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
  };
@@ -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.