1pub mod lioness;
2pub mod packet;
3pub mod pow;
4pub mod surb;
5
6use chacha20::cipher::{KeyIvInit, StreamCipher};
7use chacha20::{ChaCha20, Key, Nonce};
8use curve25519_dalek::constants::X25519_BASEPOINT;
9use curve25519_dalek::montgomery::MontgomeryPoint;
10use curve25519_dalek::scalar::Scalar;
11use hmac::{Hmac, Mac};
12use serde::{Deserialize, Serialize};
13use serde_big_array::BigArray;
14use sha2::{Digest, Sha256};
15use subtle::ConstantTimeEq;
16use thiserror::Error;
17use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519SecretKey};
18pub(crate) fn unpad_iso7816_inner(data: &[u8]) -> Option<Vec<u8>> {
20 use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
21
22 if data.is_empty() {
23 return None;
24 }
25
26 let mut marker_pos: i64 = -1;
27 let mut found_marker = Choice::from(0u8);
28
29 for i in (0..data.len()).rev() {
30 let is_marker = data[i].ct_eq(&0x80);
31 let should_update = is_marker & !found_marker;
32 marker_pos = i64::conditional_select(&marker_pos, &(i as i64), should_update);
33 found_marker |= is_marker;
34 }
35
36 if marker_pos < 0 {
37 return None;
38 }
39
40 let marker_idx = marker_pos as usize;
41
42 let mut all_zeros = Choice::from(1u8);
43 for &byte in &data[marker_idx + 1..] {
44 all_zeros &= byte.ct_eq(&0x00);
45 }
46
47 if !bool::from(all_zeros) {
48 return None;
49 }
50
51 Some(data[..marker_idx].to_vec())
52}
53
54pub const ROUTING_INFO_SIZE: usize = 400;
55pub const MAC_SIZE: usize = 32;
56pub const NONCE_SIZE: usize = 8;
57pub const HEADER_SIZE: usize = 32 + ROUTING_INFO_SIZE + MAC_SIZE + NONCE_SIZE;
58pub const SHIFT_SIZE: usize = 128;
60
61#[derive(Debug, Error)]
62pub enum SphinxError {
63 #[error("Integrity Check Failed (Invalid MAC)")]
64 MacMismatch,
65 #[error("Serialization Error: {0}")]
66 Serialization(String),
67 #[error("Packet too short")]
68 InvalidSize,
69 #[error("Crypto Error: {0}")]
70 Crypto(String),
71 #[error("PoW error: {0}")]
72 Pow(#[from] pow::PowError),
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76pub struct SphinxHeader {
77 pub ephemeral_key: X25519PublicKey,
78 #[serde(with = "BigArray")]
79 pub routing_info: [u8; ROUTING_INFO_SIZE],
80 pub mac: [u8; MAC_SIZE],
81 pub nonce: u64,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85pub struct PathHop {
86 pub public_key: X25519PublicKey,
87 pub address: String,
88}
89
90#[cfg(feature = "hop-metrics")]
92#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
93pub struct HopTimings {
94 pub ecdh_ns: u64,
95 pub key_derive_ns: u64,
96 pub mac_verify_ns: u64,
97 pub routing_decrypt_ns: u64,
98 pub body_decrypt_ns: u64,
99 pub blinding_ns: u64,
100 pub total_sphinx_ns: u64,
101}
102
103#[derive(Debug)]
104pub enum ProcessResult {
105 Forward {
106 next_hop: String,
107 next_packet: Box<SphinxHeader>,
108 processed_body: Vec<u8>,
109 },
110 Exit {
111 payload: Vec<u8>,
112 },
113}
114
115#[cfg(feature = "hop-metrics")]
116pub type ProcessOutput = (ProcessResult, HopTimings);
117
118#[cfg(not(feature = "hop-metrics"))]
119pub type ProcessOutput = ProcessResult;
120
121#[inline]
123#[must_use]
124pub fn into_result(output: ProcessOutput) -> ProcessResult {
125 #[cfg(feature = "hop-metrics")]
126 {
127 output.0
128 }
129 #[cfg(not(feature = "hop-metrics"))]
130 {
131 output
132 }
133}
134impl SphinxHeader {
135 #[must_use]
137 pub fn compute_replay_tag(&self) -> [u8; 32] {
138 let mut hasher = blake3::Hasher::new();
139 hasher.update(self.ephemeral_key.as_bytes());
140 hasher.update(&self.mac);
141 hasher.update(&self.nonce.to_le_bytes());
142 *hasher.finalize().as_bytes()
143 }
144
145 pub fn process(
147 &self,
148 node_sk: &X25519SecretKey,
149 body: Vec<u8>,
150 ) -> Result<ProcessOutput, SphinxError> {
151 #[cfg(feature = "hop-metrics")]
152 let total_start = std::time::Instant::now();
153
154 #[cfg(feature = "hop-metrics")]
155 let t0 = std::time::Instant::now();
156
157 let shared_secret = node_sk.diffie_hellman(&self.ephemeral_key);
158
159 #[cfg(feature = "hop-metrics")]
160 let ecdh_ns = t0.elapsed().as_nanos() as u64;
161
162 #[cfg(feature = "hop-metrics")]
163 let t1 = std::time::Instant::now();
164
165 let (rho, mu, pi, blinding_factor) = derive_keys(shared_secret.as_bytes());
166
167 #[cfg(feature = "hop-metrics")]
168 let key_derive_ns = t1.elapsed().as_nanos() as u64;
169
170 #[cfg(feature = "hop-metrics")]
171 let t2 = std::time::Instant::now();
172
173 let calculated_mac = compute_mac(&mu, &self.routing_info);
174 if calculated_mac.ct_eq(&self.mac).unwrap_u8() == 0 {
175 return Err(SphinxError::MacMismatch);
176 }
177
178 #[cfg(feature = "hop-metrics")]
179 let mac_verify_ns = t2.elapsed().as_nanos() as u64;
180
181 #[cfg(feature = "hop-metrics")]
182 let t3 = std::time::Instant::now();
183
184 let mut extended_routing = [0u8; ROUTING_INFO_SIZE + SHIFT_SIZE];
185 extended_routing[..ROUTING_INFO_SIZE].copy_from_slice(&self.routing_info);
186
187 apply_stream_cipher(&rho, &[0u8; 12], &mut extended_routing);
189
190 let decrypted_routing: [u8; ROUTING_INFO_SIZE] = extended_routing[..ROUTING_INFO_SIZE]
191 .try_into()
192 .map_err(|_| SphinxError::Serialization("Routing size mismatch".into()))?;
193 let shifted_routing: [u8; ROUTING_INFO_SIZE] = extended_routing
194 [SHIFT_SIZE..SHIFT_SIZE + ROUTING_INFO_SIZE]
195 .try_into()
196 .map_err(|_| SphinxError::Serialization("Routing size mismatch".into()))?;
197
198 #[cfg(feature = "hop-metrics")]
199 let routing_decrypt_ns = t3.elapsed().as_nanos() as u64;
200
201 #[cfg(feature = "hop-metrics")]
202 let t4 = std::time::Instant::now();
203
204 let mut processed_body = body;
205 if processed_body.len() < lioness::MIN_BODY_SIZE {
206 return Err(SphinxError::Serialization(format!(
207 "Body too small for Lioness SPRP: {} bytes, minimum {}",
208 processed_body.len(),
209 lioness::MIN_BODY_SIZE
210 )));
211 }
212 let lioness_keys = lioness::LionessKeys::from_pi(&pi);
213 lioness::lioness_decrypt(&lioness_keys, &mut processed_body);
214
215 #[cfg(feature = "hop-metrics")]
216 let body_decrypt_ns = t4.elapsed().as_nanos() as u64;
217
218 let hop_type = decrypted_routing[0];
219
220 #[cfg(feature = "hop-metrics")]
221 let mut blinding_ns = 0u64;
222
223 let result = if hop_type == 0x00 {
224 let addr_len = decrypted_routing[1] as usize;
225 let next_mac_start = 2;
226 let next_mac_end = 34;
227 let addr_start = 34;
228 let addr_end = 34 + addr_len;
229
230 if addr_end > SHIFT_SIZE {
231 return Err(SphinxError::Serialization(
232 "Address length exceeds SHIFT_SIZE".into(),
233 ));
234 }
235
236 let mut next_mac = [0u8; 32];
237 next_mac.copy_from_slice(&decrypted_routing[next_mac_start..next_mac_end]);
238
239 let next_hop = std::str::from_utf8(&decrypted_routing[addr_start..addr_end])
240 .map(str::to_owned)
241 .map_err(|_| SphinxError::Serialization("Invalid Address UTF8".into()))?;
242
243 #[cfg(feature = "hop-metrics")]
244 let t5 = std::time::Instant::now();
245
246 let point = MontgomeryPoint(self.ephemeral_key.to_bytes());
247 let blinded_point = point * blinding_factor;
248 let next_ephemeral_key = X25519PublicKey::from(blinded_point.to_bytes());
249
250 #[cfg(feature = "hop-metrics")]
251 {
252 blinding_ns = t5.elapsed().as_nanos() as u64;
253 }
254
255 ProcessResult::Forward {
256 next_hop,
257 next_packet: Box::new(SphinxHeader {
258 ephemeral_key: next_ephemeral_key,
259 routing_info: shifted_routing,
260 mac: next_mac,
261 nonce: 0,
262 }),
263 processed_body,
264 }
265 } else {
266 ProcessResult::Exit {
267 payload: processed_body,
268 }
269 };
270
271 #[cfg(feature = "hop-metrics")]
272 {
273 let total_sphinx_ns = total_start.elapsed().as_nanos() as u64;
274 Ok((
275 result,
276 HopTimings {
277 ecdh_ns,
278 key_derive_ns,
279 mac_verify_ns,
280 routing_decrypt_ns,
281 body_decrypt_ns,
282 blinding_ns,
283 total_sphinx_ns,
284 },
285 ))
286 }
287
288 #[cfg(not(feature = "hop-metrics"))]
289 Ok(result)
290 }
291
292 pub fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), SphinxError> {
293 if bytes.len() < HEADER_SIZE {
294 return Err(SphinxError::InvalidSize);
295 }
296
297 let pk_end = 32;
298 let route_end = pk_end + ROUTING_INFO_SIZE;
299 let mac_end = route_end + MAC_SIZE;
300 let nonce_end = mac_end + NONCE_SIZE;
301
302 let pk_bytes: [u8; 32] = bytes[0..pk_end]
303 .try_into()
304 .map_err(|_| SphinxError::InvalidSize)?;
305 let ephemeral_key = X25519PublicKey::from(pk_bytes);
306
307 let mut routing_info = [0u8; ROUTING_INFO_SIZE];
308 routing_info.copy_from_slice(&bytes[pk_end..route_end]);
309
310 let mut mac = [0u8; MAC_SIZE];
311 mac.copy_from_slice(&bytes[route_end..mac_end]);
312
313 let nonce_bytes: [u8; 8] = bytes[mac_end..nonce_end]
314 .try_into()
315 .map_err(|_| SphinxError::InvalidSize)?;
316 let nonce = u64::from_be_bytes(nonce_bytes);
317
318 let header = SphinxHeader {
319 ephemeral_key,
320 routing_info,
321 mac,
322 nonce,
323 };
324
325 let payload = &bytes[nonce_end..];
326 Ok((header, payload))
327 }
328
329 #[must_use]
330 pub fn to_bytes(&self, payload: &[u8]) -> Vec<u8> {
331 let mut out = Vec::with_capacity(HEADER_SIZE + payload.len());
332 out.extend_from_slice(self.ephemeral_key.as_bytes());
333 out.extend_from_slice(&self.routing_info);
334 out.extend_from_slice(&self.mac);
335 out.extend_from_slice(&self.nonce.to_be_bytes());
336 out.extend_from_slice(payload);
337 out
338 }
339
340 #[must_use]
341 pub fn verify_pow(&self, difficulty: u32) -> bool {
342 if difficulty == 0 {
343 return true;
344 }
345 let hash = self.compute_hash();
346 pow::count_leading_zeros(&hash) >= difficulty
347 }
348
349 pub fn solve_pow(&mut self, difficulty: u32) -> Result<(), SphinxError> {
351 if difficulty == 0 {
352 return Ok(());
353 }
354 if difficulty > pow::MAX_DIFFICULTY {
355 return Err(pow::PowError::DifficultyTooHigh { difficulty }.into());
356 }
357 loop {
358 if self.verify_pow(difficulty) {
359 break;
360 }
361 self.nonce = self.nonce.wrapping_add(1);
362 }
363 Ok(())
364 }
365
366 pub fn solve_pow_parallel(
368 &mut self,
369 difficulty: u32,
370 threads: usize,
371 ) -> Result<(), SphinxError> {
372 if difficulty == 0 {
373 return Ok(());
374 }
375
376 let solver = pow::PowSolver::new(pow::Sha256Pow, threads);
377 let hash_data = self.compute_hash_for_pow();
378 self.nonce = solver.solve(&hash_data, difficulty, self.nonce)?;
379 Ok(())
380 }
381
382 fn compute_hash_for_pow(&self) -> [u8; 32 + ROUTING_INFO_SIZE + MAC_SIZE] {
384 let mut data = [0u8; 32 + ROUTING_INFO_SIZE + MAC_SIZE];
385 data[..32].copy_from_slice(self.ephemeral_key.as_bytes());
386 data[32..32 + ROUTING_INFO_SIZE].copy_from_slice(&self.routing_info);
387 data[32 + ROUTING_INFO_SIZE..].copy_from_slice(&self.mac);
388 data
389 }
390
391 fn compute_hash(&self) -> [u8; 32] {
392 let mut hasher = Sha256::new();
393 hasher.update(self.ephemeral_key.as_bytes());
394 hasher.update(self.routing_info);
395 hasher.update(self.mac);
396 hasher.update(self.nonce.to_be_bytes());
397 hasher.finalize().into()
398 }
399}
400
401#[inline]
402#[must_use]
403pub fn derive_keys(shared_secret: &[u8; 32]) -> ([u8; 32], [u8; 32], [u8; 32], Scalar) {
404 let ss_bytes = shared_secret;
405
406 let mut hasher = Sha256::new();
407 hasher.update(b"rho");
408 hasher.update(ss_bytes);
409 let rho: [u8; 32] = hasher.finalize().into();
410
411 let mut hasher = Sha256::new();
412 hasher.update(b"mu");
413 hasher.update(ss_bytes);
414 let mu: [u8; 32] = hasher.finalize().into();
415
416 let mut hasher = Sha256::new();
417 hasher.update(b"pi");
418 hasher.update(ss_bytes);
419 let pi: [u8; 32] = hasher.finalize().into();
420
421 let mut hasher = Sha256::new();
422 hasher.update(b"blind");
423 hasher.update(ss_bytes);
424 let blind_bytes: [u8; 32] = hasher.finalize().into();
425 let blind_scalar = Scalar::from_bytes_mod_order(blind_bytes);
426
427 (rho, mu, pi, blind_scalar)
428}
429
430#[inline]
431#[allow(clippy::expect_used)]
432pub(crate) fn compute_mac(key: &[u8; 32], data: &[u8]) -> [u8; 32] {
433 let mut mac =
434 Hmac::<Sha256>::new_from_slice(key).expect("HMAC key length is always valid for SHA256");
435 mac.update(data);
436 mac.finalize().into_bytes().into()
437}
438
439#[inline]
440pub(crate) fn apply_stream_cipher(key: &[u8; 32], iv: &[u8; 12], data: &mut [u8]) {
441 let key = Key::from_slice(key);
442 let nonce = Nonce::from_slice(iv);
443 let mut cipher = ChaCha20::new(key, nonce);
444 cipher.apply_keystream(data);
445}
446
447#[cfg(test)]
449pub fn build_packet(
450 node_pk: X25519PublicKey,
451 payload: &[u8],
452 _is_exit: bool,
453 pow_difficulty: u32,
454) -> Result<Vec<u8>, SphinxError> {
455 let path = vec![PathHop {
456 public_key: node_pk,
457 address: "0xEXIT".into(),
458 }];
459 build_multi_hop_packet(&path, payload, pow_difficulty)
460}
461
462pub fn build_multi_hop_packet(
463 path: &[PathHop],
464 payload: &[u8],
465 pow_difficulty: u32,
466) -> Result<Vec<u8>, SphinxError> {
467 if path.is_empty() {
468 return Err(SphinxError::Serialization("Empty path".into()));
469 }
470
471 let mut rng = rand::rngs::OsRng;
472
473 let body_capacity = packet::PACKET_SIZE - HEADER_SIZE;
477 let mut current_body = vec![0u8; body_capacity];
478 let copy_len = payload.len().min(body_capacity);
479 current_body[..copy_len].copy_from_slice(&payload[..copy_len]);
480
481 let mut ephemeral_public_keys = Vec::with_capacity(path.len());
482 let mut shared_secrets = Vec::with_capacity(path.len());
483 let mut blinding_factors = Vec::with_capacity(path.len());
484
485 let mut current_secret_scalar = Scalar::random(&mut rng);
487 let initial_pk_point = X25519_BASEPOINT * current_secret_scalar;
488 let mut accumulated_blinding = Scalar::ONE;
489
490 for hop in path {
491 let hop_pk_point = MontgomeryPoint(hop.public_key.to_bytes());
492
493 let shared_point = hop_pk_point * current_secret_scalar;
494 let shared_bytes = shared_point.to_bytes();
495 shared_secrets.push(shared_bytes);
496
497 let (_, _, _, blinding) = derive_keys(&shared_bytes);
498 blinding_factors.push(blinding);
499
500 let blinded_point = initial_pk_point * accumulated_blinding;
501 let hop_pk = X25519PublicKey::from(blinded_point.to_bytes());
502 ephemeral_public_keys.push(hop_pk);
503
504 accumulated_blinding *= blinding;
505 current_secret_scalar *= blinding;
506 }
507
508 let mut filler = Vec::new();
509 for shared_secret in shared_secrets.iter().take(path.len().saturating_sub(1)) {
510 let (rho, _, _, _) = derive_keys(shared_secret);
511 let mut keystream = [0u8; ROUTING_INFO_SIZE + SHIFT_SIZE];
512 apply_stream_cipher(&rho, &[0u8; 12], &mut keystream);
513
514 let filler_start_in_keystream = ROUTING_INFO_SIZE - filler.len();
515 for (j, byte) in filler.iter_mut().enumerate() {
516 *byte ^= keystream[filler_start_in_keystream + j];
517 }
518 filler.extend_from_slice(&keystream[ROUTING_INFO_SIZE..ROUTING_INFO_SIZE + SHIFT_SIZE]);
519 }
520
521 if !filler.is_empty() && !path.is_empty() {
522 let (rho, _, _, _) = derive_keys(&shared_secrets[path.len() - 1]);
523 let mut keystream = [0u8; ROUTING_INFO_SIZE];
524 apply_stream_cipher(&rho, &[0u8; 12], &mut keystream);
525 let filler_start = ROUTING_INFO_SIZE - filler.len();
526 for (j, byte) in filler.iter_mut().enumerate() {
527 *byte ^= keystream[filler_start + j];
528 }
529 }
530
531 let mut routing_info = [0u8; ROUTING_INFO_SIZE];
532 let mut next_mac = [0u8; 32];
533
534 for i in (0..path.len()).rev() {
535 let shared = shared_secrets[i];
536 let (rho, mu, pi, _) = derive_keys(&shared);
537
538 let body_keys = lioness::LionessKeys::from_pi(&pi);
539 lioness::lioness_encrypt(&body_keys, &mut current_body);
540
541 let mut current_routing = [0u8; ROUTING_INFO_SIZE];
542 if i == path.len() - 1 {
543 current_routing[0] = 0x01;
544 if !filler.is_empty() {
545 let filler_start = ROUTING_INFO_SIZE - filler.len();
546 current_routing[filler_start..].copy_from_slice(&filler);
547 }
548 } else {
549 current_routing[0] = 0x00;
550 let next_addr_bytes = path[i + 1].address.as_bytes();
551 current_routing[1] = u8::try_from(next_addr_bytes.len()).map_err(|_| {
552 SphinxError::Serialization(format!(
553 "Address too long ({} bytes, max 255)",
554 next_addr_bytes.len()
555 ))
556 })?;
557 current_routing[2..34].copy_from_slice(&next_mac);
558 current_routing[34..34 + next_addr_bytes.len()].copy_from_slice(next_addr_bytes);
559
560 let remainder_len = ROUTING_INFO_SIZE - SHIFT_SIZE;
561 current_routing[SHIFT_SIZE..].copy_from_slice(&routing_info[0..remainder_len]);
562 }
563
564 apply_stream_cipher(&rho, &[0u8; 12], &mut current_routing);
565 let mac = compute_mac(&mu, ¤t_routing);
566
567 routing_info = current_routing;
568 next_mac = mac;
569 }
570
571 let mut final_header = SphinxHeader {
572 ephemeral_key: ephemeral_public_keys[0],
573 routing_info,
574 mac: next_mac,
575 nonce: 0,
576 };
577
578 final_header.solve_pow(pow_difficulty)?;
579 Ok(final_header.to_bytes(¤t_body))
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn test_multi_hop_integrity() {
589 let mut rng = rand::thread_rng();
590
591 let sks: Vec<X25519SecretKey> = (0..3)
592 .map(|_| X25519SecretKey::random_from_rng(&mut rng))
593 .collect();
594 let pks: Vec<X25519PublicKey> = sks.iter().map(X25519PublicKey::from).collect();
595
596 let path = vec![
597 PathHop {
598 public_key: pks[0],
599 address: "node_0_addr".into(),
600 },
601 PathHop {
602 public_key: pks[1],
603 address: "node_1_addr".into(),
604 },
605 PathHop {
606 public_key: pks[2],
607 address: "EXIT_ADDR".into(),
608 },
609 ];
610
611 let payload = b"DeepOnion".to_vec();
612 println!("Original Payload: {:?}", payload);
613
614 let packet = build_multi_hop_packet(&path, &payload, 0).expect("Build failed");
615
616 println!("\n>>> Processing Hop 0");
617 let (header0, body0) = SphinxHeader::from_bytes(&packet).unwrap();
618 let res0 = into_result(
619 header0
620 .process(&sks[0], body0.to_vec())
621 .expect("Hop 0 failed"),
622 );
623
624 let (next_hop0, packet0, body0_peeled) = match res0 {
625 ProcessResult::Forward {
626 next_hop,
627 next_packet,
628 processed_body,
629 ..
630 } => (next_hop, next_packet, processed_body),
631 _ => panic!("Hop 0 should be forward"),
632 };
633 assert_eq!(next_hop0, "node_1_addr");
634
635 let bytes1 = packet0.to_bytes(&body0_peeled);
636 let (header1, body1) = SphinxHeader::from_bytes(&bytes1).unwrap();
637
638 println!("\n>>> Processing Hop 1");
639 let res1 = into_result(
640 header1
641 .process(&sks[1], body1.to_vec())
642 .expect("Hop 1 failed"),
643 );
644
645 let (next_hop1, packet1, body1_peeled) = match res1 {
646 ProcessResult::Forward {
647 next_hop,
648 next_packet,
649 processed_body,
650 ..
651 } => (next_hop, next_packet, processed_body),
652 _ => panic!("Hop 1 should be forward"),
653 };
654 assert_eq!(next_hop1, "EXIT_ADDR");
655 let bytes2 = packet1.to_bytes(&body1_peeled);
656 let (header2, body2) = SphinxHeader::from_bytes(&bytes2).unwrap();
657
658 println!("\n>>> Processing Hop 2");
659 let res2 = into_result(
660 header2
661 .process(&sks[2], body2.to_vec())
662 .expect("Hop 2 failed"),
663 );
664
665 match res2 {
666 ProcessResult::Exit {
667 payload: final_payload,
668 } => {
669 let body_capacity = packet::PACKET_SIZE - HEADER_SIZE;
672 assert_eq!(final_payload.len(), body_capacity);
673 assert_eq!(&final_payload[..payload.len()], payload.as_slice(),);
674 assert!(final_payload[payload.len()..].iter().all(|&b| b == 0),);
676 }
677 _ => panic!("Hop 2 should be exit"),
678 }
679 }
680
681 #[test]
683 fn test_lioness_body_size_consistency() {
684 let mut rng = rand::thread_rng();
685 let sks: Vec<X25519SecretKey> = (0..3)
686 .map(|_| X25519SecretKey::random_from_rng(&mut rng))
687 .collect();
688 let pks: Vec<X25519PublicKey> = sks.iter().map(X25519PublicKey::from).collect();
689
690 let path = vec![
691 PathHop {
692 public_key: pks[0],
693 address: "entry".into(),
694 },
695 PathHop {
696 public_key: pks[1],
697 address: "mix".into(),
698 },
699 PathHop {
700 public_key: pks[2],
701 address: "exit".into(),
702 },
703 ];
704
705 let payload = vec![0x42u8; 91];
706 let packet = build_multi_hop_packet(&path, &payload, 0).expect("Build failed");
707
708 assert_eq!(packet.len(), packet::PACKET_SIZE);
709
710 let (h0, b0) = SphinxHeader::from_bytes(&packet).expect("Parse failed");
711 let body_capacity = packet::PACKET_SIZE - HEADER_SIZE;
712 assert_eq!(b0.len(), body_capacity);
713
714 let r0 = into_result(h0.process(&sks[0], b0.to_vec()).expect("Hop 0 failed"));
715 let (p1, b1) = match r0 {
716 ProcessResult::Forward {
717 next_packet,
718 processed_body,
719 ..
720 } => (next_packet, processed_body),
721 _ => panic!("Expected forward at hop 0"),
722 };
723 assert_eq!(b1.len(), body_capacity);
724
725 let bytes1 = p1.to_bytes(&b1);
726 let (h1, b1r) = SphinxHeader::from_bytes(&bytes1).expect("Parse failed");
727 let r1 = into_result(h1.process(&sks[1], b1r.to_vec()).expect("Hop 1 failed"));
728 let (p2, b2) = match r1 {
729 ProcessResult::Forward {
730 next_packet,
731 processed_body,
732 ..
733 } => (next_packet, processed_body),
734 _ => panic!("Expected forward at hop 1"),
735 };
736 assert_eq!(b2.len(), body_capacity);
737
738 let bytes2 = p2.to_bytes(&b2);
739 let (h2, b2r) = SphinxHeader::from_bytes(&bytes2).expect("Parse failed");
740 let r2 = into_result(h2.process(&sks[2], b2r.to_vec()).expect("Hop 2 failed"));
741
742 match r2 {
743 ProcessResult::Exit {
744 payload: exit_payload,
745 } => {
746 assert_eq!(exit_payload.len(), body_capacity);
747 assert_eq!(&exit_payload[..payload.len()], payload.as_slice());
748 assert!(exit_payload[payload.len()..].iter().all(|&b| b == 0));
749 }
750 _ => panic!("Expected exit at hop 2"),
751 }
752 }
753
754 #[test]
755 fn test_sphinx_peel_hop() {
756 let mut rng = rand::thread_rng();
757
758 let node_sk = X25519SecretKey::random_from_rng(&mut rng);
759 let node_pk = X25519PublicKey::from(&node_sk);
760
761 let sender_ephemeral_sk = X25519SecretKey::random_from_rng(&mut rng);
762 let sender_ephemeral_pk = X25519PublicKey::from(&sender_ephemeral_sk);
763
764 let shared_secret = sender_ephemeral_sk.diffie_hellman(&node_pk);
765 let (rho, mu, _pi, _blind) = derive_keys(shared_secret.as_bytes());
766
767 let next_hop = "NEXT";
768 let mut routing_data = [0u8; ROUTING_INFO_SIZE];
769 routing_data[0] = 0x00; routing_data[1] = next_hop.len() as u8;
771 routing_data[2..34].copy_from_slice(&[0u8; 32]); routing_data[34..34 + next_hop.len()].copy_from_slice(next_hop.as_bytes());
773
774 apply_stream_cipher(&rho, &[0u8; 12], &mut routing_data);
775 let mac = compute_mac(&mu, &routing_data);
776
777 let header = SphinxHeader {
778 ephemeral_key: sender_ephemeral_pk,
779 routing_info: routing_data,
780 mac,
781 nonce: 0,
782 };
783
784 let body = vec![0u8; lioness::MIN_BODY_SIZE];
785 let result = into_result(header.process(&node_sk, body).unwrap());
786
787 match result {
788 ProcessResult::Forward { next_hop: h, .. } => {
789 assert_eq!(h, "NEXT");
790 }
791 _ => panic!("Expected Forward result"),
792 }
793 }
794
795 #[test]
796 fn test_integrity_failure() {
797 let mut rng = rand::thread_rng();
798 let node_sk = X25519SecretKey::random_from_rng(&mut rng);
799 let dummy_sk = X25519SecretKey::random_from_rng(&mut rng);
800 let dummy_pk = X25519PublicKey::from(&dummy_sk);
801
802 let header = SphinxHeader {
803 ephemeral_key: dummy_pk,
804 routing_info: [0u8; ROUTING_INFO_SIZE],
805 mac: [0u8; 32],
806 nonce: 0,
807 };
808
809 let result = header.process(&node_sk, vec![]);
810 assert!(matches!(result, Err(SphinxError::MacMismatch)));
811 }
812
813 #[test]
814 fn test_identical_payloads_produce_unique_packets() {
815 use sha2::{Digest, Sha256};
816
817 let mut rng = rand::thread_rng();
818
819 let node_sk = X25519SecretKey::random_from_rng(&mut rng);
820 let node_pk = X25519PublicKey::from(&node_sk);
821
822 let payload = b"PAYLOAD_REPLAY_CHECK".to_vec();
823
824 let packet_1 = build_packet(node_pk, &payload, true, 0).unwrap();
825 let packet_2 = build_packet(node_pk, &payload, true, 0).unwrap();
826
827 assert_ne!(packet_1, packet_2);
828
829 let mut hasher1 = Sha256::new();
830 hasher1.update(&packet_1);
831 let hash1 = hasher1.finalize();
832
833 let mut hasher2 = Sha256::new();
834 hasher2.update(&packet_2);
835 let hash2 = hasher2.finalize();
836
837 assert_ne!(hash1, hash2);
838 }
839
840 #[test]
841 fn test_sphinx_fuzzing_no_panic() {
842 use rand::Rng;
843
844 let mut rng = rand::thread_rng();
845
846 for _ in 0..1000 {
847 let len = rng.gen_range(0..2000);
848 let mut bytes = vec![0u8; len];
849 rng.fill(&mut bytes[..]);
850
851 let result = SphinxHeader::from_bytes(&bytes);
852 if let Err(e) = result {
853 assert!(!format!("{}", e).is_empty());
854 }
855 }
856 }
857
858 #[test]
859 fn test_derive_keys_deterministic() {
860 let ss = [0x42u8; 32];
861 let (rho1, mu1, pi1, blind1) = derive_keys(&ss);
862 let (rho2, mu2, pi2, blind2) = derive_keys(&ss);
863 assert_eq!(rho1, rho2);
864 assert_eq!(mu1, mu2);
865 assert_eq!(pi1, pi2);
866 assert_eq!(blind1, blind2);
867 }
868
869 #[test]
870 fn test_derive_keys_different_shared_secrets() {
871 let ss_a = [0x01u8; 32];
872 let ss_b = [0x02u8; 32];
873 let (rho_a, mu_a, pi_a, _) = derive_keys(&ss_a);
874 let (rho_b, mu_b, pi_b, _) = derive_keys(&ss_b);
875 assert_ne!(rho_a, rho_b);
876 assert_ne!(mu_a, mu_b);
877 assert_ne!(pi_a, pi_b);
878 }
879
880 #[test]
881 fn test_derive_keys_outputs_are_distinct() {
882 let ss = [0x55u8; 32];
883 let (rho, mu, pi, blind) = derive_keys(&ss);
884 let blind_bytes = blind.to_bytes();
885 assert_ne!(rho, mu);
886 assert_ne!(rho, pi);
887 assert_ne!(mu, pi);
888 assert_ne!(rho, blind_bytes);
889 }
890
891 #[test]
892 fn test_compute_mac_known_vector() {
893 let key = [0u8; 32];
894 let data = b"test";
895 let mac = compute_mac(&key, data);
896
897 let mac2 = compute_mac(&key, data);
898 assert_eq!(mac, mac2);
899
900 assert_ne!(mac, [0u8; 32]);
901 assert_eq!(mac.len(), 32);
902
903 let mac_diff = compute_mac(&key, b"different");
904 assert_ne!(mac, mac_diff);
905 }
906
907 #[test]
909 fn test_apply_stream_cipher_identity() {
910 let key = [0xABu8; 32];
911 let iv = [0u8; 12];
912 let original = b"sphinx routing information payload".to_vec();
913
914 let mut data = original.clone();
915 apply_stream_cipher(&key, &iv, &mut data);
916 assert_ne!(data, original);
917
918 apply_stream_cipher(&key, &iv, &mut data);
919 assert_eq!(data, original);
920 }
921
922 #[test]
923 fn test_apply_stream_cipher_different_keys() {
924 let key_a = [0x11u8; 32];
925 let key_b = [0x22u8; 32];
926 let iv = [0u8; 12];
927 let plaintext = b"hello sphinx world".to_vec();
928
929 let mut ct_a = plaintext.clone();
930 apply_stream_cipher(&key_a, &iv, &mut ct_a);
931
932 let mut ct_b = plaintext.clone();
933 apply_stream_cipher(&key_b, &iv, &mut ct_b);
934
935 assert_ne!(ct_a, ct_b);
936 }
937
938 #[test]
939 fn test_replay_tag_uniqueness() {
940 use rand::Rng;
941 let mut rng = rand::thread_rng();
942
943 let mut tags = std::collections::HashSet::new();
944 for _ in 0..20 {
945 let mut routing_info = [0u8; ROUTING_INFO_SIZE];
946 rng.fill(&mut routing_info[..]);
947 let mut mac = [0u8; MAC_SIZE];
948 rng.fill(&mut mac[..]);
949 let nonce: u64 = rng.gen();
950
951 let header = SphinxHeader {
952 ephemeral_key: X25519PublicKey::from(&X25519SecretKey::random_from_rng(&mut rng)),
953 routing_info,
954 mac,
955 nonce,
956 };
957 let tag = header.compute_replay_tag();
958 tags.insert(tag);
959 }
960 assert_eq!(tags.len(), 20);
961 }
962
963 #[test]
965 fn test_sphinx_header_size_constant() {
966 use rand::Rng;
967 let mut rng = rand::thread_rng();
968
969 let mut routing_info = [0u8; ROUTING_INFO_SIZE];
970 rng.fill(&mut routing_info[..]);
971 let mut mac = [0u8; MAC_SIZE];
972 rng.fill(&mut mac[..]);
973
974 let header = SphinxHeader {
975 ephemeral_key: X25519PublicKey::from(&X25519SecretKey::random_from_rng(&mut rng)),
976 routing_info,
977 mac,
978 nonce: 12345,
979 };
980
981 let serialized = header.to_bytes(&[]);
982 assert_eq!(
983 serialized.len(),
984 HEADER_SIZE,
985 "to_bytes(&[]) produced {} bytes, expected HEADER_SIZE={HEADER_SIZE}",
986 serialized.len()
987 );
988
989 assert_eq!(HEADER_SIZE, 32 + ROUTING_INFO_SIZE + MAC_SIZE + NONCE_SIZE);
990 }
991}