@noble/post-quantum 0.6.1 → 0.7.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/falcon.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  baswap64If,
28
28
  type BytesCoderLen,
29
29
  cleanBytes,
30
+ copyBytes,
30
31
  type Coder,
31
32
  type CryptoKeys,
32
33
  getMask,
@@ -37,6 +38,7 @@ import {
37
38
  type TRet,
38
39
  validateSigOpts,
39
40
  validateVerOpts,
41
+ SIG_OPT_KEYS,
40
42
  type VerOpts,
41
43
  } from './utils.ts';
42
44
  /*
@@ -256,7 +258,12 @@ const compCoder = (n: number) => {
256
258
  const sign = readBits(1);
257
259
  const low = readBits(7);
258
260
  let high = 0;
259
- for (; !readBits(1); high++);
261
+ // Reference comp_decode adds 128 for each unary zero and rejects immediately above 2047.
262
+ // Waiting for the terminating one first lets an invalid coefficient scan the entire input.
263
+ while (!readBits(1)) {
264
+ high++;
265
+ if (high > LIMIT >>> 7) throw new Error(`limit: ${low | (high << 7)} > ${LIMIT}`);
266
+ }
260
267
  const v = low | (high << 7);
261
268
  if (sign && v === 0) throw new Error('negative zero encoding');
262
269
  if (v > LIMIT) throw new Error(`limit: ${v} > ${LIMIT}`);
@@ -443,6 +450,17 @@ const Q: number = 12289; // 12 * 1024 + 1
443
450
  // Falcon's midpoint floor(q/2); the only live use is the mirrored G-reconstruction reduction below.
444
451
  const Qhalf: number = Q >> 1;
445
452
  const QBig = BigInt(Q);
453
+ const _0n = /* @__PURE__ */ BigInt(0);
454
+ const _1n = /* @__PURE__ */ BigInt(1);
455
+ const _10n = /* @__PURE__ */ BigInt(10);
456
+ const _25n = /* @__PURE__ */ BigInt(25);
457
+ const _31n = /* @__PURE__ */ BigInt(31);
458
+ const _32n = /* @__PURE__ */ BigInt(32);
459
+ const _63n = /* @__PURE__ */ BigInt(63);
460
+ const _64n = /* @__PURE__ */ BigInt(64);
461
+ // Low 32 bits and low 63 bits of a bigint, used by the chacha20 counter and gaussian sampler.
462
+ const MASK_32n = /* @__PURE__ */ BigInt('0xffffffff');
463
+ const MASK_63n = /* @__PURE__ */ BigInt('0x7fffffffffffffff');
446
464
  //const R = 4091; // 2^16 mod q
447
465
  // This 16-bit Montgomery kernel uses R = 2^16, so mul(x, R2) converts x into Montgomery form.
448
466
  const R2 = 10952; // 2^32 mod q
@@ -479,35 +497,35 @@ const BITLENGTH = [
479
497
  // Smaller Falcon dimensions reuse the N = 1024, q = 12289 table by summing 2^(10-logn) draws.
480
498
  // The trailing 0 sentinel guarantees gaussSingle()
481
499
  // 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
- ];
500
+ const gauss_1024_12289 = /* @__PURE__ */ [
501
+ '1283868770400643928',
502
+ '6416574995475331444',
503
+ '4078260278032692663',
504
+ '2353523259288686585',
505
+ '1227179971273316331',
506
+ '575931623374121527',
507
+ '242543240509105209',
508
+ '91437049221049666',
509
+ '30799446349977173',
510
+ '9255276791179340',
511
+ '2478152334826140',
512
+ '590642893610164',
513
+ '125206034929641',
514
+ '23590435911403',
515
+ '3948334035941',
516
+ '586753615614',
517
+ '77391054539',
518
+ '9056793210',
519
+ '940121950',
520
+ '86539696',
521
+ '7062824',
522
+ '510971',
523
+ '32764',
524
+ '1862',
525
+ '94',
526
+ '4',
527
+ '0',
528
+ ].map(BigInt);
511
529
 
512
530
  // Exact binary64 1/sigma payloads from round-3 fpr.h. Nearby decimal spellings round 1 ULP low in
513
531
  // JS, so keep these as decoded bit patterns and recheck the raw payloads after edits.
@@ -541,6 +559,9 @@ const SIGMA_MIN = /* @__PURE__ */ Object.freeze([
541
559
  f64b(BigInt('4608433670533905013')),
542
560
  f64b(BigInt('4608525754002622308')),
543
561
  ]);
