Files
nym/common/nymsphinx/cover/src/lib.rs
T
Jędrzej Stuczyński d8c84cc4d6 feat: key rotation (#5777)
* wip

* wip: wrap node's sphinx key with a manager

* wip: choosing correct key for packet processing

* further propagation of key rotation information

* attaching key rotation information to reply surbs

* added basic key rotation information to mixnet contract

* wip: introducing cached queries for key rotation info from nym api

* unified nym-api contract cache refreshing

* finish packet decoding

* multi api client + retrieving rotation id

* rotating sphinx key files

* logic for migrating config file

* wip: putting new sphinx keys to self described endpoints

* processing loop of KeyRotationController

* fixed sphinx key loading

* rotating bloomfilters

* wired up KeyRotationController

* flushing bloomfilters to disk and loading

* most of nym-node changes

* post rebase fixes

* fixes due to backwards compatible hostkeys

* split http state.rs file

* dont use deprecated fields

* fixed backwards compatible deserialisation of host information

* split up node describe cache

* added a dedicated CacheRefresher listener to perform full refresh outside the set interval

* controlling announced sphinx keys within nym-api

* retrieving rotation id when pulling topology

* split nym-nodes http handlers

* v2 nym-api endpoints to retrieve nodes with additional metadata information

* bug fixes...

* additional bugfixes and guards against stuck epoch

* testnet manager: set first nym-api as the rewarder

* fixed host information deserialisation

* fixed panic during first key rotation

* post rebase fixes

* clippy

* more guards against stuck epochs

* added helper method to reset node's sphinx key

* instantiate mixnet contract with custom key rotation validity

* additional bugfixes and debugging nym-api deadlock

* passing shutdown to nym apis client

* remove dead test

* post rebasing fixes

* missing MixnetQueryClient variants

* remove usage of deprecated methods in sdk example

* fix: incorrect method signature

* post rebasing fixes

* attempt to retrieve key rotation id before doing any config migration work

* ignore tests relying on networking behaviour

* allow networking failures in certain tests
2025-06-03 11:22:51 +01:00

223 lines
6.6 KiB
Rust

// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::shared_key::new_ephemeral_shared_key;
use nym_crypto::symmetric::stream_cipher;
use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError};
use nym_sphinx_acknowledgements::AckKey;
use nym_sphinx_addressing::clients::Recipient;
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx_chunking::fragment::COVER_FRAG_ID;
use nym_sphinx_forwarding::packet::MixPacket;
use nym_sphinx_params::packet_sizes::PacketSize;
use nym_sphinx_params::{
PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType, SphinxKeyRotation,
};
use nym_sphinx_types::NymPacket;
use nym_topology::{NymRouteProvider, NymTopologyError};
use rand::{CryptoRng, RngCore};
use std::time;
use thiserror::Error;
pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!";
#[derive(Debug, Error)]
pub enum CoverMessageError {
#[error("Could not construct cover message due to invalid topology - {0}")]
InvalidTopologyError(#[from] NymTopologyError),
#[error("SurbAck: {0}")]
SurbAck(#[from] SurbAckRecoveryError),
#[error("NymPacket: {0}")]
NymPacket(#[from] nym_sphinx_types::NymPacketError),
}
pub fn generate_loop_cover_surb_ack<R>(
rng: &mut R,
use_legacy_sphinx_format: bool,
topology: &NymRouteProvider,
ack_key: &AckKey,
full_address: &Recipient,
average_ack_delay: time::Duration,
packet_type: PacketType,
) -> Result<SurbAck, CoverMessageError>
where
R: RngCore + CryptoRng,
{
Ok(SurbAck::construct(
rng,
use_legacy_sphinx_format,
full_address,
ack_key,
COVER_FRAG_ID.to_bytes(),
average_ack_delay,
topology,
packet_type,
false, // make sure mix hops are enabled
)?)
}
#[allow(clippy::too_many_arguments)]
pub fn generate_loop_cover_packet<R>(
rng: &mut R,
use_legacy_sphinx_format: bool,
topology: &NymRouteProvider,
ack_key: &AckKey,
full_address: &Recipient,
average_ack_delay: time::Duration,
average_packet_delay: time::Duration,
packet_size: PacketSize,
packet_type: PacketType,
) -> Result<MixPacket, CoverMessageError>
where
R: RngCore + CryptoRng,
{
// we don't care about total ack delay - we will not be retransmitting it anyway
let (_, ack_bytes) = generate_loop_cover_surb_ack(
rng,
use_legacy_sphinx_format,
topology,
ack_key,
full_address,
average_ack_delay,
packet_type,
)?
.prepare_for_sending()?;
// cover message can't be distinguishable from a normal traffic so we have to go through
// all the effort of key generation, encryption, etc. Note here we are generating shared key
// with ourselves!
let (ephemeral_keypair, shared_key) = new_ephemeral_shared_key::<
PacketEncryptionAlgorithm,
PacketHkdfAlgorithm,
_,
>(rng, full_address.encryption_key());
let public_key_bytes = ephemeral_keypair.public_key().to_bytes();
let cover_size = packet_size.plaintext_size() - public_key_bytes.len() - ack_bytes.len();
let mut cover_content: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD
.iter()
.cloned()
.chain(std::iter::once(1))
.chain(std::iter::repeat(0))
.take(cover_size)
.collect();
let zero_iv = stream_cipher::zero_iv::<PacketEncryptionAlgorithm>();
stream_cipher::encrypt_in_place::<PacketEncryptionAlgorithm>(
&shared_key,
&zero_iv,
&mut cover_content,
);
// combine it together as follows:
// SURB_ACK_FIRST_HOP || SURB_ACK_DATA || EPHEMERAL_KEY || COVER_CONTENT
// (note: surb_ack_bytes contains SURB_ACK_FIRST_HOP || SURB_ACK_DATA )
let packet_payload: Vec<_> = ack_bytes
.into_iter()
.chain(ephemeral_keypair.public_key().to_bytes().iter().cloned())
.chain(cover_content)
.collect();
let route = topology.random_route_to_egress(rng, full_address.gateway())?;
let delays = nym_sphinx_routing::generate_hop_delays(average_packet_delay, route.len());
let destination = full_address.as_sphinx_destination();
let rotation_id = topology.current_key_rotation();
let sphinx_key_rotation = SphinxKeyRotation::from(rotation_id);
let first_hop_address =
NymNodeRoutingAddress::try_from(route.first().unwrap().address).unwrap();
// once merged, that's an easy rng injection point for sphinx packets : )
let packet = match packet_type {
PacketType::Mix => NymPacket::sphinx_build(
use_legacy_sphinx_format,
packet_size.payload_size(),
packet_payload,
&route,
&destination,
&delays,
)?,
PacketType::Outfox => NymPacket::outfox_build(
packet_payload,
&route,
&destination,
Some(packet_size.plaintext_size()),
)?,
};
Ok(MixPacket::new(
first_hop_address,
packet,
packet_type,
sphinx_key_rotation,
))
}
/// Helper function used to determine if given message represents a loop cover message.
// It kinda seems like there must exist "prefix" or "starts_with" method for bytes
// or something, but I couldn't find anything
pub fn is_cover(data: &[u8]) -> bool {
if data.len() < LOOP_COVER_MESSAGE_PAYLOAD.len() {
return false;
}
for i in 0..LOOP_COVER_MESSAGE_PAYLOAD.len() {
if data[i] != LOOP_COVER_MESSAGE_PAYLOAD[i] {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::iter::repeat_n;
#[test]
fn is_cover_works_for_identical_input() {
assert!(is_cover(LOOP_COVER_MESSAGE_PAYLOAD))
}
#[test]
fn is_cover_works_for_longer_input() {
let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD
.iter()
.cloned()
.chain(repeat_n(42, 100))
.collect();
assert!(is_cover(&input))
}
#[test]
fn is_cover_returns_false_for_unrelated_input() {
// make sure the length checks out
let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD.iter().map(|_| 42).collect();
assert!(!is_cover(&input))
}
#[test]
fn is_cover_returns_false_for_part_of_correct_input() {
let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD
.iter()
.cloned()
.take(LOOP_COVER_MESSAGE_PAYLOAD.len() - 1)
.chain(std::iter::once(42))
.collect();
assert!(!is_cover(&input))
}
#[test]
fn is_cover_returns_false_for_empty_input() {
let empty = Vec::new();
assert!(!is_cover(&empty))
}
}