@noble/post-quantum 0.7.0 → 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}`);
@@ -552,6 +559,9 @@ const SIGMA_MIN = /* @__PURE__ */ Object.freeze([
552
559
  f64b(BigInt('4608433670533905013')),
553
560
  f64b(BigInt('4608525754002622308')),
554
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;
555
565
 
556
566
  // Falcon Table 3.1 RCDT values for chi, split into 24-bit limbs; storage is [high, mid, low],
557
567
  // so gaussian0() intentionally compares them against v0, v1, v2 in reverse order. The final
@@ -1164,6 +1174,54 @@ function getFloatPoly(logn: number) {
1164
1174
  };
1165
1175
  }
1166
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
+
1167
1225
  function ApproxExp(x: number, ccs: number): number {
1168
1226
  // Algorithm 13: ApproxExp(x, ccs), (Page 42)
1169
1227
  // Require: Floating-point values x ∈ [0, ln(2)] and ccs ∈ [0, 1]
@@ -1195,19 +1253,16 @@ function ApproxExp(x: number, ccs: number): number {
1195
1253
  // Actual api
1196
1254
  type FalconOpts = {
1197
1255
  N: number;
1198
- // Table 3.3 total padded detached bytes; kept as reference config, not read by genFalcon() today.
1199
- // In padded mode it still drives `.lengths.signature`
1200
- // 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`.
1201
1258
  sigLen: number;
1202
- padded?: boolean;
1259
+ padded: boolean;
1203
1260
  fgBits: number;
1204
1261
  FGBits: number;
1205
1262
  // Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
1206
1263
  paddedLen: number;
1207
- // Max compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
1208
- // Reference unpadded payload ceiling only: detached encode/decode use each signature's runtime
1209
- // `s2` length, while signRaw() enforces `maxS2Len` separately.
1210
- detachedLen: number;
1264
+ // Maximum compressed-s payload emitted by sign(). Unpadded detached verification enforces the
1265
+ // same Round-3 ceiling before decoding.
1211
1266
  maxS2Len: number;
1212
1267
  };
1213
1268
 
@@ -1230,7 +1285,7 @@ export type FalconAttached = CryptoKeys & {
1230
1285
  * @param sig Attached Falcon signature bytes.
1231
1286
  * @param publicKey Falcon public key bytes.
1232
1287
  * @param opts Optional verification options.
1233
- * @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.
1234
1289
  */
1235
1290
  open(sig: Uint8Array, publicKey: Uint8Array, opts?: VerOpts): Uint8Array;
1236
1291
  };
@@ -1700,25 +1755,30 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1700
1755
  };
1701
1756
  // [ 1B header ] [ 40B nonce ] [ compressed_sig ]
1702
1757
  const SignatureCoderDetached = (logn: number) => {
1703
- const sigLen = opts.padded ? opts.sigLen - 1 - NONCELEN : opts.detachedLen;
1704
- 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);
1705
1760
  return {
1706
1761
  encode({ nonce, s2 }: TArg<{ nonce: Uint8Array; s2: Uint8Array }>): TRet<Uint8Array> {
1707
1762
  return headerCoder(
1708
1763
  0x30 + logn,
1709
1764
  splitCoder('falcon.detachedSignature', NONCELEN, getSigLen(s2))
1710
- ).encode([nonce, opts.padded ? pad(sigLen).encode(s2) : s2]);
1765
+ ).encode([nonce, opts.padded ? pad(paddedSigLen).encode(s2) : s2]);
1711
1766
  },
