Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
555 lines
25 KiB
Solidity
555 lines
25 KiB
Solidity
// 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<<d)); w1 = UseHint(h, w) ---
|
|
bytes memory rho = new bytes(32);
|
|
for (uint256 i = 0; i < 32; i++) rho[i] = pk[i];
|
|
|
|
uint256[256][K] memory w1;
|
|
for (uint256 i = 0; i < K; i++) {
|
|
uint256[256] memory acc;
|
|
for (uint256 jj = 0; jj < L; jj++) {
|
|
uint256[256] memory Aij = _rejNTTPoly(bytes.concat(rho, bytes1(uint8(jj)), bytes1(uint8(i))));
|
|
for (uint256 t = 0; t < 256; t++) {
|
|
acc[t] = addmod(acc[t], mulmod(Aij[t], zhat[jj][t], Q), Q);
|
|
}
|
|
}
|
|
// t1[i] << d, then NTT
|
|
uint256[256] memory t1i = _unpack(pk, 32 + i * 320, 10);
|
|
for (uint256 t = 0; t < 256; t++) {
|
|
t1i[t] = mulmod(t1i[t], TWO_D, Q);
|
|
}
|
|
_ntt(t1i, zt);
|
|
uint256[256] memory wpoly;
|
|
for (uint256 t = 0; t < 256; t++) {
|
|
uint256 ct1 = mulmod(chat[t], t1i[t], Q);
|
|
wpoly[t] = addmod(acc[t], Q - ct1, Q);
|
|
}
|
|
_invntt(wpoly, zt);
|
|
for (uint256 t = 0; t < 256; t++) {
|
|
w1[i][t] = _useHint(h[i][t], wpoly[t]);
|
|
}
|
|
}
|
|
|
|
// --- c~' = H(mu || w1Encode(w1), 32) ; compare ---
|
|
bytes memory ctilde2 = _shake(bytes.concat(mu, _w1Encode(w1)), RATE256, CTILDE_BYTES);
|
|
for (uint256 i = 0; i < CTILDE_BYTES; i++) {
|
|
if (ctilde2[i] != sig[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// @notice State-changing wrapper: verify and record the result on-chain.
|
|
/// @dev A full ML-DSA-44 verify is heavy; if the estimate exceeds the EIP-7825
|
|
/// per-tx gas cap this cannot be mined as an L1 tx (use verify() via eth_call).
|
|
function verifyAndRecord(bytes memory pk, bytes memory message, bytes memory sig)
|
|
external
|
|
returns (bool result)
|
|
{
|
|
result = verify(pk, message, sig);
|
|
lastResult = result;
|
|
uint256 idx = verifyCount;
|
|
verifyCount = idx + 1;
|
|
emit Verified(msg.sender, result, idx);
|
|
}
|
|
}
|