nodedb_cluster/rpc_codec/
auth_envelope.rs1use crate::error::{ClusterError, Result};
29
30use super::header::MAX_RPC_PAYLOAD_SIZE;
31use super::mac::{MAC_LEN, MacKey, compute_hmac, verify_hmac};
32
33pub const ENVELOPE_VERSION: u8 = 1;
36
37pub const ENVELOPE_OVERHEAD: usize = 1 + 8 + 8 + 4 + MAC_LEN;
40
41const 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#[derive(Debug, Clone, Copy)]
52pub struct EnvelopeFields {
53 pub from_node_id: u64,
54 pub seq: u64,
55}
56
57pub 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
93pub 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 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 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}