diff --git a/Cargo.toml b/Cargo.toml index 1daa196..dfe4b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ codec2 = "0.3" # Crypto x25519-dalek = { version = "2", features = ["static_secrets"] } -ed25519-dalek = { version = "2", features = ["rand_core"] } +ed25519-dalek = { version = "2", features = ["rand_core", "pkcs8"] } chacha20poly1305 = "0.10" hkdf = "0.12" sha2 = "0.10" diff --git a/crates/wzp-relay/src/main.rs b/crates/wzp-relay/src/main.rs index abc380b..3388754 100644 --- a/crates/wzp-relay/src/main.rs +++ b/crates/wzp-relay/src/main.rs @@ -267,7 +267,9 @@ async fn main() -> anyhow::Result<()> { info!(" fingerprint: \"{relay_fp}\""); } - let (server_config, _cert) = wzp_transport::server_config(); + let (server_config, cert_der) = wzp_transport::server_config_from_seed(&relay_seed.0); + let tls_fp = wzp_transport::tls_fingerprint(&cert_der); + info!(tls_fingerprint = %tls_fp, "TLS certificate (deterministic from relay identity)"); let endpoint = wzp_transport::create_endpoint(config.listen_addr, Some(server_config))?; // Forward mode diff --git a/crates/wzp-transport/Cargo.toml b/crates/wzp-transport/Cargo.toml index 5d32bda..671dfd9 100644 --- a/crates/wzp-transport/Cargo.toml +++ b/crates/wzp-transport/Cargo.toml @@ -16,6 +16,9 @@ async-trait = { workspace = true } serde_json = "1" rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } rcgen = "0.13" +ed25519-dalek = { workspace = true } +hkdf = { workspace = true } +sha2 = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/wzp-transport/src/config.rs b/crates/wzp-transport/src/config.rs index 6138fd9..2854bf9 100644 --- a/crates/wzp-transport/src/config.rs +++ b/crates/wzp-transport/src/config.rs @@ -6,20 +6,74 @@ use std::time::Duration; use quinn::crypto::rustls::QuicClientConfig; use quinn::crypto::rustls::QuicServerConfig; -/// Create a server configuration with a self-signed certificate (for testing). +/// Create a server configuration with a self-signed certificate (random keypair). /// -/// Tunes QUIC transport parameters for lossy VoIP: -/// - 30s idle timeout -/// - 5s keep-alive interval -/// - DATAGRAM extension enabled -/// - Conservative flow control for bandwidth-constrained links +/// The certificate changes on every call. Use `server_config_from_seed` for +/// a deterministic certificate that survives relay restarts. pub fn server_config() -> (quinn::ServerConfig, Vec) { let cert_key = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]) .expect("failed to generate self-signed cert"); let cert_der = rustls::pki_types::CertificateDer::from(cert_key.cert); let key_der = rustls::pki_types::PrivateKeyDer::try_from(cert_key.key_pair.serialize_der()).unwrap(); + build_server_config(cert_der, key_der) +} +/// Create a server configuration with a deterministic self-signed certificate +/// derived from a 32-byte seed. Same seed = same cert = same TLS fingerprint. +pub fn server_config_from_seed(seed: &[u8; 32]) -> (quinn::ServerConfig, Vec) { + use ed25519_dalek::pkcs8::EncodePrivateKey; + use ed25519_dalek::SigningKey; + use hkdf::Hkdf; + use sha2::Sha256; + + // Derive Ed25519 key bytes from seed via HKDF + let hk = Hkdf::::new(None, seed); + let mut ed_bytes = [0u8; 32]; + hk.expand(b"wzp-tls-ed25519", &mut ed_bytes) + .expect("HKDF expand failed"); + + // Create Ed25519 signing key and export as PKCS8 DER + let signing_key = SigningKey::from_bytes(&ed_bytes); + let pkcs8_doc = signing_key.to_pkcs8_der() + .expect("failed to encode Ed25519 key as PKCS8"); + let key_der_for_rcgen = rustls::pki_types::PrivateKeyDer::try_from(pkcs8_doc.as_bytes().to_vec()) + .expect("failed to wrap PKCS8 DER"); + + // Create rcgen KeyPair from DER + let key_pair = rcgen::KeyPair::from_der_and_sign_algo( + &key_der_for_rcgen, + &rcgen::PKCS_ED25519, + ) + .expect("failed to create KeyPair from seed-derived Ed25519 key"); + + // Build self-signed cert with this deterministic keypair + let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]) + .expect("failed to create CertificateParams"); + let cert = params.self_signed(&key_pair).expect("failed to self-sign cert"); + let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec()); + let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der()) + .expect("failed to serialize key DER"); + + build_server_config(cert_der, key_der) +} + +/// Compute a hex-formatted SHA-256 fingerprint of a DER-encoded certificate. +/// +/// Format: `xx:xx:xx:xx:...` (32 bytes = 64 hex chars with colons). +pub fn tls_fingerprint(cert_der: &[u8]) -> String { + use sha2::{Sha256, Digest}; + let hash = Sha256::digest(cert_der); + hash.iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join(":") +} + +fn build_server_config( + cert_der: rustls::pki_types::CertificateDer<'static>, + key_der: rustls::pki_types::PrivateKeyDer<'static>, +) -> (quinn::ServerConfig, Vec) { let mut server_crypto = rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(vec![cert_der.clone()], key_der) diff --git a/crates/wzp-transport/src/lib.rs b/crates/wzp-transport/src/lib.rs index 978155d..4034701 100644 --- a/crates/wzp-transport/src/lib.rs +++ b/crates/wzp-transport/src/lib.rs @@ -22,7 +22,7 @@ pub mod path_monitor; pub mod quic; pub mod reliable; -pub use config::{client_config, server_config}; +pub use config::{client_config, server_config, server_config_from_seed, tls_fingerprint}; pub use connection::{accept, connect, create_endpoint}; pub use path_monitor::PathMonitor; pub use quic::QuinnTransport;