Skip to main content

nodedb_cluster/rpc_codec/
auth_envelope.rs

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