1712
1767
  decode(data: TArg<Uint8Array>): TRet<{
1713
1768
  nonce: Uint8Array;
1714
1769
  s2: Uint8Array;
1715
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');
1716
1776
  // Padded detached signatures are fixed-length (`lengths.signature`), so the payload width
1717
1777
  // must come from the parameter set, not from the input: deriving it would accept appended
1718
1778
  // zero bytes and truncated padding as extra valid encodings of the same signature.
1719
1779
  // Unpadded signatures are variable-length; decodeUnpaddedSig() enforces the exact canonical
1720
1780
  // bitlength of whatever remains.
1721
- const payloadLen = opts.padded ? sigLen : data.length - NONCELEN - 1;
1781
+ const payloadLen = opts.padded ? paddedSigLen : data.length - NONCELEN - 1;
1722
1782
  const [nonce, raw] = headerCoder(
1723
1783
  0x30 + logn,
1724
1784
  splitCoder('falcon.detachedSignature', NONCELEN, payloadLen)
@@ -1792,7 +1852,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1792
1852
  // Round-3 Falcon keeps 16-bit draws only in 0..61444, i.e. below 61445 = 5*q, the largest
1793
1853
  // 16-bit multiple of q below 2^16; a literal ceil(2^16/q)*q would accept every sample.
1794
1854
  const kQ = 5 * Q;
1795
- for (let i = 0; i < N; ) {
1855
+ for (let i = 0; i < N;) {
1796
1856
  const tmp = h.xof(2); // 6: t ← SHAKE-256-Extract(ctx, 16)
1797
1857
  let w = (tmp[0] << 8) | tmp[1];
1798
1858
  if (w < kQ) c[i++] = w % Q; // 8: cᵢ ← t mod q
@@ -1970,48 +2030,6 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
1970
2030
  if (this.berExp(x, ccs)) return s + z;
1971
2031
  }
1972
2032
  }
1973
- private ldlFFT(logn: number, g00t: CPoly, g01t: CPoly, g11t: CPoly) {
1974
- // Algorithm 8: LDL*(G)
1975
- // (Page 37)
1976
- // Require: A full-rank self-adjoint matrix G = (Gᵢⱼ) ∈ FFT(Q[x]/(φ))²ˣ²
1977
- // Ensure: The LDL* decomposition G = LDL* over FFT(Q[x]/(φ))
1978
- // Format: All polynomials are in FFT representation.
1979
- // 1: D₀₀ ← G₀₀
1980
- // 2: L₁₀ ← G₁₀/G₀₀
1981
- // 3: D₁₁ ← G₁₁ - L₁₀ ⊙ L₁₀* ⊙ G₀₀
1982
- // 4: L ← [ 1 0 ; L₁₀ 1 ], D ← [ D₀₀ 0 ; 0 D₁₁ ]
1983
- // 5: return (L, D)
1984
-
1985
- // Algorithm 9: ffLDL*(G)
1986
- // (Page 37)
1987
- // Require: A full-rank Gram matrix G ∈ FFT(Q[x]/(xⁿ + 1))²ˣ²
1988
- // Ensure: A binary tree T
1989
- // Format: All polynomials are in FFT representation.
1990
- // 1: (L, D) ← LDL*(G) ▷ L = [ 1 0 ; L₁₀ 1 ], D = [ D₀₀ 0 ; 0 D₁₁ ]
1991
- // 2: T.value ← L₁₀
1992
- // 3: if (n = 2) then
1993
- // 4: T.leftchild ← D₀₀
1994
- // 5: T.rightchild ← D₁₁
1995
- // 6: return T
1996
- // 7: else
1997
- // 8: d₀₀, d₀₁ ← splitfft(D₀₀) ▷ dᵢⱼ ∈ FFT(Q[x]/(x^{n/2} + 1))
1998
- // 9: d₁₀, d₁₁ ← splitfft(D₁₁)
1999
- // 10: G₀ ← [ d₀₀ d₀₁ ; d₀₁* d₀₀ ], G₁ ← [ d₁₀ d₁₁ ; d₁₁* d₁₀ ]
2000
- // ▷ Since D₀₀, D₁₁ are self-adjoint, (3.30) applies
2001
- // 11: T.leftchild ← ffLDL*(G₀) ▷ Recursive calls
2002
- // 12: T.rightchild ← ffLDL*(G₁)
2003
- // 13: return T
2004
- g00t = g00t.slice(); // can be same as g11t and everything will break!
2005
- const hn = 1 << (logn - 1);
2006
- for (let i = 0; i < hn; i++) {
2007
- const g01 = g01t[i];
2008
- const g11 = g11t[i];
2009
- const mu = fComplex.scale(g01, 1.0 / g00t[i].re);
2010
- g11t[i] = { re: g11.re - (mu.re * g01.re + mu.im * g01.im), im: g11.im };
2011
- g01t[i] = fComplex.conj(mu);
2012
- }
2013
- return { g00: g00t, g01: g01t, g11: g11t };
2014
- }
2015
2033
  private splitFFT(logn: number, f: CPoly) {
2016
2034
  // Algorithm 1: splitfft(FFT(f))
2017
2035
  // (Page 29)
@@ -2127,7 +2145,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2127
2145
  // 13: z₀ ← mergefft(z'₀)
2128
2146
  // 14: return z = (z₀, z₁)
2129
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.
2130
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');
2131
2155
  // 3: z₀ ← SamplerZ(t₀, σ')
2132
2156
  // ▷ Since n=1, tᵢ = invFFT(tᵢ) ∈ Q and zᵢ = invFFT(zᵢ) ∈ Z
2133
2157
  const t0re = this.samplerZ(t0[0].re, leaf);
@@ -2135,7 +2159,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2135
2159
  return { t0: [{ re: t0re, im: 0.0 }], t1: [{ re: t1re, im: 0.0 }] };
2136
2160
  }
2137
2161
  // 6: (l, T₀, T₁) ← (T.value, T.leftchild, T.rightchild)
2138
- const { g00, g01, g11 } = this.ldlFFT(logn, g00i, g01i, g11i);
2162
+ const { g00, g01, g11 } = ldlFFT(logn, g00i, g01i, g11i);
2139
2163
  const { f0: g00f0, f1: g00f1 } = this.splitSelfAdjFFT(logn, g00);
2140
2164
  const { f0: g11f0, f1: g11f1 } = this.splitSelfAdjFFT(logn, g11);
2141
2165
  // 7: t'₁ ← splitfft(t₁)
@@ -2308,10 +2332,15 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2308
2332
  publicKey: publicKeyCoder.bytesLen,
2309
2333
  secretKey: secretKeyCoder.bytesLen,
2310
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;
2311
2338
  // Noble exposes a 48-byte sampler-seed hook,
2312
2339
  // but Falcon still samples/encodes a separate 40-byte nonce per signature.
2313
2340
  const getRnd = (opts: TArg<FalconSigOpts> = {}): TRet<FalconRandom> => {
2314
- 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);
2315
2344
  if (opts.context !== undefined) throw new Error('context is not supported');
2316
2345
  if (opts.random !== undefined && typeof opts.random !== 'function')
2317
2346
  throw new TypeError('"opts.random" expected function, got type=' + typeof opts.random);
@@ -2323,8 +2352,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2323
2352
  return (len = 0) => drbg.randomBytes(len) as TRet<Uint8Array>;
2324
2353
  };
2325
2354
  const checkVerOpts = (opts: TArg<VerOpts> = {}) => {
2326
- validateVerOpts(opts);
2327
- 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');
2328
2357
  };
2329
2358
  const tests = Object.freeze({
2330
2359
  publicKeyCoder: Object.freeze(publicKeyCoder),
@@ -2407,11 +2436,39 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2407
2436
  },
2408
2437
  open(sig: TArg<Uint8Array>, pk: TArg<Uint8Array>, verOpts: TArg<VerOpts> = {}) {
2409
2438
  checkVerOpts(verOpts);
2410
- const { s2, nonce, msg } = SignatureCoder.decode(sig);
2411
- // Zero-copy API: returned message aliases the caller-provided signature buffer.
2412
- // Copy it if ownership is needed.
2413
- if (verifyRaw(pk, s2, nonce, msg)) return msg;
2414
- 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
+ }
2415
2472
  },
2416
2473
  });
