Skip to main content

ic_mac/
hmac.rs

1//! FIPS 198-1 HMAC, generic over any digest.
2
3use ic_core::traits::{Algorithm, Digest, Mac, SelfTest};
4use ic_core::{ensure, Result, Zeroize};
5
6/// The largest block size among supported digests.
7///
8/// SHA-512 uses 128 bytes; SHA3-224 has the widest rate at 144, so the padded
9/// key buffers are sized for it.
10const MAX_BLOCK_LEN: usize = 144;
11
12/// HMAC over the digest `D`.
13///
14/// The key is processed per FIPS 198-1: hashed if longer than the block size,
15/// zero-padded otherwise. The padded key, and the hash of a long key, are
16/// zeroized before the constructor returns.
17#[derive(Clone)]
18pub struct Hmac<D: Digest> {
19    inner: D,
20    outer: D,
21}
22
23/// A digest that can name its HMAC instantiation in the ontology.
24///
25/// Rust cannot concatenate `&'static str` constants at compile time, so the
26/// composed identifier (`"hmac-sha2-256"`) is declared explicitly per digest
27/// rather than derived from [`Digest::ID`].
28pub trait HmacDigest: Digest {
29    /// Ontology identifier of the HMAC built on this digest.
30    const HMAC_ID: &'static str;
31    /// Display name of the HMAC built on this digest.
32    const HMAC_NAME: &'static str;
33}
34
35impl<D: HmacDigest> Algorithm for Hmac<D> {
36    const ID: &'static str = D::HMAC_ID;
37    const NAME: &'static str = D::HMAC_NAME;
38}
39
40impl<D: Digest> Hmac<D> {
41    /// Absorb the inner and outer pads into the two fresh states.
42    ///
43    /// One buffer, built as the inner pad directly -- the zero-padded key
44    /// XOR 0x36 -- and turned into the outer pad in place, since
45    /// `k ^ 0x5c == (k ^ 0x36) ^ (0x36 ^ 0x5c)`. The wipe is volatile and
46    /// byte by byte, which is what makes it stick and also what makes it
47    /// cost: it covers the one block this digest used, not two buffers sized
48    /// for the widest digest there is. For SHA-256 that is 64 bytes where it
49    /// was 288, on every HMAC key setup -- and HKDF sets up a key per call.
50    ///
51    /// The states are absorbed into where they will live rather than built as
52    /// locals and moved into the result: for SHA-384 on Cortex-M4 the locals,
53    /// the result and a third state for a long key had shared one 4.4 KiB frame.
54    fn absorb_pads(&mut self, key: &[u8]) {
55        let mut pad = [0x36u8; MAX_BLOCK_LEN];
56        let block = &mut pad[..D::BLOCK_LEN];
57        if key.len() > D::BLOCK_LEN {
58            xor_hashed_key::<D>(key, block);
59        } else {
60            for (p, k) in block.iter_mut().zip(key) {
61                *p ^= k;
62            }
63        }
64
65        self.inner.update(block);
66        for p in block.iter_mut() {
67            *p ^= 0x36 ^ 0x5c;
68        }
69        self.outer.update(block);
70
71        block.zeroize();
72    }
73}
74
75// # Where the frames go
76//
77// A SHA-512 state carries its 640-byte message schedule, so SHA-384 and
78// SHA-512 states are about 860 bytes and an HMAC over either is 1.7 KiB.
79// Inlined into a caller -- HKDF, say -- key setup and finalization each
80// brought their own copies of those states into the caller's frame beside the
81// caller's: one HMAC-SHA384 tag reached 11 KiB of stack on Cortex-M4. For
82// those digests, key setup and finalization stay out of line, so their
83// frames are used one after another instead of all at once.
84//
85// Not for the small states. SHA-256 gained nothing from it on Cortex-M4, and
86// the call cost a short HMAC-SHA256 about 8% on x86-64. The test is a
87// constant for each digest, so whichever branch is not taken compiles away.
88
89/// Whether `D`'s state is large enough to keep HMAC's work out of line.
90const fn large_state<D>() -> bool {
91    core::mem::size_of::<D>() > 512
92}
93
94/// `Hmac` keyed with `key`.
95#[inline(always)]
96fn keyed<D: Digest>(key: &[u8]) -> Hmac<D> {
97    let mut mac = Hmac {
98        inner: D::new(),
99        outer: D::new(),
100    };
101    mac.absorb_pads(key);
102    mac
103}
104
105#[inline(never)]
106fn keyed_out_of_line<D: Digest>(key: &[u8]) -> Hmac<D> {
107    keyed(key)
108}
109
110/// The tag: the outer hash over the inner one.
111#[inline(always)]
112fn tag<D: Digest>(mut mac: Hmac<D>) -> D::Output {
113    let inner_digest = mac.inner.finalize();
114    mac.outer.update(inner_digest.as_ref());
115    mac.outer.finalize()
116}
117
118#[inline(never)]
119fn tag_out_of_line<D: Digest>(mac: Hmac<D>) -> D::Output {
120    tag(mac)
121}
122
123/// XOR `D(key)` into `block`, for a key longer than one block.
124///
125/// Out of line so that the third digest state it needs is on the stack only
126/// while it runs, not for the whole of key setup.
127#[inline(never)]
128fn xor_hashed_key<D: Digest>(key: &[u8], block: &mut [u8]) {
129    let mut hashed = D::digest(key);
130    for (p, k) in block.iter_mut().zip(hashed.as_ref()) {
131        *p ^= k;
132    }
133    // Key-equivalent: HMAC under the hash is HMAC under the key.
134    hashed.as_mut().zeroize();
135}
136
137impl<D: HmacDigest> Mac for Hmac<D> {
138    type Tag = D::Output;
139    const TAG_LEN: usize = D::OUTPUT_LEN;
140
141    fn new(key: &[u8]) -> Result<Self> {
142        ic_core::module::operational()?;
143        ensure!(
144            D::BLOCK_LEN <= MAX_BLOCK_LEN,
145            InvalidParameter,
146            "digest block exceeds hmac buffer"
147        );
148        if large_state::<D>() {
149            Ok(keyed_out_of_line(key))
150        } else {
151            Ok(keyed(key))
152        }
153    }
154
155    fn update(&mut self, data: &[u8]) {
156        self.inner.update(data);
157    }
158
159    fn finalize(self) -> Self::Tag {
160        if large_state::<D>() {
161            tag_out_of_line(self)
162        } else {
163            tag(self)
164        }
165    }
166}
167
168macro_rules! hmac_alias {
169    (
170        $name:ident, $digest:ty, $id:literal, $disp:literal, $taglen:literal,
171        $kat_key:expr, $kat_msg:expr, $kat_tag:literal
172    ) => {
173        #[doc = concat!($disp, ".")]
174        pub type $name = Hmac<$digest>;
175
176        impl HmacDigest for $digest {
177            const HMAC_ID: &'static str = $id;
178            const HMAC_NAME: &'static str = $disp;
179        }
180
181        impl SelfTest for Hmac<$digest> {
182            fn self_test() -> Result<()> {
183                let mut key = [0u8; 20];
184                ic_core::codec::hex_decode($kat_key.as_bytes(), &mut key)?;
185                let tag = <Self as Mac>::mac(&key, $kat_msg)?;
186                let mut want = [0u8; $taglen];
187                ic_core::codec::hex_decode($kat_tag.as_bytes(), &mut want)?;
188                ensure!(
189                    ic_core::ct::verify(&want, tag.as_ref()),
190                    SelfTestFailed,
191                    $id
192                );
193                Ok(())
194            }
195        }
196    };
197}
198
199/// The RFC 4231 test-case-1 key: twenty `0x0b` bytes.
200const KAT_KEY: &str = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b";
201
202// Known-answer vectors, all over the RFC 4231 test-case-1 input
203// (a 20-byte 0x0b key over "Hi There"):
204//
205// * SHA-256 / SHA-384 / SHA-512 tags are RFC 4231 test case 1.
206// * SHA-512/256 and the SHA-3 tags are the corresponding NIST HMAC sample
207//   values for the same input.
208hmac_alias!(
209    HmacSha256,
210    ic_hash::Sha256,
211    "hmac-sha2-256",
212    "HMAC-SHA-256",
213    32,
214    KAT_KEY,
215    b"Hi There",
216    "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
217);
218hmac_alias!(
219    HmacSha384,
220    ic_hash::Sha384,
221    "hmac-sha2-384",
222    "HMAC-SHA-384",
223    48,
224    KAT_KEY,
225    b"Hi There",
226    "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6"
227);
228hmac_alias!(
229    HmacSha512,
230    ic_hash::Sha512,
231    "hmac-sha2-512",
232    "HMAC-SHA-512",
233    64,
234    KAT_KEY,
235    b"Hi There",
236    "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854"
237);
238hmac_alias!(
239    HmacSha512_256,
240    ic_hash::Sha512_256,
241    "hmac-sha2-512-256",
242    "HMAC-SHA-512/256",
243    32,
244    KAT_KEY,
245    b"Hi There",
246    "9f9126c3d9c3c330d760425ca8a217e31feae31bfe70196ff81642b868402eab"
247);
248hmac_alias!(
249    HmacSha3_256,
250    ic_hash::Sha3_256,
251    "hmac-sha3-256",
252    "HMAC-SHA3-256",
253    32,
254    KAT_KEY,
255    b"Hi There",
256    "ba85192310dffa96e2a3a40e69774351140bb7185e1202cdcc917589f95e16bb"
257);
258hmac_alias!(
259    HmacSha3_512,
260    ic_hash::Sha3_512,
261    "hmac-sha3-512",
262    "HMAC-SHA3-512",
263    64,
264    KAT_KEY,
265    b"Hi There",
266    "eb3fbd4b2eaab8f5c504bd3a41465aacec15770a7cabac531e482f860b5ec7ba47ccb2c6f2afce8f88d22b6dc61380f23a668fd3888bb80537c0a0b86407689e"
267);
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use ic_core::codec::hex;
273
274    #[test]
275    fn rfc4231_case_1() {
276        let key = [0x0bu8; 20];
277        assert_eq!(
278            hex(HmacSha256::mac(&key, b"Hi There").unwrap().as_ref()),
279            "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
280        );
281        assert_eq!(
282            hex(HmacSha512::mac(&key, b"Hi There").unwrap().as_ref()),
283            "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854"
284        );
285    }
286
287    #[test]
288    fn rfc4231_case_2_short_key() {
289        assert_eq!(
290            hex(HmacSha256::mac(b"Jefe", b"what do ya want for nothing?")
291                .unwrap()
292                .as_ref()),
293            "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
294        );
295    }
296
297    /// Case 3 uses a key and message that both exceed one block.
298    #[test]
299    fn rfc4231_case_3_long_data() {
300        let key = [0xaau8; 20];
301        let data = [0xddu8; 50];
302        assert_eq!(
303            hex(HmacSha256::mac(&key, &data).unwrap().as_ref()),
304            "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"
305        );
306    }
307
308    /// Case 6: a 131-byte key, longer than the SHA-256 block, so it is hashed
309    /// down first.
310    #[test]
311    fn rfc4231_case_6_oversized_key() {
312        let key = [0xaau8; 131];
313        assert_eq!(
314            hex(HmacSha256::mac(
315                &key,
316                b"Test Using Larger Than Block-Size Key - Hash Key First"
317            )
318            .unwrap()
319            .as_ref()),
320            "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
321        );
322    }
323
324    /// Case 6 again, for SHA-384 and SHA-512: 131 bytes is also longer than
325    /// their 128-byte block. These are the digests whose key setup runs out of
326    /// line, so this is what checks the long-key path there. RFC 4231 section
327    /// 4.7; both tags also checked against Python's `hmac` module.
328    #[test]
329    fn rfc4231_case_6_oversized_key_sha2_512_family() {
330        let key = [0xaau8; 131];
331        let msg = b"Test Using Larger Than Block-Size Key - Hash Key First";
332        assert_eq!(
333            hex(HmacSha384::mac(&key, msg).unwrap().as_ref()),
334            "4ece084485813e9088d2c63a041bc5b44f9ef1012a2b588f3cd11f05033ac4c60c2ef6ab4030fe8296248df163f44952"
335        );
336        assert_eq!(
337            hex(HmacSha512::mac(&key, msg).unwrap().as_ref()),
338            "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598"
339        );
340    }
341
342    #[test]
343    fn empty_key_and_message() {
344        assert_eq!(
345            hex(HmacSha256::mac(b"", b"").unwrap().as_ref()),
346            "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
347        );
348    }
349
350    #[test]
351    fn streaming_matches_one_shot() {
352        let data: Vec<u8> = (0..200u8).collect();
353        for split in [0usize, 1, 63, 64, 128, 200] {
354            let mut m = HmacSha256::new(b"k").unwrap();
355            m.update(&data[..split]);
356            m.update(&data[split..]);
357            assert_eq!(m.finalize(), HmacSha256::mac(b"k", &data).unwrap());
358        }
359    }
360
361    #[test]
362    fn verify_rejects_wrong_tag_and_length() {
363        let tag = HmacSha256::mac(b"k", b"m").unwrap();
364        HmacSha256::verify(b"k", b"m", tag.as_ref()).unwrap();
365        let mut bad = tag;
366        bad[0] ^= 1;
367        assert!(HmacSha256::verify(b"k", b"m", bad.as_ref()).is_err());
368        assert!(HmacSha256::verify(b"k", b"m", &tag.as_ref()[..31]).is_err());
369    }
370
371    #[test]
372    fn self_tests_pass() {
373        HmacSha256::self_test().unwrap();
374        HmacSha384::self_test().unwrap();
375        HmacSha512::self_test().unwrap();
376        HmacSha512_256::self_test().unwrap();
377        HmacSha3_256::self_test().unwrap();
378        HmacSha3_512::self_test().unwrap();
379    }
380}