Skip to content

Commit fda3288

Browse files
committed
[frontend] Add xmss verifier
1 parent 40c06bf commit fda3288

File tree

4 files changed

+713
-0
lines changed

4 files changed

+713
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod codeword;
33
pub mod merkle_tree;
44
pub mod tweak;
55
pub mod winternitz_ots;
6+
pub mod xmss;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
mod base;
33
mod chain;
44
mod message;
5+
mod public_key;
56
mod tree;
67

78
pub use chain::{CHAIN_TWEAK, FIXED_MESSAGE_OVERHEAD, build_chain_tweak, verify_chain_tweak};
89
pub use message::{MESSAGE_TWEAK, build_message_tweak, verify_message_tweak};
10+
pub use public_key::{PUBLIC_KEY_TWEAK, build_public_key_tweak, verify_public_key_tweak};
911
pub use tree::{TREE_MESSAGE_OVERHEAD, TREE_TWEAK, build_tree_tweak, verify_tree_tweak};
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
use super::base::verify_tweaked_keccak;
2+
// Note: PublicKeyTweak reuses TREE_TWEAK (0x01) for consistency with XMSS spec
3+
pub use super::tree::TREE_TWEAK as PUBLIC_KEY_TWEAK;
4+
use crate::{
5+
circuits::{concat::Term, keccak::Keccak},
6+
compiler::{CircuitBuilder, Wire},
7+
};
8+
9+
/// A circuit that verifies a public key hash for XMSS.
10+
///
11+
/// This circuit verifies Keccak-256 of a message that's been tweaked with
12+
/// multiple public key hashes: `Keccak256(param || 0x01 || pk_hash_0 || pk_hash_1 || ... ||
13+
/// pk_hash_n)`
14+
///
15+
/// # Arguments
16+
///
17+
/// * `builder` - Circuit builder for constructing constraints
18+
/// * `param_wires` - The cryptographic parameter wires, where each wire holds 8 bytes as a 64-bit
19+
/// LE-packed value
20+
/// * `param_len` - The actual parameter length in bytes
21+
/// * `pk_hashes` - The public key end hashes (32 bytes each as 4x64-bit LE-packed wires)
22+
/// * `digest` - Output: The computed Keccak-256 digest (32 bytes as 4x64-bit LE-packed wires)
23+
///
24+
/// # Returns
25+
///
26+
/// A `Keccak` circuit that needs to be populated with the tweaked message and digest
27+
pub fn verify_public_key_tweak(
28+
builder: &CircuitBuilder,
29+
param_wires: Vec<Wire>,
30+
param_len: usize,
31+
pk_hashes: &[[Wire; 4]],
32+
digest: [Wire; 4],
33+
) -> Keccak {
34+
let num_hashes = pk_hashes.len();
35+
let message_len = param_len + 1 + (num_hashes * 32); // +1 for tweak byte
36+
assert_eq!(param_wires.len(), param_len.div_ceil(8));
37+
38+
// Build additional terms for all public key hashes
39+
let mut additional_terms = Vec::new();
40+
41+
// Add all public key hashes
42+
for pk_hash in pk_hashes {
43+
let hash_term = Term {
44+
len: builder.add_constant_64(32),
45+
data: pk_hash.to_vec(),
46+
max_len: 32,
47+
};
48+
additional_terms.push(hash_term);
49+
}
50+
51+
verify_tweaked_keccak(
52+
builder,
53+
param_wires,
54+
param_len,
55+
PUBLIC_KEY_TWEAK,
56+
additional_terms,
57+
message_len,
58+
digest,
59+
)
60+
}
61+
62+
/// Build the tweaked message for public key hashing.
63+
///
64+
/// Constructs the complete message for Keccak-256 hashing by concatenating:
65+
/// `param || 0x01 || pk_hash_0 || pk_hash_1 || ... || pk_hash_n`
66+
///
67+
/// This function is typically used when populating witness data for the
68+
/// `verify_public_key_tweak` circuit.
69+
///
70+
/// # Arguments
71+
///
72+
/// * `param_bytes` - The cryptographic parameter bytes
73+
/// * `pk_hashes` - Array of 32-byte public key hashes
74+
///
75+
/// # Returns
76+
///
77+
/// A vector containing the complete tweaked message ready for hashing
78+
pub fn build_public_key_tweak(param_bytes: &[u8], pk_hashes: &[[u8; 32]]) -> Vec<u8> {
79+
let mut message = Vec::new();
80+
message.extend_from_slice(param_bytes);
81+
message.push(PUBLIC_KEY_TWEAK);
82+
for pk_hash in pk_hashes {
83+
message.extend_from_slice(pk_hash);
84+
}
85+
message
86+
}
87+
88+
#[cfg(test)]
89+
mod tests {
90+
use proptest::prelude::*;
91+
use sha3::{Digest, Keccak256};
92+
93+
use super::*;
94+
use crate::{
95+
compiler::{CircuitBuilder, circuit::Circuit},
96+
constraint_verifier::verify_constraints,
97+
util::pack_bytes_into_wires_le,
98+
};
99+
100+
/// Helper struct for PublicKeyTweak testing
101+
struct PublicKeyTestCircuit {
102+
circuit: Circuit,
103+
keccak: Keccak,
104+
param_wires: Vec<Wire>,
105+
param_len: usize,
106+
pk_hashes: Vec<[Wire; 4]>,
107+
}
108+
109+
impl PublicKeyTestCircuit {
110+
fn new(param_len: usize, num_hashes: usize) -> Self {
111+
let builder = CircuitBuilder::new();
112+
113+
let digest: [Wire; 4] = std::array::from_fn(|_| builder.add_inout());
114+
115+
let num_param_wires = param_len.div_ceil(8);
116+
let param_wires: Vec<Wire> =
117+
(0..num_param_wires).map(|_| builder.add_inout()).collect();
118+
119+
let pk_hashes: Vec<[Wire; 4]> = (0..num_hashes)
120+
.map(|_| std::array::from_fn(|_| builder.add_inout()))
121+
.collect();
122+
123+
let keccak = verify_public_key_tweak(
124+
&builder,
125+
param_wires.clone(),
126+
param_len,
127+
&pk_hashes,
128+
digest,
129+
);
130+
131+
let circuit = builder.build();
132+
133+
Self {
134+
circuit,
135+
keccak,
136+
param_wires,
137+
param_len,
138+
pk_hashes,
139+
}
140+
}
141+
142+
/// Populate witness and verify constraints with given test data
143+
fn populate_and_verify(
144+
&self,
145+
param_bytes: &[u8],
146+
pk_hashes_bytes: &[[u8; 32]],
147+
message: &[u8],
148+
digest: [u8; 32],
149+
) -> Result<(), Box<dyn std::error::Error>> {
150+
let mut w = self.circuit.new_witness_filler();
151+
152+
// Populate param
153+
assert_eq!(param_bytes.len(), self.param_len);
154+
pack_bytes_into_wires_le(&mut w, &self.param_wires, param_bytes);
155+
156+
// Populate public key hashes
157+
assert_eq!(pk_hashes_bytes.len(), self.pk_hashes.len());
158+
for (wires, bytes) in self.pk_hashes.iter().zip(pk_hashes_bytes.iter()) {
159+
pack_bytes_into_wires_le(&mut w, wires, bytes);
160+
}
161+
162+
// Populate message for Keccak
163+
let expected_len = self.param_len + 1 + (self.pk_hashes.len() * 32);
164+
assert_eq!(
165+
message.len(),
166+
expected_len,
167+
"Message length {} doesn't match expected length {}",
168+
message.len(),
169+
expected_len
170+
);
171+
self.keccak.populate_message(&mut w, message);
172+
173+
// Populate digest
174+
self.keccak.populate_digest(&mut w, digest);
175+
176+
self.circuit.populate_wire_witness(&mut w)?;
177+
let cs = self.circuit.constraint_system();
178+
verify_constraints(cs, &w.into_value_vec())?;
179+
Ok(())
180+
}
181+
}
182+
183+
#[test]
184+
fn test_public_key_tweak_basic() {
185+
let test_circuit = PublicKeyTestCircuit::new(32, 3);
186+
187+
let param_bytes = b"test_parameter_32_bytes_long!!!!";
188+
let pk_hashes = [
189+
*b"first_public_key_hash_32_bytes!!",
190+
*b"second_public_key_hash_32_bytes!",
191+
*b"third_public_key_hash_32_bytes!!",
192+
];
193+
194+
let message = build_public_key_tweak(param_bytes, &pk_hashes);
195+
196+
let expected_digest = Keccak256::digest(&message);
197+
198+
test_circuit
199+
.populate_and_verify(param_bytes, &pk_hashes, &message, expected_digest.into())
200+
.unwrap();
201+
}
202+
203+
#[test]
204+
fn test_public_key_tweak_with_18_byte_param() {
205+
// Test with 18-byte param as per XMSS specifications
206+
let test_circuit = PublicKeyTestCircuit::new(18, 2);
207+
208+
let param_bytes: &[u8; 18] = b"test_param_18bytes";
209+
let pk_hashes = [
210+
*b"first_public_key_hash_32_bytes!!",
211+
*b"second_public_key_hash_32_bytes!",
212+
];
213+
214+
let message = build_public_key_tweak(param_bytes, &pk_hashes);
215+
216+
let expected_digest = Keccak256::digest(&message);
217+
218+
test_circuit
219+
.populate_and_verify(param_bytes, &pk_hashes, &message, expected_digest.into())
220+
.unwrap();
221+
}
222+
223+
#[test]
224+
fn test_public_key_tweak_single_hash() {
225+
let test_circuit = PublicKeyTestCircuit::new(32, 1);
226+
227+
let param_bytes = b"test_parameter_32_bytes_long!!!!";
228+
let pk_hashes = [*b"single_public_key_hash_32_bytes!"];
229+
230+
let message = build_public_key_tweak(param_bytes, &pk_hashes);
231+
232+
let expected_digest = Keccak256::digest(&message);
233+
234+
test_circuit
235+
.populate_and_verify(param_bytes, &pk_hashes, &message, expected_digest.into())
236+
.unwrap();
237+
}
238+
239+
#[test]
240+
fn test_public_key_tweak_many_hashes() {
241+
// Test with many hashes (e.g., Winternitz with 72 chains)
242+
let num_hashes = 72;
243+
let test_circuit = PublicKeyTestCircuit::new(18, num_hashes);
244+
245+
let param_bytes: &[u8; 18] = b"test_param_18bytes";
246+
247+
// Generate deterministic hashes for testing
248+
let mut pk_hashes = Vec::new();
249+
for i in 0..num_hashes {
250+
let mut hash = [0u8; 32];
251+
hash[0] = i as u8;
252+
hash[1] = (i >> 8) as u8;
253+
for j in 2..32 {
254+
hash[j] = ((i * j) % 256) as u8;
255+
}
256+
pk_hashes.push(hash);
257+
}
258+
259+
let message = build_public_key_tweak(param_bytes, &pk_hashes);
260+
261+
let expected_digest = Keccak256::digest(&message);
262+
263+
test_circuit
264+
.populate_and_verify(param_bytes, &pk_hashes, &message, expected_digest.into())
265+
.unwrap();
266+
}
267+
268+
#[test]
269+
fn test_public_key_tweak_wrong_digest() {
270+
let test_circuit = PublicKeyTestCircuit::new(32, 2);
271+
272+
let param_bytes = b"test_parameter_32_bytes_long!!!!";
273+
let pk_hashes = [
274+
*b"first_public_key_hash_32_bytes!!",
275+
*b"second_public_key_hash_32_bytes!",
276+
];
277+
278+
let message = build_public_key_tweak(param_bytes, &pk_hashes);
279+
280+
// Populate with WRONG digest - this should cause verification to fail
281+
let wrong_digest = [0u8; 32];
282+
283+
let result =
284+
test_circuit.populate_and_verify(param_bytes, &pk_hashes, &message, wrong_digest);
285+
286+
assert!(result.is_err(), "Expected error for wrong digest");
287+
}
288+
289+
#[test]
290+
fn test_public_key_tweak_ensures_tweak_byte() {
291+
// This test verifies that the PUBLIC_KEY_TWEAK byte (0x01) is correctly inserted
292+
let test_circuit = PublicKeyTestCircuit::new(16, 1);
293+
294+
let param_bytes = b"param_16_bytes!!";
295+
let pk_hashes = [*b"single_public_key_hash_32_bytes!"];
296+
297+
let message = build_public_key_tweak(param_bytes, &pk_hashes);
298+
299+
// Verify the tweak byte is at the correct position
300+
assert_eq!(message[16], PUBLIC_KEY_TWEAK);
301+
assert_eq!(message.len(), 16 + 1 + 32); // param + tweak + one hash
302+
303+
let expected_digest = Keccak256::digest(&message);
304+
305+
test_circuit
306+
.populate_and_verify(param_bytes, &pk_hashes, &message, expected_digest.into())
307+
.unwrap();
308+
}
309+
310+
proptest! {
311+
#[test]
312+
fn test_public_key_tweak_property_based(
313+
param_len in 1usize..=100,
314+
num_hashes in 1usize..=10,
315+
) {
316+
use rand::SeedableRng;
317+
use rand::prelude::StdRng;
318+
319+
let mut rng = StdRng::seed_from_u64(0);
320+
321+
// Generate random param bytes
322+
let mut param_bytes = vec![0u8; param_len];
323+
rng.fill_bytes(&mut param_bytes);
324+
325+
// Generate random public key hashes
326+
let mut pk_hashes = Vec::new();
327+
for _ in 0..num_hashes {
328+
let mut hash = [0u8; 32];
329+
rng.fill_bytes(&mut hash);
330+
pk_hashes.push(hash);
331+
}
332+
333+
// Create circuit
334+
let test_circuit = PublicKeyTestCircuit::new(param_len, num_hashes);
335+
336+
// Build message and compute digest
337+
let message = build_public_key_tweak(&param_bytes, &pk_hashes);
338+
339+
// Verify message structure
340+
prop_assert_eq!(message.len(), param_len + 1 + (num_hashes * 32));
341+
prop_assert_eq!(message[param_len], PUBLIC_KEY_TWEAK);
342+
343+
let expected_digest: [u8; 32] = Keccak256::digest(&message).into();
344+
345+
// Verify circuit
346+
test_circuit
347+
.populate_and_verify(&param_bytes, &pk_hashes, &message, expected_digest)
348+
.unwrap();
349+
}
350+
}
351+
}

0 commit comments

Comments
 (0)