2417
2474
  const res = {
@@ -2429,14 +2486,14 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
2429
2486
 
2430
2487
  const falcon512opts = {
2431
2488
  N: 512,
2489
+ // Keep the mode an own property: omitted config fields must not inherit from Object.prototype.
2490
+ padded: false,
2432
2491
  // Table 3.3 fixed padded detached bytes, including the detached header byte and 40-byte nonce.
2433
2492
  sigLen: 666,
2434
2493
  fgBits: 6,
2435
2494
  FGBits: 8,
2436
2495
  // Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
2437
2496
  paddedLen: 625,
2438
- // Payload-only budget: genFalcon() adds the detached header byte and 40-byte nonce around it.
2439
- detachedLen: 690,
2440
2497
  };
2441
2498
  /**
2442
2499
  * Falcon-512 detached-signature API with the attached helper exposed as `.attached`.
@@ -2471,14 +2528,13 @@ export const falcon512padded: TRet<Falcon> = /* @__PURE__ */ (() =>
2471
2528
 
2472
2529
  const falcon1024opts = {
2473
2530
  N: 1024,
2531
+ padded: false,
2474
2532
  // Table 3.3 fixed padded detached bytes, including the detached header byte and 40-byte nonce.
2475
2533
  sigLen: 1280,
2476
2534
  fgBits: 5,
2477
2535
  FGBits: 8,
2478
2536
  // Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
2479
2537
  paddedLen: 1239,
2480
- // Payload-only budget: genFalcon() adds the detached header byte and 40-byte nonce around it.
2481
- detachedLen: 1280,
2482
2538
  };
2483
2539
  /**
2484
2540
  * Falcon-1024 detached-signature API with the attached helper exposed as `.attached`.
@@ -2523,6 +2579,7 @@ export const __tests: any = /* @__PURE__ */ (() =>
2523
2579
  INV_SIGMA,
2524
2580
  SIGMA_MIN,
2525
2581
  getFloatPoly,
2582
+ ldlFFT,
2526
2583
  cleanCPoly,
2527
2584
  falcon512: (falcon512 as any).__test,
2528
2585
  falcon512padded: (falcon512padded as any).__test,