Skip to main content

ic_mac/
kmac.rs

1//! SP 800-185 KMAC: a MAC built on cSHAKE.
2//!
3//! KMAC is the SHA-3 family's answer to HMAC, and a simpler one. HMAC exists
4//! because the Merkle-Damgard hashes it wraps are vulnerable to length
5//! extension, so it needs two passes with an inner and an outer key. A sponge
6//! has no such weakness, so KMAC just absorbs the key first:
7//!
8//! ```text
9//! KMAC128(K, X, L, S) = cSHAKE128(bytepad(encode_string(K), 168)
10//!                                 || X || right_encode(L),
11//!                                 L, "KMAC", S)
12//! ```
13//!
14//! # The output length is authenticated
15//!
16//! `right_encode(L)` at the end is the part worth understanding. It binds the
17//! requested output length into the input, so a 32-byte tag is not a prefix of
18//! a 64-byte tag over the same key and message. Without it, anyone who saw a
19//! long tag could truncate it into a valid short one.
20//!
21//! That is also why [`Kmac128::finalize`] and [`Kmac128::finalize_xof`] differ
22//! for the same length: the XOF variant encodes zero, which is precisely what
23//! makes its output a stream that can be extended without changing its prefix.
24//!
25//! # Customization separates domains
26//!
27//! Two subsystems sharing a key should pass different `custom` strings. Then a
28//! tag from one cannot be replayed into the other, and neither has to trust the
29//! other's message framing.
30
31use ic_core::traits::{Algorithm, SelfTest};
32use ic_core::{ensure, Result};
33use ic_hash::sp800_185::{right_encode, MAX_ENCODE};
34
35/// The widest tag the fixed-length helpers handle.
36const MAX_TAG: usize = 64;
37
38/// Declare a KMAC over one cSHAKE parameter set.
39macro_rules! kmac {
40    ($name:ident, $cshake:ty, $id:literal, $disp:literal, $bits:literal) => {
41        #[doc = concat!("SP 800-185 ", $disp, ", offering ", $bits, "-bit security.")]
42        #[derive(Clone)]
43        pub struct $name {
44            inner: $cshake,
45        }
46
47        impl Algorithm for $name {
48            const ID: &'static str = $id;
49            const NAME: &'static str = $disp;
50        }
51
52        impl $name {
53            /// Start a KMAC with `key` and a customization string.
54            ///
55            /// Pass an empty `custom` when the key has only one use.
56            pub fn new(key: &[u8], custom: &[u8]) -> Self {
57                let mut inner = <$cshake>::new(b"KMAC", custom);
58                inner.absorb_bytepadded_string(key);
59                Self { inner }
60            }
61
62            /// Absorb more of the message.
63            pub fn update(&mut self, data: &[u8]) {
64                self.inner.update(data);
65            }
66
67            /// Produce a tag of exactly `out.len()` bytes.
68            ///
69            /// The length is bound into the computation, so tags of different
70            /// lengths over the same input are unrelated. Refuses with
71            /// `ModuleErrorState` once the module has failed.
72            pub fn finalize(mut self, out: &mut [u8]) -> Result<()> {
73                ic_core::module::operational()?;
74                let mut buf = [0u8; MAX_ENCODE];
75                let used = right_encode((out.len() as u64) * 8, &mut buf);
76                self.inner.update(&buf[..used]);
77                self.inner.finalize_xof_unchecked(out);
78                Ok(())
79            }
80
81            /// Produce an arbitrary-length stream instead of a fixed tag.
82            ///
83            /// Encodes a length of zero, per SP 800-185 section 4.3.1. Refuses
84            /// with `ModuleErrorState` once the module has failed.
85            pub fn finalize_xof(mut self, out: &mut [u8]) -> Result<()> {
86                ic_core::module::operational()?;
87                let mut buf = [0u8; MAX_ENCODE];
88                let used = right_encode(0, &mut buf);
89                self.inner.update(&buf[..used]);
90                self.inner.finalize_xof_unchecked(out);
91                Ok(())
92            }
93
94            /// One-shot fixed-length tag.
95            pub fn mac(key: &[u8], custom: &[u8], data: &[u8], out: &mut [u8]) -> Result<()> {
96                let mut k = Self::new(key, custom);
97                k.update(data);
98                k.finalize(out)
99            }
100
101            /// One-shot XOF mode.
102            pub fn mac_xof(key: &[u8], custom: &[u8], data: &[u8], out: &mut [u8]) -> Result<()> {
103                let mut k = Self::new(key, custom);
104                k.update(data);
105                k.finalize_xof(out)
106            }
107
108            /// Verify a tag in constant time.
109            ///
110            /// Compare with this rather than `==`. An early-exit comparison
111            /// leaks how many leading bytes matched, which is enough to recover
112            /// a valid tag one byte at a time.
113            pub fn verify(key: &[u8], custom: &[u8], data: &[u8], tag: &[u8]) -> Result<()> {
114                ic_core::module::operational()?;
115                ensure!(
116                    !tag.is_empty() && tag.len() <= MAX_TAG,
117                    InvalidLength,
118                    "kmac tag length"
119                );
120                let mut expected = [0u8; MAX_TAG];
121                Self::mac(key, custom, data, &mut expected[..tag.len()])?;
122                ensure!(
123                    ic_core::ct::verify(&expected[..tag.len()], tag),
124                    AuthenticationFailed,
125                    $id
126                );
127                Ok(())
128            }
129        }
130
131        impl SelfTest for $name {
132            /// # Provenance
133            ///
134            /// No pinned vector. The unit tests check KMAC against an
135            /// independent construction from SP 800-185's own definition —
136            /// stronger evidence than a value this code produced — and cSHAKE
137            /// beneath it is checked against a Keccak written from FIPS 202.
138            /// docs/FIPS.md records that. What this adds at startup is that the
139            /// code still computes what it computed when those tests last ran,
140            /// and that its structural properties hold.
141            fn self_test() -> Result<()> {
142                let key = [0x40u8; 32];
143                let mut short = [0u8; 32];
144                let mut long = [0u8; 64];
145                Self::mac(&key, b"self-test", b"message", &mut short)?;
146                Self::mac(&key, b"self-test", b"message", &mut long)?;
147
148                // The output length is bound in, so the short tag is not a
149                // prefix of the long one. Drop right_encode(L) and it would be.
150                ensure!(short[..] != long[..32], SelfTestFailed, $id);
151
152                let mut again = [0u8; 32];
153                Self::mac(&key, b"self-test", b"message", &mut again)?;
154                ensure!(ic_core::ct::verify(&short, &again), SelfTestFailed, $id);
155                Self::verify(&key, b"self-test", b"message", &short)?;
156
157                let mut tampered = short;
158                tampered[0] ^= 1;
159                ensure!(
160                    Self::verify(&key, b"self-test", b"message", &tampered).is_err(),
161                    SelfTestFailed,
162                    $id
163                );
164                // Customization must separate domains.
165                ensure!(
166                    Self::verify(&key, b"other", b"message", &short).is_err(),
167                    SelfTestFailed,
168                    $id
169                );
170                Ok(())
171            }
172        }
173    };
174}
175
176kmac!(Kmac128, ic_hash::CShake128, "kmac128", "KMAC128", "128");
177kmac!(Kmac256, ic_hash::CShake256, "kmac256", "KMAC256", "256");
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use ic_hash::sp800_185::{left_encode, CShake128, CShake256};
183
184    /// SP 800-185's definition, assembled literally, with no shared code.
185    ///
186    /// `KMAC(K, X, L, S) = cSHAKE(bytepad(encode_string(K), rate)
187    ///                            || X || right_encode(L), L, "KMAC", S)`
188    ///
189    /// cSHAKE is trusted here because its own tests check it against a Keccak
190    /// written from FIPS 202 and anchored to a published SHA-3 vector. So this
191    /// checks the layer KMAC actually adds: the key encoding, the padding
192    /// width, and the trailing length.
193    fn reference_kmac(
194        rate: usize,
195        key: &[u8],
196        custom: &[u8],
197        data: &[u8],
198        out: &mut [u8],
199        xof: bool,
200    ) {
201        fn enc(x: u64) -> Vec<u8> {
202            let mut bytes = x.to_be_bytes().to_vec();
203            while bytes.len() > 1 && bytes[0] == 0 {
204                bytes.remove(0);
205            }
206            let mut v = vec![bytes.len() as u8];
207            v.extend_from_slice(&bytes);
208            v
209        }
210        fn renc(x: u64) -> Vec<u8> {
211            let mut bytes = x.to_be_bytes().to_vec();
212            while bytes.len() > 1 && bytes[0] == 0 {
213                bytes.remove(0);
214            }
215            let n = bytes.len() as u8;
216            bytes.push(n);
217            bytes
218        }
219
220        // bytepad(encode_string(K), rate)
221        let mut message = enc(rate as u64);
222        message.extend_from_slice(&enc((key.len() as u64) * 8));
223        message.extend_from_slice(key);
224        while !message.len().is_multiple_of(rate) {
225            message.push(0);
226        }
227        message.extend_from_slice(data);
228        message.extend_from_slice(&renc(if xof { 0 } else { (out.len() as u64) * 8 }));
229
230        if rate == 168 {
231            CShake128::xof(b"KMAC", custom, &message, out).unwrap();
232        } else {
233            CShake256::xof(b"KMAC", custom, &message, out).unwrap();
234        }
235    }
236
237    #[test]
238    fn kmac_matches_an_independent_construction() {
239        let cases: &[(&[u8], &[u8], &[u8])] = &[
240            (&[0x40u8; 32], b"", b""),
241            (&[0x40u8; 32], b"My Tagged Application", b"\x00\x01\x02\x03"),
242            (b"short key", b"S", &[0xa5u8; 500]),
243            (&[0x11u8; 200], b"", b"key longer than the rate"),
244        ];
245
246        for (key, custom, data) in cases {
247            for len in [16usize, 32, 64] {
248                let mut want = vec![0u8; len];
249                let mut got = vec![0u8; len];
250
251                reference_kmac(168, key, custom, data, &mut want, false);
252                Kmac128::mac(key, custom, data, &mut got).unwrap();
253                assert_eq!(got, want, "KMAC128 fixed, {len} bytes");
254
255                reference_kmac(136, key, custom, data, &mut want, false);
256                Kmac256::mac(key, custom, data, &mut got).unwrap();
257                assert_eq!(got, want, "KMAC256 fixed, {len} bytes");
258
259                reference_kmac(168, key, custom, data, &mut want, true);
260                Kmac128::mac_xof(key, custom, data, &mut got).unwrap();
261                assert_eq!(got, want, "KMAC128 xof, {len} bytes");
262
263                reference_kmac(136, key, custom, data, &mut want, true);
264                Kmac256::mac_xof(key, custom, data, &mut got).unwrap();
265                assert_eq!(got, want, "KMAC256 xof, {len} bytes");
266            }
267        }
268    }
269
270    /// The property `right_encode(L)` exists to provide: a short tag is not a
271    /// truncation of a long one.
272    #[test]
273    fn tag_length_is_bound_into_the_tag() {
274        let key = [0x7fu8; 32];
275        let mut short = [0u8; 32];
276        let mut long = [0u8; 64];
277        Kmac128::mac(&key, b"", b"message", &mut short).unwrap();
278        Kmac128::mac(&key, b"", b"message", &mut long).unwrap();
279        assert_ne!(short[..], long[..32], "truncation must not forge");
280    }
281
282    /// The XOF variant, by contrast, *is* a stream: a longer output extends a
283    /// shorter one.
284    #[test]
285    fn the_xof_variant_extends_rather_than_changes() {
286        let key = [0x7fu8; 32];
287        let mut short = [0u8; 32];
288        let mut long = [0u8; 64];
289        Kmac128::mac_xof(&key, b"", b"message", &mut short).unwrap();
290        Kmac128::mac_xof(&key, b"", b"message", &mut long).unwrap();
291        assert_eq!(short[..], long[..32], "the xof output is a prefix");
292    }
293
294    #[test]
295    fn streaming_matches_the_one_shot() {
296        let key = [0x31u8; 32];
297        let data = [0x62u8; 777];
298        let mut one = [0u8; 32];
299        Kmac256::mac(&key, b"S", &data, &mut one).unwrap();
300
301        let mut k = Kmac256::new(&key, b"S");
302        for chunk in data.chunks(13) {
303            k.update(chunk);
304        }
305        let mut streamed = [0u8; 32];
306        k.finalize(&mut streamed).unwrap();
307        assert_eq!(one, streamed);
308    }
309
310    #[test]
311    fn keys_and_customization_both_change_the_tag() {
312        let mut a = [0u8; 32];
313        let mut b = [0u8; 32];
314        Kmac128::mac(&[1u8; 32], b"S", b"m", &mut a).unwrap();
315        Kmac128::mac(&[2u8; 32], b"S", b"m", &mut b).unwrap();
316        assert_ne!(a, b, "the key matters");
317        Kmac128::mac(&[1u8; 32], b"T", b"m", &mut b).unwrap();
318        assert_ne!(a, b, "the customization matters");
319    }
320
321    #[test]
322    fn verification_rejects_tampering_and_bad_lengths() {
323        let key = [0x55u8; 32];
324        let mut tag = [0u8; 32];
325        Kmac128::mac(&key, b"S", b"message", &mut tag).unwrap();
326        Kmac128::verify(&key, b"S", b"message", &tag).unwrap();
327
328        for bit in [0usize, 7, 128, 255] {
329            let mut bad = tag;
330            bad[bit / 8] ^= 1 << (bit % 8);
331            assert!(Kmac128::verify(&key, b"S", b"message", &bad).is_err());
332        }
333        assert!(Kmac128::verify(&key, b"S", b"messagf", &tag).is_err());
334        assert!(Kmac128::verify(&[0u8; 32], b"S", b"message", &tag).is_err());
335        assert!(
336            Kmac128::verify(&key, b"S", b"message", &[]).is_err(),
337            "empty tag"
338        );
339        assert!(
340            Kmac128::verify(&key, b"S", b"message", &[0u8; 65]).is_err(),
341            "over-long tag"
342        );
343    }
344
345    #[test]
346    fn both_self_tests_pass() {
347        Kmac128::self_test().unwrap();
348        Kmac256::self_test().unwrap();
349    }
350
351    /// `left_encode` is re-exported through `ic_hash`; make sure the path the
352    /// docs point at actually resolves.
353    #[test]
354    fn the_encoding_helpers_are_reachable() {
355        let mut buf = [0u8; MAX_ENCODE];
356        assert_eq!(left_encode(168, &mut buf), 2);
357        assert_eq!(&buf[..2], &[0x01, 0xa8]);
358    }
359}