Skip to content

Commit 47e629e

Browse files
committed
[frontend] Add Winternitz OTS verifier
1 parent bc486f3 commit 47e629e

File tree

2 files changed

+351
-0
lines changed

2 files changed

+351
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod chain;
22
pub mod codeword;
33
pub mod tweak;
4+
pub mod winternitz_ots;
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
use super::{chain::verify_chain, codeword::codeword, tweak::verify_message_tweak};
2+
use crate::{
3+
circuits::keccak::Keccak,
4+
compiler::{CircuitBuilder, Wire},
5+
};
6+
7+
/// Verifies a Winternitz One-Time Signature.
8+
///
9+
/// This circuit implements verification for the Winternitz OTS scheme, which combines:
10+
/// 1. Message hashing with tweaking
11+
/// 2. Codeword extraction from the hash
12+
/// 3. Hash chain verification for each coordinate
13+
///
14+
/// # Arguments
15+
///
16+
/// * `builder` - Circuit builder for constructing constraints
17+
/// * `param` - Cryptographic parameter (32 bytes as 4x64-bit LE wires)
18+
/// * `message` - Message to verify (32 bytes as 4x64-bit LE wires)
19+
/// * `nonce` - Random nonce used in signing (23 bytes)
20+
/// * `signature_hashes` - Signature hash values (one per chain)
21+
/// * `public_key_hashes` - Public key end hashes (one per chain)
22+
/// * `spec` - Winternitz specification parameters
23+
///
24+
/// # Returns
25+
///
26+
/// A tuple containing:
27+
/// - Keccak hasher for the message tweak
28+
/// - Vector of Keccak hashers for the chain tweaks
29+
pub fn verify_winternitz_ots(
30+
builder: &CircuitBuilder,
31+
param: &[Wire],
32+
message: &[Wire],
33+
nonce: &[Wire],
34+
signature_hashes: &[[Wire; 4]],
35+
public_key_hashes: &[[Wire; 4]],
36+
spec: &WinternitzSpec,
37+
) -> (Keccak, Vec<Keccak>) {
38+
assert_eq!(param.len(), 4, "param must be 32 bytes as 4 wires");
39+
assert_eq!(message.len(), 4, "message must be 32 bytes as 4 wires");
40+
41+
// Step 1: Hash the message with tweaking (param || TWEAK_MESSAGE || nonce || message)
42+
let message_hash_output: [Wire; 4] = std::array::from_fn(|_| builder.add_witness());
43+
// Note: nonce is 23 bytes, but packed into 3 wires (24 bytes)
44+
let nonce_len = 23; // Actual nonce length in bytes
45+
let message_len = 32; // Message is 32 bytes (4 wires * 8 bytes)
46+
let message_hasher = verify_message_tweak(
47+
builder,
48+
param.to_vec(),
49+
param.len() * 8,
50+
nonce.to_vec(),
51+
nonce_len,
52+
message.to_vec(),
53+
message_len,
54+
message_hash_output,
55+
);
56+
57+
// Step 2: Extract codeword from message hash
58+
// Only use the first spec.message_hash_len bytes
59+
let message_hash_bytes = spec.message_hash_len;
60+
let message_hash_wires_needed = message_hash_bytes.div_ceil(8);
61+
let message_hash_for_codeword = &message_hash_output[..message_hash_wires_needed];
62+
63+
let coordinates = codeword(
64+
builder,
65+
spec.dimension,
66+
spec.coordinate_resolution_bits,
67+
spec.target_sum,
68+
message_hash_for_codeword,
69+
);
70+
71+
assert_eq!(coordinates.len(), spec.dimension, "Codeword dimension mismatch");
72+
assert_eq!(signature_hashes.len(), spec.dimension, "Signature hashes count mismatch");
73+
assert_eq!(public_key_hashes.len(), spec.dimension, "Public key hashes count mismatch");
74+
75+
// Step 3: Verify hash chains for each coordinate
76+
let mut all_chain_hashers = Vec::new();
77+
let max_chain_len = spec.chain_len();
78+
79+
for chain_index in 0..spec.dimension {
80+
let chain_index_wire = builder.add_constant_64(chain_index as u64);
81+
82+
// For each chain, verify that hashing from signature_hash for `coordinate` steps
83+
// produces the public_key_hash
84+
let chain_hashers = verify_chain(
85+
builder,
86+
param,
87+
param.len() * 8, // param_len in bytes
88+
chain_index_wire,
89+
signature_hashes[chain_index],
90+
coordinates[chain_index],
91+
max_chain_len as u64,
92+
public_key_hashes[chain_index],
93+
);
94+
95+
all_chain_hashers.extend(chain_hashers);
96+
}
97+
98+
(message_hasher, all_chain_hashers)
99+
}
100+
101+
/// Specification for Winternitz OTS parameters
102+
pub struct WinternitzSpec {
103+
/// Number of bytes from message hash to use
104+
pub message_hash_len: usize,
105+
/// Number of bits per coordinate in the codeword
106+
pub coordinate_resolution_bits: usize,
107+
/// Number of coordinates/chains
108+
pub dimension: usize,
109+
/// Expected sum of all coordinates
110+
pub target_sum: u64,
111+
}
112+
113+
impl WinternitzSpec {
114+
/// Returns the chain length (2^coordinate_resolution_bits)
115+
pub fn chain_len(&self) -> usize {
116+
1 << self.coordinate_resolution_bits
117+
}
118+
119+
/// Create a spec matching SPEC_1 from leansig-xmss
120+
pub fn spec_1() -> Self {
121+
Self {
122+
message_hash_len: 18,
123+
coordinate_resolution_bits: 2,
124+
dimension: 72, // 18 * 8 / 2
125+
target_sum: 119,
126+
}
127+
}
128+
129+
/// Create a spec matching SPEC_2 from leansig-xmss
130+
pub fn spec_2() -> Self {
131+
Self {
132+
message_hash_len: 18,
133+
coordinate_resolution_bits: 4,
134+
dimension: 36, // 18 * 8 / 4
135+
target_sum: 297,
136+
}
137+
}
138+
}
139+
140+
#[cfg(test)]
141+
mod tests {
142+
use rand::{RngCore, SeedableRng, rngs::StdRng};
143+
use sha3::{Digest, Keccak256};
144+
145+
use super::{
146+
super::tweak::{build_chain_tweak, build_message_tweak},
147+
*,
148+
};
149+
use crate::{constraint_verifier::verify_constraints, util::pack_bytes_into_wires_le};
150+
151+
fn hash_message_keccak(param: &[u8], nonce: &[u8], message: &[u8]) -> [u8; 32] {
152+
let mut hasher = Keccak256::new();
153+
hasher.update(param);
154+
hasher.update([0x02]); // TWEAK_MESSAGE
155+
hasher.update(nonce);
156+
hasher.update(message);
157+
hasher.finalize().into()
158+
}
159+
160+
fn hash_chain_keccak(
161+
param: &[u8],
162+
chain_index: usize,
163+
start_hash: &[u8; 32],
164+
start_pos: usize,
165+
num_hashes: usize,
166+
) -> [u8; 32] {
167+
let mut current = *start_hash;
168+
for i in 0..num_hashes {
169+
let position = start_pos + i + 1;
170+
let mut hasher = Keccak256::new();
171+
hasher.update(param);
172+
hasher.update([0x00]); // TWEAK_CHAIN
173+
hasher.update(current);
174+
hasher.update(chain_index.to_le_bytes());
175+
hasher.update(position.to_le_bytes());
176+
current = hasher.finalize().into();
177+
}
178+
current
179+
}
180+
181+
fn extract_coordinates(hash: &[u8], dimension: usize, resolution_bits: usize) -> Vec<u8> {
182+
let mut coords = Vec::new();
183+
let coords_per_byte = 8 / resolution_bits;
184+
let mask = (1u8 << resolution_bits) - 1;
185+
186+
for i in 0..dimension {
187+
let byte_idx = i / coords_per_byte;
188+
let coord_idx = i % coords_per_byte;
189+
let shift = coord_idx * resolution_bits;
190+
let coord = (hash[byte_idx] >> shift) & mask;
191+
coords.push(coord);
192+
}
193+
coords
194+
}
195+
196+
#[test]
197+
fn test_winternitz_ots_verification() {
198+
let spec = WinternitzSpec::spec_1();
199+
let builder = CircuitBuilder::new();
200+
201+
// Create input wires
202+
let param: Vec<Wire> = (0..4).map(|_| builder.add_inout()).collect();
203+
let message: Vec<Wire> = (0..4).map(|_| builder.add_inout()).collect();
204+
let nonce: Vec<Wire> = (0..3).map(|_| builder.add_inout()).collect(); // 23 bytes = 3*8 - 1
205+
206+
let signature_hashes: Vec<[Wire; 4]> = (0..spec.dimension)
207+
.map(|_| std::array::from_fn(|_| builder.add_inout()))
208+
.collect();
209+
210+
let public_key_hashes: Vec<[Wire; 4]> = (0..spec.dimension)
211+
.map(|_| std::array::from_fn(|_| builder.add_inout()))
212+
.collect();
213+
214+
// Create the verification circuit
215+
let (message_hasher, chain_hashers) = verify_winternitz_ots(
216+
&builder,
217+
&param,
218+
&message,
219+
&nonce,
220+
&signature_hashes,
221+
&public_key_hashes,
222+
&spec,
223+
);
224+
225+
let circuit = builder.build();
226+
let mut w = circuit.new_witness_filler();
227+
228+
// Generate test data
229+
let mut rng = StdRng::seed_from_u64(42);
230+
231+
// Set up param (32 bytes)
232+
let mut param_bytes = [0u8; 32];
233+
rng.fill_bytes(&mut param_bytes);
234+
pack_bytes_into_wires_le(&mut w, &param, &param_bytes);
235+
236+
// Set up message (32 bytes)
237+
let mut message_bytes = [0u8; 32];
238+
rng.fill_bytes(&mut message_bytes);
239+
pack_bytes_into_wires_le(&mut w, &message, &message_bytes);
240+
241+
// Grind for a valid nonce that produces the target sum
242+
let mut nonce_bytes = [0u8; 23];
243+
let mut coords = Vec::new();
244+
let mut message_hash = [0u8; 32];
245+
246+
// Try up to 1000 times to find a valid nonce
247+
let mut found = false;
248+
for _ in 0..1000 {
249+
rng.fill_bytes(&mut nonce_bytes);
250+
message_hash = hash_message_keccak(&param_bytes, &nonce_bytes, &message_bytes);
251+
252+
coords = extract_coordinates(
253+
&message_hash[..spec.message_hash_len],
254+
spec.dimension,
255+
spec.coordinate_resolution_bits,
256+
);
257+
258+
let coord_sum: usize = coords.iter().map(|&c| c as usize).sum();
259+
if coord_sum == spec.target_sum as usize {
260+
found = true;
261+
break;
262+
}
263+
}
264+
265+
assert!(found, "Failed to find valid nonce after 1000 attempts");
266+
267+
// Pack nonce into wires (24 bytes, last byte is 0)
268+
let mut nonce_padded = [0u8; 24];
269+
nonce_padded[..23].copy_from_slice(&nonce_bytes);
270+
pack_bytes_into_wires_le(&mut w, &nonce, &nonce_padded);
271+
272+
// Generate signature and public key hashes
273+
let mut sig_hashes = Vec::new();
274+
let mut pk_hashes = Vec::new();
275+
276+
for (chain_idx, &coord) in coords.iter().enumerate() {
277+
// Generate random signature hash
278+
let mut sig_hash = [0u8; 32];
279+
rng.fill_bytes(&mut sig_hash);
280+
sig_hashes.push(sig_hash);
281+
282+
// Compute public key hash by hashing forward
283+
let pk_hash = hash_chain_keccak(
284+
&param_bytes,
285+
chain_idx,
286+
&sig_hash,
287+
coord as usize,
288+
spec.chain_len() - 1 - coord as usize,
289+
);
290+
pk_hashes.push(pk_hash);
291+
292+
// Pack into wires
293+
pack_bytes_into_wires_le(&mut w, &signature_hashes[chain_idx], &sig_hash);
294+
pack_bytes_into_wires_le(&mut w, &public_key_hashes[chain_idx], &pk_hash);
295+
}
296+
297+
// Populate message hasher
298+
// Note: param and message are already populated above
299+
// Populate nonce (23 bytes into 3 wires)
300+
let mut padded_nonce = [0u8; 24];
301+
padded_nonce[..23].copy_from_slice(&nonce_bytes);
302+
pack_bytes_into_wires_le(&mut w, &nonce, &padded_nonce);
303+
304+
let tweaked_message = build_message_tweak(&param_bytes, &nonce_bytes, &message_bytes);
305+
message_hasher.populate_message(&mut w, &tweaked_message);
306+
// Populate the digest (only first 32 bytes needed for SHA3-256)
307+
message_hasher.populate_digest(&mut w, message_hash);
308+
309+
// Populate chain hashers
310+
let mut hasher_idx = 0;
311+
for (chain_idx, &coord) in coords.iter().enumerate() {
312+
let mut current_hash = sig_hashes[chain_idx];
313+
314+
for step in 0..spec.chain_len() {
315+
let position = step + coord as usize;
316+
let position_plus_one = position + 1;
317+
318+
// Compute next hash
319+
let next_hash =
320+
hash_chain_keccak(&param_bytes, chain_idx, &current_hash, position, 1);
321+
322+
// Populate the Keccak hasher
323+
let keccak = &chain_hashers[hasher_idx];
324+
325+
let chain_message = build_chain_tweak(
326+
&param_bytes,
327+
&current_hash,
328+
chain_idx as u64,
329+
position_plus_one as u64,
330+
);
331+
keccak.populate_message(&mut w, &chain_message);
332+
// Populate the digest
333+
keccak.populate_digest(&mut w, next_hash);
334+
335+
// Update current hash if used
336+
if position_plus_one < spec.chain_len() {
337+
current_hash = next_hash;
338+
}
339+
340+
hasher_idx += 1;
341+
}
342+
}
343+
344+
// Populate witness and verify
345+
circuit.populate_wire_witness(&mut w).unwrap();
346+
347+
let cs = circuit.constraint_system();
348+
verify_constraints(cs, &w.into_value_vec()).unwrap();
349+
}
350+
}

0 commit comments

Comments
 (0)