// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AereMLDSA44Verifier — on-chain NIST ML-DSA-44 (Dilithium2, FIPS 204) verifier * @notice A REAL, standalone module-lattice signature verifier that runs entirely on * AERE chain 2800. Given an ML-DSA-44 public key, a message, and a signature, * it performs the full FIPS 204 Verify_internal algorithm (Algorithm 8): * * 1. pkDecode: split pk into rho (32 bytes) and t1 (k=4 polynomials, * coefficients packed 10 bits each, SimpleBitUnpack). * 2. sigDecode: split sigma into c~ (32 bytes), z (l=4 polynomials, 18-bit * BitUnpack, z[i] = gamma1 - raw), and the hint h (omega+k=84 bytes, * HintBitUnpack with strict-increasing / bound validation). * 3. ExpandA: sample the 4x4 matrix A_hat directly in the NTT domain from * rho via SHAKE128 rejection sampling (CoeffFromThreeBytes). * 4. mu = H(H(pk,64) || M, 64) with H = SHAKE256; c = SampleInBall(c~). * 5. w'approx = NTT^{-1}(A_hat o NTT(z) - NTT(c) o NTT(t1 * 2^d)), * a negacyclic ring product in Z_q[x]/(x^256+1), q = 8380417. * 6. w1 = UseHint(h, w'approx); c~' = H(mu || w1Encode(w1), 32). * 7. Accept iff ||z||_inf < gamma1 - beta (= 130994), the hint is valid, * and c~' == c~. * * This is the actual lattice/hash cryptography executed on-chain, not a * proof-of-a-proof. The NTT here is over q = 8380417 (a different modulus and * different roots than the Falcon verifiers' q = 12289), replicating the * pq-crystals/dilithium reference NTT coefficient ordering so that the * directly-sampled A_hat multiplies correctly against NTT(z). * * VALIDATION: this verifier is validated bit-for-bit against the OFFICIAL NIST * ACVP ML-DSA-sigVer-FIPS204 test vectors, test group 8 (ML-DSA-44, internal * interface, externalMu = false): all 15 cases (3 valid + 12 crafted-invalid * covering modified message / z / commitment / hint) reproduce NIST's expected * testPassed values. See test/mldsa44Verifier.test.js and the deploy script. * * @dev Parameter set: ML-DSA-44 (Dilithium2) — q=8380417, n=256, (k,l)=(4,4), d=13, * tau=39, gamma1=2^17, gamma2=(q-1)/88, eta=2, beta=78, omega=80, lambda=128. */ contract AereMLDSA44Verifier { // ----- ML-DSA-44 parameters ----- uint256 internal constant Q = 8380417; uint256 internal constant N = 256; uint256 internal constant K = 4; uint256 internal constant L = 4; uint256 internal constant D = 13; uint256 internal constant TAU = 39; uint256 internal constant GAMMA1 = 131072; // 2^17 uint256 internal constant GAMMA2 = 95232; // (q-1)/88 uint256 internal constant BETA = 78; // tau*eta uint256 internal constant OMEGA = 80; uint256 internal constant M_HINT = 44; // (q-1)/(2*gamma2) uint256 internal constant Z_NORM_BOUND = 130994; // gamma1 - beta uint256 internal constant ROOT = 1753; // primitive 512th root of unity mod q uint256 internal constant INV256 = 8347681; // 256^-1 mod q uint256 internal constant TWO_D = 8192; // 2^d = 2^13 // Encoded byte sizes uint256 internal constant PK_BYTES = 1312; // 32 + k*320 uint256 internal constant SIG_BYTES = 2420; // 32 + l*576 + (omega+k) uint256 internal constant CTILDE_BYTES = 32; // lambda/4 // SHAKE rates (bytes) uint256 internal constant RATE256 = 136; // SHAKE256 uint256 internal constant RATE128 = 168; // SHAKE128 // ----- recorded on-chain verification results (state-changing path) ----- uint256 public verifyCount; bool public lastResult; event Verified(address indexed caller, bool result, uint256 index); // ========================================================================== // Keccak-f[1600] (FIPS 202) // ========================================================================== // Same assembly permutation used by AereFalcon512Verifier (validated against the // FIPS 202 SHAKE KAT). Rate-independent; SHAKE128/256 differ only in absorb rate. function _keccakf(uint64[25] memory st) internal pure { assembly { function rol(x, n) -> r { r := and(0xffffffffffffffff, or(shl(n, x), shr(sub(64, n), x))) } let s := st let b := mload(0x40) let rc := add(b, 0x320) mstore(add(rc, 0x000), 0x0000000000000001) mstore(add(rc, 0x020), 0x0000000000008082) mstore(add(rc, 0x040), 0x800000000000808a) mstore(add(rc, 0x060), 0x8000000080008000) mstore(add(rc, 0x080), 0x000000000000808b) mstore(add(rc, 0x0a0), 0x0000000080000001) mstore(add(rc, 0x0c0), 0x8000000080008081) mstore(add(rc, 0x0e0), 0x8000000000008009) mstore(add(rc, 0x100), 0x000000000000008a) mstore(add(rc, 0x120), 0x0000000000000088) mstore(add(rc, 0x140), 0x0000000080008009) mstore(add(rc, 0x160), 0x000000008000000a) mstore(add(rc, 0x180), 0x000000008000808b) mstore(add(rc, 0x1a0), 0x800000000000008b) mstore(add(rc, 0x1c0), 0x8000000000008089) mstore(add(rc, 0x1e0), 0x8000000000008003) mstore(add(rc, 0x200), 0x8000000000008002) mstore(add(rc, 0x220), 0x8000000000000080) mstore(add(rc, 0x240), 0x000000000000800a) mstore(add(rc, 0x260), 0x800000008000000a) mstore(add(rc, 0x280), 0x8000000080008081) mstore(add(rc, 0x2a0), 0x8000000000008080) mstore(add(rc, 0x2c0), 0x0000000080000001) mstore(add(rc, 0x2e0), 0x8000000080008008) for { let rnd := 0 } lt(rnd, 24) { rnd := add(rnd, 1) } { for { let x := 0 } lt(x, 5) { x := add(x, 1) } { let o := mul(x, 0x20) mstore(add(b, o), xor(xor(xor(xor( mload(add(s, o)), mload(add(s, add(o, 0xa0)))), mload(add(s, add(o, 0x140)))), mload(add(s, add(o, 0x1e0)))), mload(add(s, add(o, 0x280))))) } for { let x := 0 } lt(x, 5) { x := add(x, 1) } { let cm := mload(add(b, mul(mod(add(x, 4), 5), 0x20))) let cp := mload(add(b, mul(mod(add(x, 1), 5), 0x20))) mstore(add(b, add(0xa0, mul(x, 0x20))), xor(cm, rol(cp, 1))) } for { let x := 0 } lt(x, 5) { x := add(x, 1) } { let d := mload(add(b, add(0xa0, mul(x, 0x20)))) let o := mul(x, 0x20) mstore(add(s, o), xor(mload(add(s, o)), d)) mstore(add(s, add(o, 0xa0)), xor(mload(add(s, add(o, 0xa0))), d)) mstore(add(s, add(o, 0x140)), xor(mload(add(s, add(o, 0x140))), d)) mstore(add(s, add(o, 0x1e0)), xor(mload(add(s, add(o, 0x1e0))), d)) mstore(add(s, add(o, 0x280)), xor(mload(add(s, add(o, 0x280))), d)) } mstore(add(b, 0x000), rol(mload(add(s, 0x000)), 0)) mstore(add(b, 0x140), rol(mload(add(s, 0x020)), 1)) mstore(add(b, 0x280), rol(mload(add(s, 0x040)), 62)) mstore(add(b, 0x0a0), rol(mload(add(s, 0x060)), 28)) mstore(add(b, 0x1e0), rol(mload(add(s, 0x080)), 27)) mstore(add(b, 0x200), rol(mload(add(s, 0x0a0)), 36)) mstore(add(b, 0x020), rol(mload(add(s, 0x0c0)), 44)) mstore(add(b, 0x160), rol(mload(add(s, 0x0e0)), 6)) mstore(add(b, 0x2a0), rol(mload(add(s, 0x100)), 55)) mstore(add(b, 0x0c0), rol(mload(add(s, 0x120)), 20)) mstore(add(b, 0x0e0), rol(mload(add(s, 0x140)), 3)) mstore(add(b, 0x220), rol(mload(add(s, 0x160)), 10)) mstore(add(b, 0x040), rol(mload(add(s, 0x180)), 43)) mstore(add(b, 0x180), rol(mload(add(s, 0x1a0)), 25)) mstore(add(b, 0x2c0), rol(mload(add(s, 0x1c0)), 39)) mstore(add(b, 0x2e0), rol(mload(add(s, 0x1e0)), 41)) mstore(add(b, 0x100), rol(mload(add(s, 0x200)), 45)) mstore(add(b, 0x240), rol(mload(add(s, 0x220)), 15)) mstore(add(b, 0x060), rol(mload(add(s, 0x240)), 21)) mstore(add(b, 0x1a0), rol(mload(add(s, 0x260)), 8)) mstore(add(b, 0x1c0), rol(mload(add(s, 0x280)), 18)) mstore(add(b, 0x300), rol(mload(add(s, 0x2a0)), 2)) mstore(add(b, 0x120), rol(mload(add(s, 0x2c0)), 61)) mstore(add(b, 0x260), rol(mload(add(s, 0x2e0)), 56)) mstore(add(b, 0x080), rol(mload(add(s, 0x300)), 14)) for { let yo := 0 } lt(yo, 0x320) { yo := add(yo, 0xa0) } { let b0 := mload(add(b, yo)) let b1 := mload(add(b, add(yo, 0x20))) let b2 := mload(add(b, add(yo, 0x40))) let b3 := mload(add(b, add(yo, 0x60))) let b4 := mload(add(b, add(yo, 0x80))) mstore(add(s, yo), and(0xffffffffffffffff, xor(b0, and(not(b1), b2)))) mstore(add(s, add(yo, 0x20)), and(0xffffffffffffffff, xor(b1, and(not(b2), b3)))) mstore(add(s, add(yo, 0x40)), and(0xffffffffffffffff, xor(b2, and(not(b3), b4)))) mstore(add(s, add(yo, 0x60)), and(0xffffffffffffffff, xor(b3, and(not(b4), b0)))) mstore(add(s, add(yo, 0x80)), and(0xffffffffffffffff, xor(b4, and(not(b0), b1)))) } mstore(add(s, 0), xor(mload(add(s, 0)), mload(add(rc, mul(rnd, 0x20))))) } } } /// @dev Absorb `input` into a fresh SHAKE sponge at the given rate (domain 0x1F, /// pad10*1) and return the state after the final permutation. rate is a /// multiple of 8 (136 for SHAKE256, 168 for SHAKE128). function _absorb(bytes memory input, uint256 rate) internal pure returns (uint64[25] memory st) { uint256 words = rate / 8; uint256 len = input.length; uint256 off = 0; while (len - off >= rate) { for (uint256 j = 0; j < words; j++) { uint64 lane = 0; uint256 base = off + j * 8; for (uint256 b = 0; b < 8; b++) { lane |= uint64(uint8(input[base + b])) << (8 * b); } st[j] ^= lane; } _keccakf(st); off += rate; } uint256 rem = len - off; bytes memory blk = new bytes(rate); for (uint256 i = 0; i < rem; i++) { blk[i] = input[off + i]; } blk[rem] = bytes1(uint8(blk[rem]) ^ 0x1F); blk[rate - 1] = bytes1(uint8(blk[rate - 1]) ^ 0x80); for (uint256 j = 0; j < words; j++) { uint64 lane = 0; uint256 base = j * 8; for (uint256 b = 0; b < 8; b++) { lane |= uint64(uint8(blk[base + b])) << (8 * b); } st[j] ^= lane; } _keccakf(st); } /// @dev Extract the current `rate`-byte rate block (little-endian lanes). function _squeezeBlock(uint64[25] memory st, uint256 rate) internal pure returns (bytes memory out) { uint256 words = rate / 8; out = new bytes(rate); for (uint256 j = 0; j < words; j++) { uint64 lane = st[j]; for (uint256 b = 0; b < 8; b++) { out[j * 8 + b] = bytes1(uint8(lane >> (8 * b))); } } } /// @dev SHAKE(input) squeezed to `outLen` bytes at the given rate. function _shake(bytes memory input, uint256 rate, uint256 outLen) internal pure returns (bytes memory out) { uint64[25] memory st = _absorb(input, rate); out = new bytes(outLen); uint256 pos = 0; while (pos < outLen) { bytes memory blk = _squeezeBlock(st, rate); uint256 take = outLen - pos; if (take > rate) take = rate; for (uint256 i = 0; i < take; i++) { out[pos + i] = blk[i]; } pos += take; if (pos < outLen) _keccakf(st); } } // ========================================================================== // NTT over Z_q[x]/(x^256+1) // ========================================================================== function _modpow(uint256 base, uint256 e) internal pure returns (uint256 r) { r = 1; base %= Q; while (e > 0) { if (e & 1 == 1) r = mulmod(r, base, Q); base = mulmod(base, base, Q); e >>= 1; } } /// @dev 8-bit reversal. function _brv8(uint256 i) internal pure returns (uint256 r) { for (uint256 b = 0; b < 8; b++) { r = (r << 1) | ((i >> b) & 1); } } /// @dev Plain zetas table: zetas[i] = ROOT^brv8(i) mod q. Replicates the /// pq-crystals/dilithium ntt.c coefficient ordering (their table carries the /// Montgomery factor 2^32 which montgomery_reduce strips, leaving exactly this). function _zetas() internal pure returns (uint256[256] memory z) { for (uint256 i = 0; i < 256; i++) { z[i] = _modpow(ROOT, _brv8(i)); } } /// @dev In-place forward NTT (Cooley-Tukey), reference ordering, plain arithmetic. function _ntt(uint256[256] memory a, uint256[256] memory zt) internal pure { uint256 k = 0; for (uint256 len = 128; len >= 1; len >>= 1) { for (uint256 start = 0; start < 256; start += (len << 1)) { k++; uint256 zeta = zt[k]; for (uint256 j = start; j < start + len; j++) { uint256 t = mulmod(zeta, a[j + len], Q); a[j + len] = addmod(a[j], Q - t, Q); a[j] = addmod(a[j], t, Q); } } } } /// @dev In-place inverse NTT (Gentleman-Sande), reference ordering, plain /// arithmetic; final scaling by 256^-1 (pure inverse, no residual factor). function _invntt(uint256[256] memory a, uint256[256] memory zt) internal pure { uint256 k = 256; for (uint256 len = 1; len < 256; len <<= 1) { for (uint256 start = 0; start < 256; start += (len << 1)) { k--; uint256 zeta = Q - zt[k]; // -zetas[k] for (uint256 j = start; j < start + len; j++) { uint256 t = a[j]; uint256 u = a[j + len]; a[j] = addmod(t, u, Q); a[j + len] = mulmod(zeta, addmod(t, Q - u, Q), Q); } } } for (uint256 i = 0; i < 256; i++) { a[i] = mulmod(a[i], INV256, Q); } } // ========================================================================== // Decode helpers // ========================================================================== /// @dev SimpleBitUnpack: 256 coefficients, `bitlen` bits each, LSB-first stream, /// starting at `byteOff` in `data`. Returns raw unpacked values in [0,2^bitlen). function _unpack(bytes memory data, uint256 byteOff, uint256 bitlen) internal pure returns (uint256[256] memory out) { uint256 mask = (uint256(1) << bitlen) - 1; uint256 acc = 0; uint256 accBits = 0; uint256 p = byteOff; for (uint256 i = 0; i < 256; i++) { while (accBits < bitlen) { acc |= uint256(uint8(data[p])) << accBits; p++; accBits += 8; } out[i] = acc & mask; acc >>= bitlen; accBits -= bitlen; } } /// @dev HintBitUnpack (FIPS 204 Algorithm 21). y is the last omega+k bytes of the /// signature. Returns (h, ok). ok=false on any malformed encoding. function _hintUnpack(bytes memory sig, uint256 off) internal pure returns (uint8[256][K] memory h, bool ok) { uint256 index = 0; for (uint256 i = 0; i < K; i++) { uint256 end = uint8(sig[off + OMEGA + i]); if (end < index || end > OMEGA) return (h, false); uint256 first = index; while (index < end) { if (index > first) { if (uint8(sig[off + index - 1]) >= uint8(sig[off + index])) return (h, false); } h[i][uint8(sig[off + index])] = 1; index++; } } for (uint256 i = index; i < OMEGA; i++) { if (uint8(sig[off + i]) != 0) return (h, false); } return (h, true); } // ========================================================================== // ExpandA / SampleInBall / UseHint // ========================================================================== /// @dev RejNTTPoly (FIPS 204 Algorithm 14): SHAKE128 rejection sampling into an /// NTT-domain polynomial. seed = rho || s || r (34 bytes). function _rejNTTPoly(bytes memory seed) internal pure returns (uint256[256] memory a) { uint64[25] memory st = _absorb(seed, RATE128); bytes memory blk = _squeezeBlock(st, RATE128); uint256 bi = 0; uint256 j = 0; while (j < 256) { if (bi == RATE128) { _keccakf(st); blk = _squeezeBlock(st, RATE128); bi = 0; } // RATE128 (168) is divisible by 3, so 3-byte groups never straddle a block. uint256 z = (uint256(uint8(blk[bi + 2])) & 0x7F) << 16 | uint256(uint8(blk[bi + 1])) << 8 | uint256(uint8(blk[bi])); bi += 3; if (z < Q) { a[j] = z; j++; } } } /// @dev SampleInBall (FIPS 204 Algorithm 29): challenge polynomial c with tau /// nonzero +/-1 coefficients, from SHAKE256(c~). Returns c as coeffs mod q. function _sampleInBall(bytes memory ctilde) internal pure returns (uint256[256] memory c) { uint64[25] memory st = _absorb(ctilde, RATE256); bytes memory blk = _squeezeBlock(st, RATE256); uint256 signs = 0; for (uint256 i = 0; i < 8; i++) { signs |= uint256(uint8(blk[i])) << (8 * i); } uint256 bi = 8; for (uint256 i = 256 - TAU; i < 256; i++) { uint256 jb; while (true) { if (bi == RATE256) { _keccakf(st); blk = _squeezeBlock(st, RATE256); bi = 0; } jb = uint8(blk[bi]); bi++; if (jb <= i) break; } c[i] = c[jb]; c[jb] = (signs & 1) == 1 ? Q - 1 : 1; // -1 mod q or +1 signs >>= 1; } } /// @dev Decompose (FIPS 204 Algorithm 36) then UseHint (Algorithm 32) for a single /// coefficient r in [0,q). Returns w1 in [0, M_HINT). function _useHint(uint8 hint, uint256 r) internal pure returns (uint256) { uint256 twoG = 2 * GAMMA2; uint256 rp = r % Q; uint256 r0m = rp % twoG; int256 r0 = r0m > GAMMA2 ? int256(r0m) - int256(twoG) : int256(r0m); int256 r1; if (int256(rp) - r0 == int256(Q) - 1) { r1 = 0; r0 = r0 - 1; } else { r1 = (int256(rp) - r0) / int256(twoG); } if (hint == 0) { return uint256(r1); } if (r0 > 0) { return uint256((r1 + 1 + int256(M_HINT)) % int256(M_HINT)); } else { return uint256((r1 - 1 + int256(M_HINT)) % int256(M_HINT)); } } // ========================================================================== // w1Encode // ========================================================================== /// @dev w1Encode: SimpleBitPack each of the k polynomials with 6 bits/coeff, /// LSB-first. Returns 768 bytes (k*192). function _w1Encode(uint256[256][K] memory w1) internal pure returns (bytes memory out) { out = new bytes(K * 192); uint256 outPos = 0; uint256 acc = 0; uint256 accBits = 0; for (uint256 i = 0; i < K; i++) { for (uint256 t = 0; t < 256; t++) { acc |= (w1[i][t] & 0x3F) << accBits; accBits += 6; while (accBits >= 8) { out[outPos] = bytes1(uint8(acc & 0xFF)); outPos++; acc >>= 8; accBits -= 8; } } } } // ========================================================================== // Verify // ========================================================================== /// @notice ML-DSA-44 Verify_internal (FIPS 204 Algorithm 8), internal interface, /// externalMu = false: mu is computed from tr = H(pk) and the message M. /// @param pk 1312-byte ML-DSA-44 public key (rho || t1). /// @param message the signed message M (= M' for the internal interface). /// @param sig 2420-byte ML-DSA-44 signature (c~ || z || h). /// @return ok true iff the signature is valid. function verify(bytes memory pk, bytes memory message, bytes memory sig) public pure returns (bool ok) { if (pk.length != PK_BYTES || sig.length != SIG_BYTES) return false; uint256[256] memory zt = _zetas(); // --- sigDecode: hint --- (uint8[256][K] memory h, bool okH) = _hintUnpack(sig, 32 + L * 576); if (!okH) return false; // --- sigDecode: z (and ||z||_inf check) --- uint256[256][L] memory zhat; for (uint256 jj = 0; jj < L; jj++) { uint256[256] memory raw = _unpack(sig, 32 + jj * 576, 18); for (uint256 t = 0; t < 256; t++) { // z = gamma1 - raw ; centered abs and mod-q reduction int256 zc = int256(GAMMA1) - int256(raw[t]); uint256 absz = zc < 0 ? uint256(-zc) : uint256(zc); if (absz >= Z_NORM_BOUND) return false; zhat[jj][t] = zc < 0 ? uint256(int256(Q) + zc) : uint256(zc); } _ntt(zhat[jj], zt); } // --- mu = H(H(pk,64) || M, 64) --- bytes memory tr = _shake(pk, RATE256, 64); bytes memory mu = _shake(bytes.concat(tr, message), RATE256, 64); // --- c = SampleInBall(c~) then NTT --- bytes memory ctilde = new bytes(CTILDE_BYTES); for (uint256 i = 0; i < CTILDE_BYTES; i++) ctilde[i] = sig[i]; uint256[256] memory chat = _sampleInBall(ctilde); _ntt(chat, zt); // --- w = NTT^-1(A_hat o zhat - chat o NTT(t1<