The public history carried kat/__pycache__/mlkem768_reference.cpython-314.pyc, a compiled Python artifact embedding the operator's absolute local path. Text secret scanners do not read compiled binaries, which is exactly how it slipped through, and removing it from the tip would have left it reachable through the old root commits. So this repository is republished from a single clean root. This root also carries, from the previously unpublished line of work: - corrected LICENSE year, LICENSING.md, VERIFY-POLICY.md, and CITATIONS-UNRESOLVED.md remeasured 2026-08-11 (101 paths, README aligned) - O-018: run_consensus_verification.py ran 19 of 29 models and reported PASS; it now runs all 29, and computemarket_smt.py gains resolveByTimeout / reclaimUnsettled cases plus a negative control - O-006: the word 'audited' removed from next to Bouncy Castle, twice, after a concurrent edit resurrected it - O-014: prior art named and dated - Algorand's native falcon_verify shipped about ten months before AERE's precompiles; the primacy claim is withdrawn where it was implied - bench/ scripts parametrized so they actually run for an outsider (the earlier textual sanitization left $STAGING unexpanded inside Python strings) - AIP-2/AIP-3 errata with measured figures, spec remeasurements at 2026-08-01, and the spec-zk-stack retractions (owner is an operational key, not the Foundation; 'maximally sound' withdrawn; aggregator V1 deprecated) The redacted bench-host environment files from the sanitized line are kept exactly as published; the unredacted local variants are not carried.
287 lines
10 KiB
Rust
287 lines
10 KiB
Rust
//! RFC 8391 XMSS-SHA2_10_256 signature VERIFICATION core.
|
|
//!
|
|
//! This is the exact inner computation the Post-Quantum Finality aggregation zkVM
|
|
//! guest runs PER VALIDATOR: recover a validator's long-term XMSS root from one
|
|
//! hash-based signature over the domain-bound block message. A validator's
|
|
//! attestation is VALID iff the recovered root equals the root the registry
|
|
//! committed for that validator.
|
|
//!
|
|
//! Parameter set XMSS-SHA2_10_256 (RFC 8391 OID 0x00000001):
|
|
//! n = 32 (SHA-256), Winternitz w = 16, len = 67 chains, single tree height h = 10.
|
|
//!
|
|
//! It is a byte-for-byte model of the already-validated on-chain
|
|
//! `AereXmssVerifier.sol` and its independent JS oracle
|
|
//! (`contracts/test/xmssVerifier.test.js`), which are checked against the official
|
|
//! github.com/XMSS/xmss-reference known-answer vector. Deterministic, `no_std`,
|
|
//! allocation-free, float-free, hash-only: exactly the shape a zkVM circuit needs.
|
|
//!
|
|
//! RFC 8391 keyed hash toolbox (padding_len = n = 32):
|
|
//! F(KEY, M) = SHA256( toByte(0,32) || KEY || M )
|
|
//! H(KEY, L||R) = SHA256( toByte(1,32) || KEY || L || R )
|
|
//! H_msg(KEY, M) = SHA256( toByte(2,32) || KEY || M )
|
|
//! PRF(KEY, ADRS) = SHA256( toByte(3,32) || KEY || ADRS )
|
|
//! with per-address keys/bitmasks derived by PRF(SEED, ADRS) over the 32-byte hash
|
|
//! address (type + OTS/L-tree/hash-tree fields + key_and_mask in {0,1,2}).
|
|
|
|
use crate::sha256::sha256;
|
|
|
|
/// Hash output size / node size in bytes (SHA-256).
|
|
pub const N: usize = 32;
|
|
/// Winternitz parameter.
|
|
pub const W: usize = 16;
|
|
/// Total WOTS+ chains (64 message digits + 3 checksum digits).
|
|
pub const LEN: usize = 67;
|
|
/// Single-tree Merkle height.
|
|
pub const H: usize = 10;
|
|
|
|
/// A 32-byte hash / node.
|
|
pub type Hash = [u8; N];
|
|
|
|
// XMSS hash-address types (RFC 8391 section 2.5).
|
|
const ADDR_OTS: u32 = 0;
|
|
const ADDR_LTREE: u32 = 1;
|
|
const ADDR_HASHTREE: u32 = 2;
|
|
|
|
/// An XMSS-SHA2_10_256 public key: the long-term root plus the public SEED.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct XmssPubKey {
|
|
/// Long-term XMSS public root (first 32 bytes of the XMSS public key).
|
|
pub root: Hash,
|
|
/// XMSS public SEED (last 32 bytes of the XMSS public key).
|
|
pub seed: Hash,
|
|
}
|
|
|
|
/// A single-tree XMSS-SHA2_10_256 signature.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct XmssSig {
|
|
/// Leaf index used by the signer (0 .. 2^h - 1).
|
|
pub idx: u32,
|
|
/// Per-signature randomizer R.
|
|
pub r: Hash,
|
|
/// The 67 WOTS+ one-time-signature chain values.
|
|
pub wots: [Hash; LEN],
|
|
/// The h = 10 Merkle authentication-path nodes.
|
|
pub auth: [Hash; H],
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// RFC 8391 keyed hash toolbox (SHA-256)
|
|
// --------------------------------------------------------------------------
|
|
|
|
/// A 32-byte big-endian encoding of a small domain-separation prefix
|
|
/// (`toByte(v, 32)`): 31 zero bytes then `v`.
|
|
#[inline]
|
|
fn to_byte32(v: u8) -> Hash {
|
|
let mut b = [0u8; 32];
|
|
b[31] = v;
|
|
b
|
|
}
|
|
|
|
/// Pack an XMSS 32-byte hash address. layer (bytes 0..4) and tree (bytes 4..12)
|
|
/// are always zero for single-tree XMSS-SHA2_10_256; then type (bytes 12..16),
|
|
/// the type-specific words a4/a5/a6 (bytes 16..20 / 20..24 / 24..28) and
|
|
/// key_and_mask (bytes 28..32). Matches `AereXmssVerifier._addr` exactly:
|
|
/// value = (typ<<128) | (a4<<96) | (a5<<64) | (a6<<32) | km (big-endian bytes32).
|
|
#[inline]
|
|
fn addr(typ: u32, a4: u32, a5: u32, a6: u32, km: u32) -> Hash {
|
|
let mut a = [0u8; 32];
|
|
a[12..16].copy_from_slice(&typ.to_be_bytes());
|
|
a[16..20].copy_from_slice(&a4.to_be_bytes());
|
|
a[20..24].copy_from_slice(&a5.to_be_bytes());
|
|
a[24..28].copy_from_slice(&a6.to_be_bytes());
|
|
a[28..32].copy_from_slice(&km.to_be_bytes());
|
|
a
|
|
}
|
|
|
|
/// PRF(SEED, ADRS) = SHA256( toByte(3,32) || SEED || ADRS ).
|
|
#[inline]
|
|
fn prf(seed: &Hash, address: &Hash) -> Hash {
|
|
let mut buf = [0u8; 96];
|
|
buf[0..32].copy_from_slice(&to_byte32(3));
|
|
buf[32..64].copy_from_slice(seed);
|
|
buf[64..96].copy_from_slice(address);
|
|
sha256(&buf)
|
|
}
|
|
|
|
#[inline]
|
|
fn xor32(a: &Hash, b: &Hash) -> Hash {
|
|
let mut o = [0u8; 32];
|
|
for i in 0..32 {
|
|
o[i] = a[i] ^ b[i];
|
|
}
|
|
o
|
|
}
|
|
|
|
/// RFC 8391 F: one hash-chain step. key = PRF(SEED, addr(km=0)),
|
|
/// mask = PRF(SEED, addr(km=1)), then F = SHA256( toByte(0,32) || key || (x^mask) ).
|
|
#[inline]
|
|
fn f_hash(seed: &Hash, typ: u32, a4: u32, a5: u32, a6: u32, x: &Hash) -> Hash {
|
|
let key = prf(seed, &addr(typ, a4, a5, a6, 0));
|
|
let mask = prf(seed, &addr(typ, a4, a5, a6, 1));
|
|
let masked = xor32(x, &mask);
|
|
let mut buf = [0u8; 96];
|
|
// toByte(0,32) is all zero.
|
|
buf[32..64].copy_from_slice(&key);
|
|
buf[64..96].copy_from_slice(&masked);
|
|
sha256(&buf)
|
|
}
|
|
|
|
/// RFC 8391 RAND_HASH / H over two n-byte children. key = PRF(addr(km=0)),
|
|
/// m0 = PRF(addr(km=1)), m1 = PRF(addr(km=2)), then
|
|
/// H = SHA256( toByte(1,32) || key || (L^m0) || (R^m1) ).
|
|
#[inline]
|
|
fn h_hash(seed: &Hash, typ: u32, a4: u32, a5: u32, a6: u32, left: &Hash, right: &Hash) -> Hash {
|
|
let key = prf(seed, &addr(typ, a4, a5, a6, 0));
|
|
let m0 = prf(seed, &addr(typ, a4, a5, a6, 1));
|
|
let m1 = prf(seed, &addr(typ, a4, a5, a6, 2));
|
|
let l = xor32(left, &m0);
|
|
let r = xor32(right, &m1);
|
|
let mut buf = [0u8; 128];
|
|
buf[0..32].copy_from_slice(&to_byte32(1));
|
|
buf[32..64].copy_from_slice(&key);
|
|
buf[64..96].copy_from_slice(&l);
|
|
buf[96..128].copy_from_slice(&r);
|
|
sha256(&buf)
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// WOTS+ (leaf)
|
|
// --------------------------------------------------------------------------
|
|
|
|
/// Derive the 67 base-16 chain lengths (64 message digits + 3 checksum digits)
|
|
/// from a 32-byte message hash, exactly per RFC 8391 chain_lengths.
|
|
pub fn chain_lengths(m: &Hash) -> [u32; LEN] {
|
|
let mut d = [0u32; LEN];
|
|
let mut csum: u32 = 0;
|
|
for i in 0..32 {
|
|
let b = m[i];
|
|
let hi = (b >> 4) as u32;
|
|
let lo = (b & 0x0f) as u32;
|
|
d[2 * i] = hi;
|
|
d[2 * i + 1] = lo;
|
|
csum += (15 - hi) + (15 - lo);
|
|
}
|
|
// csum << (8 - (len2*log_w % 8)) == csum << 4, then base_w over 2 big-endian bytes.
|
|
let c = csum << 4;
|
|
d[64] = (c >> 12) & 0x0f;
|
|
d[65] = (c >> 8) & 0x0f;
|
|
d[66] = (c >> 4) & 0x0f;
|
|
d
|
|
}
|
|
|
|
/// WOTS_PKFromSig: complete each of the 67 chains from the signature value to the
|
|
/// chain end (position w-1), returning the 67 WOTS+ public-key chain values.
|
|
fn wots_pk_from_sig(seed: &Hash, idx_leaf: u32, mhash: &Hash, sig: &[Hash; LEN]) -> [Hash; LEN] {
|
|
let lens = chain_lengths(mhash);
|
|
let mut pk = [[0u8; 32]; LEN];
|
|
for i in 0..LEN {
|
|
let mut x = sig[i];
|
|
// gen_chain: for s in [lens[i], w-1): F with hash-address s, chain-address i.
|
|
let mut s = lens[i];
|
|
while s < (W as u32) - 1 {
|
|
x = f_hash(seed, ADDR_OTS, idx_leaf, i as u32, s, &x);
|
|
s += 1;
|
|
}
|
|
pk[i] = x;
|
|
}
|
|
pk
|
|
}
|
|
|
|
/// L-tree: compress the 67 WOTS+ public-key values into a single n-byte leaf.
|
|
fn l_tree(seed: &Hash, idx_leaf: u32, pk: &[Hash; LEN]) -> Hash {
|
|
let mut nodes = *pk;
|
|
let mut l = LEN;
|
|
let mut height: u32 = 0;
|
|
while l > 1 {
|
|
let parent = l >> 1;
|
|
for i in 0..parent {
|
|
nodes[i] = h_hash(
|
|
seed,
|
|
ADDR_LTREE,
|
|
idx_leaf,
|
|
height,
|
|
i as u32,
|
|
&nodes[2 * i],
|
|
&nodes[2 * i + 1],
|
|
);
|
|
}
|
|
if l & 1 == 1 {
|
|
nodes[parent] = nodes[l - 1];
|
|
l = parent + 1;
|
|
} else {
|
|
l = parent;
|
|
}
|
|
height += 1;
|
|
}
|
|
nodes[0]
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Merkle authentication path
|
|
// --------------------------------------------------------------------------
|
|
|
|
/// compute_root: fold the leaf with the h=10 authentication path up to the root,
|
|
/// using the hash-tree address (type 2) with the correct per-level height/index.
|
|
fn compute_root(seed: &Hash, leaf: &Hash, leaf_idx: u32, auth: &[Hash; H]) -> Hash {
|
|
let mut left: Hash;
|
|
let mut right: Hash;
|
|
if leaf_idx & 1 == 1 {
|
|
left = auth[0];
|
|
right = *leaf;
|
|
} else {
|
|
left = *leaf;
|
|
right = auth[0];
|
|
}
|
|
let mut li = leaf_idx;
|
|
for i in 0..(H - 1) {
|
|
li >>= 1;
|
|
let node = h_hash(seed, ADDR_HASHTREE, 0, i as u32, li, &left, &right);
|
|
if li & 1 == 1 {
|
|
left = auth[i + 1];
|
|
right = node;
|
|
} else {
|
|
left = node;
|
|
right = auth[i + 1];
|
|
}
|
|
}
|
|
li >>= 1;
|
|
h_hash(seed, ADDR_HASHTREE, 0, (H - 1) as u32, li, &left, &right)
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Verify
|
|
// --------------------------------------------------------------------------
|
|
|
|
/// Recover the candidate XMSS root from a signature over `message`, binding to the
|
|
/// claimed public `root` in the message hash exactly as RFC 8391 H_msg requires:
|
|
/// M' = SHA256( toByte(2,32) || R || root || toByte(idx, 32) || M )
|
|
/// then WOTS_PKFromSig -> L-tree leaf -> compute_root along the auth path.
|
|
///
|
|
/// The signature is VALID iff the returned value equals `pk.root`. This function
|
|
/// is the exact per-validator obligation the aggregation circuit discharges.
|
|
pub fn xmss_recover_root(pk: &XmssPubKey, sig: &XmssSig, message: &[u8]) -> Hash {
|
|
// M' = H_msg( R || root || toByte(idx,32) || M ).
|
|
let mut msg_buf = [0u8; 32 + 32 + 32 + 32]; // prefix || R || root || toByte(idx,32)
|
|
msg_buf[0..32].copy_from_slice(&to_byte32(2));
|
|
msg_buf[32..64].copy_from_slice(&sig.r);
|
|
msg_buf[64..96].copy_from_slice(&pk.root);
|
|
// toByte(idx, 32): 28 zero bytes then the 4-byte big-endian index.
|
|
msg_buf[124..128].copy_from_slice(&sig.idx.to_be_bytes());
|
|
|
|
let mut hasher = crate::sha256::Sha256::new();
|
|
hasher.update(&msg_buf);
|
|
hasher.update(message);
|
|
let mhash = hasher.finalize();
|
|
|
|
let idx_leaf = sig.idx & ((1u32 << H) - 1);
|
|
let wpk = wots_pk_from_sig(&pk.seed, idx_leaf, &mhash, &sig.wots);
|
|
let leaf = l_tree(&pk.seed, idx_leaf, &wpk);
|
|
compute_root(&pk.seed, &leaf, idx_leaf, &sig.auth)
|
|
}
|
|
|
|
/// Verify an XMSS-SHA2_10_256 signature: true iff the recovered root equals the
|
|
/// public key's committed root. Constant-shape, hash-only, deterministic.
|
|
pub fn xmss_verify(pk: &XmssPubKey, sig: &XmssSig, message: &[u8]) -> bool {
|
|
xmss_recover_root(pk, sig, message) == pk.root
|
|
}
|