Skip to main content

ic_mac/
hmac.rs

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