@noble/post-quantum 0.6.0 → 0.7.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.
package/src/hybrid.ts CHANGED
@@ -80,6 +80,8 @@ import { type ECDSA } from '@noble/curves/abstract/weierstrass.js';
80
80
  import { x25519 } from '@noble/curves/ed25519.js';
81
81
  import { p256, p384 } from '@noble/curves/nist.js';
82
82
  import {
83
+ abool,
84
+ afunction,
83
85
  asciiToBytes,
84
86
  bytesToNumberBE,
85
87
  bytesToNumberLE,
@@ -92,6 +94,8 @@ import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
92
94
  import { abytes, ahash, anumber, type CHash, type CHashXOF } from '@noble/hashes/utils.js';
93
95
  import { ml_kem1024, ml_kem768 } from './ml-kem.ts';
94
96
  import {
97
+ aobject,
98
+ astring,
95
99
  cleanBytes,
96
100
  copyBytes,
97
101
  randomBytes,
@@ -101,14 +105,39 @@ import {
101
105
  type CryptoKeys,
102
106
  type KEM,
103
107
  type Signer,
108
+ type TArg,
109
+ type TRet,
104
110
  } from './utils.ts';
105
111
 
106
112
  type CurveAll = ECDSA | EdDSA | MontgomeryECDH;
107
113
  type CurveECDH = ECDSA | MontgomeryECDH;
108
114
  type CurveSign = ECDSA | EdDSA;
115
+ const validateKEM = (kem: TArg<KEM>, title: string): TRet<KEM> => {
116
+ const k = aobject<KEM>(kem, title);
117
+ aobject(k.lengths, `${title}.lengths`);
118
+ afunction(k.keygen, `${title}.keygen`);
119
+ afunction(k.getPublicKey, `${title}.getPublicKey`);
120
+ afunction(k.encapsulate, `${title}.encapsulate`);
121
+ afunction(k.decapsulate, `${title}.decapsulate`);
122
+ return k as TRet<KEM>;
123
+ };
124
+ const validateSigner = (signer: TArg<Signer>, title: string): TRet<Signer> => {
125
+ const s = aobject<Signer>(signer, title);
126
+ aobject(s.lengths, `${title}.lengths`);
127
+ afunction(s.keygen, `${title}.keygen`);
128
+ afunction(s.getPublicKey, `${title}.getPublicKey`);
129
+ afunction(s.sign, `${title}.sign`);
130
+ afunction(s.verify, `${title}.verify`);
131
+ return s as TRet<Signer>;
132
+ };
109
133
 
110
134
  // Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
111
135
  function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
136
+ const c = aobject<CurveAll>(curve, 'curve');
137
+ aobject(c.lengths, 'curve.lengths');
138
+ afunction(c.keygen, 'curve.keygen');
139
+ afunction(c.getPublicKey, 'curve.getPublicKey');
140
+ abool(allowZeroKey, 'allowZeroKey');
112
141
  const lengths = curve.lengths;
113
142
  let keygen = curve.keygen;
114
143
  if (allowZeroKey) {
@@ -126,18 +155,26 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
126
155
  // Unlike noble-curves' seeded Weierstrass keygen, this path removes the post-reduction +1.
127
156
  // That is enough to match exact reduced-vector bytes, but an all-zero seed still reduces to
128
157
  // scalar 0 here and getPublicKey(secretKey) throws instead of "allowing zero".
129
- keygen = (seed: Uint8Array = randomBytes(lengths.seed)) => {
158
+ keygen = (seed: TArg<Uint8Array> = randomBytes(lengths.seed)) => {
130
159
  abytes(seed, lengths.seed!, 'seed');
131
160
  const seedScalar = Fn.isLE ? bytesToNumberLE(seed) : bytesToNumberBE(seed);
132
161
  // Reduce directly into [0, ORDER); scalar 0 still stays invalid.
133
162
  const secretKey = Fn.toBytes(Fn.create(seedScalar));
134
- return { secretKey, publicKey: curve.getPublicKey(secretKey) };
163
+ return {
164
+ secretKey: secretKey as TRet<Uint8Array>,
165
+ publicKey: curve.getPublicKey(secretKey) as TRet<Uint8Array>,
166
+ };
135
167
  };
136
168
  }
137
169
  return {
138
170
  lengths: { secretKey: lengths.secretKey, publicKey: lengths.publicKey, seed: lengths.seed },
139
- keygen,
140
- getPublicKey: (secretKey: Uint8Array) => curve.getPublicKey(secretKey),
171
+ keygen: (seed?: TArg<Uint8Array>) =>
172
+ keygen(seed) as TRet<{
173
+ secretKey: Uint8Array;
174
+ publicKey: Uint8Array;
175
+ }>,
176
+ getPublicKey: (secretKey: TArg<Uint8Array>) =>
177
+ curve.getPublicKey(secretKey) as TRet<Uint8Array>,
141
178
  };
142
179
  }
143
180
 
@@ -164,22 +201,30 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
164
201
  * const publicKeyLen = kem.lengths.publicKey;
165
202
  * ```
166
203
  */
167
- export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): KEM {
204
+ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<KEM> {
168
205
  const kg = ecKeygen(curve, allowZeroKey);
169
206
  if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
207
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
208
+ const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
209
+ const res = curve.getSharedSecret(secretKey, cipherText);
210
+ return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
211
+ };
170
212
  return {
171
213
  lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
172
214
  keygen: kg.keygen,
173
215
  getPublicKey: kg.getPublicKey,
174
- encapsulate(publicKey: Uint8Array, rand: Uint8Array = randomBytes(curve.lengths.seed)) {
216
+ encapsulate(
217
+ publicKey: TArg<Uint8Array>,
218
+ rand: TArg<Uint8Array> = randomBytes(curve.lengths.seed)
219
+ ) {
175
220
  // Some curve.keygen(seed) paths reuse the provided seed buffer as secretKey; detach caller
176
221
  // randomness first so cleanBytes() only wipes wrapper-owned material.
177
222
  const seed = copyBytes(rand);
178
223
  let ek: Uint8Array | undefined = undefined;
179
224
  try {
180
- ek = this.keygen(seed).secretKey;
181
- const sharedSecret = this.decapsulate(publicKey, ek);
182
- const cipherText = curve.getPublicKey(ek);
225
+ ek = kg.keygen(seed).secretKey;
226
+ const sharedSecret = decapsulate(publicKey, ek);
227
+ const cipherText = curve.getPublicKey(ek) as TRet<Uint8Array>;
183
228
  return { sharedSecret, cipherText };
184
229
  } finally {
185
230
  // Invalid peer public keys can make decapsulation throw; wipe both the detached seed and
@@ -188,10 +233,7 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): KEM {
188
233
  if (ek) cleanBytes(ek);
189
234
  }
190
235
  },
191
- decapsulate(cipherText: Uint8Array, secretKey: Uint8Array) {
192
- const res = curve.getSharedSecret(secretKey, cipherText);
193
- return curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res;
194
- },
236
+ decapsulate,
195
237
  };
196
238
  }
197
239
 
@@ -216,7 +258,7 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): KEM {
216
258
  * const sigLen = signer.lengths.signature;
217
259
  * ```
218
260
  */
219
- export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): Signer {
261
+ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<Signer> {
220
262
  const kg = ecKeygen(curve, allowZeroKey);
221
263
  if (!curve.sign || !curve.verify) throw new Error('wrong curve'); // ed25519 doesn't have one!
222
264
  return {
@@ -234,7 +276,7 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): Signe
234
276
  );
235
277
  if (opts.context !== undefined)
236
278
  throw new Error('ecSigner does not support context; use the underlying curve directly');
237
- return curve.sign(message, secretKey);
279
+ return curve.sign(message, secretKey) as TRet<Uint8Array>;
238
280
  },
239
281
  /** Verify one wrapped curve signature.
240
282
  * Returns the wrapped curve's `verify()` result for well-formed inputs. Throws on unsupported
@@ -264,7 +306,7 @@ function splitLengths<K extends string, T extends { lengths: Partial<Record<K, n
264
306
  }
265
307
 
266
308
  /** Seed-expansion callback used by the hybrid combiners. */
267
- export type ExpandSeed = (seed: Uint8Array, len: number) => Uint8Array;
309
+ export type ExpandSeed = (seed: TArg<Uint8Array>, len: number) => TRet<Uint8Array>;
268
310
  type XOF = CHashXOF<any, { dkLen: number }>;
269
311
 
270
312
  // It is XOF for most cases, but can be more complex!
@@ -282,30 +324,36 @@ type XOF = CHashXOF<any, { dkLen: number }>;
282
324
  * const seed = expandSeed(new Uint8Array([1]), 4);
283
325
  * ```
284
326
  */
285
- export function expandSeedXof(xof: XOF): ExpandSeed {
327
+ export function expandSeedXof(xof: TArg<XOF>): TRet<ExpandSeed> {
286
328
  // Forward the caller seed directly: XOFs are expected to treat inputs as read-only, and this
287
329
  // adapter only translates the requested byte length into the hash API's `dkLen` option.
288
- return (seed: Uint8Array, seedLen: number) => xof(seed, { dkLen: seedLen });
330
+ return ((seed: TArg<Uint8Array>, seedLen: number): TRet<Uint8Array> =>
331
+ (xof as XOF)(seed, { dkLen: seedLen }) as TRet<Uint8Array>) as TRet<ExpandSeed>;
289
332
  }
290
333
 
291
334
  /** Combines public keys, ciphertexts, and shared secrets into one shared secret. */
292
335
  export type Combiner = (
293
- publicKeys: Uint8Array[],
294
- cipherTexts: Uint8Array[],
295
- sharedSecrets: Uint8Array[]
296
- ) => Uint8Array;
336
+ publicKeys: TArg<Uint8Array[]>,
337
+ cipherTexts: TArg<Uint8Array[]>,
338
+ sharedSecrets: TArg<Uint8Array[]>
339
+ ) => TRet<Uint8Array>;
297
340
 
298
341
  function combineKeys(
299
342
  realSeedLen: number | undefined, // how much bytes expandSeed expects
300
- expandSeed: ExpandSeed,
301
- ...ck: CryptoKeys[]
343
+ expandSeed_: TArg<ExpandSeed>,
344
+ ...ck_: TArg<CryptoKeys[]>
302
345
  ) {
346
+ const expandSeed = expandSeed_ as ExpandSeed;
347
+ const ck = ck_ as CryptoKeys[];
303
348
  const seedCoder = splitLengths(ck, 'seed');
304
349
  const pkCoder = splitLengths(ck, 'publicKey');
305
350
  // Allows to use identity functions for combiner/expandSeed
306
351
  if (realSeedLen === undefined) realSeedLen = seedCoder.bytesLen;
307
352
  anumber(realSeedLen);
308
- function expandDecapsulationKey(seed: Uint8Array) {
353
+ function expandDecapsulationKey(seed: TArg<Uint8Array>): TRet<{
354
+ secretKey: Uint8Array[];
355
+ publicKey: Uint8Array[];
356
+ }> {
309
357
  abytes(seed, realSeedLen!);
310
358
  const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
311
359
  // Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
@@ -328,7 +376,10 @@ function combineKeys(
328
376
  publicKey.push(keys.publicKey);
329
377
  }
330
378
  ok = true;
331
- return { secretKey, publicKey };
379
+ return { secretKey, publicKey } as TRet<{
380
+ secretKey: Uint8Array[];
381
+ publicKey: Uint8Array[];
382
+ }>;
332
383
  } finally {
333
384
  // Child keygen() can throw after deriving only a prefix of the composite key schedule. Keep
334
385
  // the exported copies on success, but wipe all temporary and partially built secret material
@@ -337,25 +388,40 @@ function combineKeys(
337
388
  if (!ok) cleanBytes(secretKey);
338
389
  }
339
390
  }
340
- return {
341
- info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
342
- getPublicKey(secretKey: Uint8Array) {
343
- // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
344
- // that seed instead of decoding a packed child-secret-key structure.
345
- return this.keygen(secretKey).publicKey;
346
- },
347
- keygen(seed: Uint8Array = randomBytes(realSeedLen)) {
348
- const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
391
+ // Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
392
+ const keygen = (seed?: TArg<Uint8Array>) => {
393
+ // Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
394
+ // caller mutation of the seed cannot silently change the secret key (and vice versa).
395
+ const root = seed === undefined ? randomBytes(realSeedLen!) : copyBytes(seed);
396
+ let res;
397
+ try {
398
+ const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
349
399
  try {
350
- const publicKey = pkCoder.encode(pk);
351
- return { secretKey: seed, publicKey };
400
+ res = {
401
+ secretKey: root as TRet<Uint8Array>,
402
+ publicKey: pkCoder.encode(pk) as TRet<Uint8Array>,
403
+ };
352
404
  } finally {
353
- cleanBytes(pk);
354
- // The exported secretKey is the caller/root seed itself; child secret keys are internal
405
+ // The exported secretKey is the (detached) root seed; child secret keys are internal
355
406
  // expansion outputs that are cleaned whether encoding succeeds or throws.
356
- cleanBytes(secretKey);
407
+ cleanBytes(pk, secretKey);
357
408
  }
409
+ return res;
410
+ } finally {
411
+ if (!res) cleanBytes(root);
412
+ }
413
+ };
414
+ return {
415
+ info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
416
+ // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
417
+ // that seed instead of decoding a packed child-secret-key structure.
418
+ getPublicKey: (secretKey: TArg<Uint8Array>) => {
419
+ const keys = keygen(secretKey);
420
+ // keygen detaches its exported root; getPublicKey discards that half of the result.
421
+ cleanBytes(keys.secretKey);
422
+ return keys.publicKey as TRet<Uint8Array>;
358
423
  },
424
+ keygen,
359
425
  expandDecapsulationKey,
360
426
  realSeedLen,
361
427
  };
@@ -390,41 +456,54 @@ function combineKeys(
390
456
  export function combineKEMS(
391
457
  realSeedLen: number | undefined, // how much bytes expandSeed expects
392
458
  realMsgLen: number | undefined, // how much bytes combiner returns
393
- expandSeed: ExpandSeed,
394
- combiner: Combiner,
395
- ...kems: KEM[]
396
- ): KEM {
397
- const keys = combineKeys(realSeedLen, expandSeed, ...kems);
398
- const ctCoder = splitLengths(kems, 'cipherText');
399
- const pkCoder = splitLengths(kems, 'publicKey');
400
- const msgCoder = splitLengths(kems, 'msg');
459
+ expandSeed: TArg<ExpandSeed>,
460
+ combiner: TArg<Combiner>,
461
+ ...kems: TArg<KEM[]>
462
+ ): TRet<KEM> {
463
+ if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
464
+ if (realMsgLen !== undefined) anumber(realMsgLen, 'realMsgLen');
465
+ if (typeof expandSeed !== 'function')
466
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
467
+ if (typeof combiner !== 'function')
468
+ throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
469
+ const rawCombiner = combiner as Combiner;
470
+ const rawKems = kems as KEM[];
471
+ for (let i = 0; i < rawKems.length; i++) validateKEM(rawKems[i], `kems[${i}]`);
472
+ const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
473
+ const ctCoder = splitLengths(rawKems, 'cipherText');
474
+ const pkCoder = splitLengths(rawKems, 'publicKey');
475
+ const msgCoder = splitLengths(rawKems, 'msg');
401
476
  if (realMsgLen === undefined) realMsgLen = msgCoder.bytesLen;
402
- anumber(realMsgLen);
403
- return {
404
- lengths: {
405
- ...keys.info.lengths,
406
- msg: realMsgLen,
407
- msgRand: msgCoder.bytesLen,
408
- cipherText: ctCoder.bytesLen,
409
- },
477
+ anumber(realMsgLen, 'realMsgLen');
478
+ const lengths = Object.freeze({
479
+ ...keys.info.lengths,
480
+ msg: realMsgLen,
481
+ msgRand: msgCoder.bytesLen,
482
+ cipherText: ctCoder.bytesLen,
483
+ });
484
+ return Object.freeze({
485
+ lengths,
410
486
  getPublicKey: keys.getPublicKey,
411
487
  keygen: keys.keygen,
412
- encapsulate(pk: Uint8Array, randomness: Uint8Array = randomBytes(msgCoder.bytesLen)) {
488
+ encapsulate(
489
+ pk: TArg<Uint8Array>,
490
+ randomness: TArg<Uint8Array> = randomBytes(msgCoder.bytesLen)
491
+ ) {
413
492
  const pks = pkCoder.decode(pk);
414
493
  const rand = msgCoder.decode(randomness);
415
494
  const sharedSecret: Uint8Array[] = [];
416
495
  const cipherText: Uint8Array[] = [];
417
496
  try {
418
- for (let i = 0; i < kems.length; i++) {
419
- const enc = kems[i].encapsulate(pks[i], rand[i]);
497
+ for (let i = 0; i < rawKems.length; i++) {
498
+ const enc = rawKems[i].encapsulate(pks[i], rand[i]);
420
499
  sharedSecret.push(enc.sharedSecret);
421
500
  cipherText.push(enc.cipherText);
422
501
  }
423
502
  return {
424
503
  // Detach the combiner result before cleanup: a caller-provided combiner may alias one of
425
504
  // the child sharedSecret buffers, and those child buffers are zeroized immediately below.
426
- sharedSecret: copyBytes(combiner(pks, cipherText, sharedSecret)),
427
- cipherText: ctCoder.encode(cipherText),
505
+ sharedSecret: copyBytes(rawCombiner(pks, cipherText, sharedSecret)),
506
+ cipherText: ctCoder.encode(cipherText) as TRet<Uint8Array>,
428
507
  };
429
508
  } finally {
430
509
  // Child encapsulation or combiner failures can happen after some components already
@@ -432,21 +511,21 @@ export function combineKEMS(
432
511
  cleanBytes(sharedSecret, cipherText);
433
512
  }
434
513
  },
435
- decapsulate(ct: Uint8Array, seed: Uint8Array) {
514
+ decapsulate(ct: TArg<Uint8Array>, seed: TArg<Uint8Array>) {
436
515
  const cts = ctCoder.decode(ct);
437
516
  const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
438
- const sharedSecret = kems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
517
+ const sharedSecret = rawKems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
439
518
  try {
440
519
  // Detach the decapsulation result before cleanup: the combiner may hand back one of the
441
520
  // child shared-secret buffers, and those temporary buffers are zeroized below.
442
- return copyBytes(combiner(publicKey, cts, sharedSecret));
521
+ return copyBytes(rawCombiner(publicKey, cts, sharedSecret));
443
522
  } finally {
444
523
  // Decapsulation only needs the expanded child secret keys and child shared secrets for this
445
524
  // call; keep the caller/root seed intact, but wipe all derived material even on errors.
446
525
  cleanBytes(secretKey, sharedSecret);
447
526
  }
448
527
  },
449
- };
528
+ });
450
529
  }
451
530
  // There is no specs for this, but can be useful
452
531
  // realSeedLen: how much bytes expandSeed expects.
@@ -463,17 +542,26 @@ export function combineKEMS(
463
542
  * import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
464
543
  * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
465
544
  * const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
466
- * const { publicKey } = hybrid.keygen();
545
+ * const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
546
+ * const { secretKey, publicKey } = hybrid.keygen(seed);
547
+ * const msg = new TextEncoder().encode('hello noble');
548
+ * const sig = hybrid.sign(msg, secretKey);
549
+ * const isValid = hybrid.verify(sig, msg, publicKey);
467
550
  * ```
468
551
  */
469
552
  export function combineSigners(
470
553
  realSeedLen: number | undefined,
471
- expandSeed: ExpandSeed,
472
- ...signers: Signer[]
473
- ): Signer {
474
- const keys = combineKeys(realSeedLen, expandSeed, ...signers);
475
- const sigCoder = splitLengths(signers, 'signature');
476
- const pkCoder = splitLengths(signers, 'publicKey');
554
+ expandSeed: TArg<ExpandSeed>,
555
+ ...signers: TArg<Signer[]>
556
+ ): TRet<Signer> {
557
+ if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
558
+ if (typeof expandSeed !== 'function')
559
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
560
+ const rawSigners = signers as Signer[];
561
+ for (let i = 0; i < rawSigners.length; i++) validateSigner(rawSigners[i], `signers[${i}]`);
562
+ const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
563
+ const sigCoder = splitLengths(rawSigners, 'signature');
564
+ const pkCoder = splitLengths(rawSigners, 'publicKey');
477
565
  return {
478
566
  lengths: { ...keys.info.lengths, signature: sigCoder.bytesLen, signRand: 0 },
479
567
  getPublicKey: keys.getPublicKey,
@@ -493,8 +581,8 @@ export function combineSigners(
493
581
  );
494
582
  const { secretKey } = keys.expandDecapsulationKey(seed);
495
583
  try {
496
- const sigs = signers.map((i, j) => i.sign(message, secretKey[j]));
497
- return sigCoder.encode(sigs);
584
+ const sigs = rawSigners.map((i, j) => i.sign(message, secretKey[j]));
585
+ return sigCoder.encode(sigs) as TRet<Uint8Array>;
498
586
  } finally {
499
587
  // Composite secret keys are root seeds; the per-signer child secret keys are temporary
500
588
  // expansion outputs and must not stay live after the combined signature is produced.
@@ -502,8 +590,8 @@ export function combineSigners(
502
590
  }
503
591
  },
504
592
  /** Verify one combined signature.
505
- * Returns `false` when the aggregate signature/publicKey decode succeeds but any child verify
506
- * check fails. Throws on unsupported generic opts or malformed aggregate encodings.
593
+ * Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
594
+ * does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
507
595
  */
508
596
  verify: (signature, message, publicKey, opts = {}) => {
509
597
  validateVerOpts(opts);
@@ -511,10 +599,16 @@ export function combineSigners(
511
599
  throw new Error(
512
600
  'combineSigners does not support context; use the underlying signer directly'
513
601
  );
602
+ // Malformed signature *length* is a verification failure, not a thrown type error —
603
+ // consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
604
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
605
+ abytes(signature, undefined, 'signature');
606
+ // A signature failure must not hide malformed aggregate public-key bytes.
514
607
  const pks = pkCoder.decode(publicKey);
608
+ if (signature.length !== sigCoder.bytesLen) return false;
515
609
  const sigs = sigCoder.decode(signature);
516
- for (let i = 0; i < signers.length; i++) {
517
- if (!signers[i].verify(sigs[i], message, pks[i])) return false;
610
+ for (let i = 0; i < rawSigners.length; i++) {
611
+ if (!rawSigners[i].verify(sigs[i], message, pks[i])) return false;
518
612
  }
519
613
  return true;
520
614
  },
@@ -545,21 +639,35 @@ export function combineSigners(
545
639
  * const publicKeyLen = kem.lengths.publicKey;
546
640
  * ```
547
641
  */
548
- export function QSF(label: string, pqc: KEM, curveKEM: KEM, xof: XOF, kdf: CHash): KEM {
642
+ export function QSF(
643
+ label: string,
644
+ pqc: TArg<KEM>,
645
+ curveKEM: TArg<KEM>,
646
+ xof: TArg<XOF>,
647
+ kdf: CHash
648
+ ): TRet<KEM> {
649
+ astring(label, 'label');
650
+ validateKEM(pqc, 'pqc');
651
+ validateKEM(curveKEM, 'curveKEM');
652
+ if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
653
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
549
654
  ahash(xof);
655
+ if (typeof kdf !== 'function' || typeof (kdf as any).create !== 'function')
656
+ throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
550
657
  ahash(kdf);
551
658
  return combineKEMS(
552
659
  32,
553
660
  kdf.outputLen,
554
661
  expandSeedXof(xof),
555
- (pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
662
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
663
+ kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
556
664
  pqc,
557
665
  curveKEM
558
666
  );
559
667
  }
560
668
 
561
669
  /** QSF preset combining ML-KEM-768 with P-256. */
562
- export const QSF_ml_kem768_p256: KEM = /* @__PURE__ */ (() =>
670
+ export const QSF_ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
563
671
  QSF(
564
672
  'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
565
673
  ml_kem768,
@@ -568,7 +676,7 @@ export const QSF_ml_kem768_p256: KEM = /* @__PURE__ */ (() =>
568
676
  sha3_256
569
677
  ))();
570
678
  /** QSF preset combining ML-KEM-1024 with P-384. */
571
- export const QSF_ml_kem1024_p384: KEM = /* @__PURE__ */ (() =>
679
+ export const QSF_ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
572
680
  QSF(
573
681
  'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
574
682
  ml_kem1024,
@@ -605,18 +713,25 @@ export const QSF_ml_kem1024_p384: KEM = /* @__PURE__ */ (() =>
605
713
  */
606
714
  export function createKitchenSink(
607
715
  label: string,
608
- pqc: KEM,
609
- curveKEM: KEM,
610
- xof: XOF,
716
+ pqc: TArg<KEM>,
717
+ curveKEM: TArg<KEM>,
718
+ xof: TArg<XOF>,
611
719
  hash: CHash
612
- ): KEM {
720
+ ): TRet<KEM> {
721
+ astring(label, 'label');
722
+ validateKEM(pqc, 'pqc');
723
+ validateKEM(curveKEM, 'curveKEM');
724
+ if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
725
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
613
726
  ahash(xof);
727
+ if (typeof hash !== 'function' || typeof (hash as any).create !== 'function')
728
+ throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
614
729
  ahash(hash);
615
730
  return combineKEMS(
616
731
  32,
617
732
  32,
618
733
  expandSeedXof(xof),
619
- (pk, ct, ss) => {
734
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) => {
620
735
  const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
621
736
  const len = 32;
622
737
  const ikm = concatBytes(asciiToBytes('hybrid_prk'), preimage);
@@ -641,7 +756,7 @@ const x25519kem = /* @__PURE__ */ ecdhKem(x25519);
641
756
  /** KitchenSink preset combining ML-KEM-768 with X25519.
642
757
  * Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
643
758
  */
644
- export const KitchenSink_ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
759
+ export const KitchenSink_ml_kem768_x25519: TRet<KEM> = /* @__PURE__ */ (() =>
645
760
  createKitchenSink(
646
761
  'KitchenSink-KEM(ML-KEM-768,X25519)-XOF(SHAKE256)-KDF(HKDF-SHA-256)',
647
762
  ml_kem768,
@@ -655,13 +770,14 @@ export const KitchenSink_ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
655
770
  * Uses the hard-coded domain-separation label `\\.//^\\` and hashes only `ct1 || pk1`
656
771
  * from the X25519 side in addition to the two component shared secrets.
657
772
  */
658
- export const ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
773
+ export const ml_kem768_x25519: TRet<KEM> = /* @__PURE__ */ (() =>
659
774
  combineKEMS(
660
775
  32,
661
776
  32,
662
777
  expandSeedXof(shake256),
663
778
  // Awesome label, so much escaping hell in a single line.
664
- (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes('\\.//^\\'))),
779
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
780
+ sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes('\\.//^\\'))),
665
781
  ml_kem768,
666
782
  x25519kem
667
783
  ))();
@@ -674,12 +790,15 @@ export const ml_kem768_x25519: KEM = /* @__PURE__ */ (() =>
674
790
  * prefix, not the SEC 1 `x_P`-only primitive output, because current hybrid combiners hash
675
791
  * both coordinates.
676
792
  */
677
- function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: number): KEM {
793
+ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: number): TRet<KEM> {
678
794
  const Fn = curve.Point.Fn;
679
795
  if (!Fn) throw new Error('no Point.Fn');
680
796
  // Scan scalar-sized windows until one decodes to a nonzero scalar in `[1, n-1]`; if every
681
797
  // window is zero or out of range, fail instead of silently reducing modulo `n`.
682
- function rejectionSampling(seed: Uint8Array): { secretKey: Uint8Array; publicKey: Uint8Array } {
798
+ function rejectionSampling(seed: TArg<Uint8Array>): TRet<{
799
+ secretKey: Uint8Array;
800
+ publicKey: Uint8Array;
801
+ }> {
683
802
  let sk: bigint;
684
803
  for (let start = 0, end = scalarLen; ; start = end, end += scalarLen) {
685
804
  if (end > seed.length) throw new Error('rejection sampling failed');
@@ -688,9 +807,17 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
688
807
  }
689
808
  const secretKey = Fn.toBytes(Fn.create(sk));
690
809
  const publicKey = curve.getPublicKey(secretKey, false);
691
- return { secretKey, publicKey };
810
+ return { secretKey, publicKey } as TRet<{
811
+ secretKey: Uint8Array;
812
+ publicKey: Uint8Array;
813
+ }>;
692
814
  }
693
815
 
816
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
817
+ const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
818
+ const full = curve.getSharedSecret(secretKey, cipherText);
819
+ return full.subarray(1) as TRet<Uint8Array>;
820
+ };
694
821
  return {
695
822
  lengths: {
696
823
  secretKey: scalarLen,
@@ -699,20 +826,20 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
699
826
  msg: nseed,
700
827
  cipherText: elemLen,
701
828
  },
702
- keygen(seed: Uint8Array = randomBytes(nseed)) {
829
+ keygen(seed: TArg<Uint8Array> = randomBytes(nseed)) {
703
830
  abytes(seed, nseed, 'seed');
704
831
  return rejectionSampling(seed);
705
832
  },
706
- getPublicKey(secretKey: Uint8Array) {
707
- return curve.getPublicKey(secretKey, false);
833
+ getPublicKey(secretKey: TArg<Uint8Array>) {
834
+ return curve.getPublicKey(secretKey, false) as TRet<Uint8Array>;
708
835
  },
709
- encapsulate(publicKey: Uint8Array, rand: Uint8Array = randomBytes(nseed)) {
836
+ encapsulate(publicKey: TArg<Uint8Array>, rand: TArg<Uint8Array> = randomBytes(nseed)) {
710
837
  abytes(rand, nseed, 'rand');
711
838
  let ek: Uint8Array | undefined = undefined;
712
839
  try {
713
840
  ek = rejectionSampling(rand).secretKey;
714
- const sharedSecret = this.decapsulate(publicKey, ek);
715
- const cipherText = curve.getPublicKey(ek, false);
841
+ const sharedSecret = decapsulate(publicKey, ek);
842
+ const cipherText = curve.getPublicKey(ek, false) as TRet<Uint8Array>;
716
843
  return { sharedSecret, cipherText };
717
844
  } finally {
718
845
  // Rejection-sampled NIST-curve ephemeral secret keys are temporary encapsulation state and
@@ -720,10 +847,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
720
847
  if (ek) cleanBytes(ek);
721
848
  }
722
849
  },
723
- decapsulate(cipherText: Uint8Array, secretKey: Uint8Array) {
724
- const full = curve.getSharedSecret(secretKey, cipherText);
725
- return full.subarray(1);
726
- },
850
+ decapsulate,
727
851
  };
728
852
  }
729
853
 
@@ -735,7 +859,12 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
735
859
  * `shake256(seed, { dkLen: 64 + nseed })`,
736
860
  * and the combiner hashes `ss0 || ss1 || ct1 || pk1 || label`.
737
861
  */
738
- function concreteHybridKem(label: string, mlkem: KEM, curve: ECDSA, nseed: number): KEM {
862
+ function concreteHybridKem(
863
+ label: string,
864
+ mlkem: TArg<KEM>,
865
+ curve: ECDSA,
866
+ nseed: number
867
+ ): TRet<KEM> {
739
868
  const { secretKey: scalarLen, publicKeyUncompressed: elemLen } = curve.lengths;
740
869
  if (!scalarLen || !elemLen) throw new Error('wrong curve');
741
870
  const curveKem = nistCurveKem(curve, scalarLen, elemLen, nseed);
@@ -745,40 +874,25 @@ function concreteHybridKem(label: string, mlkem: KEM, curve: ECDSA, nseed: numbe
745
874
  return combineKEMS(
746
875
  32,
747
876
  32,
748
- (seed: Uint8Array) => {
877
+ (seed: TArg<Uint8Array>): TRet<Uint8Array> => {
749
878
  abytes(seed, 32);
750
- const expanded = shake256(seed, { dkLen: totalSeedLen });
751
- const mlkemSeed = expanded.subarray(0, mlkemSeedLen);
752
- const curveSeed = expanded.subarray(mlkemSeedLen, totalSeedLen);
753
- return concatBytes(mlkemSeed, curveSeed);
879
+ // One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
880
+ // Returned directly: the previous concatBytes of two adjacent subarrays produced an
881
+ // identical copy while leaving this original buffer unwiped; expandDecapsulationKey
882
+ // wipes the returned buffer after the child seeds are copied out.
883
+ return shake256(seed, { dkLen: totalSeedLen }) as TRet<Uint8Array>;
754
884
  },
755
- (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
885
+ (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
886
+ sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
756
887
  mlkem,
757
888
  curveKem
758
889
  );
759
890
  }
760
891
 
761
892
  /** P-256 + ML-KEM-768 hybrid preset. */
762
- export const ml_kem768_p256: KEM = /* @__PURE__ */ (() =>
893
+ export const ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
763
894
  concreteHybridKem('MLKEM768-P256', ml_kem768, p256, 128))();
764
895
 
765
896
  /** P-384 + ML-KEM-1024 hybrid preset. */
766
- export const ml_kem1024_p384: KEM = /* @__PURE__ */ (() =>
897
+ export const ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
767
898
  concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
768
-
769
- // Legacy aliases
770
- /** Legacy alias for `ml_kem768_x25519`. */
771
- export const XWing: KEM = /* @__PURE__ */ (() => ml_kem768_x25519)();
772
- /** Legacy alias for `ml_kem768_x25519`. */
773
- export const MLKEM768X25519: KEM = /* @__PURE__ */ (() => ml_kem768_x25519)();
774
- /** Legacy alias for `ml_kem768_p256`. */
775
- export const MLKEM768P256: KEM = /* @__PURE__ */ (() => ml_kem768_p256)();
776
- /** Legacy alias for `ml_kem1024_p384`. */
777
- export const MLKEM1024P384: KEM = /* @__PURE__ */ (() => ml_kem1024_p384)();
778
- /** Legacy alias for `QSF_ml_kem768_p256`. */
779
- export const QSFMLKEM768P256: KEM = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
780
- /** Legacy alias for `QSF_ml_kem1024_p384`. */
781
- export const QSFMLKEM1024P384: KEM = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
782
- /** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
783
- export const KitchenSinkMLKEM768X25519: KEM = /* @__PURE__ */ (() =>
784
- KitchenSink_ml_kem768_x25519)();