Skip to main content

nodedb_cluster/rpc_codec/
mac.rs

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