@noble/post-quantum 0.6.1 → 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/slh-dsa.js CHANGED
@@ -28,9 +28,10 @@
28
28
  */
29
29
  /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
30
30
  import { hmac } from '@noble/hashes/hmac.js';
31
+ import { bytesToNumberBE, numberToBytesBE } from '@noble/curves/utils.js';
31
32
  import { sha256, sha512 } from '@noble/hashes/sha2.js';
32
33
  import { shake256 } from '@noble/hashes/sha3.js';
33
- import { bytesToHex, concatBytes, createView, hexToBytes, } from '@noble/hashes/utils.js';
34
+ import { concatBytes, createView } from '@noble/hashes/utils.js';
34
35
  import { abytes, checkHash, cleanBytes, copyBytes, equalBytes, getMask, getMessage, getMessagePrehash, randomBytes, splitCoder, validateSigOpts, validateVerOpts, vecCoder, } from "./utils.js";
35
36
  /** Winternitz signature params. */
36
37
  /**
@@ -61,19 +62,6 @@ const AddressType = {
61
62
  WOTSPRF: 5,
62
63
  FORSPRF: 6,
63
64
  };
64
- function hexToNumber(hex) {
65
- if (typeof hex !== 'string')
66
- throw new Error('hex string expected, got ' + typeof hex);
67
- return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
68
- }
69
- // BE: Big Endian, LE: Little Endian. This is the local FIPS 205 `toInt(...)` equivalent.
70
- function bytesToNumberBE(bytes) {
71
- return hexToNumber(bytesToHex(bytes));
72
- }
73
- // Local in-range FIPS 205 `toByte(x, n)` equivalent; callers must keep `n < 256^len`.
74
- function numberToBytesBE(n, len) {
75
- return hexToBytes(n.toString(16).padStart(len * 2, '0'));
76
- }
77
65
  // Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
78
66
  // order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
79
67
  // short inputs are not rejected and would zero-extend implicitly.
@@ -92,8 +80,11 @@ const base2b = (outLen, b) => {
92
80
  return baseB;
93
81
  };
94
82
  };
83
+ const _1n = /* @__PURE__ */ BigInt(1);
84
+ const _8n = /* @__PURE__ */ BigInt(8);
85
+ const _0xffn = /* @__PURE__ */ BigInt(0xff);
95
86
  function getMaskBig(bits) {
96
- return (1n << BigInt(bits)) - 1n; // 4 -> 0b1111
87
+ return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
97
88
  }
