Files
tsunagi/tests/identity.rs
T

176 lines
5.8 KiB
Rust
Raw Normal View History

//! Scenario 1: deterministic network identity.
//!
//! The same name and secret must yield the same network space on different
//! devices, and nothing else — hostname, device key, restart — may change it.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod common;
use common::{TestAgent, config_with, network};
use tsunagi::Agent;
use tsunagi::discovery::SharedMemoryDiscovery;
use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret};
#[test]
fn derivation_is_a_pure_function_of_name_and_secret() {
let name = NetworkName::new("home").unwrap();
let other = NetworkName::new("Home").unwrap();
let secret = NetworkSecret::generate();
let other_secret = NetworkSecret::generate();
let a = NetworkKeys::derive(&name, &secret);
let b = NetworkKeys::derive(&name, &secret);
assert_eq!(a.network_id(), b.network_id());
assert_eq!(a.discovery_key(), b.discovery_key());
assert_eq!(a.descriptor(), b.descriptor());
// A different name is a different space. Names are used verbatim, so case
// matters.
assert_ne!(
a.network_id(),
NetworkKeys::derive(&other, &secret).network_id()
);
// A different secret is a different space.
assert_ne!(
a.network_id(),
NetworkKeys::derive(&name, &other_secret).network_id()
);
// Separated key material: the discovery key is not the network id.
assert_ne!(a.network_id().as_bytes(), a.discovery_key().as_bytes());
}
#[test]
fn descriptor_carries_no_creator_time_or_secret() {
let name = NetworkName::new("shared").unwrap();
let secret = NetworkSecret::generate();
let first = NetworkKeys::derive(&name, &secret).descriptor();
let second = NetworkKeys::derive(&name, &secret).descriptor();
// Two independently built descriptors are byte identical: no random
// creator id, no creation timestamp, no owner signature.
assert_eq!(first.to_canonical_bytes(), second.to_canonical_bytes());
let encoded = first.to_canonical_bytes();
let secret_bytes = secret.encode();
assert!(
!encoded
.windows(secret_bytes.len())
.any(|window| window == secret_bytes.as_bytes()),
"the secret must never appear in the public descriptor"
);
}
#[test]
fn names_are_validated_not_silently_normalised() {
assert!(NetworkName::new("").is_err());
assert!(NetworkName::new(" home").is_err(), "must not be trimmed");
assert!(NetworkName::new("home ").is_err(), "must not be trimmed");
assert!(NetworkName::new("ho\nme").is_err());
assert!(NetworkName::new("a".repeat(65)).is_err());
assert_eq!(NetworkName::new("home").unwrap().as_str(), "home");
}
#[test]
fn secrets_are_not_truncated_or_normalised() {
let secret = NetworkSecret::generate();
let text = secret.encode();
let round_tripped = NetworkSecret::decode(&text).unwrap();
assert_eq!(secret, round_tripped);
// Short secrets are rejected rather than stretched.
assert!(NetworkSecret::from_bytes(vec![7u8; 15]).is_err());
assert!(NetworkSecret::from_bytes(vec![7u8; 16]).is_ok());
// Debug output must not leak the secret.
let rendered = format!("{secret:?}");
assert_eq!(rendered, "NetworkSecret(<redacted>)");
}
#[tokio::test]
async fn different_devices_and_hostnames_agree_on_the_network_id() {
let discovery = SharedMemoryDiscovery::new();
let (name, secret) = network("agreement");
let a = TestAgent::spawn_with(|cfg| cfg.with_hostname("alpha"), &discovery)
.await
.unwrap();
let b = TestAgent::spawn_with(|cfg| cfg.with_hostname("beta"), &discovery)
.await
.unwrap();
assert_ne!(
a.agent.endpoint_id(),
b.agent.endpoint_id(),
"different devices must have different endpoint ids"
);
assert_eq!(a.agent.hostname(), "alpha");
assert_eq!(b.agent.hostname(), "beta");
let id_a = a.agent.join_network(&name, &secret).await.unwrap();
let id_b = b.agent.join_network(&name, &secret).await.unwrap();
assert_eq!(id_a, id_b);
a.agent.shutdown().await;
b.agent.shutdown().await;
}
#[tokio::test]
async fn restart_keeps_the_device_and_network_identity() {
let discovery = SharedMemoryDiscovery::new();
let (name, secret) = network("stable");
let first = TestAgent::spawn(&discovery).await.unwrap();
let device_id = first.agent.endpoint_id();
let network_id = first.agent.join_network(&name, &secret).await.unwrap();
let dir = first.stop().await;
let reopened = Agent::spawn(config_with(dir.path(), &discovery))
.await
.unwrap();
assert_eq!(
reopened.endpoint_id(),
device_id,
"restarting must not mint a new peer"
);
let networks = reopened.list_networks().await.unwrap();
assert_eq!(networks.len(), 1);
assert_eq!(networks[0].network_id, network_id);
assert!(networks[0].active, "auto-start networks come back up");
reopened.shutdown().await;
drop(reopened);
drop(dir);
}
#[tokio::test]
async fn changing_the_secret_keeps_the_device_identity() {
let discovery = SharedMemoryDiscovery::new();
let dir = tempfile::TempDir::new().unwrap();
let agent = Agent::spawn(config_with(dir.path(), &discovery))
.await
.unwrap();
let device_id = agent.endpoint_id();
let name = NetworkName::new("rotating").unwrap();
let old = agent
.join_network(&name, &NetworkSecret::generate())
.await
.unwrap();
let new = agent
.join_network(&name, &NetworkSecret::generate())
.await
.unwrap();
assert_ne!(old, new, "a new secret is a new network space");
assert_eq!(
agent.endpoint_id(),
device_id,
"rotating the network secret must not change the persistent iroh id"
);
agent.shutdown().await;
drop(agent);
drop(dir);
}