Skip to main content

macula_rust/
transport.rs

1//! Dialing a macula 12 station over QUIC, as macula-go's `transport` does.
2//!
3//! Raw QUIC (RFC 9000) with the ALPN `"macula"`, TLS 1.3 only, and one key
4//! exchange group: SecP384r1MLKEM1024 (ML-KEM-1024, NIST category 5), as
5//! macula-go offers since 0.23.0. macula-pqc's provider also carries
6//! SecP256r1MLKEM768 for peers that still lead with it; a dial keeps only
7//! SecP384r1MLKEM1024 from it. Offering one group is what refuses every
8//! other: rustls aborts a handshake whose ServerHello or HelloRetryRequest
9//! names a group the client did not offer (RFC 8446, 4.1.3 and 4.1.4), and a
10//! station that has no SecP384r1MLKEM1024 fails the handshake itself. The
11//! negotiated group cannot be read back afterwards: quinn 0.11 reports it
12//! only under a private test feature (macula-rust#18). A station's certificate is self-signed, so it is not
13//! checked against a CA: macula-pqc's [`KeyPossessionVerifier`] accepts
14//! exactly one certificate whose key is ML-DSA-87, then the station's
15//! handshake signature under that key. That proves the station holds the key,
16//! not who it is: the handshake then checks the station's TLS binding, which
17//! ties this leaf to the identity key whose node_id the target pins (see
18//! `crate::handshake`). A target without an expected node_id is refused before
19//! anything is dialed.
20//!
21//! quinn protects QUIC Initial packets with the suite it finds in the rustls
22//! provider, and RFC 9001 fixes that suite at AES-128-GCM, which macula-pqc's
23//! provider does not offer for the handshake itself; the configuration is
24//! therefore built with `with_initial` and macula-pqc's `quic_initial_suite`.
25
26use std::net::{SocketAddr, ToSocketAddrs};
27use std::sync::Arc;
28use std::time::Duration;
29
30use macula_pqc::KeyPossessionVerifier;
31use quinn::crypto::rustls::QuicClientConfig;
32use quinn::{ClientConfig, Endpoint, IdleTimeout, TransportConfig};
33
34use crate::profile::Profile;
35
36/// The ALPN macula stations listen for.
37pub const ALPN: &[u8] = b"macula";
38
39/// macula's QUIC idle timeout and keep-alive: long enough to tolerate a real
40/// gap between frames, with pings often enough that a healthy connection is
41/// never mistaken for a dead one.
42pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
43pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(15);
44
45/// A station to dial: where it listens, the profile the node runs, and the
46/// node_id the station must prove in the handshake.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Target {
49    pub host: String,
50    pub port: u16,
51    pub profile: Profile,
52    pub expected_node_id: [u8; 32],
53}
54
55/// A dialed station: the QUIC connection, the endpoint it runs on, the leaf
56/// certificate the station presented (DER), and the target it was dialed as.
57/// The endpoint must live as long as the connection.
58pub struct Dialed {
59    pub connection: quinn::Connection,
60    pub endpoint: Endpoint,
61    pub leaf: Vec<u8>,
62    pub target: Target,
63}
64
65/// Why a dial failed.
66#[derive(Debug)]
67pub enum DialError {
68    /// The target names no expected node_id.
69    NoExpectedNodeId,
70    /// The host resolved to no address, or not at all.
71    Resolve(std::io::Error),
72    /// The local endpoint could not be made.
73    Endpoint(std::io::Error),
74    /// The TLS or QUIC configuration could not be built.
75    Config(String),
76    /// The connection could not be started.
77    Connect(quinn::ConnectError),
78    /// The QUIC or TLS handshake failed. Among the reasons: the station's
79    /// certificate (not exactly one, or not an ML-DSA-87 key), and a station
80    /// that does not agree on SecP384r1MLKEM1024.
81    Connection(quinn::ConnectionError),
82    /// The station presented no certificate the connection could hand back.
83    NoLeaf,
84}
85
86impl std::fmt::Display for DialError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            DialError::NoExpectedNodeId => f.write_str("the dial target names no expected node_id"),
90            DialError::Resolve(e) => write!(f, "resolving the station's address: {e}"),
91            DialError::Endpoint(e) => write!(f, "creating the QUIC endpoint: {e}"),
92            DialError::Config(e) => write!(f, "building the TLS configuration: {e}"),
93            DialError::Connect(e) => write!(f, "starting the QUIC connection: {e}"),
94            DialError::Connection(e) => write!(f, "the QUIC connection failed: {e}"),
95            DialError::NoLeaf => f.write_str("the station presented no certificate"),
96        }
97    }
98}
99
100impl std::error::Error for DialError {}
101
102/// Dials `target`: QUIC and TLS 1.3 with SecP384r1MLKEM1024 alone,
103/// the station's certificate checked for an ML-DSA-87 key it holds.
104pub async fn dial_target(target: &Target) -> Result<Dialed, DialError> {
105    if target.expected_node_id == [0u8; 32] {
106        return Err(DialError::NoExpectedNodeId);
107    }
108    let addr = (target.host.as_str(), target.port)
109        .to_socket_addrs()
110        .map_err(DialError::Resolve)?
111        .next()
112        .ok_or_else(|| DialError::Resolve(std::io::Error::other("no address")))?;
113    let bind: SocketAddr = if addr.is_ipv6() {
114        (std::net::Ipv6Addr::UNSPECIFIED, 0).into()
115    } else {
116        (std::net::Ipv4Addr::UNSPECIFIED, 0).into()
117    };
118    let mut endpoint = Endpoint::client(bind).map_err(DialError::Endpoint)?;
119    endpoint.set_default_client_config(client_config()?);
120    let connection = endpoint
121        .connect(addr, &target.host)
122        .map_err(DialError::Connect)?
123        .await
124        .map_err(DialError::Connection)?;
125    let leaf = connection
126        .peer_identity()
127        .and_then(|identity| {
128            identity
129                .downcast::<Vec<rustls::pki_types::CertificateDer<'static>>>()
130                .ok()
131        })
132        .and_then(|chain| chain.first().map(|leaf| leaf.to_vec()))
133        .ok_or(DialError::NoLeaf)?;
134    Ok(Dialed {
135        connection,
136        endpoint,
137        leaf,
138        target: target.clone(),
139    })
140}
141
142/// The QUIC client configuration every dial uses.
143fn client_config() -> Result<ClientConfig, DialError> {
144    let quic = QuicClientConfig::with_initial(
145        Arc::new(tls_client_config()?),
146        macula_pqc::quic_initial_suite(),
147    )
148    .map_err(|e| DialError::Config(e.to_string()))?;
149    let mut transport = TransportConfig::default();
150    transport.max_idle_timeout(Some(
151        IdleTimeout::try_from(IDLE_TIMEOUT).map_err(|e| DialError::Config(e.to_string()))?,
152    ));
153    transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
154    transport.stream_receive_window((16u32 * 1024 * 1024).into());
155    transport.receive_window((64u32 * 1024 * 1024).into());
156    transport.send_window(64 * 1024 * 1024);
157    let mut config = ClientConfig::new(Arc::new(quic));
158    config.transport_config(Arc::new(transport));
159    Ok(config)
160}
161
162/// SecP384r1MLKEM1024's code point, the one key exchange group a dial offers.
163const SECP384R1MLKEM1024: rustls::NamedGroup = rustls::NamedGroup::Unknown(0x11ED);
164
165/// The rustls half of a dial: macula-pqc's provider narrowed to
166/// SecP384r1MLKEM1024, its key possession verifier, the ALPN, and no session
167/// resumption.
168pub(crate) fn tls_client_config() -> Result<rustls::ClientConfig, DialError> {
169    let verifier = KeyPossessionVerifier::new();
170    let pqc = macula_pqc::client_builder();
171    let kx_groups: Vec<_> = pqc
172        .crypto_provider()
173        .kx_groups
174        .iter()
175        .copied()
176        .filter(|group| group.name() == SECP384R1MLKEM1024)
177        .collect();
178    if kx_groups.len() != 1 {
179        return Err(DialError::Config(format!(
180            "macula-pqc's provider carries {} SecP384r1MLKEM1024 groups, not 1",
181            kx_groups.len()
182        )));
183    }
184    let provider = rustls::crypto::CryptoProvider {
185        kx_groups,
186        ..(**pqc.crypto_provider()).clone()
187    };
188    let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(provider))
189        .with_protocol_versions(&[&rustls::version::TLS13])
190        .map_err(|e| DialError::Config(e.to_string()))?
191        .dangerous()
192        .with_custom_certificate_verifier(Arc::new(verifier))
193        .with_no_client_auth();
194    config.alpn_protocols = vec![ALPN.to_vec()];
195    config.resumption = rustls::client::Resumption::disabled();
196    config.enable_early_data = false;
197    Ok(config)
198}
199
200#[cfg(test)]
201mod tests {
202    //! The dial, on real QUIC against local stations: one as a macula 12
203    //! station is (macula-pqc, an ML-DSA-87 certificate), and the ones a dial
204    //! must refuse.
205
206    use std::sync::Arc;
207
208    use quinn::crypto::rustls::QuicServerConfig;
209    use rustls::crypto::aws_lc_rs::kx_group as aws;
210    use rustls::crypto::CryptoProvider;
211    use rustls::pki_types::{CertificateDer, PrivateKeyDer};
212    use rustls::{NamedGroup, ServerConfig};
213
214    use super::{dial_target, tls_client_config, DialError, Target, ALPN};
215    use crate::profile::Profile;
216
217    /// SecP384r1MLKEM1024, code point 0x11ED.
218    const SECP384R1MLKEM1024: NamedGroup = NamedGroup::Unknown(0x11ED);
219
220    fn mldsa_certificate() -> (CertificateDer<'static>, PrivateKeyDer<'static>) {
221        let (certificate, key) =
222            macula_pqc::self_signed_certificate(&[7u8; 32], vec!["localhost".to_string()])
223                .expect("a certificate");
224        (certificate, key.into())
225    }
226
227    fn classical_certificate() -> (CertificateDer<'static>, PrivateKeyDer<'static>) {
228        let key_pair = rcgen::KeyPair::generate().expect("a key pair");
229        let certificate = rcgen::CertificateParams::new(vec!["localhost".to_string()])
230            .expect("certificate params")
231            .self_signed(&key_pair)
232            .expect("a certificate");
233        (
234            certificate.der().clone(),
235            PrivateKeyDer::Pkcs8(key_pair.serialize_der().into()),
236        )
237    }
238
239    /// A station on 127.0.0.1, serving `config` over QUIC; its endpoint and
240    /// port.
241    fn station(mut config: ServerConfig, alpn: &[u8]) -> (quinn::Endpoint, u16) {
242        config.alpn_protocols = vec![alpn.to_vec()];
243        let quic =
244            QuicServerConfig::with_initial(Arc::new(config), macula_pqc::quic_initial_suite())
245                .expect("a QUIC server config");
246        let endpoint = quinn::Endpoint::server(
247            quinn::ServerConfig::with_crypto(Arc::new(quic)),
248            ([127, 0, 0, 1], 0).into(),
249        )
250        .expect("an endpoint");
251        let port = endpoint.local_addr().expect("an address").port();
252        let accepting = endpoint.clone();
253        tokio::spawn(hold_connections(accepting));
254        (endpoint, port)
255    }
256
257    /// Accepts every connection the station's endpoint receives, holding each
258    /// until it closes.
259    async fn hold_connections(accepting: quinn::Endpoint) {
260        while let Some(incoming) = accepting.accept().await {
261            tokio::spawn(hold_until_closed(incoming));
262        }
263    }
264
265    /// Holds one incoming connection, once established, until it closes.
266    async fn hold_until_closed(incoming: quinn::Incoming) {
267        if let Ok(connection) = incoming.await {
268            connection.closed().await;
269        }
270    }
271
272    fn macula_station() -> (quinn::Endpoint, u16, CertificateDer<'static>) {
273        let (certificate, key) = mldsa_certificate();
274        let config = macula_pqc::server_builder()
275            .with_no_client_auth()
276            .with_single_cert(vec![certificate.clone()], key)
277            .expect("a station configuration");
278        let (endpoint, port) = station(config, ALPN);
279        (endpoint, port, certificate)
280    }
281
282    fn target(port: u16) -> Target {
283        Target {
284            host: "127.0.0.1".to_string(),
285            port,
286            profile: Profile::PqHybrid,
287            expected_node_id: [1u8; 32],
288        }
289    }
290
291    #[test]
292    fn a_dial_offers_secp384r1mlkem1024_alone() {
293        let config = tls_client_config().expect("a configuration");
294        let offered: Vec<NamedGroup> = config
295            .crypto_provider()
296            .kx_groups
297            .iter()
298            .map(|g| g.name())
299            .collect();
300        assert_eq!(offered, vec![SECP384R1MLKEM1024]);
301    }
302
303    /// A station that offers SecP256r1MLKEM768 alone, signing with ML-DSA-87
304    /// as a macula station does: everything about it is acceptable except
305    /// its key exchange group.
306    fn station_on_group(group: NamedGroup) -> (quinn::Endpoint, u16) {
307        let (certificate, key) = mldsa_certificate();
308        let pqc = macula_pqc::server_builder();
309        let provider = CryptoProvider {
310            kx_groups: pqc
311                .crypto_provider()
312                .kx_groups
313                .iter()
314                .copied()
315                .filter(|g| g.name() == group)
316                .collect(),
317            ..(**pqc.crypto_provider()).clone()
318        };
319        assert_eq!(provider.kx_groups.len(), 1, "macula-pqc offers {group:?}");
320        let config = ServerConfig::builder_with_provider(Arc::new(provider))
321            .with_protocol_versions(&[&rustls::version::TLS13])
322            .expect("TLS 1.3")
323            .with_no_client_auth()
324            .with_single_cert(vec![certificate], key)
325            .expect("a station configuration");
326        station(config, ALPN)
327    }
328
329    #[tokio::test]
330    async fn a_station_that_offers_only_secp256r1mlkem768_is_refused() {
331        let (_station, port) = station_on_group(NamedGroup::secp256r1MLKEM768);
332        assert!(matches!(
333            dial_target(&target(port)).await,
334            Err(DialError::Connection(_))
335        ));
336    }
337
338    #[tokio::test]
339    async fn a_station_that_offers_only_secp384r1mlkem1024_is_reached() {
340        let (_station, port) = station_on_group(SECP384R1MLKEM1024);
341        let dialed = dial_target(&target(port))
342            .await
343            .expect("the station is reached");
344        dialed.connection.close(0u32.into(), b"done");
345    }
346
347    #[tokio::test]
348    async fn a_macula_12_station_is_reached_and_its_leaf_handed_back() {
349        let (_station, port, certificate) = macula_station();
350        let dialed = dial_target(&target(port))
351            .await
352            .expect("the station is reached");
353        assert_eq!(dialed.leaf, certificate.to_vec());
354        dialed.connection.close(0u32.into(), b"done");
355    }
356
357    #[tokio::test]
358    async fn a_target_without_an_expected_node_id_is_refused_before_dialing() {
359        let mut unpinned = target(9);
360        unpinned.expected_node_id = [0u8; 32];
361        assert!(matches!(
362            dial_target(&unpinned).await,
363            Err(DialError::NoExpectedNodeId)
364        ));
365    }
366
367    #[tokio::test]
368    async fn a_station_with_a_classical_certificate_is_refused() {
369        let (certificate, key) = classical_certificate();
370        let provider = CryptoProvider {
371            kx_groups: vec![aws::SECP256R1MLKEM768],
372            ..rustls::crypto::aws_lc_rs::default_provider()
373        };
374        let config = ServerConfig::builder_with_provider(Arc::new(provider))
375            .with_protocol_versions(&[&rustls::version::TLS13])
376            .expect("TLS 1.3")
377            .with_no_client_auth()
378            .with_single_cert(vec![certificate], key)
379            .expect("a station configuration");
380        let (_station, port) = station(config, ALPN);
381        assert!(matches!(
382            dial_target(&target(port)).await,
383            Err(DialError::Connection(_))
384        ));
385    }
386
387    #[tokio::test]
388    async fn a_classical_only_station_is_refused() {
389        let (certificate, key) = classical_certificate();
390        let provider = CryptoProvider {
391            kx_groups: vec![aws::X25519, aws::SECP256R1, aws::SECP384R1],
392            ..rustls::crypto::aws_lc_rs::default_provider()
393        };
394        let config = ServerConfig::builder_with_provider(Arc::new(provider))
395            .with_protocol_versions(&[&rustls::version::TLS13])
396            .expect("TLS 1.3")
397            .with_no_client_auth()
398            .with_single_cert(vec![certificate], key)
399            .expect("a station configuration");
400        let (_station, port) = station(config, ALPN);
401        assert!(matches!(
402            dial_target(&target(port)).await,
403            Err(DialError::Connection(_))
404        ));
405    }
406
407    #[tokio::test]
408    async fn a_station_that_does_not_speak_macula_is_refused() {
409        let (certificate, key) = mldsa_certificate();
410        let config = macula_pqc::server_builder()
411            .with_no_client_auth()
412            .with_single_cert(vec![certificate], key)
413            .expect("a station configuration");
414        let (_station, port) = station(config, b"h3");
415        assert!(matches!(
416            dial_target(&target(port)).await,
417            Err(DialError::Connection(_))
418        ));
419    }
420}