Skip to main content

nodedb_cluster/rpc_codec/
auth_envelope.rs

1//! Authenticated frame envelope wrapping the existing RPC wire frame.
2//!
3//! # Layout
4//!
5//! ```text
6//! ┌──────────┬──────────────┬────────┬───────────┬──────────────┬────────┐
7//! │ env_ver  │ from_node_id │ seq    │ inner_len │ inner_frame  │ mac    │
8//! │  1 byte  │   8 bytes    │ 8 B    │  4 bytes  │ inner_len B  │ 32 B   │
9//! └──────────┴──────────────┴────────┴───────────┴──────────────┴────────┘
10//! ```
11//!
12//! `inner_frame` is the legacy `header::write_frame` output (version +
13//! rpc_type + payload_len + crc32c + payload) — left untouched so every
14//! per-RPC-type encoder keeps working.
15//!
16//! The MAC covers every byte of the envelope *except* the MAC itself:
17//! `[env_ver, from_node_id, seq, inner_len, inner_frame]`. Swapping any of
18//! these fields invalidates the tag.
19//!
20//! # Bounds
21//!
22//! - Inner frame length is capped by the legacy
23//!   [`header::MAX_RPC_PAYLOAD_SIZE`], which itself bounds the inner payload.
24//! - Receiver must verify the MAC **before** trusting any declared field.
25//!
26//! [`header::MAX_RPC_PAYLOAD_SIZE`]: super::header::MAX_RPC_PAYLOAD_SIZE
27
28use crate::error::{ClusterError, Result};
29
30use super::header::MAX_RPC_PAYLOAD_SIZE;
31use super::mac::{MAC_LEN, MacKey, compute_hmac, verify_hmac};
32
33/// Envelope wire version. Bumped when the layout changes in a way that
34/// cannot be negotiated on-the-fly (adding a field, moving MAC position).
35pub const ENVELOPE_VERSION: u8 = 1;
36
37/// Fixed bytes contributed by the envelope: version + from_node_id + seq +
38/// inner_len + mac. Does not include the inner frame itself.
39pub const ENVELOPE_OVERHEAD: usize = 1 + 8 + 8 + 4 + MAC_LEN;
40
41/// Byte offsets within the envelope header (pre-inner-frame section).
42const OFF_VERSION: usize = 0;
43const OFF_FROM_NODE: usize = 1;
44const OFF_SEQ: usize = 9;
45const OFF_INNER_LEN: usize = 17;
46const ENV_HEADER_LEN: usize = 21;
47
48/// Metadata parsed from an envelope before MAC verification. `seq` and
49/// `from_node_id` are **un-trusted** until [`parse_envelope`] has
50/// returned `Ok`.
51#[derive(Debug, Clone, Copy)]
52pub struct EnvelopeFields {
53    pub from_node_id: u64,
54    pub seq: u64,
55}
56
57/// Wrap `inner_frame` in an authenticated envelope, appending to `out`.
58///
59/// MAC covers `[env_ver, from_node_id, seq, inner_len, inner_frame]`.
60pub fn write_envelope(
61    from_node_id: u64,
62    seq: u64,
63    inner_frame: &[u8],
64    key: &MacKey,
65    out: &mut Vec<u8>,
66) -> Result<()> {
67    let inner_len: u32 = inner_frame
68        .len()
69        .try_into()
70        .map_err(|_| ClusterError::Codec {
71            detail: format!("inner frame too large: {} bytes", inner_frame.len()),
72        })?;
73    if inner_len > MAX_RPC_PAYLOAD_SIZE {
74        return Err(ClusterError::Codec {
75            detail: format!(
76                "inner frame length {inner_len} exceeds maximum {MAX_RPC_PAYLOAD_SIZE}"
77            ),
78        });
79    }
80
81    let start = out.len();
82    out.reserve(ENVELOPE_OVERHEAD + inner_frame.len());
83    out.push(ENVELOPE_VERSION);
84    out.extend_from_slice(&from_node_id.to_le_bytes());
85    out.extend_from_slice(&seq.to_le_bytes());
86    out.extend_from_slice(&inner_len.to_le_bytes());
87    out.extend_from_slice(inner_frame);
88    let tag = compute_hmac(key, &out[start..]);
89    out.extend_from_slice(&tag);
90    Ok(())
91}
92
93/// Validate the envelope + MAC and return `(fields, inner_frame)`.
94///
95/// `data` must be the entire envelope — nothing before the version byte,
96/// nothing after the MAC tag.
97pub fn parse_envelope<'a>(data: &'a [u8], key: &MacKey) -> Result<(EnvelopeFields, &'a [u8])> {
98    if data.len() < ENVELOPE_OVERHEAD {
99        return Err(ClusterError::Codec {
100            detail: format!(
101                "envelope too short: {} bytes, need at least {ENVELOPE_OVERHEAD}",
102                data.len()
103            ),
104        });
105    }
106
107    let version = data[OFF_VERSION];
108    if version != ENVELOPE_VERSION {
109        return Err(ClusterError::Codec {
110            detail: format!("unsupported envelope version {version}, expected {ENVELOPE_VERSION}"),
111        });
112    }
113
114    let from_node_id = u64::from_le_bytes(data[OFF_FROM_NODE..OFF_SEQ].try_into().unwrap());
115    let seq = u64::from_le_bytes(data[OFF_SEQ..OFF_INNER_LEN].try_into().unwrap());
116    let inner_len = u32::from_le_bytes(data[OFF_INNER_LEN..ENV_HEADER_LEN].try_into().unwrap());
117
118    if inner_len > MAX_RPC_PAYLOAD_SIZE {
119        return Err(ClusterError::Codec {
120            detail: format!(
121                "envelope inner length {inner_len} exceeds maximum {MAX_RPC_PAYLOAD_SIZE}"
122            ),
123        });
124    }
125
126    let inner_end = ENV_HEADER_LEN + inner_len as usize;
127    let expected_total = inner_end + MAC_LEN;
128    if data.len() != expected_total {
129        return Err(ClusterError::Codec {
130            detail: format!(
131                "envelope length mismatch: got {} bytes, expected {expected_total}",
132                data.len()
133            ),
134        });
135    }
136
137    let tag: &[u8; MAC_LEN] = data[inner_end..].try_into().unwrap();
138    verify_hmac(key, &data[..inner_end], tag)?;
139
140    let inner_frame = &data[ENV_HEADER_LEN..inner_end];
141    Ok((EnvelopeFields { from_node_id, seq }, inner_frame))
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::rpc_codec::header::{HEADER_SIZE, write_frame};
148
149    fn sample_inner(rpc_type: u8) -> Vec<u8> {
150        let mut out = Vec::new();
151        write_frame(rpc_type, b"payload bytes", &mut out).unwrap();
152        out
153    }
154
155    #[test]
156    fn envelope_roundtrips() {
157        let key = MacKey::from_bytes([3u8; MAC_LEN]);
158        let inner = sample_inner(0x42);
159        let mut buf = Vec::new();
160        write_envelope(7, 12345, &inner, &key, &mut buf).unwrap();
161
162        assert_eq!(buf.len(), ENVELOPE_OVERHEAD + inner.len());
163
164        let (fields, parsed_inner) = parse_envelope(&buf, &key).unwrap();
165        assert_eq!(fields.from_node_id, 7);
166        assert_eq!(fields.seq, 12345);
167        assert_eq!(parsed_inner, inner.as_slice());
168        assert!(parsed_inner.len() >= HEADER_SIZE);
169    }
170
171    #[test]
172    fn rejects_unknown_envelope_version() {
173        let key = MacKey::from_bytes([3u8; MAC_LEN]);
174        let inner = sample_inner(1);
175        let mut buf = Vec::new();
176        write_envelope(1, 1, &inner, &key, &mut buf).unwrap();
177        buf[OFF_VERSION] = 99;
178        let err = parse_envelope(&buf, &key).unwrap_err();
179        assert!(err.to_string().contains("envelope version"));
180    }
181
182    #[test]
183    fn rejects_tampered_from_node_id() {
184        let key = MacKey::from_bytes([3u8; MAC_LEN]);
185        let inner = sample_inner(1);
186        let mut buf = Vec::new();
187        write_envelope(7, 42, &inner, &key, &mut buf).unwrap();
188        // Flip the low byte of from_node_id — original 7 becomes 6.
189        buf[OFF_FROM_NODE] ^= 0x01;
190        let err = parse_envelope(&buf, &key).unwrap_err();
191        assert!(err.to_string().contains("MAC verification failed"));
192    }
193
194    #[test]
195    fn rejects_tampered_seq() {
196        let key = MacKey::from_bytes([3u8; MAC_LEN]);
197        let inner = sample_inner(1);
198        let mut buf = Vec::new();
199        write_envelope(1, 100, &inner, &key, &mut buf).unwrap();
200        buf[OFF_SEQ] ^= 0xFF;
201        let err = parse_envelope(&buf, &key).unwrap_err();
202        assert!(err.to_string().contains("MAC verification failed"));
203    }
204
205    #[test]
206    fn rejects_tampered_inner() {
207        let key = MacKey::from_bytes([3u8; MAC_LEN]);
208        let inner = sample_inner(1);
209        let mut buf = Vec::new();
210        write_envelope(1, 1, &inner, &key, &mut buf).unwrap();
211        // Flip a byte in the inner frame.
212        buf[ENV_HEADER_LEN + HEADER_SIZE] ^= 0x80;
213        let err = parse_envelope(&buf, &key).unwrap_err();
214        assert!(err.to_string().contains("MAC verification failed"));
215    }
216
217    #[test]
218    fn rejects_wrong_key() {
219        let k1 = MacKey::from_bytes([1u8; MAC_LEN]);
220        let k2 = MacKey::from_bytes([2u8; MAC_LEN]);
221        let inner = sample_inner(1);
222        let mut buf = Vec::new();
223        write_envelope(1, 1, &inner, &k1, &mut buf).unwrap();
224        let err = parse_envelope(&buf, &k2).unwrap_err();
225        assert!(err.to_string().contains("MAC verification failed"));
226    }
227
228    #[test]
229    fn rejects_truncated_envelope() {
230        let key = MacKey::from_bytes([3u8; MAC_LEN]);
231        let inner = sample_inner(1);
232        let mut buf = Vec::new();
233        write_envelope(1, 1, &inner, &key, &mut buf).unwrap();
234        buf.truncate(buf.len() - 1);
235        let err = parse_envelope(&buf, &key).unwrap_err();
236        let msg = err.to_string();
237        assert!(msg.contains("envelope length mismatch") || msg.contains("envelope too short"));
238    }
239}