98
89
  /** One parameter/hash instantiation of the public SLH-DSA API.
99
90
  * `keygen(seed)` is a deterministic 3N-byte library hook around the internal keygen flow,
@@ -139,7 +130,6 @@ function gen(opts, hashOpts_) {
139
130
  const setAddr = (opts, addr = new Uint8Array(ADDR_BYTES)) => {
140
131
  const { type, height, tree, layer, index, chain, hash, keypair } = opts;
141
132
  const { subtreeAddr, keypairAddr } = opts;
142
- const v = createView(addr);
143
133
  if (height !== undefined)
144
134
  addr[OFFSET_CHAIN_ADDR] = height;
145
135
  if (layer !== undefined)
@@ -150,12 +140,21 @@ function gen(opts, hashOpts_) {
150
140
  addr[OFFSET_CHAIN_ADDR] = chain;
151
141
  if (hash !== undefined)
152
142
  addr[OFFSET_HASH_ADDR] = hash;
153
- if (index !== undefined)
154
- v.setUint32(OFFSET_TREE_INDEX, index, false);
143
+ // Manual big-endian writes: setAddr runs in the innermost WOTS/tree loops, and creating a
144
+ // DataView per call was a measurable share of sign() time.
145
+ if (index !== undefined) {
146
+ addr[OFFSET_TREE_INDEX + 0] = index >>> 24;
147
+ addr[OFFSET_TREE_INDEX + 1] = index >>> 16;
148
+ addr[OFFSET_TREE_INDEX + 2] = index >>> 8;
149
+ addr[OFFSET_TREE_INDEX + 3] = index;
150
+ }
155
151
  if (subtreeAddr)
156
152
  addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
157
- if (tree !== undefined)
158
- v.setBigUint64(OFFSET_TREE, tree, false);
153
+ if (tree !== undefined) {
154
+ let t = tree;
155
+ for (let i = 7; i >= 0; i--, t >>= _8n)
156
+ addr[OFFSET_TREE + i] = Number(t & _0xffn);
157
+ }
159
158
  if (keypair !== undefined) {
160
159
  addr[OFFSET_KP_ADDR1] = keypair;
161
160
  if (TREE_HEIGHT > 8)
@@ -209,10 +208,12 @@ function gen(opts, hashOpts_) {
209
208
  const maxIdx = (1 << height) - 1;
210
209
  const stack = new Uint8Array(height * N);
211
210
  const authPath = new Uint8Array(height * N);
211
+ // One node buffer per treehash call (not per leaf): both halves are fully overwritten at
212
+ // each use, and the returned root aliases cur1, which is never reused after return.
213
+ const current = new Uint8Array(2 * N);
214
+ const cur0 = current.subarray(0, N);
215
+ const cur1 = current.subarray(N);
212
216
  for (let idx = 0;; idx++) {
213
- const current = new Uint8Array(2 * N);
214
- const cur0 = current.subarray(0, N);
215
- const cur1 = current.subarray(N);
216
217
  const addrOffset = idx + idxOffset;
217
218
  cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
218
219
  let h = 0;
@@ -394,7 +395,9 @@ function gen(opts, hashOpts_) {
394
395
  height: 0,
395
396
  index: indices[i] + idxOffset,
396
397
  }, forsTreeAddr);
397
- const prf = context.PRFaddr(forsTreeAddr);
398
+ // Copy: PRFaddr returns a per-context scratch view, and this value is retained in
399
+ // `fors` across the many PRFaddr calls inside forsTreehash below.
400
+ const prf = copyBytes(context.PRFaddr(forsTreeAddr));
398
401
  setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
399
402
  const { root, authPath } = forsTreehash(context, indices[i], idxOffset, forsTreeAddr, forsLeaf);
400
403
  roots.push(root);
@@ -404,7 +407,9 @@ function gen(opts, hashOpts_) {
404
407
  type: AddressType.FORSPK,
405
408
  keypairAddr: wotsAddr,
406
409
  });
407
- const root = context.thashN(K, concatBytes(...roots), forsPkAddr);
410
+ // Copy: thashN returns a per-context scratch view, and `root` lives across every hash
411
+ // call in the hypertree loop below (it is also mutated via root.set).
412
+ const root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr));
408
413
  // WOTS signatures
409
414
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
410
415
  const wots = [];
@@ -424,10 +429,14 @@ function gen(opts, hashOpts_) {
424
429
  },
425
430
  verify: (sig, msg, publicKey) => {
426
431
  const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
427
- const [random, forsVec, wotsVec] = sigCoder.decode(sig);
428
432
  const pk = publicKey;
433
+ // FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
434
+ // (same as ml-dsa). Must run before sigCoder.decode, which throws on length mismatch.
435
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
436
+ abytes(sig, undefined, 'signature');
429
437
  if (sig.length !== sigCoder.bytesLen)
430
438
  return false;
439
+ const [random, forsVec, wotsVec] = sigCoder.decode(sig);
431
440
  const context = getContext(pkSeed);
432
441
  let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
433
442
  const wotsAddr = setAddr({
@@ -447,14 +456,16 @@ function gen(opts, hashOpts_) {
447
456
  const idxOffset = i << A;
448
457
  setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
449
458
  const leaf = context.thash1(prf, forsTreeAddr);
450
- // Compute inplace, because we need all roots in same byte array
451
- roots.push(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr));
459
+ // Copy: computeRoot returns a thashN scratch view, and roots are retained across the
460
+ // remaining FORS iterations (computeRoot itself copies `leaf` before hashing).
461
+ roots.push(copyBytes(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr)));
452
462
  }
453
463
  const forsPkAddr = setAddr({
454
464
  type: AddressType.FORSPK,
455
465
  keypairAddr: wotsAddr,
456
466
  });
457
- let root = context.thashN(K, concatBytes(...roots), forsPkAddr); // root = thash()
467
+ // Copy: `root` must survive the thash1/thashN calls of the WOTS chain loop below.
468
+ let root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr)); // root = thash()
458
469
  // WOTS signature
459
470
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
460
471
  const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
@@ -477,7 +488,8 @@ function gen(opts, hashOpts_) {
477
488
  }
478
489
  }
479
490
  const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
480
- root = computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr);
491
+ // Copy: `root` is read by chainLengths / equalBytes after later hash calls.
492
+ root = copyBytes(computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr));
481
493
  leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
482
494
  }
483
495
  return equalBytes(root, pubRoot);
@@ -533,21 +545,26 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
533
545
  // for each address-bound call instead of reabsorbing the same seed every time.
534
546
  const h0 = shake256.create({}).update(pubSeed);
535
547
  const h0tmp = h0.clone();
548
+ // Per-context output scratch: thash1/thashN/PRFaddr return these buffers directly, so
549
+ // callers must consume or copy a result before the next call on the same lane.
550
+ const thashOut = new Uint8Array(N);
551
+ const prfOut = new Uint8Array(N);
536
552
  const thash = (blocks, input, addr) => {
537
553
  stats.thash++;
538
- return h0
539
- ._cloneInto(h0tmp)
554
+ const len = blocks * N;
555
+ h0._cloneInto(h0tmp)
540
556
  .update(addr)
541
- .update(input.subarray(0, blocks * N))
542
- .xof(N);
557
+ .update(input.length === len ? input : input.subarray(0, len))
558
+ .xofInto(thashOut);
559
+ return thashOut;
543
560
  };
544
561
  return {
545
562
  PRFaddr: (addr) => {
546
563
  if (!skSeed)
547
564
  throw new Error('no sk seed');
548
565
  stats.prf++;
549
- const res = h0._cloneInto(h0tmp).update(addr).update(skSeed).xof(N);
550
- return res;
566
+ h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
567
+ return prfOut;
551
568
  },
552
569
  PRFmsg: (skPRF, random, msg) => {
553
570
  stats.gen_message_random++;
@@ -568,6 +585,7 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
568
585
  clean: () => {
569
586
  h0.destroy();
570
587
  h0tmp.destroy();
588
+ cleanBytes(thashOut, prfOut);
571
589
  //console.log(stats);
572
590
  },
573
591
  };
@@ -635,6 +653,16 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
635
653
  .update(new Uint8Array(h1.blockLen - N));
636
654
  const h0tmp = h0ps.clone();
637
655
  const h1tmp = h1ps.clone();
656
+ // Per-context output scratch: thash1/thashN/PRFaddr return views into these buffers, so
657
+ // callers must consume or copy a result before the next call on the same lane (see Context
658
+ // docs). digestInto also skips digest()'s per-call destroy(): the tmp states are fully
659
+ // overwritten by the next _cloneInto and wiped in clean().
660
+ const h0out = new Uint8Array(h0.outputLen);
661
+ const h1out = new Uint8Array(h1.outputLen);
662
+ const prfOut = new Uint8Array(h0.outputLen);
663
+ const h0outN = h0out.subarray(0, N);
664
+ const h1outN = h1out.subarray(0, N);
665
+ const prfOutN = prfOut.subarray(0, N);
638
666
  // https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
639
667
  // This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
640
668
  // only request tiny `m`-byte outputs, but the guard below rejects `length > 2^32` instead of
@@ -653,27 +681,26 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
653
681
  cleanBytes(out.subarray(length));
654
682
  return out.subarray(0, length);
655
683
  }
656
- const thash = (_, h, hTmp) => (blocks, input, addr) => {
684
+ const thash = (h, hTmp, out, outN) => (blocks, input, addr) => {
657
685
  stats.thash++;
658
- const d = h
659
- ._cloneInto(hTmp)
686
+ const len = blocks * N;
687
+ h._cloneInto(hTmp)
660
688
  .update(addr)
661
- .update(input.subarray(0, blocks * N))
662
- .digest();
663
- return d.subarray(0, N);
689
+ .update(input.length === len ? input : input.subarray(0, len))
690
+ .digestInto(out);
691
+ return outN;
664
692
  };
665
693
  return {
666
694
  PRFaddr: (addr) => {
667
695
  if (!sk_seed)
668
696
  throw new Error('No sk seed');
669
697
  stats.prf++;
670
- const res = h0ps
698
+ h0ps
671
699
  ._cloneInto(h0tmp)
672
700
  .update(addr)
673
701
  .update(sk_seed)
674
- .digest()
675
- .subarray(0, N);
676
- return res;
702
+ .digestInto(prfOut);
703
+ return prfOutN;
677
704
  },
678
705
  PRFmsg: (skPRF, random, msg) => {
679
706
  stats.gen_message_random++;
@@ -689,13 +716,14 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
689
716
  const seed = concatBytes(R.subarray(0, N), pk.subarray(0, N), h1.create().update(R.subarray(0, N)).update(pk).update(m).digest());
690
717
  return mgf1(seed, outLen, h1);
691
718
  },
692
- thash1: thash(h0, h0ps, h0tmp).bind(null, 1),
693
- thashN: thash(h1, h1ps, h1tmp),
719
+ thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
720
+ thashN: thash(h1ps, h1tmp, h1out, h1outN),
694
721
  clean: () => {
695
722
  h0ps.destroy();
696
723
  h1ps.destroy();
697
724
  h0tmp.destroy();
698
725
  h1tmp.destroy();
726
+ cleanBytes(h0out, h1out, prfOut);
699
727
  //console.log(stats);
700
728
  },
701
729
  };
@@ -712,6 +740,23 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
712
740
  * SLH-DSA-SHA2-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
713
741
  * lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
714
742
  * Also exposes `.prehash(...)`.
743
+ * @example
744
+ * Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
745
+ * ```ts
746
+ * import { sha256 } from '@noble/hashes/sha2.js';
747
+ * import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
748
+ * const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
749
+ * const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
750
+ * const msg = new TextEncoder().encode('hello noble');
751
+ * const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
752
+ * const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
753
+ * const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
754
+ * const context = new Uint8Array([1, 2, 3]);
755
+ * const prehash = slh_dsa_sha2_128f.prehash(sha256);
756
+ * const preSig = prehash.sign(msg, secretKey, { context });
757
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
758
+ * const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
759
+ * ```
715
760
  */
