Skip to main content

metamorphic_crypto/
mac.rs

1//! Message authentication codes (HMAC).
2//!
3//! A thin, one-shot wrapper over the audited [`hmac`] RustCrypto crate. It
4//! exposes no new or novel cryptography — it makes the keyed-MAC primitive the
5//! rest of the Metamorphic stack needs available as public API.
6//!
7//! ## Why this exists
8//!
9//! The IETF KEYTRANS protocol (`draft-ietf-keytrans-protocol`) specifies that
10//! commitments in its **standard** cipher suites are computed as
11//! `HMAC(Kc, CommitmentValue)` using the suite hash (SHA-256 for both currently
12//! defined suites). [`metamorphic-log`] owns the KEYTRANS-specific framing (the
13//! fixed key `Kc` and the `CommitmentValue` TLS encoding); this module supplies
14//! only the generic HMAC-SHA256 primitive, keeping `metamorphic-crypto` the
15//! single source of truth for cryptographic primitives.
16//!
17//! ## Security note
18//!
19//! HMAC's security rests on the key being secret *when used as an
20//! authenticator*. In the KEYTRANS commitment construction the "key" is a
21//! **fixed, public** per-suite constant and hiding comes from the random
22//! `opening` inside the message — HMAC is used there as a committing PRF, not as
23//! an authenticator. This primitive is generic; callers are responsible for
24//! using it in a construction whose security properties they understand.
25//!
26//! [`metamorphic-log`]: https://github.com/moss-piglet/metamorphic-log
27
28use hmac::{Hmac, KeyInit, Mac};
29use sha2::Sha256;
30
31/// HMAC-SHA256 output length, in bytes (a SHA-256 digest).
32pub const HMAC_SHA256_LEN: usize = 32;
33
34/// Compute `HMAC-SHA256(key, msg)`, returning 32 bytes (RFC 2104 + FIPS 198-1).
35///
36/// The key may be any length: HMAC internally hashes an over-long key and
37/// zero-pads a short one, so no length validation is required or performed.
38///
39/// ## Encoding (stable — reproduce exactly for cross-language parity)
40///
41/// This is standard HMAC-SHA256; any conformant implementation (native Rust,
42/// WASM, the Elixir NIF, or hand-rolled JS) computes an identical tag for the
43/// same `(key, msg)`.
44///
45/// ```
46/// use metamorphic_crypto::mac::hmac_sha256;
47///
48/// // RFC 4231 Test Case 2.
49/// let tag = hmac_sha256(b"Jefe", b"what do ya want for nothing?");
50/// assert_eq!(
51///     &tag[..4],
52///     &[0x5b, 0xdc, 0xc1, 0x46],
53/// );
54/// ```
55#[inline]
56#[must_use]
57pub fn hmac_sha256(key: &[u8], msg: &[u8]) -> [u8; HMAC_SHA256_LEN] {
58    let mut mac =
59        <Hmac<Sha256>>::new_from_slice(key).expect("HMAC accepts a key of any length (RFC 2104)");
60    mac.update(msg);
61    mac.finalize().into_bytes().into()
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    /// Hex-decode a string literal into a `Vec<u8>` (test helper).
69    fn hex(s: &str) -> Vec<u8> {
70        assert!(s.len() % 2 == 0, "odd-length hex");
71        (0..s.len())
72            .step_by(2)
73            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
74            .collect()
75    }
76
77    // === RFC 4231 known-answer vectors for HMAC-SHA-256 ===
78
79    #[test]
80    fn rfc4231_case_1() {
81        let key = hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
82        let data = b"Hi There";
83        let expected = hex("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7");
84        assert_eq!(hmac_sha256(&key, data).to_vec(), expected);
85    }
86
87    #[test]
88    fn rfc4231_case_2() {
89        // Key shorter than the block size; ASCII key and message.
90        let expected = hex("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843");
91        assert_eq!(
92            hmac_sha256(b"Jefe", b"what do ya want for nothing?").to_vec(),
93            expected
94        );
95    }
96
97    #[test]
98    fn rfc4231_case_3() {
99        let key = vec![0xaau8; 20];
100        let data = vec![0xddu8; 50];
101        let expected = hex("773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe");
102        assert_eq!(hmac_sha256(&key, &data).to_vec(), expected);
103    }
104
105    #[test]
106    fn rfc4231_case_6_over_long_key() {
107        // Test Case 6: key longer than the block size is hashed first.
108        let key = vec![0xaau8; 131];
109        let data = b"Test Using Larger Than Block-Size Key - Hash Key First";
110        let expected = hex("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54");
111        assert_eq!(hmac_sha256(&key, data).to_vec(), expected);
112    }
113
114    #[test]
115    fn distinct_keys_distinct_tags() {
116        assert_ne!(hmac_sha256(b"k1", b"msg"), hmac_sha256(b"k2", b"msg"));
117    }
118
119    #[test]
120    fn distinct_messages_distinct_tags() {
121        assert_ne!(hmac_sha256(b"k", b"a"), hmac_sha256(b"k", b"b"));
122    }
123
124    #[test]
125    fn empty_key_and_message_is_defined() {
126        // HMAC is defined for any key/message length, including empty.
127        assert_eq!(hmac_sha256(b"", b"").len(), HMAC_SHA256_LEN);
128    }
129
130    use proptest::prelude::*;
131
132    proptest! {
133        #[test]
134        fn hmac_sha256_is_deterministic(key: Vec<u8>, msg: Vec<u8>) {
135            prop_assert_eq!(hmac_sha256(&key, &msg), hmac_sha256(&key, &msg));
136        }
137
138        #[test]
139        fn hmac_sha256_output_is_always_32_bytes(key: Vec<u8>, msg: Vec<u8>) {
140            prop_assert_eq!(hmac_sha256(&key, &msg).len(), HMAC_SHA256_LEN);
141        }
142    }
143}