1use std::cell::RefCell;
15
16use thiserror::Error;
17
18use crate::api::proto::dataplane::v1::{Name, SlimHeader};
19
20#[cfg(not(target_arch = "wasm32"))]
26#[path = "header_mac/backend_awslc.rs"]
27mod backend;
28#[cfg(target_arch = "wasm32")]
29#[path = "header_mac/backend_pure.rs"]
30mod backend;
31
32#[cfg(all(test, not(target_arch = "wasm32")))]
36#[path = "header_mac/backend_pure.rs"]
37mod backend_pure;
38
39type MacKey = backend::MacKey;
40
41thread_local! {
42 static PREIMAGE_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(512));
43}
44
45const DOMAIN_V1: &[u8] = b"SLIM-DP-HDR-v1\0";
46const MIN_KEY_LEN: usize = 32;
47const TAG_LEN: usize = 32;
48
49#[derive(Debug, Error)]
50pub enum HeaderMacError {
51 #[error("header_mac key must be at least {MIN_KEY_LEN} bytes")]
52 KeyTooShort,
53 #[error("link_id must not be empty")]
54 EmptyLinkId,
55 #[error("missing SLIM header integrity tag")]
56 MissingTag,
57 #[error("invalid integrity tag length")]
58 InvalidTagLength,
59 #[error("SLIM header integrity verification failed")]
60 VerificationFailed,
61 #[error("inter-node link key agreement failed")]
62 KeyAgreement,
63 #[error("key generation failed")]
64 KeyGenerationFailed(String),
65}
66
67#[derive(Clone)]
69pub struct HeaderMacSession {
70 key: MacKey,
71}
72
73impl std::fmt::Debug for HeaderMacSession {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("HeaderMacSession").finish_non_exhaustive()
76 }
77}
78
79impl HeaderMacSession {
80 pub fn new(secret: &[u8]) -> Result<Self, HeaderMacError> {
82 if secret.len() < MIN_KEY_LEN {
83 return Err(HeaderMacError::KeyTooShort);
84 }
85 Ok(Self {
86 key: backend::new(secret),
87 })
88 }
89
90 pub fn sign_slim_header(
92 &self,
93 header: &mut SlimHeader,
94 link_id: &str,
95 ) -> Result<(), HeaderMacError> {
96 if link_id.is_empty() {
97 return Err(HeaderMacError::EmptyLinkId);
98 }
99 header.header_mac = None;
100 PREIMAGE_BUF.with(|cell| {
101 let mut buf = cell.borrow_mut();
102 buf.clear();
103 reserve_preimage_upper_bound(&mut buf, header, link_id);
104 write_preimage(&mut buf, header, link_id.as_bytes());
105 header.header_mac = Some(backend::sign(&self.key, buf.as_slice()));
106 Ok(())
107 })
108 }
109
110 pub fn verify_slim_header(
112 &self,
113 header: &SlimHeader,
114 link_id: &str,
115 ) -> Result<(), HeaderMacError> {
116 if link_id.is_empty() {
117 return Err(HeaderMacError::EmptyLinkId);
118 }
119 let tag = header
120 .header_mac
121 .as_deref()
122 .ok_or(HeaderMacError::MissingTag)?;
123 if tag.len() != TAG_LEN {
124 return Err(HeaderMacError::InvalidTagLength);
125 }
126 PREIMAGE_BUF.with(|cell| {
127 let mut buf = cell.borrow_mut();
128 buf.clear();
129 reserve_preimage_upper_bound(&mut buf, header, link_id);
130 write_preimage(&mut buf, header, link_id.as_bytes());
131 if backend::verify(&self.key, buf.as_slice(), tag) {
132 Ok(())
133 } else {
134 Err(HeaderMacError::VerificationFailed)
135 }
136 })
137 }
138}
139
140#[inline]
142fn reserve_preimage_upper_bound(buf: &mut Vec<u8>, hdr: &SlimHeader, link_id: &str) {
143 let need = preimage_upper_bound(hdr, link_id);
144 let cap = buf.capacity();
145 if need > cap {
146 buf.reserve(need - cap);
147 }
148}
149
150#[inline]
151fn preimage_upper_bound(header: &SlimHeader, link_id: &str) -> usize {
152 const LINK_ID_LEN_PREFIX: usize = 4;
154
155 const FANOUT_SIZE: usize = 4;
158
159 const RECV_FROM_SIZE: usize = 9;
165
166 const FORWARD_TO_SIZE: usize = 9;
172
173 const ERROR_SIZE: usize = 2;
175
176 const TTL_SIZE: usize = 4;
178
179 DOMAIN_V1.len()
180 + LINK_ID_LEN_PREFIX
181 + link_id.len()
182 + FANOUT_SIZE
183 + RECV_FROM_SIZE
184 + FORWARD_TO_SIZE
185 + ERROR_SIZE
186 + TTL_SIZE
187 + encoded_name_upper_bound(&header.source)
188 + encoded_name_upper_bound(&header.destination)
189}
190
191#[inline]
192fn encoded_name_upper_bound(name_opt: &Option<Name>) -> usize {
193 match name_opt {
194 None => 1,
195 Some(name) => {
196 const PRESENCE_FLAGS_SIZE: usize = 3;
201 const ENCODED_NAME_SIZE: usize = 24 + 1 + 16;
206
207 const LENGTH_PREFIXES_SIZE: usize = 12;
210
211 let mut encoded_name_bound = PRESENCE_FLAGS_SIZE + ENCODED_NAME_SIZE;
212 if let Some(sn) = name.str_name.as_ref() {
213 encoded_name_bound += LENGTH_PREFIXES_SIZE
214 + sn.str_component_0.len()
215 + sn.str_component_1.len()
216 + sn.str_component_2.len();
217 }
218 encoded_name_bound
219 }
220 }
221}
222
223#[inline]
224fn push_bytes(buf: &mut Vec<u8>, data: &[u8]) {
225 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
226 buf.extend_from_slice(data);
227}
228
229#[inline]
230fn push_u64_opt(buf: &mut Vec<u8>, v: Option<u64>) {
231 match v {
232 None => buf.push(0),
233 Some(x) => {
234 buf.push(1);
235 buf.extend_from_slice(&x.to_le_bytes());
236 }
237 }
238}
239
240#[inline]
241fn push_bool_opt(buf: &mut Vec<u8>, v: Option<bool>) {
242 match v {
243 None => buf.push(0),
244 Some(b) => {
245 buf.push(1);
246 buf.push(u8::from(b));
247 }
248 }
249}
250
251fn push_encoded_name(buf: &mut Vec<u8>, n: &Option<Name>) {
252 match n {
253 None => buf.push(0),
254 Some(name) => {
255 buf.push(1);
256 if let Some(enc) = name.name.as_ref() {
257 buf.push(1);
258 for v in [enc.component_0, enc.component_1, enc.component_2] {
259 buf.extend_from_slice(&v.to_le_bytes());
260 }
261 match enc.name_id.as_ref() {
262 Some(nid) => {
263 buf.push(1);
264 buf.extend_from_slice(&nid.id_0.to_le_bytes());
265 buf.extend_from_slice(&nid.id_1.to_le_bytes());
266 }
267 None => buf.push(0),
268 }
269 } else {
270 buf.push(0);
271 }
272 if let Some(sn) = name.str_name.as_ref() {
273 buf.push(1);
274 push_bytes(buf, sn.str_component_0.as_bytes());
275 push_bytes(buf, sn.str_component_1.as_bytes());
276 push_bytes(buf, sn.str_component_2.as_bytes());
277 } else {
278 buf.push(0);
279 }
280 }
281 }
282}
283
284fn write_preimage(buf: &mut Vec<u8>, hdr: &SlimHeader, link_id_bytes: &[u8]) {
286 buf.extend_from_slice(DOMAIN_V1);
287 buf.extend_from_slice(&(link_id_bytes.len() as u32).to_le_bytes());
288 buf.extend_from_slice(link_id_bytes);
289 push_encoded_name(buf, &hdr.source);
290 push_encoded_name(buf, &hdr.destination);
291 push_bytes(buf, hdr.identity.as_bytes());
292 buf.extend_from_slice(&hdr.fanout.to_le_bytes());
293 push_u64_opt(buf, hdr.recv_from);
294 push_u64_opt(buf, hdr.forward_to);
295 push_bool_opt(buf, hdr.error);
296 buf.extend_from_slice(&hdr.ttl.to_le_bytes());
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::api::proto::dataplane::v1::{EncodedName, Name, NameId, StringName};
303 use crate::messages::utils::DEFAULT_TTL;
304 use uuid::Uuid;
305
306 #[cfg(target_arch = "wasm32")]
309 use super::backend as backend_pure;
310 #[cfg(not(target_arch = "wasm32"))]
311 use super::backend_pure;
312
313 fn test_key() -> Vec<u8> {
314 b"01234567890123456789012345678901".to_vec()
315 }
316
317 fn sample_header() -> SlimHeader {
318 SlimHeader {
319 source: Some(Name {
320 name: Some(EncodedName {
321 component_0: 1,
322 component_1: 2,
323 component_2: 3,
324 name_id: Some(NameId { id_0: 0, id_1: 4 }),
325 }),
326 str_name: Some(StringName {
327 str_component_0: "a".into(),
328 str_component_1: "b".into(),
329 str_component_2: "c".into(),
330 }),
331 }),
332 destination: Some(Name {
333 name: Some(EncodedName {
334 component_0: 5,
335 component_1: 6,
336 component_2: 7,
337 name_id: Some(NameId { id_0: 0, id_1: 8 }),
338 }),
339 str_name: Some(StringName {
340 str_component_0: "x".into(),
341 str_component_1: "y".into(),
342 str_component_2: "z".into(),
343 }),
344 }),
345 identity: "id1".into(),
346 fanout: 2,
347 version: String::new(),
348 recv_from: Some(9),
349 forward_to: Some(10),
350 incoming_conn: Some(999),
351 error: Some(false),
352 header_mac: None,
353 ttl: DEFAULT_TTL,
354 e2e_header_sig: None,
355 }
356 }
357
358 #[test]
359 fn sign_verify_round_trip() {
360 let lid = Uuid::new_v4().to_string();
361 let mac = HeaderMacSession::new(&test_key()).unwrap();
362 let mut hdr = sample_header();
363 mac.sign_slim_header(&mut hdr, &lid).unwrap();
364 mac.verify_slim_header(&hdr, &lid).unwrap();
365 }
366
367 #[test]
368 fn incoming_conn_does_not_affect_mac() {
369 let lid = Uuid::new_v4().to_string();
370 let mac = HeaderMacSession::new(&test_key()).unwrap();
371 let mut h1 = sample_header();
372 mac.sign_slim_header(&mut h1, &lid).unwrap();
373 let mut h2 = h1.clone();
374 h2.incoming_conn = Some(12345);
375 mac.verify_slim_header(&h2, &lid).unwrap();
376 }
377
378 #[test]
379 fn tampered_fanout_fail() {
380 let lid = Uuid::new_v4().to_string();
381 let mac = HeaderMacSession::new(&test_key()).unwrap();
382 let mut hdr = sample_header();
383 mac.sign_slim_header(&mut hdr, &lid).unwrap();
384 hdr.fanout = 3;
385 assert!(mac.verify_slim_header(&hdr, &lid).is_err());
386 }
387
388 #[test]
389 fn tampered_mac_fail() {
390 let lid = Uuid::new_v4().to_string();
391 let mac = HeaderMacSession::new(&test_key()).unwrap();
392 let mut hdr = sample_header();
393 mac.sign_slim_header(&mut hdr, &lid).unwrap();
394 let mac_val = hdr.header_mac.as_mut().unwrap();
395 mac_val[0] ^= 1;
397 let err = mac.verify_slim_header(&hdr, &lid).unwrap_err();
398 assert!(matches!(err, HeaderMacError::VerificationFailed));
399 }
400
401 #[test]
402 fn cross_link_replay_fail() {
403 let lid1 = Uuid::new_v4().to_string();
404 let lid2 = Uuid::new_v4().to_string();
405 assert_ne!(lid1, lid2);
406
407 let mac = HeaderMacSession::new(&test_key()).unwrap();
408 let mut hdr = sample_header();
409
410 mac.sign_slim_header(&mut hdr, &lid1).unwrap();
412
413 let err = mac.verify_slim_header(&hdr, &lid2).unwrap_err();
416 assert!(matches!(err, HeaderMacError::VerificationFailed));
417 }
418
419 #[test]
420 fn name_id_none_sign_verify() {
421 let lid = Uuid::new_v4().to_string();
422 let mac = HeaderMacSession::new(&test_key()).unwrap();
423 let mut hdr = sample_header();
424 hdr.source.as_mut().unwrap().name.as_mut().unwrap().name_id = None;
426 mac.sign_slim_header(&mut hdr, &lid).unwrap();
427 mac.verify_slim_header(&hdr, &lid).unwrap();
428 }
429
430 #[test]
431 fn tampered_name_id_fail() {
432 let lid = Uuid::new_v4().to_string();
433 let mac = HeaderMacSession::new(&test_key()).unwrap();
434 let mut hdr = sample_header();
435 mac.sign_slim_header(&mut hdr, &lid).unwrap();
436 hdr.source.as_mut().unwrap().name.as_mut().unwrap().name_id =
438 Some(NameId { id_0: 99, id_1: 99 });
439 assert!(mac.verify_slim_header(&hdr, &lid).is_err());
440 }
441
442 #[test]
446 fn pure_hmac_backend_matches_production_backend() {
447 let secret = test_key();
448 let data = b"SLIM-DP-HDR-v1\0canonical-preimage-under-test";
449
450 let production = backend::sign(&backend::new(&secret), data);
451 let pure = backend_pure::sign(&backend_pure::new(&secret), data);
452
453 assert_eq!(
454 production.len(),
455 TAG_LEN,
456 "HMAC-SHA256 tag must be {TAG_LEN} bytes"
457 );
458 assert_eq!(
459 production, pure,
460 "pure-Rust HMAC backend must match the production backend byte-for-byte"
461 );
462 }
463
464 #[test]
466 fn pure_hmac_backend_verify_round_trip() {
467 let secret = test_key();
468 let data = b"some-data-to-authenticate";
469
470 let key = backend_pure::new(&secret);
471 let tag = backend_pure::sign(&key, data);
472 assert!(backend_pure::verify(&key, data, &tag), "valid tag verifies");
473
474 let mut bad = tag.clone();
475 bad[0] ^= 0xFF;
476 assert!(
477 !backend_pure::verify(&key, data, &bad),
478 "tampered tag must fail"
479 );
480 assert!(
481 !backend_pure::verify(&key, b"other-data", &tag),
482 "tag must not verify over different data"
483 );
484 }
485}