From aa9dc18efefd928fe8a9b72e16513a91fcd2b61d Mon Sep 17 00:00:00 2001 From: Serkyo Date: Mon, 13 Jul 2026 14:23:30 +0200 Subject: [PATCH] feat(net): add QUIC endpoints with synvael ALPN and self-signed TLS --- crates/net/src/codec.rs | 9 +++ crates/net/src/endpoint.rs | 136 ++++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/crates/net/src/codec.rs b/crates/net/src/codec.rs index a715778..2ee541a 100644 --- a/crates/net/src/codec.rs +++ b/crates/net/src/codec.rs @@ -33,6 +33,15 @@ pub enum NetError { /// Reading an exact number of bytes from a quinn recv stream failed. #[error("quinn read error: {0}")] Read(#[from] quinn::ReadExactError), + /// Generation of the self-signed server certificate failed. + #[error("certificate generation error: {0}")] + Rcgen(#[from] rcgen::Error), + /// Construction of the `rustls` TLS configuration failed. + #[error("rustls configuration error: {0}")] + Rustls(#[from] rustls::Error), + /// The `rustls` configuration lacked a TLS 1.3 cipher suite, which QUIC requires. + #[error("no initial cipher suite for quic: {0}")] + NoInitialCipherSuite(#[from] quinn::crypto::rustls::NoInitialCipherSuite), } /// The maximum payload length, in bytes, accepted on the control stream (64 KiB), matching the mod-payload cap. Higher-bandwidth tiers such as chunk streaming define their own caps. diff --git a/crates/net/src/endpoint.rs b/crates/net/src/endpoint.rs index 7b302ef..a1e684f 100644 --- a/crates/net/src/endpoint.rs +++ b/crates/net/src/endpoint.rs @@ -2,4 +2,138 @@ //! QUIC endpoint construction for client and server. //! -//! Builds the `quinn` endpoints and configures ALPN and TLS 1.3 (self-signed server certificate, permissive client verifier for the initial milestone). Populated in a later concept; currently a placeholder. +//! Builds the `quinn` endpoints and configures ALPN and TLS 1.3. Both endpoints negotiate the `synvael` application protocol; a peer advertising any other ALPN identifier is rejected during the TLS handshake. + +use std::net::SocketAddr; +use std::sync::Arc; + +use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; +use quinn::{ClientConfig, Endpoint, ServerConfig}; +use rustls::DigitallySignedStruct; +use rustls::SignatureScheme; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime}; +use tracing::debug; + +use crate::codec::NetError; + +/// The Application-Layer Protocol Negotiation identifier for the Synvael protocol. +pub const ALPN: &[u8] = b"synvael"; + +/// Installs the process-wide default `rustls` `CryptoProvider` if one is not already installed. +fn ensure_crypto_provider() { + if rustls::crypto::ring::default_provider() + .install_default() + .is_err() + { + debug!("rustls crypto provider already installed; reusing existing default"); + } +} + +/// Builds a QUIC server endpoint bound to `bind`, using a freshly generated self-signed certificate and the `synvael` ALPN. +pub fn server_endpoint(bind: SocketAddr) -> Result { + ensure_crypto_provider(); + + // Generate a self-signed certificate for the "localhost" subject. The subject is not validated by the current client verifier and exists only to satisfy certificate structure. + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()])?; + let cert_der = cert.cert.der().clone(); + let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der())); + + let mut tls_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![cert_der], key_der)?; + tls_config.alpn_protocols = vec![ALPN.to_vec()]; + + let quic_config = QuicServerConfig::try_from(tls_config)?; + let server_config = ServerConfig::with_crypto(Arc::new(quic_config)); + + Ok(Endpoint::server(server_config, bind)?) +} + +/// Builds a QUIC client endpoint bound to an ephemeral local address, configured with the `synvael` ALPN and a permissive certificate verifier. +pub fn client_endpoint() -> Result { + ensure_crypto_provider(); + + let mut tls_config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(AcceptAnyServerCert)) + .with_no_client_auth(); + tls_config.alpn_protocols = vec![ALPN.to_vec()]; + + let quic_config = QuicClientConfig::try_from(tls_config)?; + let client_config = ClientConfig::new(Arc::new(quic_config)); + + let mut endpoint = Endpoint::client("0.0.0.0:0".parse().map_err(std::io::Error::other)?)?; + endpoint.set_default_client_config(client_config); + + Ok(endpoint) +} + +/// A certificate verifier that unconditionally accepts any server certificate, disabling server authentication. +// TODO: replace with trust-on-first-use + certificate pinning. +#[derive(Debug)] +struct AcceptAnyServerCert; + +impl ServerCertVerifier for AcceptAnyServerCert { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + // Advertise the schemes the installed provider can verify, so the shim does not artificially restrict handshake negotiation. + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn alpn_identifier_is_synvael() { + assert_eq!(ALPN, b"synvael"); + } + + // A tokio runtime is required because `quinn::Endpoint` spawns its driver task on construction. + #[tokio::test] + async fn server_endpoint_constructs_and_binds() -> Result<(), NetError> { + let bind = "127.0.0.1:0".parse().map_err(std::io::Error::other)?; + let endpoint = server_endpoint(bind)?; + // A concrete port is assigned once the UDP socket is bound. + assert_ne!(endpoint.local_addr()?.port(), 0, "socket must bind a port"); + Ok(()) + } + + #[tokio::test] + async fn client_endpoint_constructs_and_binds() -> Result<(), NetError> { + client_endpoint()?; + Ok(()) + } +}