562
+ // Upper end of the SamplerZ proof interval from Falcon section 3.9.1. The per-leaf sigma is
563
+ // derived from the reconstructed private basis, so imported keys must be checked against it.
564
+ const SIGMA_MAX = 1.8205;
544
565
 
545
566
  // Falcon Table 3.1 RCDT values for chi, split into 24-bit limbs; storage is [high, mid, low],
546
567
  // so gaussian0() intentionally compares them against v0, v1, v2 in reverse order. The final
@@ -1153,6 +1174,54 @@ function getFloatPoly(logn: number) {
1153
1174
  };
1154
1175
  }
1155
1176
 
1177
+ function ldlFFT(logn: number, g00t: CPoly, g01t: CPoly, g11t: CPoly) {
1178
+ // Algorithm 8: LDL*(G)
1179
+ // (Page 37)
1180
+ // Require: A full-rank self-adjoint matrix G = (Gᵢⱼ) ∈ FFT(Q[x]/(φ))²ˣ²
1181
+ // Ensure: The LDL* decomposition G = LDL* over FFT(Q[x]/(φ))
1182
+ // Format: All polynomials are in FFT representation.
1183
+ // 1: D₀₀ ← G₀₀
1184
+ // 2: L₁₀ ← G₁₀/G₀₀
1185
+ // 3: D₁₁ ← G₁₁ - L₁₀ ⊙ L₁₀* ⊙ G₀₀
1186
+ // 4: L ← [ 1 0 ; L₁₀ 1 ], D ← [ D₀₀ 0 ; 0 D₁₁ ]
1187
+ // 5: return (L, D)
1188
+
1189
+ // Algorithm 9: ffLDL*(G)
1190
+ // (Page 37)
1191
+ // Require: A full-rank Gram matrix G ∈ FFT(Q[x]/(xⁿ + 1))²ˣ²
1192
+ // Ensure: A binary tree T
1193
+ // Format: All polynomials are in FFT representation.
1194
+ // 1: (L, D) ← LDL*(G) ▷ L = [ 1 0 ; L₁₀ 1 ], D = [ D₀₀ 0 ; 0 D₁₁ ]
1195
+ // 2: T.value ← L₁₀
1196
+ // 3: if (n = 2) then
1197
+ // 4: T.leftchild ← D₀₀
1198
+ // 5: T.rightchild ← D₁₁
1199
+ // 6: return T
1200
+ // 7: else
1201
+ // 8: d₀₀, d₀₁ ← splitfft(D₀₀) ▷ dᵢⱼ ∈ FFT(Q[x]/(x^{n/2} + 1))
1202
+ // 9: d₁₀, d₁₁ ← splitfft(D₁₁)
1203
+ // 10: G₀ ← [ d₀₀ d₀₁ ; d₀₁* d₀₀ ], G₁ ← [ d₁₀ d₁₁ ; d₁₁* d₁₀ ]
1204
+ // ▷ Since D₀₀, D₁₁ are self-adjoint, (3.30) applies
1205
+ // 11: T.leftchild ← ffLDL*(G₀) ▷ Recursive calls
1206
+ // 12: T.rightchild ← ffLDL*(G₁)
1207
+ // 13: return T
1208
+
1209
+ // Recursive calls may alias g00t and g11t, and the top-level arrays persist across signing
1210
+ // retries. LDL replaces array entries, so shallow copies keep both kinds of caller state intact.
1211
+ g00t = g00t.slice();
1212
+ g01t = g01t.slice();
1213
+ g11t = g11t.slice();
1214
+ const hn = 1 << (logn - 1);
1215
+ for (let i = 0; i < hn; i++) {
1216
+ const g01 = g01t[i];
1217
+ const g11 = g11t[i];
1218
+ const mu = fComplex.scale(g01, 1.0 / g00t[i].re);
1219
+ g11t[i] = { re: g11.re - (mu.re * g01.re + mu.im * g01.im), im: g11.im };
1220
+ g01t[i] = fComplex.conj(mu);
1221
+ }
1222
+ return { g00: g00t, g01: g01t, g11: g11t };
1223
+ }
1224
+
1156
1225
  function ApproxExp(x: number, ccs: number): number {
1157
1226
  // Algorithm 13: ApproxExp(x, ccs), (Page 42)
1158
1227
  // Require: Floating-point values x ∈ [0, ln(2)] and ccs ∈ [0, 1]
@@ -1184,19 +1253,16 @@ function ApproxExp(x: number, ccs: number): number {
1184
1253
  // Actual api
1185
1254
  type FalconOpts = {
1186
1255
  N: number;
1187
- // Table 3.3 total padded detached bytes; kept as reference config, not read by genFalcon() today.
1188
- // In padded mode it still drives `.lengths.signature`
1189
- // and the payload width `sigLen - 1 - NONCELEN`.
1256
+ // Table 3.3 total padded detached bytes. In padded mode it drives `.lengths.signature` and the
1257
+ // payload width `sigLen - 1 - NONCELEN`.
1190
1258
  sigLen: number;
1191
- padded?: boolean;
1259
+ padded: boolean;
1192
1260
  fgBits: number;
1193
1261
  FGBits: number;
1194
1262
  // Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
1195
1263
  paddedLen: number;
1196
- // Max compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
1197
- // Reference unpadded payload ceiling only: detached encode/decode use each signature's runtime
1198
- // `s2` length, while signRaw() enforces `maxS2Len` separately.
1199
- detachedLen: number;
1264
+ // Maximum compressed-s payload emitted by sign(). Unpadded detached verification enforces the
1265
+ // same Round-3 ceiling before decoding.
1200
1266
  maxS2Len: number;
1201
1267
  };
1202
1268
 
@@ -1219,7 +1285,7 @@ export type FalconAttached = CryptoKeys & {
1219
1285
  * @param sig Attached Falcon signature bytes.
1220
1286
  * @param publicKey Falcon public key bytes.
1221
1287
  * @param opts Optional verification options.
1222
- * @returns Embedded message bytes when the signature is valid.
1288
+ * @returns Fresh message bytes that do not alias either input when the signature is valid.
1223
1289
  */
1224
1290
  open(sig: Uint8Array, publicKey: Uint8Array, opts?: VerOpts): Uint8Array;
1225
1291
  };
@@ -1248,9 +1314,9 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1248
1314
  let val = 0;
1249
1315
  for (let i = 0; i < g; i++) {
1250
1316
  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);
1317
+ const r1 = r128 & MASK_63n;
1318
+ const r2 = (r128 >> _64n) & MASK_63n;
1319
+ const sign = Number((r128 >> _63n) & _1n);
1254
1320
  let f = r1 < gauss_1024_12289[0] ? 1 : 0;
1255
1321
  let v = 0;
1256
1322
  for (let k = 1; k < gauss_1024_12289.length; k++) {
@@ -1283,13 +1349,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1283
1349
  const n = 1 << logn;
1284
1350
  const d = new Array(n >> 1);
1285
1351
  for (let k = 0; k < n; k += 2) {
1286
- let s: bigint = 0n;
1352
+ let s: bigint = _0n;
1287
1353
  for (let i = 0; i <= k; i += 2) s += a[i] * a[k - i];
1288
1354
  for (let i = k + 2; i < n; i += 2) s -= a[i] * a[k + n - i];
1289
1355
  d[k >>> 1] = s;
1290
1356
  }
1291
1357
  for (let k = 0; k < n; k += 2) {
1292
- let s: bigint = 0n;
1358
+ let s: bigint = _0n;
1293
1359
  for (let i = 1; i < k; i += 2) s += a[i] * a[k - i];
1294
1360
  for (let i = k + 1; i < n; i += 2) s -= a[i] * a[k + n - i];
1295
1361
  d[k >>> 1] -= s;
@@ -1299,7 +1365,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1299
1365
  private mulConjD(logn: number, d: BPoly, a: BPoly, b: BPoly): BPoly {
1300
1366
  const n = 1 << logn;
1301
1367
  for (let k = 0; k < n; k++) {
1302
- let s: bigint = 0n;
1368
+ let s: bigint = _0n;
1303
1369
  for (let i = 0; i <= k; i += 2) s += b[i >>> 1] * a[k - i];
1304
1370
  for (let i = k + 2 - (k & 1); i < n; i += 2) s -= b[i >>> 1] * a[k + n - i];
1305
1371
  if ((k & 1) === 0) d[k] = s;
@@ -1310,7 +1376,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1310
1376
  private subMul(logn: number, a: BPoly, b: BPoly, c: BPoly, e: bigint): BPoly {
1311
1377
  const n = 1 << logn;
1312
1378
  for (let k = 0; k < n; k++) {
1313
- let s: bigint = 0n;
1379
+ let s: bigint = _0n;
1314
1380
  for (let i = 0; i <= k; i++) s += b[i] * c[k - i];
1315
1381
  for (let i = k + 1; i < n; i++) s -= b[i] * c[k + n - i];
1316
1382
  a[k] -= s << e;
@@ -1353,7 +1419,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1353
1419
  const Gx = new Float64Array(n);
1354
1420
  const k = new Array(n);
1355
1421
  while (true) {
1356
- let scaleFG = 31n * (FGlen - 10n);
1422
+ let scaleFG = _31n * (FGlen - _10n);
1357
1423
  for (let i = 0; i < n; i++) {
1358
1424
  Fx[i] = Number(F[i] >> scaleFG);
1359
1425
  Gx[i] = Number(G[i] >> scaleFG);
@@ -1373,12 +1439,12 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1373
1439
  }
1374
1440
  F = this.subMul(logn, F, f, k, scaleK); // 3: F ← F - kf
1375
1441
  G = this.subMul(logn, G, g, k, scaleK); // 4: G ← G - kg
1376
- const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + 10n;
1442
+ const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + _10n;
1377
1443
  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;
1444
+ if (FGlen > _1n && FGlen * _31n >= maxFGBits + _31n) FGlen--;
1445
+ if (scaleK <= _0n) break;
1446
+ scaleK -= _25n;
1447
+ if (scaleK < _0n) scaleK = _0n;
1382
1448
  }
1383
1449
  return true;
1384
1450
  }
@@ -1406,10 +1472,10 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1406
1472
  const xf = f[0];
1407
1473
  const xg = g[0];
1408
1474
  // We can rely on 'invert' to throw if they are not coprime.
1409
- if (xf <= 0n || xg <= 0n) return false;
1475
+ if (xf <= _0n || xg <= _0n) return false;
1410
1476
  try {
1411
1477
  const u1 = invert(xf, xg); // if gcd(f, g) ≠ 1 then
1412
- const v1 = (1n - u1 * xf) / xg;
1478
+ const v1 = (_1n - u1 * xf) / xg;
1413
1479
  F[0] = -v1 * QBig; // 5: (F, G) ← (vq, uq)
1414
1480
  G[0] = u1 * QBig;
1415
1481
  return true;
@@ -1672,7 +1738,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1672
1738
  ).encode([nonce, pad(sigLen).encode(s2), msg]);
1673
1739
  },
1674
1740
  decode(data: TArg<Uint8Array>): TRet<SignatureRaw> {
1741
+ // Keep API misuse on the coder's TypeError path before reading the container length.
1742
+ abytes(data, undefined, 'signature');
1743
+ // The compressed-signature field is fixed-width here; only the message is variable. A
1744
+ // container shorter than the fixed part would make the s2 field borrow bytes from nowhere
1745
+ // and let a truncated encoding open to the same message.
1675
1746
  const msgLen = data.length - NONCELEN - sigLen - 1;
1747
+ if (msgLen < 0) throw new Error('signature coder: wrong length');
1676
1748
  const [nonce, s2, msg] = headerCoder(
1677
1749
  0x30 + logn,
1678
1750
  splitCoder('falcon.signature', NONCELEN, sigLen, msgLen)
@@ -1683,22 +1755,33 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1683
1755
  };
1684
1756
  // [ 1B header ] [ 40B nonce ] [ compressed_sig ]
1685
1757
  const SignatureCoderDetached = (logn: number) => {
1686
- const sigLen = opts.padded ? opts.sigLen - 1 - NONCELEN : opts.detachedLen;
1687
- const getSigLen = (s2: TArg<Uint8Array>) => (opts.padded ? sigLen : s2.length);
1758
+ const paddedSigLen = opts.sigLen - 1 - NONCELEN;
1759
+ const getSigLen = (s2: TArg<Uint8Array>) => (opts.padded ? paddedSigLen : s2.length);
1688
1760
  return {
1689
1761
  encode({ nonce, s2 }: TArg<{ nonce: Uint8Array; s2: Uint8Array }>): TRet<Uint8Array> {
1690
1762
  return headerCoder(
1691
1763
  0x30 + logn,
1692
1764
  splitCoder('falcon.detachedSignature', NONCELEN, getSigLen(s2))
1693
- ).encode([nonce, opts.padded ? pad(sigLen).encode(s2) : s2]);
1765
+ ).encode([nonce, opts.padded ? pad(paddedSigLen).encode(s2) : s2]);
1694
1766
  },
1695
1767
  decode(data: TArg<Uint8Array>): TRet<{
1696
1768
  nonce: Uint8Array;
1697
1769
  s2: Uint8Array;
1698
1770
  }> {
1771
+ // Unpadded Round-3 signatures have parameter-set maxima (header + nonce + s2):
1772
+ // 752 bytes for Falcon-512 and 1462 for Falcon-1024. Reject before creating views or
1773
+ // entering the bit decoder so attacker-sized inputs cannot cause proportional work.
1774
+ if (!opts.padded && data.length > 1 + NONCELEN + opts.maxS2Len)
1775
+ throw new Error('detached signature too long');
1776
+ // Padded detached signatures are fixed-length (`lengths.signature`), so the payload width
1777
+ // must come from the parameter set, not from the input: deriving it would accept appended
1778
+ // zero bytes and truncated padding as extra valid encodings of the same signature.
1779
+ // Unpadded signatures are variable-length; decodeUnpaddedSig() enforces the exact canonical
1780
+ // bitlength of whatever remains.
1781
+ const payloadLen = opts.padded ? paddedSigLen : data.length - NONCELEN - 1;
1699
1782
  const [nonce, raw] = headerCoder(
1700
1783
  0x30 + logn,
1701
- splitCoder('falcon.detachedSignature', NONCELEN, data.length - NONCELEN - 1)
1784
+ splitCoder('falcon.detachedSignature', NONCELEN, payloadLen)
1702
1785
  ).decode(data);
1703
1786
  const s2 = decodeSig(raw);
1704
1787
  return { nonce, s2 } as TRet<{ nonce: Uint8Array; s2: Uint8Array }>;
@@ -1769,7 +1852,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1769
1852
  // Round-3 Falcon keeps 16-bit draws only in 0..61444, i.e. below 61445 = 5*q, the largest
1770
1853
  // 16-bit multiple of q below 2^16; a literal ceil(2^16/q)*q would accept every sample.
1771
1854
  const kQ = 5 * Q;
1772
- for (let i = 0; i < N; ) {
1855
+ for (let i = 0; i < N;) {
1773
1856
  const tmp = h.xof(2); // 6: t ← SHAKE-256-Extract(ctx, 16)
1774
1857
  let w = (tmp[0] << 8) | tmp[1];
1775
1858
  if (w < kQ) c[i++] = w % Q; // 8: cᵢ ← t mod q
@@ -1785,7 +1868,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1785
1868
  private shakeBuf: Uint8Array;
1786
1869
  private ctrView: DataView;
1787
1870
  // ChaCha
1788
- private ctr: bigint = 0n;
1871
+ private ctr: bigint = _0n;
1789
1872
  private buf: Uint8Array;
1790
1873
  private buf32: Uint32Array;
1791
1874
  private pos: number;
@@ -1835,8 +1918,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1835
1918
  const out32 = swap32IfBE(this.buf32);
1836
1919
  for (let i = 0; i < 8; i++, this.ctr++) {
1837
1920
  const n = swap32IfBE(this.nonce32.slice()); // [n0, n1, n2, n3]
1838
- n[2] ^= Number(this.ctr & 0xffffffffn);
1839
- n[3] ^= Number(this.ctr >> 32n);
1921
+ n[2] ^= Number(this.ctr & MASK_32n);
1922
+ n[3] ^= Number(this.ctr >> _32n);
1840
1923
  // chacha20() takes raw nonce bytes; on BE the word-normalized temp must be swapped back.
1841
1924
  swap32IfBE(n.subarray(1));
1842
1925
  chacha20(this.key, u8(n.subarray(1)), EMPTY_CHACHA20_BLOCK, this.curBlock, n[0]);
@@ -1947,48 +2030,6 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1947
2030
  if (this.berExp(x, ccs)) return s + z;
1948
2031
  }
1949
2032
  }
1950
- private ldlFFT(logn: number, g00t: CPoly, g01t: CPoly, g11t: CPoly) {
1951
- // Algorithm 8: LDL*(G)
1952
- // (Page 37)
1953
- // Require: A full-rank self-adjoint matrix G = (Gᵢⱼ) ∈ FFT(Q[x]/(φ))²ˣ²
1954
- // Ensure: The LDL* decomposition G = LDL* over FFT(Q[x]/(φ))
1955
- // Format: All polynomials are in FFT representation.
1956
- // 1: D₀₀ ← G₀₀
1957
- // 2: L₁₀ ← G₁₀/G₀₀
1958
- // 3: D₁₁ ← G₁₁ - L₁₀ ⊙ L₁₀* ⊙ G₀₀
1959
- // 4: L ← [ 1 0 ; L₁₀ 1 ], D ← [ D₀₀ 0 ; 0 D₁₁ ]
1960
- // 5: return (L, D)
1961
-
1962
- // Algorithm 9: ffLDL*(G)
1963
- // (Page 37)
1964
- // Require: A full-rank Gram matrix G ∈ FFT(Q[x]/(xⁿ + 1))²ˣ²
1965
- // Ensure: A binary tree T
1966
- // Format: All polynomials are in FFT representation.
1967
- // 1: (L, D) ← LDL*(G) ▷ L = [ 1 0 ; L₁₀ 1 ], D = [ D₀₀ 0 ; 0 D₁₁ ]
1968
- // 2: T.value ← L₁₀
1969
- // 3: if (n = 2) then
1970
- // 4: T.leftchild ← D₀₀
1971
- // 5: T.rightchild ← D₁₁
1972
- // 6: return T
1973
- // 7: else
1974
- // 8: d₀₀, d₀₁ ← splitfft(D₀₀) ▷ dᵢⱼ ∈ FFT(Q[x]/(x^{n/2} + 1))
1975
- // 9: d₁₀, d₁₁ ← splitfft(D₁₁)
1976
- // 10: G₀ ← [ d₀₀ d₀₁ ; d₀₁* d₀₀ ], G₁ ← [ d₁₀ d₁₁ ; d₁₁* d₁₀ ]
1977
- // ▷ Since D₀₀, D₁₁ are self-adjoint, (3.30) applies
1978
- // 11: T.leftchild ← ffLDL*(G₀) ▷ Recursive calls
1979
- // 12: T.rightchild ← ffLDL*(G₁)
1980
- // 13: return T
1981
- g00t = g00t.slice(); // can be same as g11t and everything will break!
1982
- const hn = 1 << (logn - 1);
1983
- for (let i = 0; i < hn; i++) {
1984
- const g01 = g01t[i];
1985
- const g11 = g11t[i];
1986
- const mu = fComplex.scale(g01, 1.0 / g00t[i].re);
1987
- g11t[i] = { re: g11.re - (mu.re * g01.re + mu.im * g01.im), im: g11.im };
1988
- g01t[i] = fComplex.conj(mu);
1989
- }
1990
- return { g00: g00t, g01: g01t, g11: g11t };
1991
- }
1992
2033
  private splitFFT(logn: number, f: CPoly) {
1993
2034
  // Algorithm 1: splitfft(FFT(f))
1994
2035
  // (Page 29)
@@ -2104,7 +2145,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2104
2145
  // 13: z₀ ← mergefft(z'₀)
2105
2146
  // 14: return z = (z₀, z₁)
2106
2147
  if (logn === 0) {
2148
+ // The dynamic sampler stores 1/σ' instead of σ'. Keygen guarantees this interval,
2149
+ // but an imported compact key may reconstruct an invalid basis. Check the actual LDL*
2150
+ // leaf before it can drive SamplerZ; the negated comparison also rejects NaN/infinity.
2107
2151
  const leaf = Math.sqrt(g00i[0].re) * INV_SIGMA[this.logn];
2152
+ const sigmaPrime = 1 / leaf;
2153
+ if (!(sigmaPrime >= SIGMA_MIN[this.logn] && sigmaPrime <= SIGMA_MAX))
2154
+ throw new Error('invalid secretKey: sampler sigma out of range');
2108
2155
  // 3: z₀ ← SamplerZ(t₀, σ')
2109
2156
  // ▷ Since n=1, tᵢ = invFFT(tᵢ) ∈ Q and zᵢ = invFFT(zᵢ) ∈ Z
2110
2157
  const t0re = this.samplerZ(t0[0].re, leaf);
@@ -2112,7 +2159,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2112
2159
  return { t0: [{ re: t0re, im: 0.0 }], t1: [{ re: t1re, im: 0.0 }] };
2113
2160
  }
2114
2161
  // 6: (l, T₀, T₁) ← (T.value, T.leftchild, T.rightchild)
2115
- const { g00, g01, g11 } = this.ldlFFT(logn, g00i, g01i, g11i);
2162
+ const { g00, g01, g11 } = ldlFFT(logn, g00i, g01i, g11i);
2116
2163
  const { f0: g00f0, f1: g00f1 } = this.splitSelfAdjFFT(logn, g00);
2117
2164
  const { f0: g11f0, f1: g11f1 } = this.splitSelfAdjFFT(logn, g11);
2118
2165
  // 7: t'₁ ← splitfft(t₁)
@@ -2181,7 +2228,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2181
2228
  // ▷ Remove 1 byte for the header, and 40 bytes for r
2182
2229
  // 11: while (s = ⊥)
2183
2230
  // 12: return sig = (r, s)
2184
- abytes(msg);
2231
+ abytes(msg, undefined, 'msg');
2232
+ abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
2185
2233
  // One RNG stream drives both the public 40-byte nonce and the 48-byte sampler seed, so
2186
2234
  // deterministic rnd hooks make signatures deterministic for fixed secretKey/message inputs.
2187
2235
  const nonce = rnd(40);
@@ -2284,11 +2332,18 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2284
2332
  publicKey: publicKeyCoder.bytesLen,
2285
2333
  secretKey: secretKeyCoder.bytesLen,
2286
2334
  });
2335
+ // Falcon takes a sampler callback the other schemes do not, and rejects `context`
2336
+ // with its own message; both stay in the accepted set so the specific errors fire.
2337
+ const FALCON_SIG_OPT_KEYS = [...SIG_OPT_KEYS, 'random'] as const;
2287
2338
  // Noble exposes a 48-byte sampler-seed hook,
2288
2339
  // but Falcon still samples/encodes a separate 40-byte nonce per signature.
2289
2340
  const getRnd = (opts: TArg<FalconSigOpts> = {}): TRet<FalconRandom> => {
2290
- validateSigOpts(opts);
2341
+ // `context` stays in the accepted set so the specific "not supported" error below
2342
+ // still fires, rather than the generic unexpected-option one.
2343
+ opts = validateSigOpts(opts, FALCON_SIG_OPT_KEYS);
2291
2344
  if (opts.context !== undefined) throw new Error('context is not supported');
2345
+ if (opts.random !== undefined && typeof opts.random !== 'function')
2346
+ throw new TypeError('"opts.random" expected function, got type=' + typeof opts.random);
2292
2347
  if (opts.random !== undefined) return opts.random as TRet<FalconRandom>;
2293
2348
  if (opts.extraEntropy === undefined) return randomBytes;
2294
2349
  const seed = opts.extraEntropy === false ? new Uint8Array(48) : opts.extraEntropy;
@@ -2297,8 +2352,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2297
2352
  return (len = 0) => drbg.randomBytes(len) as TRet<Uint8Array>;
2298
2353
  };
2299
2354
  const checkVerOpts = (opts: TArg<VerOpts> = {}) => {
2300
- validateVerOpts(opts);
2301
- if (opts.context !== undefined) throw new Error('context is not supported');
2355
+ const normalized = validateVerOpts(opts);
2356
+ if (normalized.context !== undefined) throw new Error('context is not supported');
2302
2357
  };
2303
2358
  const tests = Object.freeze({
2304
2359
  publicKeyCoder: Object.freeze(publicKeyCoder),
@@ -2328,6 +2383,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2328
2383
  }>;
2329
2384
  };
2330
2385
  const getPublicKey = (sk: TArg<Uint8Array>): TRet<Uint8Array> => {
2386
+ abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
2331
2387
  const [f, g, F] = secretKeyCoder.decode(sk);
2332
2388
  try {
2333
2389
  const h = computePublic(f, g);
@@ -2358,9 +2414,10 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2358
2414
  verOpts: TArg<VerOpts> = {}
2359
2415
  ) => {
2360
2416
  checkVerOpts(verOpts);
2361
- abytes(sig);
2362
- abytes(msg);
2363
- abytes(pk);
2417
+ abytes(sig, undefined, 'signature');
2418
+ abytes(msg, undefined, 'msg');
2419
+ // Length/canonical public-key failures are decoded below and return false; only type is fatal.
2420
+ abytes(pk, undefined, 'publicKey');
2364
2421
  try {
2365
2422
  const { s2, nonce } = SignatureCoderDetached(logn).decode(sig);
2366
2423
  return verifyRaw(pk, s2, nonce, msg);
@@ -2379,11 +2436,39 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2379
2436
  },
2380
2437
  open(sig: TArg<Uint8Array>, pk: TArg<Uint8Array>, verOpts: TArg<VerOpts> = {}) {
2381
2438
  checkVerOpts(verOpts);
2382
- const { s2, nonce, msg } = SignatureCoder.decode(sig);
2383
- // Zero-copy API: returned message aliases the caller-provided signature buffer.
2384
- // Copy it if ownership is needed.
2385
- if (verifyRaw(pk, s2, nonce, msg)) return msg;
2386
- throw new Error('invalid signature');
2439
+ // Wrong argument types are caller bugs and must stay TypeErrors; only what happens
2440
+ // after this is untrusted input. Detached verify() type-checks the public key the same
2441
+ // way, so open() does too: a wrong type is fatal, and a malformed (wrong-length or
2442
+ // non-canonical) key folds into the single rejection below rather than leaking a raw
2443
+ // codec error, exactly as detached verify() folds it into `false`.
2444
+ abytes(sig, undefined, 'signature');
2445
+ abytes(pk, undefined, 'publicKey');
2446
+ // Decode and verify owned snapshots. Apart from keeping the authenticated result stable
2447
+ // after open() returns, this ensures every verification step observes the same bytes when
2448
+ // an input is backed by SharedArrayBuffer or has subclass-overridden view methods.
2449
+ const ownedSig = copyBytes(sig);
2450
+ const ownedPk = copyBytes(pk);
2451
+ // Decode failures and malformed-key failures are rejected signatures, not internal
2452
+ // faults. Letting the codec's own errors out gave a caller handling untrusted input
2453
+ // several different messages for one corrupt byte, including "end of buffer: len=2
2454
+ // buf=0 lastByte=undefined", which reads as a library bug. Detached verify already
2455
+ // treats every such failure uniformly; open() collapses them into one Error (the
2456
+ // original preserved as `cause`). A well-formed signature that simply does not
2457
+ // validate falls through to the same message with no cause.
2458
+ try {
2459
+ let verifiedMsg: Uint8Array | undefined;
2460
+ try {
2461
+ const { s2, nonce, msg } = SignatureCoder.decode(ownedSig);
2462
+ if (verifyRaw(ownedPk, s2, nonce, msg)) verifiedMsg = msg;
2463
+ } catch (cause) {
2464
+ throw new Error('invalid signature', { cause });
2465
+ }
2466
+ if (verifiedMsg === undefined) throw new Error('invalid signature');
2467
+ // Do not retain or expose the full attached-signature allocation through `.buffer`.
2468
+ return copyBytes(verifiedMsg);
2469
+ } finally {
2470
+ cleanBytes(ownedSig, ownedPk);
2471
+ }
2387
2472
  },
2388
2473
  });
2389
2474
  const res = {
@@ -2401,14 +2486,14 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2401
2486
 
2402
2487
  const falcon512opts = {
2403
2488
  N: 512,
2489
+ // Keep the mode an own property: omitted config fields must not inherit from Object.prototype.
2490
+ padded: false,
2404
2491
  // Table 3.3 fixed padded detached bytes, including the detached header byte and 40-byte nonce.
2405
2492
  sigLen: 666,
2406
2493
  fgBits: 6,
2407
2494
  FGBits: 8,
2408
2495
  // Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
2409
2496
  paddedLen: 625,
2410
- // Payload-only budget: genFalcon() adds the detached header byte and 40-byte nonce around it.
2411
- detachedLen: 690,
2412
2497
  };
2413
2498
  /**
2414
2499
  * Falcon-512 detached-signature API with the attached helper exposed as `.attached`.
@@ -2443,14 +2528,13 @@ export const falcon512padded: TRet<Falcon> = /* @__PURE__ */ (() =>
2443
2528
 
2444
2529
  const falcon1024opts = {
2445
2530
  N: 1024,
2531
+ padded: false,
2446
2532
  // Table 3.3 fixed padded detached bytes, including the detached header byte and 40-byte nonce.
2447
2533
  sigLen: 1280,
2448
2534
  fgBits: 5,
2449
2535
  FGBits: 8,
2450
2536
  // Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
2451
2537
  paddedLen: 1239,
2452
- // Payload-only budget: genFalcon() adds the detached header byte and 40-byte nonce around it.
2453
- detachedLen: 1280,
2454
2538
  };
2455
2539
  /**
2456
2540
  * Falcon-1024 detached-signature API with the attached helper exposed as `.attached`.
@@ -2495,6 +2579,7 @@ export const __tests: any = /* @__PURE__ */ (() =>
2495
2579
  INV_SIGMA,
2496
2580
  SIGMA_MIN,
2497
2581
  getFloatPoly,
2582
+ ldlFFT,
2498
2583
  cleanCPoly,
2499
2584
  falcon512: (falcon512 as any).__test,
2500
2585
  falcon512padded: (falcon512padded as any).__test,