@noble/post-quantum 0.5.4 → 0.6.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/src/hybrid.ts CHANGED
@@ -50,7 +50,8 @@
50
50
  *
51
51
  * - GPG:
52
52
  * • Concatenate keys.
53
- * • Combiner: SHA3-256(kemShare || ecdhShare || ciphertext || pubKey || algId || domSep || len(domSep))
53
+ * • Combiner:
54
+ * SHA3-256(kemShare || ecdhShare || ciphertext || pubKey || algId || domSep || len(domSep))
54
55
  *
55
56
  * - TLS:
56
57
  * • Transcript-based derivation (HKDF).
@@ -92,11 +93,16 @@ import { abytes, ahash, anumber, type CHash, type CHashXOF } from '@noble/hashes
92
93
  import { ml_kem1024, ml_kem768 } from './ml-kem.ts';
93
94
  import {
94
95
  cleanBytes,
96
+ copyBytes,
95
97
  randomBytes,
96
98
  splitCoder,
99
+ validateSigOpts,
100
+ validateVerOpts,
97
101
  type CryptoKeys,
98
102
  type KEM,
99
103
  type Signer,
104
+ type TArg,
105
+ type TRet,
100
106
  } from './utils.ts';
101
107
 
102
108
  type CurveAll = ECDSA | EdDSA | MontgomeryECDH;
@@ -108,54 +114,151 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
108
114
  const lengths = curve.lengths;
109
115
  let keygen = curve.keygen;
110
116
  if (allowZeroKey) {
117
+ // Only the ECDSA/Weierstrass branch uses raw scalar-byte secret keys here. Edwards seeds are
118
+ // hashed/pruned and Montgomery keys are clamped byte strings, so forcing Point.Fn semantics on
119
+ // those curves would change key construction instead of just relaxing scalar range handling.
120
+ if (!('getSharedSecret' in curve && 'sign' in curve && 'verify' in curve))
121
+ throw new Error('allowZeroKey requires a Weierstrass curve');
122
+ // This legacy flag is really "skip the +1 shift" for vector matching, not "accept scalar 0".
123
+ // It swaps seeded Weierstrass keygen from reduction into [1, ORDER) to direct reduction into
124
+ // [0, ORDER), which preserves exact reduced bytes but still leaves scalar 0 invalid.
111
125
  // This is ugly, but we need to return exact results here.
112
- const wCurve = curve as typeof p256;
126
+ const wCurve = curve as ECDSA;
113
127
  const Fn = wCurve.Point.Fn;
114
- if (!Fn) throw new Error('No Point.Fn');
115
- keygen = (seed: Uint8Array = randomBytes(lengths.seed)) => {
128
+ // Unlike noble-curves' seeded Weierstrass keygen, this path removes the post-reduction +1.
129
+ // That is enough to match exact reduced-vector bytes, but an all-zero seed still reduces to
130
+ // scalar 0 here and getPublicKey(secretKey) throws instead of "allowing zero".
131
+ keygen = (seed: TArg<Uint8Array> = randomBytes(lengths.seed)) => {
116
132
  abytes(seed, lengths.seed!, 'seed');
117
133
  const seedScalar = Fn.isLE ? bytesToNumberLE(seed) : bytesToNumberBE(seed);
118
- const secretKey = Fn.toBytes(Fn.create(seedScalar)); // Fixes modulo bias, but not zero
119
- return { secretKey, publicKey: curve.getPublicKey(secretKey) };
134
+ // Reduce directly into [0, ORDER); scalar 0 still stays invalid.
135
+ const secretKey = Fn.toBytes(Fn.create(seedScalar));
136
+ return {
137
+ secretKey: secretKey as TRet<Uint8Array>,
138
+ publicKey: curve.getPublicKey(secretKey) as TRet<Uint8Array>,
139
+ };
120
140
  };
121
141
  }
122
142
  return {
123
143
  lengths: { secretKey: lengths.secretKey, publicKey: lengths.publicKey, seed: lengths.seed },
124
- keygen,
125
- getPublicKey: (secretKey: Uint8Array) => curve.getPublicKey(secretKey),
144
+ keygen: (seed?: TArg<Uint8Array>) =>
145
+ keygen(seed) as TRet<{
146
+ secretKey: Uint8Array;
147
+ publicKey: Uint8Array;
148
+ }>,
149
+ getPublicKey: (secretKey: TArg<Uint8Array>) =>
150
+ curve.getPublicKey(secretKey) as TRet<Uint8Array>,
126
151
  };
127
152
  }
128
153
 
