Skip to main content

binius_circuits/
hmac.rs

1// Copyright 2025 Irreducible Inc.
2//! HMAC (Hash-based Message Authentication Code) circuit implementation using SHA-512.
3//!
4//! This module provides circuit gadgets for computing HMAC-SHA512 on fixed-length messages.
5//! HMAC is defined in RFC 2104 and provides message authentication using a shared secret key.
6
7use binius_core::word::Word;
8use binius_frontend::{CircuitBuilder, Wire};
9
10use super::sha512::sha512_fixed;
11
12/// Computes HMAC-SHA512 on a fixed-length message with a given key.
13///
14/// Implements the HMAC construction: `H((K ⊕ opad) || H((K ⊕ ipad) || M))`
15/// where H is SHA-512, and ipad/opad are the inner and outer padding constants.
16///
17/// # Arguments
18/// * `builder` - The circuit builder to add constraints to
19/// * `key` - The secret key as wires (words in big-endian format)
20/// * `message` - The message to authenticate as wires (words in big-endian format)
21/// * `message_len_bytes` - The actual message length in bytes
22///
23/// # Returns
24/// * Array of 8 wires containing the 512-bit HMAC output
25///
26/// # Preconditions
27/// * Key length must be at most 128 bytes (SHA-512 block size): `key.len() <= 16`
28/// * Message length must match wire count: `message.len() == message_len_bytes.div_ceil(8)`
29pub fn hmac_sha512_fixed(
30	builder: &mut CircuitBuilder,
31	key: &[Wire],
32	message: &[Wire],
33	message_len_bytes: usize,
34) -> [Wire; 8] {
35	const BLOCK_SIZE_WORDS: usize = 16; // 128 bytes = 16 * 8-byte words
36	const IPAD_BYTE: u8 = 0x36;
37	const OPAD_BYTE: u8 = 0x5C;
38
39	// Precondition: key must be at most block size
40	assert!(
41		key.len() <= BLOCK_SIZE_WORDS,
42		"Key length {} words exceeds maximum {} words (128 bytes)",
43		key.len(),
44		BLOCK_SIZE_WORDS
45	);
46
47	// Precondition: message wires must match message length
48	assert_eq!(
49		message.len(),
50		message_len_bytes.div_ceil(8),
51		"Message wire count {} doesn't match message length {} bytes",
52		message.len(),
53		message_len_bytes
54	);
55
56	// Create padded key (pad with zeros to block size)
57	let mut padded_key = Vec::with_capacity(BLOCK_SIZE_WORDS);
58	padded_key.extend_from_slice(key);
59	let zero = builder.add_constant(Word::ZERO);
60	padded_key.resize(BLOCK_SIZE_WORDS, zero);
61
62	// Create ipad and opad constants
63	let ipad_word = builder.add_constant(Word(u64::from_le_bytes([IPAD_BYTE; 8])));
64	let opad_word = builder.add_constant(Word(u64::from_le_bytes([OPAD_BYTE; 8])));
65
66	// Compute K ⊕ ipad
67	let key_xor_ipad = padded_key
68		.iter()
69		.map(|&key_word| builder.bxor(key_word, ipad_word))
70		.collect::<Vec<_>>();
71
72	// Compute K ⊕ opad
73	let key_xor_opad = padded_key
74		.iter()
75		.map(|&key_word| builder.bxor(key_word, opad_word))
76		.collect::<Vec<_>>();
77
78	// Inner hash: H((K ⊕ ipad) || M)
79	let mut inner_input = Vec::with_capacity(BLOCK_SIZE_WORDS + message.len());
80	inner_input.extend_from_slice(&key_xor_ipad);
81	inner_input.extend_from_slice(message);
82	let inner_len_bytes = 128 + message_len_bytes; // block size + message length
83	let inner_hash = sha512_fixed(builder, &inner_input, inner_len_bytes);
84
85	// Outer hash: H((K ⊕ opad) || inner_hash)
86	let mut outer_input = Vec::with_capacity(BLOCK_SIZE_WORDS + 8);
87	outer_input.extend_from_slice(&key_xor_opad);
88	outer_input.extend_from_slice(&inner_hash);
89	let outer_len_bytes = 128 + 64; // block size + SHA-512 output size
90	sha512_fixed(builder, &outer_input, outer_len_bytes)
91}
92
93#[cfg(test)]
94mod tests {
95	use std::{array, iter::repeat_with};
96
97	use binius_core::word::Word;
98	use hmac::{Hmac, KeyInit, Mac};
99	use rand::prelude::*;
100	use sha2::Sha512;
101
102	use super::*;
103
104	type HmacSha512 = Hmac<Sha512>;
105
106	#[test]
107	fn test_hmac_sha512_random_messages() {
108		let mut rng = StdRng::seed_from_u64(0);
109
110		for _ in 0..10 {
111			// Generate random key (up to 128 bytes)
112			let key_len = rng.random_range(1..=16) * 8; // Multiple of 8 bytes
113			let mut key_bytes = vec![0u8; key_len];
114			rng.fill(&mut key_bytes[..]);
115
116			// Generate random message
117			let message_len = rng.random_range(0..=256);
118			let mut message_bytes = vec![0u8; message_len];
119			rng.fill(&mut message_bytes[..]);
120
121			// Compute expected HMAC using hmac crate
122			let mut mac = HmacSha512::new_from_slice(&key_bytes).unwrap();
123			mac.update(&message_bytes);
124			let expected = mac.finalize().into_bytes();
125
126			// Build circuit
127			let mut builder = CircuitBuilder::new();
128
129			// Create key witness wires
130			let mut key_wires = Vec::new();
131			for _ in 0..key_bytes.len().div_ceil(8) {
132				key_wires.push(builder.add_witness());
133			}
134
135			// Create message witness wires
136			let message_wires = repeat_with(|| builder.add_witness())
137				.take(message_len.div_ceil(8))
138				.collect::<Vec<_>>();
139
140			// Compute HMAC
141			let output =
142				hmac_sha512_fixed(&mut builder, &key_wires, &message_wires, message_bytes.len());
143
144			// Create expected output witness wires
145			let expected_wires = array::from_fn::<_, 8, _>(|_| builder.add_witness());
146
147			// Assert equality
148			for (i, (&computed, &expected)) in output.iter().zip(expected_wires.iter()).enumerate()
149			{
150				builder.assert_eq(format!("hmac_output_{i}"), computed, expected);
151			}
152
153			let circuit = builder.build();
154			let mut witness = circuit.new_witness_filler();
155
156			// Populate key witness values
157			for (i, chunk) in key_bytes.chunks(8).enumerate() {
158				let mut word_bytes = [0u8; 8];
159				word_bytes[..chunk.len()].copy_from_slice(chunk);
160				let word = u64::from_be_bytes(word_bytes);
161				witness[key_wires[i]] = Word(word);
162			}
163
164			// Populate message witness values
165			for (i, chunk) in message_bytes.chunks(8).enumerate() {
166				let mut word_bytes = [0u8; 8];
167				word_bytes[..chunk.len()].copy_from_slice(chunk);
168				let word = u64::from_be_bytes(word_bytes);
169				witness[message_wires[i]] = Word(word);
170			}
171
172			// Populate expected output witness values
173			for (i, chunk) in expected.chunks(8).enumerate() {
174				let word = u64::from_be_bytes(chunk.try_into().unwrap());
175				witness[expected_wires[i]] = Word(word);
176			}
177
178			circuit
179				.populate_wire_witness(&mut witness)
180				.expect("Circuit should populate witnesses successfully");
181
182			// Verify constraints
183			let cs = circuit.constraint_system();
184			cs.verify(&witness.into_value_vec())
185				.expect("Circuit constraints should be satisfied with random data");
186		}
187	}
188
189	#[test]
190	#[should_panic(expected = "Key length 17 words exceeds maximum 16 words")]
191	fn test_hmac_key_too_large() {
192		let mut builder = CircuitBuilder::new();
193
194		// Create key that's too large (17 words = 136 bytes)
195		let mut key_wires = Vec::new();
196		for _ in 0..17 {
197			key_wires.push(builder.add_constant(Word::ZERO));
198		}
199
200		let message = vec![builder.add_constant(Word::ZERO)];
201
202		// This should panic
203		hmac_sha512_fixed(&mut builder, &key_wires, &message, 8);
204	}
205
206	#[test]
207	#[should_panic(expected = "Message wire count 2 doesn't match message length 7 bytes")]
208	fn test_hmac_message_wire_mismatch() {
209		let mut builder = CircuitBuilder::new();
210
211		let key = vec![builder.add_constant(Word::ZERO)];
212		let message = vec![
213			builder.add_constant(Word::ZERO),
214			builder.add_constant(Word::ZERO),
215		];
216
217		// This should panic - 2 wires but claiming 7 bytes (needs only 1 wire)
218		hmac_sha512_fixed(&mut builder, &key, &message, 7);
219	}
220}