hopr_crypto_sphinx/
routing.rs

1use hopr_crypto_random::random_fill;
2use hopr_crypto_types::prelude::*;
3use hopr_primitive_types::prelude::*;
4
5use crate::derivation::derive_mac_key;
6use crate::prg::{PRGParameters, PRG};
7use crate::routing::ForwardedHeader::{FinalNode, RelayNode};
8use crate::shared_keys::{SharedSecret, SphinxSuite};
9
10const RELAYER_END_PREFIX: u8 = 0xff;
11
12const PATH_POSITION_LEN: usize = 1;
13
14/// Length of the header routing information per hop
15const fn routing_information_length<S: SphinxSuite>(additional_data_relayer_len: usize) -> usize {
16    <S::P as Keypair>::Public::SIZE + PATH_POSITION_LEN + SimpleMac::SIZE + additional_data_relayer_len
17}
18
19/// Returns the size of the packet header given the information about the number of hops and additional relayer info.
20pub const fn header_length<S: SphinxSuite>(
21    max_hops: usize,
22    additional_data_relayer_len: usize,
23    additional_data_last_hop_len: usize,
24) -> usize {
25    let last_hop = additional_data_last_hop_len + 1; // 1 = end prefix length
26    last_hop + (max_hops - 1) * routing_information_length::<S>(additional_data_relayer_len)
27}
28
29fn generate_filler(
30    max_hops: usize,
31    routing_info_len: usize,
32    routing_info_last_hop_len: usize,
33    secrets: &[SharedSecret],
34) -> Box<[u8]> {
35    if secrets.len() < 2 {
36        return vec![].into_boxed_slice();
37    }
38
39    assert!(max_hops >= secrets.len(), "too few hops");
40    assert!(routing_info_len > 0, "invalid routing info length");
41
42    let header_len = routing_info_last_hop_len + (max_hops - 1) * routing_info_len;
43    let padding_len = (max_hops - secrets.len()) * routing_info_len;
44
45    let mut ret = vec![0u8; header_len - padding_len - routing_info_last_hop_len];
46
47    let mut length = routing_info_len;
48    let mut start = header_len;
49
50    for secret in secrets.iter().take(secrets.len() - 1) {
51        let prg = PRG::from_parameters(PRGParameters::new(secret));
52
53        let digest = prg.digest(start, header_len + routing_info_len);
54        xor_inplace(&mut ret[0..length], digest.as_ref());
55
56        length += routing_info_len;
57        start -= routing_info_len;
58    }
59
60    ret.into_boxed_slice()
61}
62
63/// Carries routing information for the mixnet packet.
64pub struct RoutingInfo {
65    pub routing_information: Box<[u8]>,
66    pub mac: [u8; SimpleMac::SIZE],
67}
68
69impl Default for RoutingInfo {
70    fn default() -> Self {
71        Self {
72            routing_information: Box::default(),
73            mac: [0u8; SimpleMac::SIZE],
74        }
75    }
76}
77
78impl RoutingInfo {
79    /// Creates the routing information of the mixnet packet
80    /// # Arguments
81    /// * `max_hops` maximal number of hops
82    /// * `path` IDs of the nodes along the path
83    /// * `secrets` shared secrets with the nodes along the path
84    /// * `additional_data_relayer_len` length of each additional data for all relayers
85    /// * `additional_data_relayer` additional data for each relayer
86    /// * `additional_data_last_hop` additional data for the final recipient
87    pub fn new<S: SphinxSuite>(
88        max_hops: usize,
89        path: &[<S::P as Keypair>::Public],
90        secrets: &[SharedSecret],
91        additional_data_relayer_len: usize,
92        additional_data_relayer: &[&[u8]],
93        additional_data_last_hop: Option<&[u8]>,
94    ) -> Self {
95        assert!(
96            secrets.len() <= max_hops && !secrets.is_empty(),
97            "invalid number of secrets given"
98        );
99        assert!(
100            additional_data_relayer
101                .iter()
102                .all(|r| r.len() == additional_data_relayer_len),
103            "invalid relayer data length"
104        );
105        assert!(
106            additional_data_last_hop.is_none() || !additional_data_last_hop.unwrap().is_empty(),
107            "invalid additional data for last hop"
108        );
109
110        let routing_info_len = routing_information_length::<S>(additional_data_relayer_len);
111        let last_hop_len = additional_data_last_hop.map(|d| d.len()).unwrap_or(0) + 1; // end prefix length
112        let header_len = header_length::<S>(max_hops, additional_data_relayer_len, last_hop_len - 1);
113
114        let extended_header_len = last_hop_len + max_hops * routing_info_len;
115
116        let mut extended_header = vec![0u8; extended_header_len];
117        let mut ret = RoutingInfo::default();
118
119        for idx in 0..secrets.len() {
120            let inverted_idx = secrets.len() - idx - 1;
121            let prg = PRG::from_parameters(PRGParameters::new(&secrets[inverted_idx]));
122
123            if idx == 0 {
124                extended_header[0] = RELAYER_END_PREFIX;
125
126                if let Some(data) = additional_data_last_hop {
127                    extended_header[1..data.len()].copy_from_slice(data);
128                }
129
130                let padding_len = (max_hops - secrets.len()) * routing_info_len;
131                if padding_len > 0 {
132                    random_fill(&mut extended_header[last_hop_len..padding_len]);
133                }
134
135                let key_stream = prg.digest(0, last_hop_len + padding_len);
136                xor_inplace(&mut extended_header[0..last_hop_len + padding_len], &key_stream);
137
138                if secrets.len() > 1 {
139                    let filler = generate_filler(max_hops, routing_info_len, last_hop_len, secrets);
140                    extended_header[last_hop_len + padding_len..last_hop_len + padding_len + filler.len()]
141                        .copy_from_slice(&filler);
142                }
143            } else {
144                extended_header.copy_within(0..header_len, routing_info_len);
145
146                let pub_key_size = <S::P as Keypair>::Public::SIZE;
147
148                // Path position must come first,to ensure prefix RELAYER_END_PREFIX prefix safety
149                // of Ed25519 public keys.
150                extended_header[0] = idx as u8;
151                extended_header[PATH_POSITION_LEN..PATH_POSITION_LEN + pub_key_size]
152                    .copy_from_slice(path[inverted_idx + 1].as_ref());
153                extended_header[PATH_POSITION_LEN + pub_key_size..PATH_POSITION_LEN + pub_key_size + SimpleMac::SIZE]
154                    .copy_from_slice(&ret.mac);
155                extended_header[PATH_POSITION_LEN + pub_key_size + SimpleMac::SIZE
156                    ..PATH_POSITION_LEN + pub_key_size + SimpleMac::SIZE + additional_data_relayer[inverted_idx].len()]
157                    .copy_from_slice(additional_data_relayer[inverted_idx]);
158
159                let key_stream = prg.digest(0, header_len);
160                xor_inplace(&mut extended_header, &key_stream);
161            }
162
163            let mut m = SimpleMac::new(&derive_mac_key(&secrets[inverted_idx]));
164            m.update(&extended_header[0..header_len]);
165            m.finalize_into(&mut ret.mac);
166        }
167
168        ret.routing_information = (&extended_header[0..header_len]).into();
169        ret
170    }
171}
172
173/// Enum carry information about the packet based on whether it is destined for the current node (`FinalNode`)
174/// or if the packet is supposed to be only relayed (`RelayNode`).
175pub enum ForwardedHeader {
176    /// Packet is supposed to be relayed
177    RelayNode {
178        /// Transformed header
179        header: Box<[u8]>,
180        /// Authentication tag
181        mac: [u8; SimpleMac::SIZE],
182        /// Position of the relay in the path
183        path_pos: u8,
184        /// Public key of the next node
185        next_node: Box<[u8]>,
186        /// Additional data for the relayer
187        additional_info: Box<[u8]>,
188    },
189
190    /// Packet is at its final destination
191    FinalNode {
192        /// Additional data for the final destination
193        additional_data: Box<[u8]>,
194    },
195}
196
197/// Applies the forward transformation to the header.
198/// If the packet is destined for this node, returns the additional data
199/// for the final destination (`FinalNode`), otherwise it returns the transformed header, the
200/// next authentication tag, the public key of the next node, and the additional data
201/// for the relayer (`RelayNode`).
202/// # Arguments
203/// * `secret` shared secret with the creator of the packet
204/// * `header` u8a containing the header
205/// * `mac` current mac
206/// * `max_hops` maximal number of hops
207/// * `additional_data_relayer_len` length of the additional data for each relayer
208/// * `additional_data_last_hop_len` length of the additional data for the final destination
209pub fn forward_header<S: SphinxSuite>(
210    secret: &SecretKey,
211    header: &mut [u8],
212    mac: &[u8],
213    max_hops: usize,
214    additional_data_relayer_len: usize,
215    additional_data_last_hop_len: usize,
216) -> hopr_crypto_types::errors::Result<ForwardedHeader> {
217    assert_eq!(SimpleMac::SIZE, mac.len(), "invalid mac length");
218
219    let routing_info_len = routing_information_length::<S>(additional_data_relayer_len);
220    let last_hop_len = additional_data_last_hop_len + 1; // end prefix
221    let header_len = header_length::<S>(max_hops, additional_data_relayer_len, last_hop_len - 1);
222
223    assert_eq!(header_len, header.len(), "invalid pre-header length");
224
225    let mut computed_mac = SimpleMac::new(&derive_mac_key(secret));
226    computed_mac.update(header);
227    if !mac.eq(computed_mac.finalize().as_slice()) {
228        return Err(CryptoError::TagMismatch);
229    }
230
231    // Unmask the header using the keystream
232    let prg = PRG::from_parameters(PRGParameters::new(secret));
233    let key_stream = prg.digest(0, header_len);
234    xor_inplace(header, &key_stream);
235
236    if header[0] != RELAYER_END_PREFIX {
237        // Path position
238        let path_pos: u8 = header[0];
239
240        let pub_key_size = <S::P as Keypair>::Public::SIZE;
241
242        // Try to deserialize the public key to validate it
243        let next_node: Box<[u8]> = (&header[PATH_POSITION_LEN..PATH_POSITION_LEN + pub_key_size]).into();
244
245        // Authentication tag
246        let mac: [u8; SimpleMac::SIZE] = (&header
247            [PATH_POSITION_LEN + pub_key_size..PATH_POSITION_LEN + pub_key_size + SimpleMac::SIZE])
248            .try_into()
249            .unwrap();
250
251        let additional_info: Box<[u8]> = (&header[PATH_POSITION_LEN + pub_key_size + SimpleMac::SIZE
252            ..PATH_POSITION_LEN + pub_key_size + SimpleMac::SIZE + additional_data_relayer_len])
253            .into();
254
255        header.copy_within(routing_info_len.., 0);
256        let key_stream = prg.digest(header_len, header_len + routing_info_len);
257        header[header_len - routing_info_len..].copy_from_slice(&key_stream);
258
259        Ok(RelayNode {
260            header: (&header[..header_len]).into(),
261            mac,
262            path_pos,
263            next_node,
264            additional_info,
265        })
266    } else {
267        Ok(FinalNode {
268            additional_data: (&header[1..1 + additional_data_last_hop_len]).into(),
269        })
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use hopr_crypto_types::keypairs::OffchainKeypair;
277    use parameterized::parameterized;
278
279    #[parameterized(hops = { 3, 4 })]
280    fn test_filler_generate_verify(hops: usize) {
281        let per_hop = 3;
282        let last_hop = 5;
283        let max_hops = hops;
284
285        let secrets = (0..hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
286        let extended_header_len = per_hop * max_hops + last_hop;
287        let header_len = per_hop * (max_hops - 1) + last_hop;
288
289        let mut extended_header = vec![0u8; per_hop * max_hops + last_hop];
290
291        let filler = generate_filler(max_hops, per_hop, last_hop, &secrets);
292
293        extended_header[last_hop..last_hop + filler.len()].copy_from_slice(&filler);
294        extended_header.copy_within(0..header_len, per_hop);
295
296        for i in 0..hops - 1 {
297            let idx = secrets.len() - i - 2;
298            let mask = PRG::from_parameters(PRGParameters::new(&secrets[idx])).digest(0, extended_header_len);
299
300            xor_inplace(&mut extended_header, &mask);
301
302            assert!(
303                extended_header[header_len..].iter().all(|x| *x == 0),
304                "xor blinding must erase last bits"
305            );
306
307            extended_header.copy_within(0..header_len, per_hop);
308        }
309    }
310
311    #[test]
312    fn test_filler_edge_case() {
313        let per_hop = 23;
314        let last_hop = 31;
315        let hops = 1;
316        let max_hops = hops;
317
318        let secrets = (0..hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
319
320        let first_filler = generate_filler(max_hops, per_hop, last_hop, &secrets);
321        assert_eq!(0, first_filler.len());
322
323        let second_filler = generate_filler(0, per_hop, last_hop, &[]);
324        assert_eq!(0, second_filler.len());
325    }
326
327    fn generic_test_generate_routing_info_and_forward<S>(keypairs: Vec<S::P>) -> anyhow::Result<()>
328    where
329        S: SphinxSuite,
330    {
331        const MAX_HOPS: usize = 3;
332        let mut additional_data: Vec<&[u8]> = Vec::with_capacity(keypairs.len());
333        for _ in 0..keypairs.len() {
334            let e: &[u8] = &[];
335            additional_data.push(e);
336        }
337
338        let pub_keys = keypairs.iter().map(|kp| kp.public().clone()).collect::<Vec<_>>();
339        let shares = S::new_shared_keys(&pub_keys).unwrap();
340
341        let rinfo = RoutingInfo::new::<S>(MAX_HOPS, &pub_keys, &shares.secrets, 0, &additional_data, None);
342
343        let mut header: Vec<u8> = rinfo.routing_information.into();
344
345        let mut last_mac = [0u8; SimpleMac::SIZE];
346        last_mac.copy_from_slice(&rinfo.mac);
347
348        for (i, secret) in shares.secrets.iter().enumerate() {
349            let fwd = forward_header::<S>(secret, &mut header, &last_mac, MAX_HOPS, 0, 0)?;
350
351            match fwd {
352                ForwardedHeader::RelayNode {
353                    mac,
354                    next_node,
355                    path_pos,
356                    ..
357                } => {
358                    last_mac.copy_from_slice(&mac);
359                    assert!(i < shares.secrets.len() - 1, "cannot be a relay node");
360                    assert_eq!(
361                        path_pos,
362                        (shares.secrets.len() - i - 1) as u8,
363                        "invalid path position {path_pos}"
364                    );
365                    assert_eq!(
366                        pub_keys[i + 1].as_ref(),
367                        next_node.as_ref(),
368                        "invalid public key of the next node"
369                    );
370                }
371                ForwardedHeader::FinalNode { additional_data } => {
372                    assert_eq!(shares.secrets.len() - 1, i, "cannot be a final node");
373                    assert_eq!(0, additional_data.len(), "final node must not have any additional data");
374                }
375            }
376        }
377
378        Ok(())
379    }
380
381    #[cfg(feature = "ed25519")]
382    #[parameterized(amount = { 3, 2, 1 })]
383    fn test_ed25519_generate_routing_info_and_forward(amount: usize) -> anyhow::Result<()> {
384        generic_test_generate_routing_info_and_forward::<crate::ec_groups::Ed25519Suite>(
385            (0..amount).map(|_| OffchainKeypair::random()).collect(),
386        )
387    }
388
389    #[cfg(feature = "x25519")]
390    #[parameterized(amount = { 3, 2, 1 })]
391    fn test_x25519_generate_routing_info_and_forward(amount: usize) -> anyhow::Result<()> {
392        generic_test_generate_routing_info_and_forward::<crate::ec_groups::X25519Suite>(
393            (0..amount).map(|_| OffchainKeypair::random()).collect(),
394        )
395    }
396
397    #[cfg(feature = "secp256k1")]
398    #[parameterized(amount = { 3, 2, 1 })]
399    fn test_secp256k1_generate_routing_info_and_forward(amount: usize) -> anyhow::Result<()> {
400        generic_test_generate_routing_info_and_forward::<crate::ec_groups::Secp256k1Suite>(
401            (0..amount).map(|_| ChainKeypair::random()).collect(),
402        )
403    }
404}