Skip to main content

fancy_garbling/circuits/
hmac.rs

1//! Circuit implementation of HMAC-SHA256.
2//!
3//! This module provides HMAC (Hash-based Message Authentication Code) using
4//! SHA-256 as the underlying hash function.
5
6use crate::{
7    FancyBinary,
8    circuit::Circuit,
9    circuits::{binary::PairwiseXor, sha::Sha256},
10};
11use core::marker::PhantomData;
12use swanky_channel::Channel;
13use swanky_error::Result;
14
15/// Circuit for HMAC-SHA256.
16///
17/// HMAC is computed as: `HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message))`
18///
19/// Where:
20/// - `H` is SHA-256
21/// - `ipad` is 0x36 repeated for the block size (512 bits)
22/// - `opad` is 0x5c repeated for the block size (512 bits)
23/// - `key` is padded with zeros to the block size if shorter, or hashed if longer
24///
25/// This implementation uses a 512-bit key (the SHA-256 block size) to avoid needing
26/// to hash long keys. For shorter keys, pad with zeros to 512 bits before passing
27/// to this circuit.
28#[derive(Default)]
29pub struct HmacSha256<'a>(PhantomData<&'a ()>);
30
31impl<'a> HmacSha256<'a> {
32    /// Create a new [`HmacSha256`] circuit.
33    pub fn new() -> Self {
34        Default::default()
35    }
36
37    /// Inner padding byte (0x36 = 00110110).
38    const IPAD_BYTE: &'static str = "00110110";
39
40    /// Outer padding byte (0x5C = 01011100).
41    const OPAD_BYTE: &'static str = "01011100";
42}
43
44impl<'a, F: FancyBinary> Circuit<F> for HmacSha256<'a>
45where
46    F::Item: 'a,
47{
48    /// A 512-bit key and a variable-length message.
49    type Input = (&'a [F::Item; 512], &'a [F::Item]);
50    /// A 256-bit HMAC tag.
51    type Output = [F::Item; 256];
52
53    fn execute(
54        &self,
55        backend: &mut F,
56        inputs: Self::Input,
57        channel: &mut Channel,
58    ) -> Result<Self::Output> {
59        let (key, message) = inputs;
60
61        let zero = backend.constant(0, 2, channel)?;
62        let one = backend.constant(1, 2, channel)?;
63
64        // Create ipad pattern (0x36 repeated 64 times for 512 bits).
65        let ipad: Vec<F::Item> = Self::IPAD_BYTE
66            .repeat(64) // 64 bytes = 512 bits
67            .chars()
68            .map(|c| if c == '1' { one.clone() } else { zero.clone() })
69            .collect();
70
71        // Create opad pattern (0x5C repeated 64 times for 512 bits).
72        let opad: Vec<F::Item> = Self::OPAD_BYTE
73            .repeat(64)
74            .chars()
75            .map(|c| if c == '1' { one.clone() } else { zero.clone() })
76            .collect();
77
78        // Compute `key ⊕ ipad`.
79        let key_vec = key.to_vec();
80        let key_xor_ipad = PairwiseXor::new().execute(backend, (&key_vec, &ipad), channel)?;
81
82        // Compute `key ⊕ opad`.
83        let key_xor_opad = PairwiseXor::new().execute(backend, (&key_vec, &opad), channel)?;
84
85        // Inner hash: `H((key ⊕ ipad) || message)`.
86        let mut inner_input = key_xor_ipad;
87        inner_input.extend_from_slice(message);
88        let inner_hash = Sha256::new().execute(backend, inner_input, channel)?;
89
90        // Outer hash: `H((key ⊕ opad) || inner_hash)`.
91        let mut outer_input = key_xor_opad;
92        outer_input.extend_from_slice(&inner_hash);
93        let hmac = Sha256::new().execute(backend, outer_input, channel)?;
94
95        Ok(hmac)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::dummy::{Dummy, DummyVal};
103
104    fn string_to_bool_vec(str: &str) -> Vec<DummyVal> {
105        str.chars()
106            .map(|c| match c {
107                '0' => DummyVal::new_bool(false),
108                '1' => DummyVal::new_bool(true),
109                _ => panic!("Unexpected character in boolean string"),
110            })
111            .collect()
112    }
113
114    #[test]
115    fn hmac_sha256_empty_message() {
116        // Test HMAC-SHA256 with empty message
117        // Key: 512 bits of zeros
118        // Message: empty
119
120        let hmac = HmacSha256::new();
121        let key = [DummyVal::new_bool(false); 512];
122        let message = [];
123
124        let output = Dummy::eval(&hmac, (&key, &message)).unwrap();
125
126        // Computed using: echo -n "" | openssl dgst -sha256 -mac hmac -macopt hexkey:$(python3 -c "print('00'*64)")
127        // Result: b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad
128        assert_eq!(
129            output
130                .iter()
131                .map(|i| i.val().to_string())
132                .collect::<String>(),
133            "1011011000010011011001111001101000001000000101001101100111101100011101110010111110010101110101110111100011000011010111111100010111111111000101101001011111000100100100110111000101010110010100111100011011000111000100100001010001000010100100101100010110101101"
134        );
135    }
136
137    #[test]
138    fn hmac_sha256_test_message() {
139        // Test HMAC-SHA256 with "test" message
140        // Key: 512 bits of zeros
141        // Message: "test" = 0x74657374
142
143        let hmac = HmacSha256::new();
144        let key = [DummyVal::new_bool(false); 512];
145
146        // "test" in binary
147        // 't' = 0x74 = 01110100
148        // 'e' = 0x65 = 01100101
149        // 's' = 0x73 = 01110011
150        // 't' = 0x74 = 01110100
151        let message = string_to_bool_vec("01110100011001010111001101110100");
152
153        let output = Dummy::eval(&hmac, (&key, &message[..])).unwrap();
154
155        // Computed using: echo -n "test" | openssl dgst -sha256 -mac hmac -macopt hexkey:$(python3 -c "print('00'*64)")
156        // Result: 43b0cef99265f9e34c10ea9d3501926d27b39f57c6d674561d8ba236e7a819fb
157        assert_eq!(
158            output
159                .iter()
160                .map(|i| i.val().to_string())
161                .collect::<String>(),
162            "0100001110110000110011101111100110010010011001011111100111100011010011000001000011101010100111010011010100000001100100100110110100100111101100111001111101010111110001101101011001110100010101100001110110001011101000100011011011100111101010000001100111111011"
163        );
164    }
165
166    #[test]
167    fn hmac_sha256_with_key() {
168        // Test HMAC-SHA256 with non-zero key
169        // Key: "key" (0x6b6579) padded to 512 bits with zeros
170        // Message: "The quick brown fox jumps over the lazy dog"
171
172        let hmac = HmacSha256::new();
173
174        // "key" = 0x6b6579
175        // 'k' = 0x6b = 01101011
176        // 'e' = 0x65 = 01100101
177        // 'y' = 0x79 = 01111001
178        let mut key = string_to_bool_vec("011010110110010101111001");
179        // Pad to 512 bits
180        key.resize(512, DummyVal::new_bool(false));
181        let key: [DummyVal; 512] = key.try_into().expect("Key should contain 512 elements");
182
183        let message = "The quick brown fox jumps over the lazy dog";
184        let message: Vec<DummyVal> = message
185            .bytes()
186            .flat_map(|b| {
187                (0..8)
188                    .rev()
189                    .map(move |i| DummyVal::new_bool((b >> i) & 1 == 1))
190            })
191            .collect();
192
193        let output = Dummy::eval(&hmac, (&key, &message[..])).unwrap();
194
195        // Computed using: echo -n "The quick brown fox jumps over the lazy dog" | openssl dgst -sha256 -mac hmac -macopt key:key
196        // Result: f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
197        assert_eq!(
198            output
199                .iter()
200                .map(|i| i.val().to_string())
201                .collect::<String>(),
202            "1111011110111100100000111111010000110000010100111000010000100100101100010011001010011000111001101010101001101111101100010100001111101111010011010101100110100001010010010100011000010111010110011001011101000111100111011011110000101101000110100011110011011000"
203        );
204    }
205}