Skip to main content

nodedb_cluster/rpc_codec/
mac.rs

1//! Symmetric MAC key + HMAC-SHA256 primitive for the authenticated Raft
2//! envelope.
3//!
4//! # Trust model
5//!
6//! The MAC key is a cluster-wide 32-byte shared secret generated at
7//! bootstrap and distributed to joining nodes out of band (via the mTLS
8//! join RPC, L.4). Every legitimate cluster member holds the same key;
9//! outside parties do not.
10//!
11//! # What the MAC buys
12//!
13//! Frame replay protection: a frame captured off the wire (or replayed
14//! within a compromised TLS session, or across sessions that share the
15//! same transport identity) cannot be modified or re-sent by a party
16//! that lacks the cluster key. Combined with the per-peer monotonic
17//! sequence in the envelope, every frame is consumed at most once.
18//!
19//! # What the MAC does NOT buy
20//!
21//! If a node's credentials leak wholesale (its mTLS key **and** its
22//! copy of the cluster secret), the attacker can impersonate that node
23//! in full. The MAC is defence-in-depth, not a substitute for
24//! compromising a node.
25
26use hmac::{Hmac, Mac};
27use sha2::Sha256;
28
29use crate::error::{ClusterError, Result};
30
31/// Length of the MAC tag in bytes. HMAC-SHA256 produces 32 bytes.
32pub const MAC_LEN: usize = 32;
33
34/// Cluster-wide symmetric MAC key.
35///
36/// The key is 32 bytes — HMAC-SHA256's natural block-equivalent input.
37/// All legitimate cluster members hold the same value.
38#[derive(Clone)]
39pub struct MacKey([u8; MAC_LEN]);
40
41impl MacKey {
42    /// Construct from raw bytes.
43    pub fn from_bytes(bytes: [u8; MAC_LEN]) -> Self {
44        Self(bytes)
45    }
46
47    /// A cryptographically random fresh key. Use at cluster bootstrap.
48    pub fn random() -> Self {
49        use rand::RngCore;
50        let mut out = [0u8; MAC_LEN];
51        rand::rng().fill_bytes(&mut out);
52        Self(out)
53    }
54
55    /// All-zero sentinel key used only by the `Insecure` transport mode.
56    /// When the key is zero the MAC verification is decorative — the
57    /// insecure mode already trusts any network peer.
58    pub fn zero() -> Self {
59        Self([0u8; MAC_LEN])
60    }
61
62    /// Raw bytes. Used for persistence only; callers must treat the
63    /// return value as key material.
64    pub fn as_bytes(&self) -> &[u8; MAC_LEN] {
65        &self.0
66    }
67
68    /// Whether this key is the all-zero sentinel. Insecure transports
69    /// skip replay-detection telemetry accordingly.
70    pub fn is_zero(&self) -> bool {
71        self.0 == [0u8; MAC_LEN]
72    }
73}
74
75/// Redacted `Debug` — never print key bytes.
76impl std::fmt::Debug for MacKey {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        if self.is_zero() {
79            write!(f, "MacKey(zero)")
80        } else {
81            write!(f, "MacKey(<redacted>)")
82        }
83    }
84}
85
86/// Compute HMAC-SHA256 over `data` with `key`. Length is fixed at
87/// [`MAC_LEN`].
88pub fn compute_hmac(key: &MacKey, data: &[u8]) -> [u8; MAC_LEN] {
89    let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key.as_bytes())
90        .expect("HMAC-SHA256 accepts any key length");
91    mac.update(data);
92    let out = mac.finalize().into_bytes();
93    let mut tag = [0u8; MAC_LEN];
94    tag.copy_from_slice(&out);
95    tag
96}
97
98/// Constant-time verify `tag` against HMAC-SHA256 over `data` with `key`.
99/// Returns `Err` on mismatch.
100pub fn verify_hmac(key: &MacKey, data: &[u8], tag: &[u8; MAC_LEN]) -> Result<()> {
101    let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key.as_bytes())
102        .expect("HMAC-SHA256 accepts any key length");
103    mac.update(data);
104    mac.verify_slice(tag).map_err(|_| ClusterError::Codec {
105        detail: "frame MAC verification failed".into(),
106    })
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn hmac_roundtrip() {
115        let key = MacKey::from_bytes([7u8; MAC_LEN]);
116        let tag = compute_hmac(&key, b"hello world");
117        verify_hmac(&key, b"hello world", &tag).unwrap();
118    }
119
120    #[test]
121    fn hmac_rejects_tampered_data() {
122        let key = MacKey::from_bytes([7u8; MAC_LEN]);
123        let tag = compute_hmac(&key, b"hello world");
124        let err = verify_hmac(&key, b"hello WORLD", &tag).unwrap_err();
125        assert!(err.to_string().contains("MAC verification failed"));
126    }
127
128    #[test]
129    fn hmac_rejects_wrong_key() {
130        let k1 = MacKey::from_bytes([1u8; MAC_LEN]);
131        let k2 = MacKey::from_bytes([2u8; MAC_LEN]);
132        let tag = compute_hmac(&k1, b"msg");
133        assert!(verify_hmac(&k2, b"msg", &tag).is_err());
134    }
135
136    #[test]
137    fn debug_redacts_key() {
138        let k = MacKey::from_bytes([0xAA; MAC_LEN]);
139        let s = format!("{k:?}");
140        assert!(!s.contains("aa"), "debug leaked key bytes: {s}");
141        assert!(s.contains("redacted"));
142    }
143
144    #[test]
145    fn random_keys_differ() {
146        let k1 = MacKey::random();
147        let k2 = MacKey::random();
148        assert_ne!(k1.as_bytes(), k2.as_bytes());
149    }
150
151    #[test]
152    fn zero_key_reports_zero() {
153        assert!(MacKey::zero().is_zero());
154        assert!(!MacKey::random().is_zero());
155    }
156}