Files
nym/nym-outfox/src/format.rs
T
Jędrzej Stuczyński 6e5d0dac1b feature: allow nym-nodes to understand future version of sphinx packets (#5496)
* use updated sphinx crate

* updated outfox usage of keygen in tests

* use x25519 in outfox

* remove redundant constructor

* adjusted key convertion traits
2025-02-21 11:06:07 +00:00

323 lines
13 KiB
Rust

//! # The `outfox` format
//!
//! We define a simple mix packet format geared towards simplicity and performance with the features that are
//! specifically required by a stratified mix topology.
//!
//! `Outfox` assumes that all
//! paths are the same length (no need to hide path lengths), mixes are arranged in layers and therefore
//! know their position in a message path (no need to hide this position). These assumptions allow us
//! to do away with some of the padding traditionally used; further we prioritize efficient computation
//! over very low-bandwidth, as it seems the rate of public key operations is a bottleneck for mixes
//! rather than the availablility of bandwidth.
//!
//! ## Overview and Parameters
//!
//! In a mix network with a stratified topology packets are mixed by nodes at each of the layers. Each layer
//! 'strips' the packet from one layer of encryption, recovers the address of the mix at the next layer, and
//! passes the decoded packet to them. An identifier per processed message is stored and checked to prevent
//! replays of processed messages at each layer. Additional measures, such as adding delays, adding dummy
//! traffic or dropping messages can be empued at each mix to frustrate traffic analysis.
//!
//!
//! A layer of mix processing is defined by three parameters, included in the structure [MixStageParameters]:
//! * The `routing_information_length_bytes` (`R`) states the number of bytes representing
//! routing information at this layer.
//! * The `remaining_header_length_bytes` (`H`) represents the remaining bytes of the packet header.
//! * The `payload_length_bytes` (`P`).
//!
//! In addition we define two system-wide constants, namely `GROUPELEMENTBYTES` (`GE`=32) and
//! `TAGBYTES` (`T`=24).
//!
//! ## Packet format, decoding
//!
//! A mix at this layer takes in messages of length `GE+T+R+H+P`, and outputs messages of length `H+P`.
//!
//! An input message is processed as follows:
//!
//! * The input packet is parsed as a `[Pk, Tag, Header, Payload]` of length `[GE, T, R+H, P]` respectivelly.
//! * A master key is derived by performing scalar multiplication with the mix secret 's', ie `K = s * Pk`.
//! The master key is stored and checked for duplicates (if it is found processing ends.)
//! * The master key is used to perform AEAD decryption of the `Header` with an IV of zeros and the `tag`. If
//! decryption fails processing ends. Otherwise the Header is parsed as `[Routing, Next_Header]` of length
//! `[R, H]` respectivelly. The routing data `Routing` can be used by the mix to dertermine the next mix.
//! * Finally, the master key is used to perform lion decoding of the `Payload` into `Next_Payload`.
//! * The output packet for the next mix is `[Next_Header, Next_Payload]`.
//!
//! As an AEAD we use `chacha20poly1305_ietf` and for public key operations we use `curve25519`.
//!
//! ## Packet encoding
//!
//! Encoding is
//! performed layer by layer starting with the last hop on the route, and ending with the first. At each stage
//! of encoding a new Secret key `Sk` and corresponding `Pk` is chosen. The layer master key for the layer is
//! derived using the mix public key. And the master key is used to AEAD encrypt the concatenation of the
//! routing data for the layer, and the remaining Header; separately the master key is used to lion encrypt
//! the payload. The process is repeated for each layer (from last to first) to construct the full message.
use crate::constants::groupelementbytes;
use crate::constants::tagbytes;
use crate::constants::DEFAULT_HOPS;
use crate::constants::DEFAULT_ROUTING_INFO_SIZE;
use crate::constants::GROUPELEMENTBYTES;
use crate::constants::MIX_PARAMS_LEN;
use crate::constants::ROUTING_INFORMATION_LENGTH_BY_STAGE;
use crate::constants::TAGBYTES;
use crate::error::OutfoxError;
use crate::lion::*;
use chacha20poly1305::AeadInPlace;
use chacha20poly1305::ChaCha20Poly1305;
use chacha20poly1305::KeyInit;
use chacha20poly1305::Tag;
use std::ops::Range;
/// A structure that holds mix packet construction parameters. These incluse the length
/// of the routing information at each hop, the number of hops, and the payload length.
#[derive(Eq, PartialEq, Debug)]
pub struct MixCreationParameters {
/// The routing length is inner first, so \[0\] is the innermost routing length, etc (in bytes)
/// In our stratified topology this will always be 4
pub routing_information_length_by_stage: [u8; DEFAULT_HOPS],
/// The payload length (in bytes)
pub payload_length_bytes: u16,
}
impl TryFrom<&[u8]> for MixCreationParameters {
type Error = OutfoxError;
fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
if v.len() != MIX_PARAMS_LEN {
return Err(OutfoxError::InvalidHeaderLength(v.len()));
}
let (routing, payload) = v.split_at(DEFAULT_HOPS);
Ok(MixCreationParameters {
routing_information_length_by_stage: routing.try_into()?,
payload_length_bytes: u16::from_le_bytes(payload.try_into()?),
})
}
}
impl MixCreationParameters {
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(5);
bytes.extend_from_slice(self.routing_information_length_by_stage.as_slice());
bytes.extend_from_slice(&self.payload_length_bytes.to_le_bytes());
bytes
}
pub fn payload_length_bytes(&self) -> usize {
self.payload_length_bytes as usize
}
/// Create a set of parameters for a mix packet format.
pub fn new(payload_length_bytes: u16) -> MixCreationParameters {
MixCreationParameters {
routing_information_length_by_stage: [DEFAULT_ROUTING_INFO_SIZE; DEFAULT_HOPS],
payload_length_bytes,
}
}
/// The length of the buffer needed to build a packet.
pub fn total_packet_length(&self) -> usize {
let mut len = self.payload_length_bytes();
for stage_len in ROUTING_INFORMATION_LENGTH_BY_STAGE.iter() {
len += *stage_len as usize + groupelementbytes() + tagbytes()
}
len
}
/// Get the mix packet parameters for a single stage of mixing.
pub fn get_stage_params(&self, layer_number: usize) -> (Range<usize>, MixStageParameters) {
assert!(layer_number < ROUTING_INFORMATION_LENGTH_BY_STAGE.len());
let mut remaining_header_length_bytes = 0;
for (i, stage_len) in ROUTING_INFORMATION_LENGTH_BY_STAGE.iter().enumerate() {
if i == layer_number {
let params = MixStageParameters {
routing_information_length_bytes: *stage_len,
remaining_header_length_bytes,
payload_length_bytes: self.payload_length_bytes,
};
let total_size = self.total_packet_length();
let inner_size = params.incoming_packet_length();
return (total_size - inner_size..total_size, params);
} else {
remaining_header_length_bytes += (stage_len + GROUPELEMENTBYTES + TAGBYTES) as u16;
}
}
unreachable!();
}
}
/// A structure representing the parameters of a single stage of mixing.
pub struct MixStageParameters {
/// The routing information length for this stage of mixing
pub routing_information_length_bytes: u8,
/// The reamining header length for this stage of mixing
pub remaining_header_length_bytes: u16,
/// The payload length
pub payload_length_bytes: u16,
}
impl MixStageParameters {
pub fn routing_information_length_bytes(&self) -> usize {
self.routing_information_length_bytes as usize
}
pub fn remaining_header_length_bytes(&self) -> usize {
self.remaining_header_length_bytes as usize
}
pub fn payload_length_bytes(&self) -> usize {
self.payload_length_bytes as usize
}
pub fn incoming_packet_length(&self) -> usize {
groupelementbytes() + tagbytes() + self.outgoing_packet_length()
}
pub fn outgoing_packet_length(&self) -> usize {
self.routing_information_length_bytes()
+ self.remaining_header_length_bytes()
+ self.payload_length_bytes()
}
pub fn pub_element_range(&self) -> Range<usize> {
0..groupelementbytes()
}
pub fn tag_range(&self) -> Range<usize> {
groupelementbytes()..groupelementbytes() + tagbytes()
}
pub fn routing_data_range(&self) -> Range<usize> {
groupelementbytes() + tagbytes()
..groupelementbytes() + tagbytes() + self.routing_information_length_bytes()
}
pub fn header_range(&self) -> Range<usize> {
groupelementbytes() + tagbytes()
..groupelementbytes()
+ tagbytes()
+ self.routing_information_length_bytes()
+ self.remaining_header_length_bytes()
}
pub fn payload_range(&self) -> Range<usize> {
self.incoming_packet_length() - self.payload_length_bytes()..self.incoming_packet_length()
}
pub fn encode_mix_layer(
&self,
buffer: &mut [u8],
user_secret_key: &x25519_dalek::StaticSecret,
mix_public_key: x25519_dalek::PublicKey,
destination: &[u8; 32],
) -> Result<x25519_dalek::SharedSecret, OutfoxError> {
let routing_data = destination;
if buffer.len() != self.incoming_packet_length() {
return Err(OutfoxError::LenMismatch {
expected: buffer.len(),
got: self.incoming_packet_length(),
});
}
if routing_data.len() != self.routing_information_length_bytes() {
return Err(OutfoxError::LenMismatch {
expected: routing_data.len(),
got: self.routing_information_length_bytes(),
});
}
let user_public_key = x25519_dalek::PublicKey::from(user_secret_key);
let shared_key = user_secret_key.diffie_hellman(&mix_public_key);
// Copy rounting data into buffer
buffer[self.routing_data_range()].copy_from_slice(routing_data);
// Perform the AEAD
let header_aead_key = ChaCha20Poly1305::new_from_slice(shared_key.as_bytes())?;
let nonce = [0u8; 12];
let tag = header_aead_key
.encrypt_in_place_detached(&nonce.into(), &[], &mut buffer[self.header_range()])
.map_err(|e| OutfoxError::ChaCha20Poly1305Error(e.to_string()))?;
// Copy Tag into buffer
buffer[self.tag_range()].copy_from_slice(&tag[..]);
// Copy own public key into buffer
buffer[self.pub_element_range()].copy_from_slice(user_public_key.as_bytes());
// Do a round of LION on the payload
lion_transform_encrypt(&mut buffer[self.payload_range()], shared_key.as_bytes())?;
Ok(shared_key)
}
pub fn decode_mix_layer(
&self,
buffer: &mut [u8],
mix_secret_key: &x25519_dalek::StaticSecret,
) -> Result<Vec<u8>, OutfoxError> {
// Check the length of the incoming buffer is correct.
if buffer.len() != self.incoming_packet_length() {
return Err(OutfoxError::LenMismatch {
expected: buffer.len(),
got: self.incoming_packet_length(),
});
}
// Derive the shared key for this packet
let user_public_key_bytes: [u8; 32] = buffer[self.pub_element_range()].try_into()?;
let user_public_key = x25519_dalek::PublicKey::from(user_public_key_bytes);
let shared_key = mix_secret_key.diffie_hellman(&user_public_key);
// Compute the AEAD and check the Tag, if wrong return Err
let header_aead_key = ChaCha20Poly1305::new_from_slice(shared_key.as_bytes())?;
let nonce = [0; 12];
let tag_bytes = buffer[self.tag_range()].to_vec();
let tag = Tag::from_slice(&tag_bytes);
header_aead_key
.decrypt_in_place_detached(
&nonce.into(),
&[],
&mut buffer[self.header_range()],
tag.as_slice().into(),
)
.map_err(|e| OutfoxError::ChaCha20Poly1305Error(e.to_string()))?;
let routing_data = buffer[self.routing_data_range()].to_vec();
// Do a round of LION on the payload
lion_transform_decrypt(&mut buffer[self.payload_range()], shared_key.as_bytes())?;
Ok(routing_data)
}
}
#[cfg(test)]
mod test {
use super::MixCreationParameters;
#[test]
fn test_to_bytes() {
let mix_params = MixCreationParameters::new(1024);
assert_eq!(mix_params.to_bytes(), vec![32, 32, 32, 32, 0, 4])
}
#[test]
fn test_from_bytes() {
let params_bytes = vec![32, 32, 32, 32, 0, 4];
let mix_params = MixCreationParameters::new(1024);
assert_eq!(
mix_params,
MixCreationParameters::try_from(params_bytes.as_slice()).unwrap()
)
}
}