716
761
  export const slh_dsa_sha2_128f = /* @__PURE__ */ (() => gen(PARAMS['128f'], SHA256_SIMPLE))();
717
762
  /**
@@ -744,4 +789,3 @@ export const slh_dsa_sha2_256f = /* @__PURE__ */ (() => gen(PARAMS['256f'], SHA5
744
789
  * Also exposes `.prehash(...)`.
745
790
  */
746
791
  export const slh_dsa_sha2_256s = /* @__PURE__ */ (() => gen(PARAMS['256s'], SHA512_SIMPLE))();
747
- //# sourceMappingURL=slh-dsa.js.map
package/src/_crystals.ts CHANGED
@@ -73,9 +73,15 @@ type Crystals<T extends TypedArray> = {
73
73
  smod: (a: number, modulo?: number) => number;
74
74
  nttZetas: T;
75
75
  NTT: {
76
- /** Forward transform in place. Mutates and returns `r`. */
76
+ /**
77
+ * Forward transform in place. Mutates and returns `r`.
78
+ * Kyber-mode input coefficients must already use canonical representatives in `[0, Q)`.
79
+ */
77
80
  encode: (r: T) => T;
78
- /** Inverse transform in place. Mutates and returns `r`. */
81
+ /**
82
+ * Inverse transform in place. Mutates and returns `r`.
83
+ * Kyber-mode input coefficients must already use canonical representatives in `[0, Q)`.
84
+ */
79
85
  decode: (r: T) => T;
80
86
  };
81
87
  bitsCoder: (d: number, c: Coder<number, number>) => BytesCoderLen<T>;
@@ -135,14 +141,34 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
135
141
  // Kyber has slightly different params, since there is no 512th primitive root of unity mod q,
136
142
  // only 256th primitive root of unity mod. Which also complicates MultiplyNTT.
137
143
 
138
- const field = {
139
- add: (a: number, b: number) => mod((a | 0) + (b | 0)) | 0,
140
- sub: (a: number, b: number) => mod((a | 0) - (b | 0)) | 0,
141
- mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
142
- inv: (_a: number) => {
143
- throw new Error('not implemented');
144
- },
144
+ const inv = (_a: number) => {
145
+ throw new Error('not implemented');
145
146
  };
147
+ // ML-KEM (Kyber) polynomials always enter the transform reduced to [0, Q), so add/sub only
148
+ // need one conditional correction instead of `%`; measured ~20% faster NTT there.
149
+ // ML-DSA keeps the generic mod() path on purpose: its first forward stage sees centered
150
+ // (negative) coefficients, and `sub(a, t)` can drop below -Q (t is a mul output in [0, Q)),
151
+ // so a single correction is not enough. A guarded fast path with mod() fallback was measured
152
+ // slower than plain `%` for the 23-bit Q (V8 int32 modulo is one div; the branches lose).
153
+ const field = isKyber
154
+ ? {
155
+ add: (a: number, b: number) => {
156
+ const r = (a + b) | 0;
157
+ return r >= Q ? (r - Q) | 0 : r;
158
+ },
159
+ sub: (a: number, b: number) => {
160
+ const r = (a - b) | 0;
161
+ return r < 0 ? (r + Q) | 0 : r;
162
+ },
163
+ mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
164
+ inv,
165
+ }
166
+ : {
167
+ add: (a: number, b: number) => mod((a | 0) + (b | 0)) | 0,
168
+ sub: (a: number, b: number) => mod((a | 0) - (b | 0)) | 0,
169
+ mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
170
+ inv,
171
+ };
146
172
  const nttOpts = {
147
173
  N,
148
174
  roots: nttZetas as any,
@@ -168,6 +194,12 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
168
194
  // Pack one little-endian `d`-bit word per coefficient, matching FIPS 203 ByteEncode /
169
195
  // ByteDecode and the FIPS 204 BitsToBytes-based polynomial packing helpers.
170
196
  const bitsCoder = (d: number, c: Coder<number, number>): TRet<BytesCoderLen<T>> => {
197
+ // Validate the carry shape once: JS bitwise operations silently truncate wider accumulators.
198
+ for (let i = 0, bufLen = 0; i < N; i++) {
199
+ bufLen += d;
200
+ if (bufLen > 32) getMask(bufLen);
201
+ bufLen %= 8;
202
+ }
171
203
  const mask = getMask(d);
172
204
  const bytesLen = d * (N / 8);
173
205
  return {
@@ -178,7 +210,9 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
178
210
  for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) {
179
211
  buf |= (c.encode(poly[i]) & mask) << bufLen;
180
212
  bufLen += d;
181
- for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & getMask(bufLen);
213
+ // Take the low byte directly: `& 0xff` matches the previous getMask(bufLen) result
214
+ // after Uint8Array truncation, without a validated function call per output byte.
215
+ for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & 0xff;
182
216
  }
183
217
  return r as TRet<Uint8Array>;
184
218
  },
package/src/falcon.ts CHANGED
@@ -443,6 +443,17 @@ const Q: number = 12289; // 12 * 1024 + 1
443
443
  // Falcon's midpoint floor(q/2); the only live use is the mirrored G-reconstruction reduction below.
444
444
  const Qhalf: number = Q >> 1;
445
445
  const QBig = BigInt(Q);
446
+ const _0n = /* @__PURE__ */ BigInt(0);
447
+ const _1n = /* @__PURE__ */ BigInt(1);
448
+ const _10n = /* @__PURE__ */ BigInt(10);
449
+ const _25n = /* @__PURE__ */ BigInt(25);
450
+ const _31n = /* @__PURE__ */ BigInt(31);
451
+ const _32n = /* @__PURE__ */ BigInt(32);
452
+ const _63n = /* @__PURE__ */ BigInt(63);
453
+ const _64n = /* @__PURE__ */ BigInt(64);
454
+ // Low 32 bits and low 63 bits of a bigint, used by the chacha20 counter and gaussian sampler.
455
+ const MASK_32n = /* @__PURE__ */ BigInt('0xffffffff');
456
+ const MASK_63n = /* @__PURE__ */ BigInt('0x7fffffffffffffff');
446
457
  //const R = 4091; // 2^16 mod q
447
458
  // This 16-bit Montgomery kernel uses R = 2^16, so mul(x, R2) converts x into Montgomery form.
448
459
  const R2 = 10952; // 2^32 mod q
@@ -479,35 +490,35 @@ const BITLENGTH = [
479
490
  // Smaller Falcon dimensions reuse the N = 1024, q = 12289 table by summing 2^(10-logn) draws.
480
491
  // The trailing 0 sentinel guarantees gaussSingle()
481
492
  // always selects a tail bucket when x = 0 is missed.
482
- const gauss_1024_12289 = [
483
- 1283868770400643928n,
484
- 6416574995475331444n,
485
- 4078260278032692663n,
486
- 2353523259288686585n,
487
- 1227179971273316331n,
488
- 575931623374121527n,
489
- 242543240509105209n,
490
- 91437049221049666n,
491
- 30799446349977173n,
492
- 9255276791179340n,
493
- 2478152334826140n,
494
- 590642893610164n,
495
- 125206034929641n,
496
- 23590435911403n,
497
- 3948334035941n,
498
- 586753615614n,
499
- 77391054539n,
500
- 9056793210n,
501
- 940121950n,
502
- 86539696n,
503
- 7062824n,
504
- 510971n,
505
- 32764n,
506
- 1862n,
507
- 94n,
508
- 4n,
509
- 0n,
510
- ];
493
+ const gauss_1024_12289 = /* @__PURE__ */ [
494
+ '1283868770400643928',
495
+ '6416574995475331444',
496
+ '4078260278032692663',
497
+ '2353523259288686585',
498
+ '1227179971273316331',
499
+ '575931623374121527',
500
+ '242543240509105209',
501
+ '91437049221049666',
502
+ '30799446349977173',
503
+ '9255276791179340',
504
+ '2478152334826140',
505
+ '590642893610164',
506
+ '125206034929641',
507
+ '23590435911403',
508
+ '3948334035941',
509
+ '586753615614',
510
+ '77391054539',
511
+ '9056793210',
512
+ '940121950',
513
+ '86539696',
514
+ '7062824',
515
+ '510971',
516
+ '32764',
517
+ '1862',
518
+ '94',
519
+ '4',
520
+ '0',
521
+ ].map(BigInt);
511
522
 
512
523
  // Exact binary64 1/sigma payloads from round-3 fpr.h. Nearby decimal spellings round 1 ULP low in
513
524
  // JS, so keep these as decoded bit patterns and recheck the raw payloads after edits.
@@ -1248,9 +1259,9 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1248
1259
  let val = 0;
1249
1260
  for (let i = 0; i < g; i++) {
1250
1261
  const r128 = bytesToNumberLE(this.shake.xof(16));
1251
- const r1 = r128 & 0x7fffffffffffffffn;
1252
- const r2 = (r128 >> 64n) & 0x7fffffffffffffffn;
1253
- const sign = Number((r128 >> 63n) & 1n);
1262
+ const r1 = r128 & MASK_63n;
1263
+ const r2 = (r128 >> _64n) & MASK_63n;
1264
+ const sign = Number((r128 >> _63n) & _1n);
1254
1265
  let f = r1 < gauss_1024_12289[0] ? 1 : 0;
1255
1266
  let v = 0;
1256
1267
  for (let k = 1; k < gauss_1024_12289.length; k++) {
@@ -1283,13 +1294,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1283
1294
  const n = 1 << logn;
1284
1295
  const d = new Array(n >> 1);
1285
1296
  for (let k = 0; k < n; k += 2) {
1286
- let s: bigint = 0n;
1297
+ let s: bigint = _0n;
1287
1298
  for (let i = 0; i <= k; i += 2) s += a[i] * a[k - i];
1288
1299
  for (let i = k + 2; i < n; i += 2) s -= a[i] * a[k + n - i];
1289
1300
  d[k >>> 1] = s;
1290
1301
  }
1291
1302
  for (let k = 0; k < n; k += 2) {
1292
- let s: bigint = 0n;
1303
+ let s: bigint = _0n;
1293
1304
  for (let i = 1; i < k; i += 2) s += a[i] * a[k - i];
1294
1305
  for (let i = k + 1; i < n; i += 2) s -= a[i] * a[k + n - i];
1295
1306
  d[k >>> 1] -= s;
@@ -1299,7 +1310,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1299
1310
  private mulConjD(logn: number, d: BPoly, a: BPoly, b: BPoly): BPoly {
1300
1311
  const n = 1 << logn;
1301
1312
  for (let k = 0; k < n; k++) {
1302
- let s: bigint = 0n;
1313
+ let s: bigint = _0n;
1303
1314
  for (let i = 0; i <= k; i += 2) s += b[i >>> 1] * a[k - i];
1304
1315
  for (let i = k + 2 - (k & 1); i < n; i += 2) s -= b[i >>> 1] * a[k + n - i];
1305
1316
  if ((k & 1) === 0) d[k] = s;
@@ -1310,7 +1321,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1310
1321
  private subMul(logn: number, a: BPoly, b: BPoly, c: BPoly, e: bigint): BPoly {
1311
1322
  const n = 1 << logn;
1312
1323
  for (let k = 0; k < n; k++) {
1313
- let s: bigint = 0n;
1324
+ let s: bigint = _0n;
1314
1325
  for (let i = 0; i <= k; i++) s += b[i] * c[k - i];
1315
1326
  for (let i = k + 1; i < n; i++) s -= b[i] * c[k + n - i];
1316
1327
  a[k] -= s << e;
@@ -1353,7 +1364,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1353
1364
  const Gx = new Float64Array(n);
1354
1365
  const k = new Array(n);
1355
1366
  while (true) {
1356
- let scaleFG = 31n * (FGlen - 10n);
1367
+ let scaleFG = _31n * (FGlen - _10n);
1357
1368
  for (let i = 0; i < n; i++) {
1358
1369
  Fx[i] = Number(F[i] >> scaleFG);
1359
1370
  Gx[i] = Number(G[i] >> scaleFG);
@@ -1373,12 +1384,12 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1373
1384
  }
1374
1385
  F = this.subMul(logn, F, f, k, scaleK); // 3: F ← F - kf
1375
1386
  G = this.subMul(logn, G, g, k, scaleK); // 4: G ← G - kg
1376
- const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + 10n;
1387
+ const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + _10n;
1377
1388
  if (maxfgNew < maxFGBits) maxFGBits = maxfgNew;
1378
- if (FGlen > 1n && FGlen * 31n >= maxFGBits + 31n) FGlen--;
1379
- if (scaleK <= 0n) break;
1380
- scaleK -= 25n;
1381
- if (scaleK < 0n) scaleK = 0n;
1389
+ if (FGlen > _1n && FGlen * _31n >= maxFGBits + _31n) FGlen--;
1390
+ if (scaleK <= _0n) break;
1391
+ scaleK -= _25n;
1392
+ if (scaleK < _0n) scaleK = _0n;
1382
1393
  }
1383
1394
  return true;
1384
1395
  }
@@ -1406,10 +1417,10 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1406
1417
  const xf = f[0];
1407
1418
  const xg = g[0];
1408
1419
  // We can rely on 'invert' to throw if they are not coprime.
1409
- if (xf <= 0n || xg <= 0n) return false;
1420
+ if (xf <= _0n || xg <= _0n) return false;
1410
1421
  try {
1411
1422
  const u1 = invert(xf, xg); // if gcd(f, g) ≠ 1 then
1412
- const v1 = (1n - u1 * xf) / xg;
1423
+ const v1 = (_1n - u1 * xf) / xg;
1413
1424
  F[0] = -v1 * QBig; // 5: (F, G) ← (vq, uq)
1414
1425
  G[0] = u1 * QBig;
1415
1426
  return true;
@@ -1672,7 +1683,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1672
1683
  ).encode([nonce, pad(sigLen).encode(s2), msg]);
1673
1684
  },
1674
1685
  decode(data: TArg<Uint8Array>): TRet<SignatureRaw> {
1686
+ // Keep API misuse on the coder's TypeError path before reading the container length.
1687
+ abytes(data, undefined, 'signature');
1688
+ // The compressed-signature field is fixed-width here; only the message is variable. A
1689
+ // container shorter than the fixed part would make the s2 field borrow bytes from nowhere
1690
+ // and let a truncated encoding open to the same message.
1675
1691
  const msgLen = data.length - NONCELEN - sigLen - 1;
1692
+ if (msgLen < 0) throw new Error('signature coder: wrong length');
1676
1693
  const [nonce, s2, msg] = headerCoder(
1677
1694
  0x30 + logn,
1678
1695
  splitCoder('falcon.signature', NONCELEN, sigLen, msgLen)
@@ -1696,9 +1713,15 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1696
1713
  nonce: Uint8Array;
1697
1714
  s2: Uint8Array;
1698
1715
  }> {
1716
+ // Padded detached signatures are fixed-length (`lengths.signature`), so the payload width
1717
+ // must come from the parameter set, not from the input: deriving it would accept appended
1718
+ // zero bytes and truncated padding as extra valid encodings of the same signature.
1719
+ // Unpadded signatures are variable-length; decodeUnpaddedSig() enforces the exact canonical
1720
+ // bitlength of whatever remains.
1721
+ const payloadLen = opts.padded ? sigLen : data.length - NONCELEN - 1;
1699
1722
  const [nonce, raw] = headerCoder(
1700
1723
  0x30 + logn,
1701
- splitCoder('falcon.detachedSignature', NONCELEN, data.length - NONCELEN - 1)
1724
+ splitCoder('falcon.detachedSignature', NONCELEN, payloadLen)
1702
1725
  ).decode(data);
1703
1726
  const s2 = decodeSig(raw);
1704
1727
  return { nonce, s2 } as TRet<{ nonce: Uint8Array; s2: Uint8Array }>;
@@ -1785,7 +1808,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1785
1808
  private shakeBuf: Uint8Array;
1786
1809
  private ctrView: DataView;
1787
1810
  // ChaCha
1788
- private ctr: bigint = 0n;
1811
+ private ctr: bigint = _0n;
1789
1812
  private buf: Uint8Array;
1790
1813
  private buf32: Uint32Array;
1791
1814
  private pos: number;
@@ -1835,8 +1858,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1835
1858
  const out32 = swap32IfBE(this.buf32);
1836
1859
  for (let i = 0; i < 8; i++, this.ctr++) {
1837
1860
  const n = swap32IfBE(this.nonce32.slice()); // [n0, n1, n2, n3]
1838
- n[2] ^= Number(this.ctr & 0xffffffffn);
1839
- n[3] ^= Number(this.ctr >> 32n);
1861
+ n[2] ^= Number(this.ctr & MASK_32n);
1862
+ n[3] ^= Number(this.ctr >> _32n);
1840
1863
  // chacha20() takes raw nonce bytes; on BE the word-normalized temp must be swapped back.
1841
1864
  swap32IfBE(n.subarray(1));
1842
1865
  chacha20(this.key, u8(n.subarray(1)), EMPTY_CHACHA20_BLOCK, this.curBlock, n[0]);
@@ -2181,7 +2204,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2181
2204
  // ▷ Remove 1 byte for the header, and 40 bytes for r
2182
2205
  // 11: while (s = ⊥)
2183
2206
  // 12: return sig = (r, s)
2184
- abytes(msg);
2207
+ abytes(msg, undefined, 'msg');
2208
+ abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
2185
2209
  // One RNG stream drives both the public 40-byte nonce and the 48-byte sampler seed, so
2186
2210
  // deterministic rnd hooks make signatures deterministic for fixed secretKey/message inputs.
2187
2211
  const nonce = rnd(40);
@@ -2289,6 +2313,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2289
2313
  const getRnd = (opts: TArg<FalconSigOpts> = {}): TRet<FalconRandom> => {
2290
2314
  validateSigOpts(opts);
2291
2315
  if (opts.context !== undefined) throw new Error('context is not supported');
2316
+ if (opts.random !== undefined && typeof opts.random !== 'function')
2317
+ throw new TypeError('"opts.random" expected function, got type=' + typeof opts.random);
2292
2318
  if (opts.random !== undefined) return opts.random as TRet<FalconRandom>;
2293
2319
  if (opts.extraEntropy === undefined) return randomBytes;
2294
2320
  const seed = opts.extraEntropy === false ? new Uint8Array(48) : opts.extraEntropy;
@@ -2328,6 +2354,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2328
2354
  }>;
2329
2355
  };
2330
2356
  const getPublicKey = (sk: TArg<Uint8Array>): TRet<Uint8Array> => {
2357
+ abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
2331
2358
  const [f, g, F] = secretKeyCoder.decode(sk);
2332
2359
  try {
2333
2360
  const h = computePublic(f, g);
@@ -2358,9 +2385,10 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2358
2385
  verOpts: TArg<VerOpts> = {}
2359
2386
  ) => {
2360
2387
  checkVerOpts(verOpts);
2361
- abytes(sig);
2362
- abytes(msg);
2363
- abytes(pk);
2388
+ abytes(sig, undefined, 'signature');
2389
+ abytes(msg, undefined, 'msg');
2390
+ // Length/canonical public-key failures are decoded below and return false; only type is fatal.
2391
+ abytes(pk, undefined, 'publicKey');
2364
2392
  try {
2365
2393
  const { s2, nonce } = SignatureCoderDetached(logn).decode(sig);
2366
2394
  return verifyRaw(pk, s2, nonce, msg);