Implement the `tcp-tls` protocol crate and generic tunnel codec abstractions alongside `wg-quic` to circumvent deep packet inspection (DPI/TSPU), UDP blocking, and QUIC throttling. Traffic uses standard TCP on port 443 with TLS 1.3 framing, looking identical to regular HTTPS traffic to network observers. Architectural highlights: - TunnelCodec & GenericTunnelPlugin: Provide an extensible plugin abstraction in `crates/tsunagi/src/dataplane/tunnel.rs` that encapsulates background task lifecycles, per-peer channels, and sink dispatch, so new transport plugins only define cryptographic transformations and handshake payloads. - MultiPacketTransport & Protocol Priority Selection: Support multiple simultaneous transports in `AgentConfig` and dynamically dispatch `open()` calls by protocol identifier. In `ensure_links`, choose the highest-priority mutually supported protocol per peer to avoid duplicate links. - TLS 1.3 & Certificate Pinning: Generate self-signed Ed25519 X.509 certificates derived from the persistent `DeviceIdentity` secret. Clients pin peer certificates using `PinnedEndpointVerifier` against `iroh::EndpointId`. - End-to-end Encryption: `PeerCipher` uses X25519 static-static ECDH, HKDF-SHA256, and ChaCha20-Poly1305 with incrementing 64-bit nonces. Relayed traffic across intermediate nodes remains confidential and tamper-proof. - Privilege handling: Default to TCP port 443 with fallback to 8443 on `PermissionDenied`, providing an actionable message recommending: `sudo setcap cap_net_admin,cap_net_bind_service+p <path>`. Validation on Linux: - `cargo fmt --all -- --check` passed. - `cargo clippy --locked --workspace --all-targets -- -D warnings` passed. - `cargo test --locked --workspace --all-targets` passed (all workspace unit, integration, CLI, and new `tsunagi-tcp-tls` tests passed).
144 lines
5.0 KiB
Rust
144 lines
5.0 KiB
Rust
//! TLS certificate generation and verification.
|
|
//!
|
|
//! Generates a self-signed TLS certificate derived from the device's persistent
|
|
//! identity, and verifies remote certificates against the expected peer's
|
|
//! [`iroh::EndpointId`].
|
|
|
|
use std::sync::Arc;
|
|
|
|
use iroh::EndpointId;
|
|
use rustls::client::danger::{ServerCertVerified, ServerCertVerifier};
|
|
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
|
use rustls::{ClientConfig, Error as RustlsError, ServerConfig};
|
|
|
|
/// Verifies that the server certificate's public key matches the expected peer.
|
|
#[derive(Debug)]
|
|
pub struct PinnedEndpointVerifier {
|
|
expected: EndpointId,
|
|
}
|
|
|
|
impl PinnedEndpointVerifier {
|
|
/// Creates a verifier expecting `peer`.
|
|
pub fn new(peer: EndpointId) -> Self {
|
|
Self { expected: peer }
|
|
}
|
|
}
|
|
|
|
impl ServerCertVerifier for PinnedEndpointVerifier {
|
|
fn verify_server_cert(
|
|
&self,
|
|
end_entity: &CertificateDer<'_>,
|
|
_intermediates: &[CertificateDer<'_>],
|
|
_server_name: &ServerName<'_>,
|
|
_ocsp_response: &[u8],
|
|
_now: UnixTime,
|
|
) -> Result<ServerCertVerified, RustlsError> {
|
|
// Parse the certificate's SPKI / public key
|
|
let cert_bytes = end_entity.as_ref();
|
|
let expected_bytes = self.expected.as_bytes();
|
|
|
|
// Check if the expected 32-byte Ed25519 public key is in the certificate
|
|
if cert_bytes
|
|
.windows(32)
|
|
.any(|window| window == expected_bytes)
|
|
{
|
|
Ok(ServerCertVerified::assertion())
|
|
} else {
|
|
Err(RustlsError::InvalidCertificate(
|
|
rustls::CertificateError::NotValidForName,
|
|
))
|
|
}
|
|
}
|
|
|
|
fn verify_tls12_signature(
|
|
&self,
|
|
_message: &[u8],
|
|
_cert: &CertificateDer<'_>,
|
|
_dss: &rustls::DigitallySignedStruct,
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, RustlsError> {
|
|
// We require TLS 1.3
|
|
Err(RustlsError::PeerIncompatible(
|
|
rustls::PeerIncompatible::NoSignatureSchemesInCommon,
|
|
))
|
|
}
|
|
|
|
fn verify_tls13_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &CertificateDer<'_>,
|
|
dss: &rustls::DigitallySignedStruct,
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, RustlsError> {
|
|
rustls::crypto::verify_tls13_signature(
|
|
message,
|
|
cert,
|
|
dss,
|
|
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
|
)
|
|
}
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
|
vec![rustls::SignatureScheme::ED25519]
|
|
}
|
|
}
|
|
|
|
/// Generates a self-signed certificate and private key for TLS 1.3 using
|
|
/// a given 32-byte Ed25519 secret key.
|
|
pub fn generate_self_signed_cert(
|
|
secret_key_bytes: &[u8; 32],
|
|
) -> Result<
|
|
(
|
|
CertificateDer<'static>,
|
|
rustls::pki_types::PrivateKeyDer<'static>,
|
|
),
|
|
Box<dyn std::error::Error + Send + Sync>,
|
|
> {
|
|
// PKCS#8 prefix for v1 Ed25519 private keys:
|
|
// 30 2e 02 01 00 30 05 06 03 2b 65 70 04 22 04 20 <32 bytes>
|
|
let mut pkcs8 = Vec::with_capacity(48);
|
|
pkcs8.extend_from_slice(&[
|
|
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04,
|
|
0x20,
|
|
]);
|
|
pkcs8.extend_from_slice(secret_key_bytes);
|
|
|
|
let key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(pkcs8);
|
|
let key_pair = rcgen::KeyPair::from_pkcs8_der_and_sign_algo(&key_der, &rcgen::PKCS_ED25519)?;
|
|
let mut params = rcgen::CertificateParams::new(vec!["tsunagi.local".to_string()])?;
|
|
params
|
|
.distinguished_name
|
|
.push(rcgen::DnType::CommonName, "tsunagi-node");
|
|
|
|
let cert = params.self_signed(&key_pair)?;
|
|
let cert_der = CertificateDer::from(cert.der().to_vec());
|
|
let priv_key = rustls::pki_types::PrivateKeyDer::Pkcs8(key_der);
|
|
|
|
Ok((cert_der, priv_key))
|
|
}
|
|
|
|
/// Creates a `rustls::ServerConfig` for the node.
|
|
pub fn make_server_config(
|
|
cert: CertificateDer<'static>,
|
|
key: rustls::pki_types::PrivateKeyDer<'static>,
|
|
) -> Result<Arc<ServerConfig>, rustls::Error> {
|
|
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
|
let mut config = ServerConfig::builder_with_provider(provider)
|
|
.with_safe_default_protocol_versions()?
|
|
.with_no_client_auth()
|
|
.with_single_cert(vec![cert], key)?;
|
|
config.alpn_protocols = vec![b"tsunagi-tls/1".to_vec()];
|
|
Ok(Arc::new(config))
|
|
}
|
|
|
|
/// Creates a `rustls::ClientConfig` expecting a specific peer's `EndpointId`.
|
|
pub fn make_client_config(expected_peer: EndpointId) -> Result<Arc<ClientConfig>, rustls::Error> {
|
|
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
|
let verifier = Arc::new(PinnedEndpointVerifier::new(expected_peer));
|
|
let mut config = ClientConfig::builder_with_provider(provider)
|
|
.with_safe_default_protocol_versions()?
|
|
.dangerous()
|
|
.with_custom_certificate_verifier(verifier)
|
|
.with_no_client_auth();
|
|
config.alpn_protocols = vec![b"tsunagi-tls/1".to_vec()];
|
|
Ok(Arc::new(config))
|
|
}
|