Skip to content

Commit b0fecc5

Browse files
committed
[frontend] Add xmss verifier
1 parent 84124c1 commit b0fecc5

File tree

5 files changed

+646
-0
lines changed

5 files changed

+646
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ rand = { version = "0.9.1", default-features = false, features = [
6363
rayon = "1.10.0"
6464
regex = "1.10"
6565
rsa = { version = "0.9.8", features = ["sha2"] }
66+
rstest = "0.26.1"
6667
seq-macro = "0.3.5"
6768
serde = { version = "1.0.219", features = ["derive"] }
6869
serde_json = "1.0.140"

crates/frontend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ rsa = { workspace = true, features = ["sha2"] }
3030
hex-literal = { workspace = true }
3131
num-traits = { workspace = true }
3232
proptest = { workspace = true }
33+
rstest = { workspace = true }
3334
sha2 = { workspace = true }

crates/frontend/src/circuits/hash_based_sig/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ pub mod codeword;
33
pub mod hashing;
44
pub mod merkle_tree;
55
pub mod winternitz_ots;
6+
pub mod xmss;
7+
8+
#[cfg(test)]
9+
mod test_utils;
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//! Test utilities for hash-based signature verification tests.
2+
#[cfg(test)]
3+
use super::{
4+
hashing::{
5+
build_chain_hash, build_message_hash, build_public_key_hash, build_tree_hash,
6+
hash_chain_keccak, hash_message, hash_public_key_keccak, hash_tree_node_keccak,
7+
},
8+
winternitz_ots::WinternitzSpec,
9+
xmss::XmssHashers,
10+
};
11+
12+
/// Builds a complete Merkle tree from leaf nodes.
13+
///
14+
/// This function assumes the number of leaves is a power of 2, which is the case
15+
/// for all hash-based signature tests.
16+
///
17+
/// # Returns
18+
/// A tuple containing:
19+
/// - Vector of tree levels (index 0 = leaves, last index = root)
20+
/// - The root hash
21+
#[cfg(test)]
22+
pub fn build_merkle_tree(param: &[u8], leaves: &[[u8; 32]]) -> (Vec<Vec<[u8; 32]>>, [u8; 32]) {
23+
debug_assert!(leaves.len().is_power_of_two(), "Number of leaves must be a power of 2");
24+
25+
let tree_depth = leaves.len().trailing_zeros() as usize;
26+
let mut tree_levels = vec![leaves.to_vec()];
27+
28+
for level in 0..tree_depth {
29+
let current_level = &tree_levels[level];
30+
let mut next_level = Vec::new();
31+
32+
for i in (0..current_level.len()).step_by(2) {
33+
let parent = hash_tree_node_keccak(
34+
param,
35+
&current_level[i],
36+
&current_level[i + 1],
37+
level as u32,
38+
(i / 2) as u32,
39+
);
40+
next_level.push(parent);
41+
}
42+
43+
tree_levels.push(next_level);
44+
}
45+
46+
let root = tree_levels[tree_depth][0];
47+
(tree_levels, root)
48+
}
49+
50+
/// Extracts the authentication path for a given leaf index in a Merkle tree.
51+
///
52+
/// This function assumes the tree has power-of-2 leaves, which is the case
53+
/// for all our hash-based signature tests.
54+
///
55+
/// # Arguments
56+
/// * `tree_levels` - All levels of the tree (from build_merkle_tree)
57+
/// * `leaf_index` - Index of the leaf to build path for
58+
///
59+
/// # Returns
60+
/// Vector of sibling hashes from leaf to root
61+
#[cfg(test)]
62+
pub fn extract_auth_path(tree_levels: &[Vec<[u8; 32]>], leaf_index: usize) -> Vec<[u8; 32]> {
63+
let mut auth_path = Vec::new();
64+
let mut idx = leaf_index;
65+
let tree_height = tree_levels.len() - 1;
66+
67+
for level in 0..tree_height {
68+
let sibling_idx = idx ^ 1;
69+
auth_path.push(tree_levels[level][sibling_idx]);
70+
idx /= 2;
71+
}
72+
73+
auth_path
74+
}
75+
76+
/// Data structure containing all the information needed to populate XMSS hashers.
77+
#[cfg(test)]
78+
pub struct XmssHasherData {
79+
/// Parameter bytes (variable length based on spec)
80+
pub param_bytes: Vec<u8>,
81+
/// Message bytes (32 bytes)
82+
pub message_bytes: [u8; 32],
83+
/// Nonce bytes (variable length, typically 23)
84+
pub nonce_bytes: Vec<u8>,
85+
/// Epoch/leaf index
86+
pub epoch: u64,
87+
/// Codeword coordinates
88+
pub coords: Vec<u8>,
89+
/// Signature hashes for each chain
90+
pub sig_hashes: Vec<[u8; 32]>,
91+
/// Public key hashes for each chain
92+
pub pk_hashes: Vec<[u8; 32]>,
93+
/// Authentication path for Merkle tree
94+
pub auth_path: Vec<[u8; 32]>,
95+
}
96+
97+
/// Populates all hashers in an XmssHashers struct with witness data.
98+
///
99+
/// This function fills in the message hasher, chain hashers, public key hasher,
100+
/// and Merkle path hashers with the appropriate witness data for verification.
101+
///
102+
/// # Arguments
103+
///
104+
/// * `w` - The witness filler to populate
105+
/// * `hashers` - The XMSS hashers to populate
106+
/// * `spec` - The Winternitz specification
107+
/// * `data` - The data to use for population
108+
#[cfg(test)]
109+
pub fn populate_xmss_hashers(
110+
w: &mut crate::compiler::circuit::WitnessFiller,
111+
hashers: &XmssHashers,
112+
spec: &WinternitzSpec,
113+
data: &XmssHasherData,
114+
) {
115+
// Populate message hasher
116+
let message_hash = hash_message(&data.param_bytes, &data.nonce_bytes, &data.message_bytes);
117+
let tweaked_message =
118+
build_message_hash(&data.param_bytes, &data.nonce_bytes, &data.message_bytes);
119+
120+
hashers
121+
.winternitz_ots
122+
.message_hasher
123+
.populate_message(w, &tweaked_message);
124+
hashers
125+
.winternitz_ots
126+
.message_hasher
127+
.populate_digest(w, message_hash);
128+
129+
// Populate chain hashers
130+
let mut hasher_idx = 0;
131+
for (chain_idx, &coord) in data.coords.iter().enumerate() {
132+
let mut current_hash = data.sig_hashes[chain_idx];
133+
134+
for step in 0..spec.chain_len() {
135+
let position = step + coord as usize;
136+
let position_plus_one = position + 1;
137+
138+
let next_hash =
139+
hash_chain_keccak(&data.param_bytes, chain_idx, &current_hash, position, 1);
140+
141+
let hasher = &hashers.winternitz_ots.chain_hashers[hasher_idx];
142+
let chain_message = build_chain_hash(
143+
&data.param_bytes,
144+
&current_hash,
145+
chain_idx as u64,
146+
position_plus_one as u64,
147+
);
148+
hasher.populate_message(w, &chain_message);
149+
hasher.populate_digest(w, next_hash);
150+
151+
if position_plus_one < spec.chain_len() {
152+
current_hash = next_hash;
153+
}
154+
155+
hasher_idx += 1;
156+
}
157+
}
158+
159+
// Populate public key hasher
160+
let pk_message = build_public_key_hash(&data.param_bytes, &data.pk_hashes);
161+
let pk_hash = hash_public_key_keccak(&data.param_bytes, &data.pk_hashes);
162+
hashers.public_key_hasher.populate_message(w, &pk_message);
163+
hashers.public_key_hasher.populate_digest(w, pk_hash);
164+
165+
// Populate merkle path hashers
166+
let mut current_hash = pk_hash;
167+
let mut current_index = data.epoch as usize;
168+
169+
for (level, auth_sibling) in data.auth_path.iter().enumerate() {
170+
let (left, right) = if current_index % 2 == 0 {
171+
(&current_hash, auth_sibling)
172+
} else {
173+
(auth_sibling, &current_hash)
174+
};
175+
176+
let parent = hash_tree_node_keccak(
177+
&data.param_bytes,
178+
left,
179+
right,
180+
level as u32,
181+
(current_index / 2) as u32,
182+
);
183+
184+
let tree_message = build_tree_hash(
185+
&data.param_bytes,
186+
left,
187+
right,
188+
level as u32,
189+
(current_index / 2) as u32,
190+
);
191+
192+
hashers.merkle_path_hashers[level].populate_message(w, &tree_message);
193+
hashers.merkle_path_hashers[level].populate_digest(w, parent);
194+
195+
current_hash = parent;
196+
current_index /= 2;
197+
}
198+
}

0 commit comments

Comments
 (0)