129
- export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): KEM {
154
+ /**
155
+ * Wraps an ECDH-capable curve as a KEM.
156
+ * Shared secrets stay in the wrapped curve's raw ECDH byte format with no built-in KDF.
157
+ * On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
158
+ * 1-byte `0x02` / `0x03` prefix.
159
+ * The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
160
+ * @param curve - Curve with `getSharedSecret`.
161
+ * @param allowZeroKey - Legacy vector-matching toggle for Weierstrass keygen.
162
+ * On Weierstrass curves this removes the usual post-reduction `+1` shift, changing seeded scalar
163
+ * reduction from `[1, ORDER)` to direct reduction into `[0, ORDER)`. It does not make scalar zero
164
+ * valid: an all-zero seed still derives scalar `0` and throws in `curve.getPublicKey(...)`.
165
+ * Only supported on Weierstrass/ECDSA curves.
166
+ * @returns KEM wrapper over the curve.
167
+ * @throws If the curve does not expose `getSharedSecret`. {@link Error}
168
+ * @example
169
+ * Wrap an ECDH-capable curve as a generic KEM.
170
+ * ```ts
171
+ * import { x25519 } from '@noble/curves/ed25519.js';
172
+ * import { ecdhKem } from '@noble/post-quantum/hybrid.js';
173
+ * const kem = ecdhKem(x25519);
174
+ * const publicKeyLen = kem.lengths.publicKey;
175
+ * ```
176
+ */
177
+ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<KEM> {
130
178
  const kg = ecKeygen(curve, allowZeroKey);
131
179
  if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
132
180
  return {
133
181
  lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
134
182
  keygen: kg.keygen,
135
183
  getPublicKey: kg.getPublicKey,
136
- encapsulate(publicKey: Uint8Array, rand: Uint8Array = randomBytes(curve.lengths.seed)) {
137
- const ek = this.keygen(rand).secretKey;
138
- const sharedSecret = this.decapsulate(publicKey, ek);
139
- const cipherText = curve.getPublicKey(ek);
140
- cleanBytes(ek);
141
- return { sharedSecret, cipherText };
184
+ encapsulate(
185
+ publicKey: TArg<Uint8Array>,
186
+ rand: TArg<Uint8Array> = randomBytes(curve.lengths.seed)
187
+ ) {
188
+ // Some curve.keygen(seed) paths reuse the provided seed buffer as secretKey; detach caller
189
+ // randomness first so cleanBytes() only wipes wrapper-owned material.
190
+ const seed = copyBytes(rand);
191
+ let ek: Uint8Array | undefined = undefined;
192
+ try {
193
+ ek = this.keygen(seed).secretKey;
194
+ const sharedSecret = this.decapsulate(publicKey, ek);
195
+ const cipherText = curve.getPublicKey(ek) as TRet<Uint8Array>;
196
+ return { sharedSecret, cipherText };
197
+ } finally {
198
+ // Invalid peer public keys can make decapsulation throw; wipe both the detached seed and
199
+ // derived ephemeral secret key even when encapsulation aborts before returning.
200
+ cleanBytes(seed);
201
+ if (ek) cleanBytes(ek);
202
+ }
142
203
  },
143
- decapsulate(cipherText: Uint8Array, secretKey: Uint8Array) {
204
+ decapsulate(cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) {
144
205
  const res = curve.getSharedSecret(secretKey, cipherText);
145
- return curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res;
206
+ return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
146
207
  },
147
208
  };
148
209
  }
149
210
 
150
- export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): Signer {
211
+ /**
212
+ * Wraps a curve signer as a generic `Signer`.
213
+ * Signatures stay in the wrapped curve's native byte encoding.
214
+ * This wrapper does not normalize or document which per-curve signing options are meaningful.
215
+ * @param curve - Curve with `sign` and `verify`.
216
+ * @param allowZeroKey - Legacy vector-matching toggle for Weierstrass keygen.
217
+ * On Weierstrass curves this removes the usual post-reduction `+1` shift, changing seeded scalar
218
+ * reduction from `[1, ORDER)` to direct reduction into `[0, ORDER)`. It does not make scalar zero
219
+ * valid: an all-zero seed still derives scalar `0` and throws in `curve.getPublicKey(...)`.
220
+ * Only supported on Weierstrass/ECDSA curves.
221
+ * @returns Signer wrapper over the curve.
222
+ * @throws If the curve does not expose `sign` and `verify`. {@link Error}
223
+ * @example
224
+ * Wrap a curve signer as a generic signer.
225
+ * ```ts
226
+ * import { ed25519 } from '@noble/curves/ed25519.js';
227
+ * import { ecSigner } from '@noble/post-quantum/hybrid.js';
228
+ * const signer = ecSigner(ed25519);
229
+ * const sigLen = signer.lengths.signature;
230
+ * ```
231
+ */
232
+ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<Signer> {
151
233
  const kg = ecKeygen(curve, allowZeroKey);
152
234
  if (!curve.sign || !curve.verify) throw new Error('wrong curve'); // ed25519 doesn't have one!
153
235
  return {
154
236
  lengths: { ...kg.lengths, signature: curve.lengths.signature, signRand: 0 },
155
237
  keygen: kg.keygen,
156
238
  getPublicKey: kg.getPublicKey,
157
- sign: (message, secretKey) => curve.sign(message, secretKey),
158
- verify: (signature, message, publicKey) => curve.verify(signature, message, publicKey),
239
+ sign: (message, secretKey, opts = {}) => {
240
+ validateSigOpts(opts);
241
+ // This generic wrapper intentionally keeps the Signer contract to message + key only.
242
+ // Backend-specific knobs like ECDSA extraEntropy or Ed25519ctx context cannot be forwarded
243
+ // uniformly through combineSigners(), so callers that need them must use the curve directly.
244
+ if (opts.extraEntropy !== undefined)
245
+ throw new Error(
246
+ 'ecSigner does not support extraEntropy; use the underlying curve directly'
247
+ );
248
+ if (opts.context !== undefined)
249
+ throw new Error('ecSigner does not support context; use the underlying curve directly');
250
+ return curve.sign(message, secretKey) as TRet<Uint8Array>;
251
+ },
252
+ /** Verify one wrapped curve signature.
253
+ * Returns the wrapped curve's `verify()` result for well-formed inputs. Throws on unsupported
254
+ * generic opts and lets wrapped-curve malformed-input errors escape unchanged.
255
+ */
256
+ verify: (signature, message, publicKey, opts = {}) => {
257
+ validateVerOpts(opts);
258
+ if (opts.context !== undefined)
259
+ throw new Error('ecSigner does not support context; use the underlying curve directly');
260
+ return curve.verify(signature, message, publicKey);
261
+ },
159
262
  };
160
263
  }
161
264
 
@@ -163,6 +266,7 @@ function splitLengths<K extends string, T extends { lengths: Partial<Record<K, n
163
266
  lst: T[],
164
267
  name: K
165
268
  ) {
269
+ // Preserve caller order exactly; raw numeric fields still decode as splitCoder() subarray views.
166
270
  return splitCoder(
167
271
  name,
168
272
  ...lst.map((i) => {
@@ -172,49 +276,107 @@ function splitLengths<K extends string, T extends { lengths: Partial<Record<K, n
172
276
  );
173
277
  }
174
278
 
175
- export type ExpandSeed = (seed: Uint8Array, len: number) => Uint8Array;
279
+ /** Seed-expansion callback used by the hybrid combiners. */
280
+ export type ExpandSeed = (seed: TArg<Uint8Array>, len: number) => TRet<Uint8Array>;
176
281
  type XOF = CHashXOF<any, { dkLen: number }>;
177
282
 
178
283
  // It is XOF for most cases, but can be more complex!
179
- export function expandSeedXof(xof: XOF): ExpandSeed {
180
- return (seed: Uint8Array, seedLen: number) => xof(seed, { dkLen: seedLen });
284
+ /**
285
+ * Adapts an XOF into an `ExpandSeed` callback.
286
+ * The returned callback interprets its second argument as an output byte length passed as `dkLen`.
287
+ * @param xof - Extendable-output hash function.
288
+ * @returns Seed expander using `dkLen`.
289
+ * @example
290
+ * Adapt an XOF into a seed expander.
291
+ * ```ts
292
+ * import { shake256 } from '@noble/hashes/sha3.js';
293
+ * import { expandSeedXof } from '@noble/post-quantum/hybrid.js';
294
+ * const expandSeed = expandSeedXof(shake256);
295
+ * const seed = expandSeed(new Uint8Array([1]), 4);
296
+ * ```
297
+ */
298
+ export function expandSeedXof(xof: TArg<XOF>): TRet<ExpandSeed> {
299
+ // Forward the caller seed directly: XOFs are expected to treat inputs as read-only, and this
300
+ // adapter only translates the requested byte length into the hash API's `dkLen` option.
301
+ return ((seed: TArg<Uint8Array>, seedLen: number): TRet<Uint8Array> =>
302
+ (xof as XOF)(seed, { dkLen: seedLen }) as TRet<Uint8Array>) as TRet<ExpandSeed>;
181
303
  }
182
304
 
305
+ /** Combines public keys, ciphertexts, and shared secrets into one shared secret. */
183
306
  export type Combiner = (
184
- publicKeys: Uint8Array[],
185
- cipherTexts: Uint8Array[],
186
- sharedSecrets: Uint8Array[]
187
- ) => Uint8Array;
307
+ publicKeys: TArg<Uint8Array[]>,
308
+ cipherTexts: TArg<Uint8Array[]>,
309
+ sharedSecrets: TArg<Uint8Array[]>
310
+ ) => TRet<Uint8Array>;
188
311
 
189
312
  function combineKeys(
190
313
  realSeedLen: number | undefined, // how much bytes expandSeed expects
191
- expandSeed: ExpandSeed,
192
- ...ck: CryptoKeys[]
314
+ expandSeed_: TArg<ExpandSeed>,
315
+ ...ck_: TArg<CryptoKeys[]>
193
316
  ) {
317
+ const expandSeed = expandSeed_ as ExpandSeed;
318
+ const ck = ck_ as CryptoKeys[];
194
319
  const seedCoder = splitLengths(ck, 'seed');
195
320
  const pkCoder = splitLengths(ck, 'publicKey');
196
321
  // Allows to use identity functions for combiner/expandSeed
197
322
  if (realSeedLen === undefined) realSeedLen = seedCoder.bytesLen;
198
323
  anumber(realSeedLen);
199
- function expandDecapsulationKey(seed: Uint8Array) {
324
+ function expandDecapsulationKey(seed: TArg<Uint8Array>): TRet<{
325
+ secretKey: Uint8Array[];
326
+ publicKey: Uint8Array[];
327
+ }> {
200
328
  abytes(seed, realSeedLen!);
201
- const expanded = seedCoder.decode(expandSeed(seed, seedCoder.bytesLen));
202
- const keys = ck.map((i, j) => i.keygen(expanded[j]));
203
- const secretKey = keys.map((i) => i.secretKey);
204
- const publicKey = keys.map((i) => i.publicKey);
205
- return { secretKey, publicKey };
329
+ const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
330
+ // Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
331
+ // later cleanup can wipe the expanded schedule without mutating the caller's root seed bytes.
332
+ const expandedSeed = expandedRaw.buffer === seed.buffer ? copyBytes(expandedRaw) : expandedRaw;
333
+ const expanded: Uint8Array[] = [];
334
+ const keySecret: Uint8Array[] = [];
335
+ const secretKey: Uint8Array[] = [];
336
+ const publicKey: Uint8Array[] = [];
337
+ let ok = false;
338
+ try {
339
+ // seedCoder.decode() returns zero-copy slices into expandedSeed and can throw before child
340
+ // keygen() runs, so keep the raw expanded buffer separate and copy each child seed before any
341
+ // later cleanup wipes the shared backing bytes.
342
+ for (const part of seedCoder.decode(expandedSeed)) expanded.push(copyBytes(part));
343
+ for (let i = 0; i < ck.length; i++) {
344
+ const keys = ck[i].keygen(expanded[i]);
345
+ keySecret.push(keys.secretKey);
346
+ secretKey.push(copyBytes(keys.secretKey));
347
+ publicKey.push(keys.publicKey);
348
+ }
349
+ ok = true;
350
+ return { secretKey, publicKey } as TRet<{
351
+ secretKey: Uint8Array[];
352
+ publicKey: Uint8Array[];
353
+ }>;
354
+ } finally {
355
+ // Child keygen() can throw after deriving only a prefix of the composite key schedule. Keep
356
+ // the exported copies on success, but wipe all temporary and partially built secret material
357
+ // on either path so failures do not strand derived child seeds in memory.
358
+ cleanBytes(expandedSeed, expanded, keySecret);
359
+ if (!ok) cleanBytes(secretKey);
360
+ }
206
361
  }
207
362
  return {
208
363
  info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
209
- getPublicKey(secretKey: Uint8Array) {
210
- return this.keygen(secretKey).publicKey;
364
+ getPublicKey(secretKey: TArg<Uint8Array>) {
365
+ // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
366
+ // that seed instead of decoding a packed child-secret-key structure.
367
+ return this.keygen(secretKey).publicKey as TRet<Uint8Array>;
211
368
  },
212
- keygen(seed: Uint8Array = randomBytes(realSeedLen)) {
369
+ keygen(seed: TArg<Uint8Array> = randomBytes(realSeedLen)) {
213
370
  const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
214
- const publicKey = pkCoder.encode(pk);
215
- cleanBytes(pk);
216
- cleanBytes(secretKey);
217
- return { secretKey: seed, publicKey };
371
+ try {
372
+ const publicKey = pkCoder.encode(pk) as TRet<Uint8Array>;
373
+ return { secretKey: seed as TRet<Uint8Array>, publicKey };
374
+ } finally {
375
+ cleanBytes(pk);
376
+ // The exported secretKey is the caller/root seed itself; child secret keys are internal
377
+ // expansion outputs that are cleaned whether encoding succeeds or throws.
378
+ cleanBytes(secretKey);
379
+ }
218
380
  },
219
381
  expandDecapsulationKey,
220
382
  realSeedLen,
@@ -222,123 +384,275 @@ function combineKeys(
222
384
  }
223
385
 
224
386
  // This generic function that combines multiple KEMs into single one
387
+ /**
388
+ * Combines multiple KEMs into one composite KEM.
389
+ * @param realSeedLen - Input seed length expected by `expandSeed`.
390
+ * @param realMsgLen - Shared-secret length returned by `combiner`.
391
+ * @param expandSeed - Seed expander used to derive per-KEM seeds.
392
+ * @param combiner - Combines the per-KEM outputs into one shared secret.
393
+ * @param kems - KEM implementations to combine.
394
+ * @returns Composite KEM.
395
+ * @example
396
+ * Combine multiple KEMs into one composite KEM.
397
+ * ```ts
398
+ * import { shake256 } from '@noble/hashes/sha3.js';
399
+ * import { combineKEMS, expandSeedXof } from '@noble/post-quantum/hybrid.js';
400
+ * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
401
+ * const hybrid = combineKEMS(
402
+ * 32,
403
+ * 32,
404
+ * expandSeedXof(shake256),
405
+ * (_pk, _ct, sharedSecrets) => sharedSecrets[0],
406
+ * ml_kem768,
407
+ * ml_kem768
408
+ * );
409
+ * const { publicKey } = hybrid.keygen();
410
+ * ```
411
+ */
225
412
  export function combineKEMS(
226
413
  realSeedLen: number | undefined, // how much bytes expandSeed expects
227
414
  realMsgLen: number | undefined, // how much bytes combiner returns
228
- expandSeed: ExpandSeed,
229
- combiner: Combiner,
230
- ...kems: KEM[]
231
- ): KEM {
232
- const keys = combineKeys(realSeedLen, expandSeed, ...kems);
233
- const ctCoder = splitLengths(kems, 'cipherText');
234
- const pkCoder = splitLengths(kems, 'publicKey');
235
- const msgCoder = splitLengths(kems, 'msg');
415
+ expandSeed: TArg<ExpandSeed>,
416
+ combiner: TArg<Combiner>,
417
+ ...kems: TArg<KEM[]>
418
+ ): TRet<KEM> {
419
+ const rawCombiner = combiner as Combiner;
420
+ const rawKems = kems as KEM[];
421
+ const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
422
+ const ctCoder = splitLengths(rawKems, 'cipherText');
423
+ const pkCoder = splitLengths(rawKems, 'publicKey');
424
+ const msgCoder = splitLengths(rawKems, 'msg');
236
425
  if (realMsgLen === undefined) realMsgLen = msgCoder.bytesLen;
237
426
  anumber(realMsgLen);
238
- return {
239
- lengths: {
240
- ...keys.info.lengths,
241
- msg: realMsgLen,
242
- msgRand: msgCoder.bytesLen,
243
- cipherText: ctCoder.bytesLen,
244
- },
427
+ const lengths = Object.freeze({
428
+ ...keys.info.lengths,
429
+ msg: realMsgLen,
430
+ msgRand: msgCoder.bytesLen,
431
+ cipherText: ctCoder.bytesLen,
432
+ });
433
+ return Object.freeze({
434
+ lengths,
245
435
  getPublicKey: keys.getPublicKey,
246
436
  keygen: keys.keygen,
247
- encapsulate(pk: Uint8Array, randomness: Uint8Array = randomBytes(msgCoder.bytesLen)) {
437
+ encapsulate(
438
+ pk: TArg<Uint8Array>,
439
+ randomness: TArg<Uint8Array> = randomBytes(msgCoder.bytesLen)
440
+ ) {
248
441
  const pks = pkCoder.decode(pk);
249
442
  const rand = msgCoder.decode(randomness);
250
- const enc = kems.map((i, j) => i.encapsulate(pks[j], rand[j]));
251
- const sharedSecret = enc.map((i) => i.sharedSecret);
252
- const cipherText = enc.map((i) => i.cipherText);
253
- const res = {
254
- sharedSecret: combiner(pks, cipherText, sharedSecret),
255
- cipherText: ctCoder.encode(cipherText),
256
- };
257
- cleanBytes(sharedSecret, cipherText);
258
- return res;
443
+ const sharedSecret: Uint8Array[] = [];
444
+ const cipherText: Uint8Array[] = [];
445
+ try {
446
+ for (let i = 0; i < rawKems.length; i++) {
447
+ const enc = rawKems[i].encapsulate(pks[i], rand[i]);
448
+ sharedSecret.push(enc.sharedSecret);
449
+ cipherText.push(enc.cipherText);
450
+ }
451
+ return {
452
+ // Detach the combiner result before cleanup: a caller-provided combiner may alias one of
453
+ // the child sharedSecret buffers, and those child buffers are zeroized immediately below.
454
+ sharedSecret: copyBytes(rawCombiner(pks, cipherText, sharedSecret)),
455
+ cipherText: ctCoder.encode(cipherText) as TRet<Uint8Array>,
456
+ };
457
+ } finally {
458
+ // Child encapsulation or combiner failures can happen after some components already
459
+ // returned secret material; zeroize whatever was produced before propagating the error.
460
+ cleanBytes(sharedSecret, cipherText);
461
+ }
259
462
  },
260
- decapsulate(ct: Uint8Array, seed: Uint8Array) {
463
+ decapsulate(ct: TArg<Uint8Array>, seed: TArg<Uint8Array>) {
261
464
  const cts = ctCoder.decode(ct);
262
465
  const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
263
- const sharedSecret = kems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
264
- return combiner(publicKey, cts, sharedSecret);
466
+ const sharedSecret = rawKems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
467
+ try {
468
+ // Detach the decapsulation result before cleanup: the combiner may hand back one of the
469
+ // child shared-secret buffers, and those temporary buffers are zeroized below.
470
+ return copyBytes(rawCombiner(publicKey, cts, sharedSecret));
471
+ } finally {
472
+ // Decapsulation only needs the expanded child secret keys and child shared secrets for this
473
+ // call; keep the caller/root seed intact, but wipe all derived material even on errors.
474
+ cleanBytes(secretKey, sharedSecret);
475
+ }
265
476
  },
266
- };
477
+ });
267
478
  }
268
479
  // There is no specs for this, but can be useful
269
480
  // realSeedLen: how much bytes expandSeed expects.
481
+ /**
482
+ * Combines multiple signers into one composite signer.
483
+ * @param realSeedLen - Input seed length expected by `expandSeed`.
484
+ * @param expandSeed - Seed expander used to derive per-signer seeds.
485
+ * @param signers - Signers to combine.
486
+ * @returns Composite signer.
487
+ * @example
488
+ * Combine multiple signers into one composite signer.
489
+ * ```ts
490
+ * import { shake256 } from '@noble/hashes/sha3.js';
491
+ * import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
492
+ * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
493
+ * const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
494
+ * const { publicKey } = hybrid.keygen();
495
+ * ```
496
+ */
270
497
  export function combineSigners(
271
498
  realSeedLen: number | undefined,
272
- expandSeed: ExpandSeed,
273
- ...signers: Signer[]
274
- ): Signer {
275
- const keys = combineKeys(realSeedLen, expandSeed, ...signers);
276
- const sigCoder = splitLengths(signers, 'signature');
277
- const pkCoder = splitLengths(signers, 'publicKey');
499
+ expandSeed: TArg<ExpandSeed>,
500
+ ...signers: TArg<Signer[]>
501
+ ): TRet<Signer> {
502
+ const rawSigners = signers as Signer[];
503
+ const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
504
+ const sigCoder = splitLengths(rawSigners, 'signature');
505
+ const pkCoder = splitLengths(rawSigners, 'publicKey');
278
506
  return {
279
507
  lengths: { ...keys.info.lengths, signature: sigCoder.bytesLen, signRand: 0 },
280
508
  getPublicKey: keys.getPublicKey,
281
509
  keygen: keys.keygen,
282
- sign(message, seed) {
510
+ sign(message, seed, opts = {}) {
511
+ validateSigOpts(opts);
512
+ // This generic wrapper intentionally keeps the composite signer contract to message + root
513
+ // seed only. Per-signer opts like context or extraEntropy cannot be preserved uniformly
514
+ // across mixed backends, so callers that need them must use the underlying signer directly.
515
+ if (opts.extraEntropy !== undefined)
516
+ throw new Error(
517
+ 'combineSigners does not support extraEntropy; use the underlying signer directly'
518
+ );
519
+ if (opts.context !== undefined)
520
+ throw new Error(
521
+ 'combineSigners does not support context; use the underlying signer directly'
522
+ );
283
523
  const { secretKey } = keys.expandDecapsulationKey(seed);
284
- // NOTE: we probably can make different hashes for different algorithms
285
- // same way as we do for kem, but not sure if this a good idea.
286
- const sigs = signers.map((i, j) => i.sign(message, secretKey[j]));
287
- return sigCoder.encode(sigs);
524
+ try {
525
+ const sigs = rawSigners.map((i, j) => i.sign(message, secretKey[j]));
526
+ return sigCoder.encode(sigs) as TRet<Uint8Array>;
527
+ } finally {
528
+ // Composite secret keys are root seeds; the per-signer child secret keys are temporary
529
+ // expansion outputs and must not stay live after the combined signature is produced.
530
+ cleanBytes(secretKey);
531
+ }
288
532
  },
289
- verify: (signature, message, publicKey) => {
533
+ /** Verify one combined signature.
534
+ * Returns `false` when the aggregate signature/publicKey decode succeeds but any child verify
535
+ * check fails. Throws on unsupported generic opts or malformed aggregate encodings.
536
+ */
537
+ verify: (signature, message, publicKey, opts = {}) => {
538
+ validateVerOpts(opts);
539
+ if (opts.context !== undefined)
540
+ throw new Error(
541
+ 'combineSigners does not support context; use the underlying signer directly'
542
+ );
290
543
  const pks = pkCoder.decode(publicKey);
291
544
  const sigs = sigCoder.decode(signature);
292
- for (let i = 0; i < signers.length; i++) {
293
- if (!signers[i].verify(sigs[i], message, pks[i])) return false;
545
+ for (let i = 0; i < rawSigners.length; i++) {
546
+ if (!rawSigners[i].verify(sigs[i], message, pks[i])) return false;
294
547
  }
295
548
  return true;
296
549
  },
297
550
  };
298
551
  }
299
552
 
300
- export function QSF(label: string, pqc: KEM, curveKEM: KEM, xof: XOF, kdf: CHash): KEM {
553
+ /**
554
+ * Builds a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
555
+ * The combined shared-secret length follows `kdf.outputLen`; the built-in presets use 32-byte
556
+ * SHA3-256 output, while custom `kdf` choices inherit their own digest size.
557
+ * Its combiner hashes `ss0 || ss1 || ct1 || pk1 || label`, not the full
558
+ * `(c1, c2, ek1, ek2)` example input shape from SP 800-227 equation (15).
559
+ * Labels are encoded with `asciiToBytes()`, so non-ASCII labels are rejected.
560
+ * @param label - Domain-separation label.
561
+ * @param pqc - Post-quantum KEM.
562
+ * @param curveKEM - Classical curve KEM.
563
+ * @param xof - XOF used for seed expansion.
564
+ * @param kdf - Hash used for the final combiner.
565
+ * @returns Hybrid KEM.
566
+ * @example
567
+ * Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
568
+ * ```ts
569
+ * import { p256 } from '@noble/curves/nist.js';
570
+ * import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
571
+ * import { QSF, ecdhKem } from '@noble/post-quantum/hybrid.js';
572
+ * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
573
+ * const kem = QSF('example', ml_kem768, ecdhKem(p256, true), shake256, sha3_256);
574
+ * const publicKeyLen = kem.lengths.publicKey;
575
+ * ```
576
+ */
577
+ export function QSF(
578
+ label: string,
579
+ pqc: TArg<KEM>,
580
+ curveKEM: TArg<KEM>,
581
+ xof: TArg<XOF>,
582
+ kdf: CHash
583
+ ): TRet<KEM> {
301
584
  ahash(xof);
302
585
  ahash(kdf);
303
586
  return combineKEMS(
304
587
  32,
305
- 32,
588
+ kdf.outputLen,
306
589
  expandSeedXof(xof),
307
- (pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
590
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
591
+ kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
308
592
  pqc,
309
593
  curveKEM
310
594
  );
311
595
  }
312
596
 
313
- export const QSF_ml_kem768_p256: KEM = QSF(
314
- 'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
315
- ml_kem768,
316
- ecdhKem(p256, true),
317
- shake256,
318
- sha3_256
319
- );
320
- export const QSF_ml_kem1024_p384: KEM = QSF(
321
- 'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
322
- ml_kem1024,
323
- ecdhKem(p384, true),
324
- shake256,
325
- sha3_256
326
- );
597
+ /** QSF preset combining ML-KEM-768 with P-256. */
598
+ export const QSF_ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
599
+ QSF(
600
+ 'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
601
+ ml_kem768,
602
+ ecdhKem(p256, true),
603
+ shake256,
604
+ sha3_256
605
+ ))();
606
+ /** QSF preset combining ML-KEM-1024 with P-384. */
607
+ export const QSF_ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
608
+ QSF(
609
+ 'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
610
+ ml_kem1024,
611
+ ecdhKem(p384, true),
612
+ shake256,
613
+ sha3_256
614
+ ))();
327
615
 
616
+ /**
617
+ * Builds the "KitchenSink" hybrid KEM combiner.
618
+ * The current builder always derives a fixed 32-byte output,
619
+ * regardless of the hash's native output size.
620
+ * Its HKDF extract step uses implicit zero salt with IKM
621
+ * `hybrid_prk || ss0 || ss1 || ct0 || pk0 || ct1 || pk1 || label`.
622
+ * Its HKDF expand step fixes `info` to `len || 'shared_secret' || ''`.
623
+ * Labels are encoded with `asciiToBytes()`, so non-ASCII labels are rejected.
624
+ * @param label - Domain-separation label.
625
+ * @param pqc - Post-quantum KEM.
626
+ * @param curveKEM - Classical curve KEM.
627
+ * @param xof - XOF used for seed expansion.
628
+ * @param hash - Hash used for HKDF extraction and expansion.
629
+ * @returns Hybrid KEM.
630
+ * @example
631
+ * Build the "KitchenSink" hybrid KEM combiner.
632
+ * ```ts
633
+ * import { sha256 } from '@noble/hashes/sha2.js';
634
+ * import { shake256 } from '@noble/hashes/sha3.js';
635
+ * import { createKitchenSink, ecdhKem } from '@noble/post-quantum/hybrid.js';
636
+ * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
637
+ * import { x25519 } from '@noble/curves/ed25519.js';
638
+ * const kem = createKitchenSink('example', ml_kem768, ecdhKem(x25519), shake256, sha256);
639
+ * const publicKeyLen = kem.lengths.publicKey;
640
+ * ```
641
+ */
328
642
  export function createKitchenSink(
329
643
  label: string,
330
- pqc: KEM,
331
- curveKEM: KEM,
332
- xof: XOF,
644
+ pqc: TArg<KEM>,
645
+ curveKEM: TArg<KEM>,
646
+ xof: TArg<XOF>,
333
647
  hash: CHash
334
- ): KEM {
648
+ ): TRet<KEM> {
335
649
  ahash(xof);
336
650
  ahash(hash);
337
651
  return combineKEMS(
338
652
  32,
339
653
  32,
340
654
  expandSeedXof(xof),
341
- (pk, ct, ss) => {
655
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) => {
342
656
  const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
343
657
  const len = 32;
344
658
  const ikm = concatBytes(asciiToBytes('hybrid_prk'), preimage);
@@ -357,31 +671,55 @@ export function createKitchenSink(
357
671
  );
358
672
  }
359
673
 
360
- const x25519kem = ecdhKem(x25519);
361
- export const KitchenSink_ml_kem768_x25519: KEM = createKitchenSink(
362
- 'KitchenSink-KEM(ML-KEM-768,X25519)-XOF(SHAKE256)-KDF(HKDF-SHA-256)',
363
- ml_kem768,
364
- x25519kem,
365
- shake256,
366
- sha256
367
- );
674
+ // Internal alias only: this stays exactly `ecdhKem(x25519)`
675
+ // and inherits that wrapper's mutation/oracle behavior.
676
+ const x25519kem = /* @__PURE__ */ ecdhKem(x25519);
677
+ /** KitchenSink preset combining ML-KEM-768 with X25519.
678
+ * Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
679
+ */
680
+ export const KitchenSink_ml_kem768_x25519: TRet<KEM> = /* @__PURE__ */ (() =>
681
+ createKitchenSink(
682
+ 'KitchenSink-KEM(ML-KEM-768,X25519)-XOF(SHAKE256)-KDF(HKDF-SHA-256)',
683
+ ml_kem768,
684
+ x25519kem,
685
+ shake256,
686
+ sha256
687
+ ))();
368
688
 
369
689
  // Always X25519 and ML-KEM - 768, no point to export
370
- export const ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
690
+ /** X25519 + ML-KEM-768 hybrid preset.
691
+ * Uses the hard-coded domain-separation label `\\.//^\\` and hashes only `ct1 || pk1`
692
+ * from the X25519 side in addition to the two component shared secrets.
693
+ */
694
+ export const ml_kem768_x25519: TRet<KEM> = /* @__PURE__ */ (() =>
371
695
  combineKEMS(
372
696
  32,
373
697
  32,
374
698
  expandSeedXof(shake256),
375
699
  // Awesome label, so much escaping hell in a single line.
376
- (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes('\\.//^\\'))),
700
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
701
+ sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes('\\.//^\\'))),
377
702
  ml_kem768,
378
703
  x25519kem
379
704
  ))();
380
705
 
381
- function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: number): KEM {
706
+ /**
707
+ * Internal SEC 1-style KEM wrapper for NIST curves.
708
+ * `nseed` is only the rejection-sampling byte budget for deriving one nonzero scalar:
709
+ * current presets use `128` bytes for P-256 and `48` bytes for P-384.
710
+ * `decapsulate()` returns the uncompressed shared point body `x || y` without the `0x04`
711
+ * prefix, not the SEC 1 `x_P`-only primitive output, because current hybrid combiners hash
712
+ * both coordinates.
713
+ */
714
+ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: number): TRet<KEM> {
382
715
  const Fn = curve.Point.Fn;
383
716
  if (!Fn) throw new Error('no Point.Fn');
384
- function rejectionSampling(seed: Uint8Array): { secretKey: Uint8Array; publicKey: Uint8Array } {
717
+ // Scan scalar-sized windows until one decodes to a nonzero scalar in `[1, n-1]`; if every
718
+ // window is zero or out of range, fail instead of silently reducing modulo `n`.
719
+ function rejectionSampling(seed: TArg<Uint8Array>): TRet<{
720
+ secretKey: Uint8Array;
721
+ publicKey: Uint8Array;
722
+ }> {
385
723
  let sk: bigint;
386
724
  for (let start = 0, end = scalarLen; ; start = end, end += scalarLen) {
387
725
  if (end > seed.length) throw new Error('rejection sampling failed');
@@ -390,7 +728,10 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
390
728
  }
391
729
  const secretKey = Fn.toBytes(Fn.create(sk));
392
730
  const publicKey = curve.getPublicKey(secretKey, false);
393
- return { secretKey, publicKey };
731
+ return { secretKey, publicKey } as TRet<{
732
+ secretKey: Uint8Array;
733
+ publicKey: Uint8Array;
734
+ }>;
394
735
  }
395
736
 
396
737
  return {
@@ -401,29 +742,48 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
401
742
  msg: nseed,
402
743
  cipherText: elemLen,
403
744
  },
404
- keygen(seed: Uint8Array = randomBytes(nseed)) {
745
+ keygen(seed: TArg<Uint8Array> = randomBytes(nseed)) {
405
746
  abytes(seed, nseed, 'seed');
406
747
  return rejectionSampling(seed);
407
748
  },
408
- getPublicKey(secretKey: Uint8Array) {
409
- return curve.getPublicKey(secretKey, false);
749
+ getPublicKey(secretKey: TArg<Uint8Array>) {
750
+ return curve.getPublicKey(secretKey, false) as TRet<Uint8Array>;
410
751
  },
411
- encapsulate(publicKey: Uint8Array, rand: Uint8Array = randomBytes(nseed)) {
752
+ encapsulate(publicKey: TArg<Uint8Array>, rand: TArg<Uint8Array> = randomBytes(nseed)) {
412
753
  abytes(rand, nseed, 'rand');
413
- const { secretKey: ek } = rejectionSampling(rand);
414
- const sharedSecret = this.decapsulate(publicKey, ek);
415
- const cipherText = curve.getPublicKey(ek, false);
416
- cleanBytes(ek);
417
- return { sharedSecret, cipherText };
754
+ let ek: Uint8Array | undefined = undefined;
755
+ try {
756
+ ek = rejectionSampling(rand).secretKey;
757
+ const sharedSecret = this.decapsulate(publicKey, ek);
758
+ const cipherText = curve.getPublicKey(ek, false) as TRet<Uint8Array>;
759
+ return { sharedSecret, cipherText };
760
+ } finally {
761
+ // Rejection-sampled NIST-curve ephemeral secret keys are temporary encapsulation state and
762
+ // must be wiped even if peer-key validation or shared-secret derivation throws.
763
+ if (ek) cleanBytes(ek);
764
+ }
418
765
  },
419
- decapsulate(cipherText: Uint8Array, secretKey: Uint8Array) {
766
+ decapsulate(cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) {
420
767
  const full = curve.getSharedSecret(secretKey, cipherText);
421
- return full.subarray(1);
768
+ return full.subarray(1) as TRet<Uint8Array>;
422
769
  },
423
770
  };
424
771
  }
425
772
 
426
- function concreteHybridKem(label: string, mlkem: KEM, curve: ECDSA, nseed: number): KEM {
773
+ /**
774
+ * Internal ML-KEM + NIST-curve combiner.
775
+ * `nseed` controls only the curve-side rejection-sampling budget; it is expanded from the
776
+ * 32-byte root seed and is not itself part of the exported secret-key length.
777
+ * The domain-separation `label` is used only in the final `sha3_256` combiner, not in
778
+ * `shake256(seed, { dkLen: 64 + nseed })`,
779
+ * and the combiner hashes `ss0 || ss1 || ct1 || pk1 || label`.
780
+ */
781
+ function concreteHybridKem(
782
+ label: string,
783
+ mlkem: TArg<KEM>,
784
+ curve: ECDSA,
785
+ nseed: number
786
+ ): TRet<KEM> {
427
787
  const { secretKey: scalarLen, publicKeyUncompressed: elemLen } = curve.lengths;
428
788
  if (!scalarLen || !elemLen) throw new Error('wrong curve');
429
789
  const curveKem = nistCurveKem(curve, scalarLen, elemLen, nseed);
@@ -433,30 +793,41 @@ function concreteHybridKem(label: string, mlkem: KEM, curve: ECDSA, nseed: numbe
433
793
  return combineKEMS(
434
794
  32,
435
795
  32,
436
- (seed: Uint8Array) => {
796
+ (seed: TArg<Uint8Array>): TRet<Uint8Array> => {
437
797
  abytes(seed, 32);
438
798
  const expanded = shake256(seed, { dkLen: totalSeedLen });
439
799
  const mlkemSeed = expanded.subarray(0, mlkemSeedLen);
440
800
  const curveSeed = expanded.subarray(mlkemSeedLen, totalSeedLen);
441
- return concatBytes(mlkemSeed, curveSeed);
801
+ return concatBytes(mlkemSeed, curveSeed) as TRet<Uint8Array>;
442
802
  },
443
- (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
803
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
804
+ sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
444
805
  mlkem,
445
806
  curveKem
446
807
  );
447
808
  }
448
809
 
449
- export const ml_kem768_p256: KEM = /* @__PURE__ */ (() =>
810
+ /** P-256 + ML-KEM-768 hybrid preset. */
811
+ export const ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
450
812
  concreteHybridKem('MLKEM768-P256', ml_kem768, p256, 128))();
451
813
 
452
- export const ml_kem1024_p384: KEM = /* @__PURE__ */ (() =>
814
+ /** P-384 + ML-KEM-1024 hybrid preset. */
815
+ export const ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
453
816
  concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
454
817
 
455
818
  // Legacy aliases
456
- export const XWing: KEM = ml_kem768_x25519;
457
- export const MLKEM768X25519: KEM = ml_kem768_x25519;
458
- export const MLKEM768P256: KEM = ml_kem768_p256;
459
- export const MLKEM1024P384: KEM = ml_kem1024_p384;
460
- export const QSFMLKEM768P256: KEM = QSF_ml_kem768_p256;
461
- export const QSFMLKEM1024P384: KEM = QSF_ml_kem1024_p384;
462
- export const KitchenSinkMLKEM768X25519: KEM = KitchenSink_ml_kem768_x25519;
819
+ /** Legacy alias for `ml_kem768_x25519`. */
820
+ export const XWing: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_x25519)();
821
+ /** Legacy alias for `ml_kem768_x25519`. */
822
+ export const MLKEM768X25519: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_x25519)();
823
+ /** Legacy alias for `ml_kem768_p256`. */
824
+ export const MLKEM768P256: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_p256)();
825
+ /** Legacy alias for `ml_kem1024_p384`. */
826
+ export const MLKEM1024P384: TRet<KEM> = /* @__PURE__ */ (() => ml_kem1024_p384)();
827
+ /** Legacy alias for `QSF_ml_kem768_p256`. */
828
+ export const QSFMLKEM768P256: TRet<KEM> = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
829
+ /** Legacy alias for `QSF_ml_kem1024_p384`. */
830
+ export const QSFMLKEM1024P384: TRet<KEM> = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
831
+ /** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
832
+ export const KitchenSinkMLKEM768X25519: TRet<KEM> = /* @__PURE__ */ (() =>
833
+ KitchenSink_ml_kem768_x25519)();