UNPKG

@noble/post-quantum

Version:

Auditable & minimal JS implementation of post-quantum cryptography: FIPS 203, 204, 205, Falcon

785 lines (751 loc) 30 kB
/** * ML-DSA: Module Lattice-based Digital Signature Algorithm from * [FIPS-204](https://csrc.nist.gov/pubs/fips/204/ipd). A.k.a. CRYSTALS-Dilithium. * * Has similar internals to ML-KEM, but their keys and params are different. * Check out [official site](https://www.pq-crystals.org/dilithium/index.shtml), * [repo](https://github.com/pq-crystals/dilithium). * @module */ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */ import { abool } from '@noble/curves/utils.js'; import { shake256 } from '@noble/hashes/sha3.js'; import type { CHash } from '@noble/hashes/utils.js'; import { genCrystals, type XOF, XOF128, XOF256 } from './_crystals.ts'; import { abytes, type BytesCoderLen, checkHash, cleanBytes, type CryptoKeys, equalBytes, getMessage, getMessagePrehash, randomBytes, type Signer, type SigOpts, splitCoder, type TArg, type TRet, validateOpts, validateSigOpts, validateVerOpts, vecCoder, type VerOpts, } from './utils.ts'; /** Internal ML-DSA options. */ export type DSAInternalOpts = { /** * Whether `internal.sign` / `internal.verify` receive a caller-supplied 64-byte `mu` * instead of the usual FIPS 204 formatted message `M'` / prehash-formatted message. * validateInternalOpts() only checks this flag; callers still must supply the right input length. */ externalMu?: boolean; }; function validateInternalOpts(opts: TArg<DSAInternalOpts>) { validateOpts(opts); if (opts.externalMu !== undefined) abool(opts.externalMu, 'opts.externalMu'); } /** ML-DSA signer surface with access to the internal message formatting mode. */ export type DSAInternal = CryptoKeys & { lengths: Signer['lengths']; sign: ( msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<SigOpts & DSAInternalOpts> ) => TRet<Uint8Array>; verify: ( sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, pubKey: TArg<Uint8Array>, opts?: TArg<VerOpts & DSAInternalOpts> ) => boolean; }; /** Public ML-DSA signer surface. */ export type DSA = Signer & { internal: TRet<DSAInternal> }; // Constants // FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients. const N = 256; // 2**23 − 2**13 + 1, 23 bits: multiply will be 46. We have enough precision in JS to avoid bigints const Q = 8380417; // FIPS 204 §2.5 / Table 1 fixes zeta = 1753 as the 512th root of unity used by ML-DSA's NTT. const ROOT_OF_UNITY = 1753; // f = 256**−1 mod q, pow(256, -1, q) = 8347681 (python3) const F = 8347681; // FIPS 204 Table 1 / §7.4 fixes d = 13 dropped low bits for Power2Round on t. const D = 13; // FIPS 204 Table 1 fixes gamma2 to (q-1)/88 for ML-DSA-44 and (q-1)/32 for ML-DSA-65/87; // §7.4 then uses alpha = 2*gamma2 for Decompose / MakeHint / UseHint. // Dilithium is kinda parametrized over GAMMA2, but everything will break with any other value. const GAMMA2_1 = Math.floor((Q - 1) / 88) | 0; const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0; type XofGet = ReturnType<ReturnType<XOF>['get']>; /** Various lattice params. */ /** Public ML-DSA parameter-set description. */ export type DSAParam = { /** Matrix row count. */ K: number; /** Matrix column count. */ L: number; /** Bit width used when rounding `t`. */ D: number; /** Bound used for the `y` sampling range. */ GAMMA1: number; /** Bound used during decomposition and hints. */ GAMMA2: number; /** Number of non-zero challenge coefficients. */ TAU: number; /** Centered-binomial noise parameter. */ ETA: number; /** Maximum number of hint bits in a signature. */ OMEGA: number; }; /** Internal params for different versions of ML-DSA */ // prettier-ignore /** Built-in ML-DSA parameter presets keyed by security categories `2/3/5` * for `ml_dsa44` / `ml_dsa65` / `ml_dsa87`. * This is only the Table 1 subset used directly here: `BETA = TAU * ETA` is derived later, * while `C_TILDE_BYTES`, `TR_BYTES`, `CRH_BYTES`, and `securityLevel` live in the preset wrappers. */ export const PARAMS: Record<string, DSAParam> = /* @__PURE__ */ (() => Object.freeze({ 2: Object.freeze({ K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80 }), 3: Object.freeze({ K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55 }), 5: Object.freeze({ K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75 }), } as const))(); // NOTE: there is a lot cases where negative numbers used (with smod instead of mod). type Poly = Int32Array; const newPoly = (n: number): TRet<Int32Array> => new Int32Array(n) as TRet<Int32Array>; // Shared CRYSTALS helper in the ML-DSA branch: non-Kyber mode, 8-bit bit-reversal, // and Int32Array polys because ordinary-form coefficients can be negative / centered. const crystals = /* @__PURE__ */ genCrystals({ N, Q, F, ROOT_OF_UNITY, newPoly, isKyber: false, brvBits: 8, }); const id = <T>(n: T): T => n; type IdNum = (n: number) => number; // compress()/verify() must be compatible in both directions: // wrap the shared d-bit packer with the FIPS 204 SimpleBitPack / BitPack coefficient maps. // malformed-input rejection only happens through the optional verify hook. const polyCoder = (d: number, compress: IdNum = id, verify: IdNum = id) => crystals.bitsCoder(d, { encode: (i: number) => compress(verify(i)), decode: (i: number) => verify(compress(i)), }); // Mutates `a` in place; callers must pass same-length polynomials. const polyAdd = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => { const a = a_ as Poly; const b = b_ as Poly; for (let i = 0; i < a.length; i++) a[i] = crystals.mod(a[i] + b[i]); return a as TRet<Poly>; }; // Mutates `a` in place; callers must pass same-length polynomials. const polySub = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => { const a = a_ as Poly; const b = b_ as Poly; for (let i = 0; i < a.length; i++) a[i] = crystals.mod(a[i] - b[i]); return a as TRet<Poly>; }; // Mutates `p` in place and assumes it is a decoded `t1`-range polynomial. const polyShiftl = (p_: TArg<Poly>): TRet<Poly> => { const p = p_ as Poly; for (let i = 0; i < N; i++) p[i] <<= D; return p as TRet<Poly>; }; const polyChknorm = (p_: TArg<Poly>, B: number): boolean => { const p = p_ as Poly; // FIPS 204 Algorithms 7 and 8 express the same centered-norm check with explicit inequalities. for (let i = 0; i < N; i++) if (Math.abs(crystals.smod(p[i])) >= B) return true; return false; }; // Both inputs must already be in NTT / `T_q` form. const MultiplyNTTs = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => { const a = a_ as Poly; const b = b_ as Poly; // NOTE: we don't use montgomery reduction in code, since it requires 64 bit ints, // which is not available in JS. mod(a[i] * b[i]) is ok, since Q is 23 bit, // which means a[i] * b[i] is 46 bit, which is safe to use in JS. (number is 53 bits). // Barrett reduction is slower than mod :( const c = newPoly(N); for (let i = 0; i < a.length; i++) c[i] = crystals.mod(a[i] * b[i]); return c as TRet<Poly>; }; // Return poly in NTT representation function RejNTTPoly(xof_: TArg<XofGet>): TRet<Poly> { const xof = xof_ as XofGet; // Samples a polynomial ∈ Tq. xof() must return byte lengths divisible by 3. const r = newPoly(N); // NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :( for (let j = 0; j < N; ) { const b = xof(); if (b.length % 3) throw new Error('RejNTTPoly: unaligned block'); for (let i = 0; j < N && i <= b.length - 3; i += 3) { // FIPS 204 Algorithm 14 clears the top bit of b2 before forming the 23-bit candidate. const t = (b[i + 0] | (b[i + 1] << 8) | (b[i + 2] << 16)) & 0x7fffff; // 3 bytes if (t < Q) r[j++] = t; } } return r as TRet<Poly>; } type DilithiumOpts = { K: number; L: number; GAMMA1: number; GAMMA2: number; TAU: number; ETA: number; OMEGA: number; C_TILDE_BYTES: number; CRH_BYTES: number; TR_BYTES: number; XOF128: XOF; XOF256: XOF; securityLevel: number; }; // Instantiate one ML-DSA parameter set from the Table 1 lattice constants plus the // Table 2 byte lengths / hash-width choices used by the public wrappers below. function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> { const opts = opts_ as DilithiumOpts; const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts; const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256, securityLevel } = opts; if (![2, 4].includes(ETA)) throw new Error('Wrong ETA'); if (![1 << 17, 1 << 19].includes(GAMMA1)) throw new Error('Wrong GAMMA1'); if (![GAMMA2_1, GAMMA2_2].includes(GAMMA2)) throw new Error('Wrong GAMMA2'); const BETA = TAU * ETA; const decompose = (r: number) => { // Decomposes r into (r1, r0) such that r ≡ r1(2γ2) + r0 mod q. const rPlus = crystals.mod(r); const r0 = crystals.smod(rPlus, 2 * GAMMA2) | 0; // FIPS 204 Algorithm 36 folds the top bucket `q-1` back to `(r1, r0) = (0, r0-1)`. if (rPlus - r0 === Q - 1) return { r1: 0 | 0, r0: (r0 - 1) | 0 }; const r1 = Math.floor((rPlus - r0) / (2 * GAMMA2)) | 0; return { r1, r0 }; // r1 = HighBits, r0 = LowBits }; const HighBits = (r: number) => decompose(r).r1; const LowBits = (r: number) => decompose(r).r0; const MakeHint = (z: number, r: number) => { // Compute hint bit indicating whether adding z to r alters the high bits of r. // FIPS 204 §6.2 also permits the Section 5.1 alternative from [6], which uses the // transformed low-bits/high-bits state at this call site instead of Algorithm 39 literally. // This optimized predicate only applies to those transformed Section 5.1 inputs; it is // not a drop-in replacement for Algorithm 39 on arbitrary `(z, r)` pairs. // From dilithium code const res0 = z <= GAMMA2 || z > Q - GAMMA2 || (z === Q - GAMMA2 && r === 0) ? 0 : 1; // from FIPS204: // // const r1 = HighBits(r); // // const v1 = HighBits(r + z); // // const res1 = +(r1 !== v1); // But they return different results! However, decompose is same. // So, either there is a bug in Dilithium ref implementation or in FIPS204. // For now, lets use dilithium one, so test vectors can be passed. // The round-3 Dilithium / ML-DSA code uses the same low-bits / high-bits convention after // `r0 += ct0`. // See dilithium-py README section "Optimising decomposition and making hints". return res0; }; const UseHint = (h: number, r: number) => { // Returns the high bits of r adjusted according to hint h const m = Math.floor((Q - 1) / (2 * GAMMA2)); const { r1, r0 } = decompose(r); // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m if (h === 1) return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0; return r1 | 0; }; const Power2Round = (r: number) => { // Decomposes r into (r1, r0) such that r ≡ r1*(2**d) + r0 mod q. const rPlus = crystals.mod(r); const r0 = crystals.smod(rPlus, 2 ** D) | 0; return { r1: Math.floor((rPlus - r0) / 2 ** D) | 0, r0 }; }; const hintCoder: BytesCoderLen<Poly[] | false> = { bytesLen: OMEGA + K, encode: (h_: TArg<Poly[] | false>): TRet<Uint8Array> => { const h = h_ as Poly[] | false; if (h === false) throw new Error('hint.encode: hint is false'); // should never happen const res = new Uint8Array(OMEGA + K); for (let i = 0, k = 0; i < K; i++) { for (let j = 0; j < N; j++) if (h[i][j] !== 0) res[k++] = j; res[OMEGA + i] = k; } return res as TRet<Uint8Array>; }, decode: (buf: TArg<Uint8Array>): TRet<Poly[] | false> => { const h = []; let k = 0; for (let i = 0; i < K; i++) { const hi = newPoly(N); if (buf[OMEGA + i] < k || buf[OMEGA + i] > OMEGA) return false as TRet<false>; for (let j = k; j < buf[OMEGA + i]; j++) { if (j > k && buf[j] <= buf[j - 1]) return false as TRet<false>; hi[buf[j]] = 1; } k = buf[OMEGA + i]; h.push(hi); } for (let j = k; j < OMEGA; j++) if (buf[j] !== 0) return false as TRet<false>; return h as TRet<Poly[]>; }, }; const ETACoder = polyCoder( ETA === 2 ? 3 : 4, (i: number) => ETA - i, (i: number) => { if (!(-ETA <= i && i <= ETA)) throw new Error(`malformed key s1/s3 ${i} outside of ETA range [${-ETA}, ${ETA}]`); return i; } ); const T0Coder = polyCoder(13, (i: number) => (1 << (D - 1)) - i); const T1Coder = polyCoder(10); // Requires smod. Need to fix! const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i: number) => crystals.smod(GAMMA1 - i)); const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4); const W1Vec = vecCoder(W1Coder, K); // Main structures const publicCoder = splitCoder('publicKey', 32, vecCoder(T1Coder, K)); const secretCoder = splitCoder( 'secretKey', 32, 32, TR_BYTES, vecCoder(ETACoder, L), vecCoder(ETACoder, K), vecCoder(T0Coder, K) ); const sigCoder = splitCoder('signature', C_TILDE_BYTES, vecCoder(ZCoder, L), hintCoder); const CoefFromHalfByte = ETA === 2 ? (n: number) => (n < 15 ? 2 - (n % 5) : false) : (n: number) => (n < 9 ? 4 - n : false); // Return poly in ordinary representation. // This helper returns ordinary-form `[-ETA, ETA]` coefficients for ExpandS; callers apply // `NTT.encode()` later when needed. function RejBoundedPoly(xof_: TArg<XofGet>): TRet<Poly> { const xof = xof_ as XofGet; // Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ. const r: Poly = newPoly(N); for (let j = 0; j < N; ) { const b = xof(); for (let i = 0; j < N && i < b.length; i += 1) { // half byte. Should be superfast with vector instructions. But very slow with js :( const d1 = CoefFromHalfByte(b[i] & 0x0f); const d2 = CoefFromHalfByte((b[i] >> 4) & 0x0f); if (d1 !== false) r[j++] = d1; if (j < N && d2 !== false) r[j++] = d2; } } return r as TRet<Poly>; } const SampleInBall = (seed: TArg<Uint8Array>): TRet<Poly> => { // Samples a polynomial c ∈ Rq with coeffcients from {−1, 0, 1} and Hamming weight τ const pre = newPoly(N); const s = shake256.create({}).update(seed); const buf = new Uint8Array(shake256.blockLen); s.xofInto(buf); // FIPS 204 Algorithm 29 uses the first 8 squeezed bytes as the 64 sign bits `h`, // then rejection-samples coefficient positions from the remaining XOF stream. const masks = buf.slice(0, 8); for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) { let b = i + 1; for (; b > i; ) { b = buf[pos++]; if (pos < shake256.blockLen) continue; s.xofInto(buf); pos = 0; } pre[i] = pre[b]; pre[b] = 1 - (((masks[maskPos] >> maskBit++) & 1) << 1); if (maskBit >= 8) { maskPos++; maskBit = 0; } } return pre as TRet<Poly>; }; const polyPowerRound = (p_: TArg<Poly>) => { const p = p_ as Poly; const res0 = newPoly(N); const res1 = newPoly(N); for (let i = 0; i < p.length; i++) { const { r0, r1 } = Power2Round(p[i]); res0[i] = r0; res1[i] = r1; } return { r0: res0, r1: res1 }; }; const polyUseHint = (u_: TArg<Poly>, h_: TArg<Poly>): TRet<Poly> => { const u = u_ as Poly; const h = h_ as Poly; // In-place on `u`: verification only needs the recovered high bits, so reuse the // temporary `wApprox` buffer instead of allocating another polynomial. for (let i = 0; i < N; i++) u[i] = UseHint(h[i], u[i]); return u as TRet<Poly>; }; const polyMakeHint = (a_: TArg<Poly>, b_: TArg<Poly>) => { const a = a_ as Poly; const b = b_ as Poly; const v = newPoly(N); let cnt = 0; for (let i = 0; i < N; i++) { const h = MakeHint(a[i], b[i]); v[i] = h; cnt += h; } return { v, cnt }; }; const signRandBytes = 32; const seedCoder = splitCoder('seed', 32, 64, 32); // API & argument positions are exactly as in FIPS204. const internal: TRet<DSAInternal> = Object.freeze({ info: Object.freeze({ type: 'internal-ml-dsa' }), lengths: Object.freeze({ secretKey: secretCoder.bytesLen, publicKey: publicCoder.bytesLen, seed: 32, signature: sigCoder.bytesLen, signRand: signRandBytes, }), keygen: (seed?: TArg<Uint8Array>) => { // H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) 2: ▷ expand seed const seedDst = new Uint8Array(32 + 2); const randSeed = seed === undefined; if (randSeed) seed = randomBytes(32); abytes(seed!, 32, 'seed'); seedDst.set(seed!); if (randSeed) cleanBytes(seed!); seedDst[32] = K; seedDst[33] = L; const [rho, rhoPrime, K_] = seedCoder.decode( shake256(seedDst, { dkLen: seedCoder.bytesLen }) ); const xofPrime = XOF256(rhoPrime); const s1 = []; for (let i = 0; i < L; i++) s1.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff))); const s2 = []; for (let i = L; i < L + K; i++) s2.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff))); const s1Hat = s1.map((i) => crystals.NTT.encode(i.slice())); const t0 = []; const t1 = []; const xof = XOF128(rho); const t = newPoly(N); for (let i = 0; i < K; i++) { // t ← NTT−1(A*NTT(s1)) + s2 cleanBytes(t); // don't-reallocate for (let j = 0; j < L; j++) { const aij = RejNTTPoly(xof.get(j, i)); // super slow! polyAdd(t, MultiplyNTTs(aij, s1Hat[j])); } crystals.NTT.decode(t); const { r0, r1 } = polyPowerRound(polyAdd(t, s2[i])); // (t1, t0) ← Power2Round(t, d) t0.push(r0); t1.push(r1); } const publicKey = publicCoder.encode([rho, t1]); // pk ← pkEncode(ρ, t1) const tr = shake256(publicKey, { dkLen: TR_BYTES }); // tr ← H(BytesToBits(pk), 512) // sk ← skEncode(ρ, K,tr, s1, s2, t0) const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]); xof.clean(); xofPrime.clean(); // STATS // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 }, // Kyber1024: { calls: 16, xofs: 48 } // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 }, // DSA87: { calls: 71, xofs: 71 } cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst); return { publicKey: publicKey as TRet<Uint8Array>, secretKey: secretKey as TRet<Uint8Array>, }; }, getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => { // (ρ, K,tr, s1, s2, t0) ← skDecode(sk) const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey); const xof = XOF128(rho); const s1Hat = s1.map((p) => crystals.NTT.encode(p.slice())); const t1: Poly[] = []; const tmp = newPoly(N); for (let i = 0; i < K; i++) { tmp.fill(0); for (let j = 0; j < L; j++) { const aij = RejNTTPoly(xof.get(j, i)); // A_ij in NTT polyAdd(tmp, MultiplyNTTs(aij, s1Hat[j])); // += A_ij * s1_j } crystals.NTT.decode(tmp); // NTT⁻¹ polyAdd(tmp, s2[i]); // t_i = A·s1 + s2 const { r1 } = polyPowerRound(tmp); // r1 = t1, r0 ≈ t0 t1.push(r1); } xof.clean(); cleanBytes(tmp, s1Hat, _t0, s1, s2); return publicCoder.encode([rho, t1]); }, // NOTE: random is optional. sign: ( msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts & DSAInternalOpts> = {} ): TRet<Uint8Array> => { validateSigOpts(opts); validateInternalOpts(opts); let { extraEntropy: random, externalMu = false } = opts; // This part can be pre-cached per secretKey, but there is only minor performance improvement, // since we re-use a lot of variables to computation. // (ρ, K,tr, s1, s2, t0) ← skDecode(sk) const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); // Cache matrix to avoid re-compute later const A: Poly[][] = []; // A ← ExpandA(ρ) const xof = XOF128(rho); for (let i = 0; i < K; i++) { const pv = []; for (let j = 0; j < L; j++) pv.push(RejNTTPoly(xof.get(j, i))); A.push(pv); } xof.clean(); for (let i = 0; i < L; i++) crystals.NTT.encode(s1[i]); // sˆ1 ← NTT(s1) for (let i = 0; i < K; i++) { crystals.NTT.encode(s2[i]); // sˆ2 ← NTT(s2) crystals.NTT.encode(t0[i]); // tˆ0 ← NTT(t0) } // This part is per msg const mu = externalMu ? msg : // 6: µ ← H(tr||M, 512) // ▷ Compute message representative µ shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // Compute private random seed const rnd = random === false ? new Uint8Array(32) : random === undefined ? randomBytes(signRandBytes) : random; abytes(rnd, 32, 'extraEntropy'); const rhoprime = shake256 .create({ dkLen: CRH_BYTES }) .update(_K) .update(rnd) .update(mu) .digest(); // ρ′← H(K||rnd||µ, 512) abytes(rhoprime, CRH_BYTES); const x256 = XOF256(rhoprime, ZCoder.bytesLen); // Rejection sampling loop main_loop: for (let kappa = 0; ; ) { const y = []; // y ← ExpandMask(ρ , κ) for (let i = 0; i < L; i++, kappa++) y.push(ZCoder.decode(x256.get(kappa & 0xff, kappa >> 8)())); const z = y.map((i) => crystals.NTT.encode(i.slice())); const w = []; for (let i = 0; i < K; i++) { // w ← NTT−1(A ◦ NTT(y)) const wi = newPoly(N); for (let j = 0; j < L; j++) polyAdd(wi, MultiplyNTTs(A[i][j], z[j])); crystals.NTT.decode(wi); w.push(wi); } const w1 = w.map((j) => j.map(HighBits)); // w1 ← HighBits(w) // Commitment hash: c˜ ∈{0, 1 2λ } ← H(µ||w1Encode(w1), 2λ) const cTilde = shake256 .create({ dkLen: C_TILDE_BYTES }) .update(mu) .update(W1Vec.encode(w1)) .digest(); // Verifer’s challenge // c ← SampleInBall(c˜1); cˆ ← NTT(c) const cHat = crystals.NTT.encode(SampleInBall(cTilde)); // ⟨⟨cs1⟩⟩ ← NTT−1(cˆ◦ sˆ1) const cs1 = s1.map((i) => MultiplyNTTs(i, cHat)); for (let i = 0; i < L; i++) { polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩ if (polyChknorm(cs1[i], GAMMA1 - BETA)) continue main_loop; // ||z||∞ ≥ γ1 − β } // cs1 is now z (▷ Signer’s response) let cnt = 0; const h = []; for (let i = 0; i < K; i++) { const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2) const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩) if (polyChknorm(r0, GAMMA2 - BETA)) continue main_loop; // ||r0||∞ ≥ γ2 − β const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0) if (polyChknorm(ct0, GAMMA2)) continue main_loop; polyAdd(r0, ct0); // ▷ Signer’s hint const hint = polyMakeHint(r0, w1[i]); // h ← MakeHint(−⟨⟨ct0⟩⟩, w− ⟨⟨cs2⟩⟩ + ⟨⟨ct0⟩⟩) h.push(hint.v); cnt += hint.cnt; } if (cnt > OMEGA) continue; // the number of 1’s in h is greater than ω x256.clean(); const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h) // rho, _K, tr is subarray of secretKey, cannot clean. cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, s1, s2, t0, ...A); // `externalMu` hands ownership of `mu` to the caller, // so only wipe the internally derived digest form here; // zeroizing caller memory would break the caller's own reuse / verify path. if (!externalMu) cleanBytes(mu); return res as TRet<Uint8Array>; } // @ts-ignore throw new Error('Unreachable code path reached, report this error'); }, verify: ( sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>, opts: TArg<DSAInternalOpts> = {} ) => { validateInternalOpts(opts); const { externalMu = false } = opts; // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M. const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk) const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512) if (sig.length !== sigCoder.bytesLen) return false; // return false instead of exception // (c˜, z, h) ← sigDecode(σ) // ▷ Signer’s commitment hash c ˜, response z and hint const [cTilde, z, h] = sigCoder.decode(sig); if (h === false) return false; // if h = ⊥ then return false for (let i = 0; i < L; i++) if (polyChknorm(z[i], GAMMA1 - BETA)) return false; const mu = externalMu ? msg : // 7: µ ← H(tr||M, 512) shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // Compute verifer’s challenge from c˜ const c = crystals.NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1) const zNtt = z.map((i) => i.slice()); // zNtt = NTT(z) for (let i = 0; i < L; i++) crystals.NTT.encode(zNtt[i]); const wTick1 = []; const xof = XOF128(rho); for (let i = 0; i < K; i++) { const ct12d = MultiplyNTTs(crystals.NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d) const Az = newPoly(N); // // A * z for (let j = 0; j < L; j++) { const aij = RejNTTPoly(xof.get(j, i)); // A[i][j] inplace polyAdd(Az, MultiplyNTTs(aij, zNtt[j])); } // wApprox = A*z - c*t1 * (2**d) const wApprox = crystals.NTT.decode(polySub(Az, ct12d)); // Reconstruction of signer’s commitment wTick1.push(polyUseHint(wApprox, h[i])); // w ′ ← UseHint(h, w'approx ) } xof.clean(); // c˜′← H (µ||w1Encode(w′1), 2λ), Hash it; this should match c˜ const c2 = shake256 .create({ dkLen: C_TILDE_BYTES }) .update(mu) .update(W1Vec.encode(wTick1)) .digest(); // Additional checks in FIPS-204: // [[ ||z||∞ < γ1 − β ]] and [[c ˜ = c˜′]] and [[number of 1’s in h is ≤ ω]] for (const t of h) { const sum = t.reduce((acc, i) => acc + i, 0); if (!(sum <= OMEGA)) return false; } for (const t of z) if (polyChknorm(t, GAMMA1 - BETA)) return false; return equalBytes(cTilde, c2); }, }); return Object.freeze({ info: Object.freeze({ type: 'ml-dsa' }), internal, securityLevel: securityLevel, keygen: internal.keygen, lengths: internal.lengths, getPublicKey: internal.getPublicKey, sign: ( msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {} ): TRet<Uint8Array> => { validateSigOpts(opts); const M = getMessage(msg, opts.context); const res = internal.sign(M, secretKey, opts); cleanBytes(M); return res as TRet<Uint8Array>; }, verify: ( sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>, opts: TArg<VerOpts> = {} ) => { validateVerOpts(opts); return internal.verify(sig, getMessage(msg, opts.context), publicKey); }, prehash: (hash: CHash) => { checkHash(hash, securityLevel); return Object.freeze({ info: Object.freeze({ type: 'hashml-dsa' }), securityLevel: securityLevel, lengths: internal.lengths, keygen: internal.keygen, getPublicKey: internal.getPublicKey, sign: ( msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {} ): TRet<Uint8Array> => { validateSigOpts(opts); const M = getMessagePrehash(hash, msg, opts.context); const res = internal.sign(M, secretKey, opts); cleanBytes(M); return res as TRet<Uint8Array>; }, verify: ( sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>, opts: TArg<VerOpts> = {} ) => { validateVerOpts(opts); return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey); }, }); }, }); } /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */ export const ml_dsa44: TRet<DSA> = /* @__PURE__ */ (() => getDilithium({ ...PARAMS[2], CRH_BYTES: 64, TR_BYTES: 64, C_TILDE_BYTES: 32, XOF128, XOF256, securityLevel: 128, }))(); /** ML-DSA-65 for 192-bit security level. Not recommended after 2030, as per ASD. */ export const ml_dsa65: TRet<DSA> = /* @__PURE__ */ (() => getDilithium({ ...PARAMS[3], CRH_BYTES: 64, TR_BYTES: 64, C_TILDE_BYTES: 48, XOF128, XOF256, securityLevel: 192, }))(); /** ML-DSA-87 for 256-bit security level. OK after 2030, as per ASD. */ export const ml_dsa87: TRet<DSA> = /* @__PURE__ */ (() => getDilithium({ ...PARAMS[5], CRH_BYTES: 64, TR_BYTES: 64, C_TILDE_BYTES: 64, XOF128, XOF256, securityLevel: 256, }))();