macula-rust 0.9.0

Rust SDK for the macula 12 mesh: ML-DSA-87 and LAMPS composite node keys, a post-quantum QUIC transport — mobile first, not mobile-only.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! Dialing a macula 12 station over QUIC, as macula-go's `transport` does.
//!
//! Raw QUIC (RFC 9000) with the ALPN `"macula"`, TLS 1.3 only, and one key
//! exchange group: SecP384r1MLKEM1024 (ML-KEM-1024, NIST category 5), as
//! macula-go offers since 0.23.0. macula-pqc's provider also carries
//! SecP256r1MLKEM768 for peers that still lead with it; a dial keeps only
//! SecP384r1MLKEM1024 from it. Offering one group is what refuses every
//! other: rustls aborts a handshake whose ServerHello or HelloRetryRequest
//! names a group the client did not offer (RFC 8446, 4.1.3 and 4.1.4), and a
//! station that has no SecP384r1MLKEM1024 fails the handshake itself. The
//! negotiated group cannot be read back afterwards: quinn 0.11 reports it
//! only under a private test feature (macula-rust#18). A station's certificate is self-signed, so it is not
//! checked against a CA: macula-pqc's [`KeyPossessionVerifier`] accepts
//! exactly one certificate whose key is ML-DSA-87, then the station's
//! handshake signature under that key. That proves the station holds the key,
//! not who it is: the handshake then checks the station's TLS binding, which
//! ties this leaf to the identity key whose node_id the target pins (see
//! `crate::handshake`). A target without an expected node_id is refused before
//! anything is dialed.
//!
//! quinn protects QUIC Initial packets with the suite it finds in the rustls
//! provider, and RFC 9001 fixes that suite at AES-128-GCM, which macula-pqc's
//! provider does not offer for the handshake itself; the configuration is
//! therefore built with `with_initial` and macula-pqc's `quic_initial_suite`.

use std::net::{SocketAddr, ToSocketAddrs};
use std::sync::Arc;
use std::time::Duration;

use macula_pqc::KeyPossessionVerifier;
use quinn::crypto::rustls::QuicClientConfig;
use quinn::{ClientConfig, Endpoint, IdleTimeout, TransportConfig};

use crate::profile::Profile;

/// The ALPN macula stations listen for.
pub const ALPN: &[u8] = b"macula";

/// macula's QUIC idle timeout and keep-alive: long enough to tolerate a real
/// gap between frames, with pings often enough that a healthy connection is
/// never mistaken for a dead one.
pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(15);

/// A station to dial: where it listens, the profile the node runs, and the
/// node_id the station must prove in the handshake.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Target {
    pub host: String,
    pub port: u16,
    pub profile: Profile,
    pub expected_node_id: [u8; 32],
}

/// A dialed station: the QUIC connection, the endpoint it runs on, the leaf
/// certificate the station presented (DER), and the target it was dialed as.
/// The endpoint must live as long as the connection.
pub struct Dialed {
    pub connection: quinn::Connection,
    pub endpoint: Endpoint,
    pub leaf: Vec<u8>,
    pub target: Target,
}

/// Why a dial failed.
#[derive(Debug)]
pub enum DialError {
    /// The target names no expected node_id.
    NoExpectedNodeId,
    /// The host resolved to no address, or not at all.
    Resolve(std::io::Error),
    /// The local endpoint could not be made.
    Endpoint(std::io::Error),
    /// The TLS or QUIC configuration could not be built.
    Config(String),
    /// The connection could not be started.
    Connect(quinn::ConnectError),
    /// The QUIC or TLS handshake failed. Among the reasons: the station's
    /// certificate (not exactly one, or not an ML-DSA-87 key), and a station
    /// that does not agree on SecP384r1MLKEM1024.
    Connection(quinn::ConnectionError),
    /// The station presented no certificate the connection could hand back.
    NoLeaf,
}

impl std::fmt::Display for DialError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DialError::NoExpectedNodeId => f.write_str("the dial target names no expected node_id"),
            DialError::Resolve(e) => write!(f, "resolving the station's address: {e}"),
            DialError::Endpoint(e) => write!(f, "creating the QUIC endpoint: {e}"),
            DialError::Config(e) => write!(f, "building the TLS configuration: {e}"),
            DialError::Connect(e) => write!(f, "starting the QUIC connection: {e}"),
            DialError::Connection(e) => write!(f, "the QUIC connection failed: {e}"),
            DialError::NoLeaf => f.write_str("the station presented no certificate"),
        }
    }
}

impl std::error::Error for DialError {}

/// Dials `target`: QUIC and TLS 1.3 with SecP384r1MLKEM1024 alone,
/// the station's certificate checked for an ML-DSA-87 key it holds.
pub async fn dial_target(target: &Target) -> Result<Dialed, DialError> {
    if target.expected_node_id == [0u8; 32] {
        return Err(DialError::NoExpectedNodeId);
    }
    let addr = (target.host.as_str(), target.port)
        .to_socket_addrs()
        .map_err(DialError::Resolve)?
        .next()
        .ok_or_else(|| DialError::Resolve(std::io::Error::other("no address")))?;
    let bind: SocketAddr = if addr.is_ipv6() {
        (std::net::Ipv6Addr::UNSPECIFIED, 0).into()
    } else {
        (std::net::Ipv4Addr::UNSPECIFIED, 0).into()
    };
    let mut endpoint = Endpoint::client(bind).map_err(DialError::Endpoint)?;
    endpoint.set_default_client_config(client_config()?);
    let connection = endpoint
        .connect(addr, &target.host)
        .map_err(DialError::Connect)?
        .await
        .map_err(DialError::Connection)?;
    let leaf = connection
        .peer_identity()
        .and_then(|identity| {
            identity
                .downcast::<Vec<rustls::pki_types::CertificateDer<'static>>>()
                .ok()
        })
        .and_then(|chain| chain.first().map(|leaf| leaf.to_vec()))
        .ok_or(DialError::NoLeaf)?;
    Ok(Dialed {
        connection,
        endpoint,
        leaf,
        target: target.clone(),
    })
}

/// The QUIC client configuration every dial uses.
fn client_config() -> Result<ClientConfig, DialError> {
    let quic = QuicClientConfig::with_initial(
        Arc::new(tls_client_config()?),
        macula_pqc::quic_initial_suite(),
    )
    .map_err(|e| DialError::Config(e.to_string()))?;
    let mut transport = TransportConfig::default();
    transport.max_idle_timeout(Some(
        IdleTimeout::try_from(IDLE_TIMEOUT).map_err(|e| DialError::Config(e.to_string()))?,
    ));
    transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
    transport.stream_receive_window((16u32 * 1024 * 1024).into());
    transport.receive_window((64u32 * 1024 * 1024).into());
    transport.send_window(64 * 1024 * 1024);
    let mut config = ClientConfig::new(Arc::new(quic));
    config.transport_config(Arc::new(transport));
    Ok(config)
}

/// SecP384r1MLKEM1024's code point, the one key exchange group a dial offers.
const SECP384R1MLKEM1024: rustls::NamedGroup = rustls::NamedGroup::Unknown(0x11ED);

/// The rustls half of a dial: macula-pqc's provider narrowed to
/// SecP384r1MLKEM1024, its key possession verifier, the ALPN, and no session
/// resumption.
pub(crate) fn tls_client_config() -> Result<rustls::ClientConfig, DialError> {
    let verifier = KeyPossessionVerifier::new();
    let pqc = macula_pqc::client_builder();
    let kx_groups: Vec<_> = pqc
        .crypto_provider()
        .kx_groups
        .iter()
        .copied()
        .filter(|group| group.name() == SECP384R1MLKEM1024)
        .collect();
    if kx_groups.len() != 1 {
        return Err(DialError::Config(format!(
            "macula-pqc's provider carries {} SecP384r1MLKEM1024 groups, not 1",
            kx_groups.len()
        )));
    }
    let provider = rustls::crypto::CryptoProvider {
        kx_groups,
        ..(**pqc.crypto_provider()).clone()
    };
    let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(provider))
        .with_protocol_versions(&[&rustls::version::TLS13])
        .map_err(|e| DialError::Config(e.to_string()))?
        .dangerous()
        .with_custom_certificate_verifier(Arc::new(verifier))
        .with_no_client_auth();
    config.alpn_protocols = vec![ALPN.to_vec()];
    config.resumption = rustls::client::Resumption::disabled();
    config.enable_early_data = false;
    Ok(config)
}

#[cfg(test)]
mod tests {
    //! The dial, on real QUIC against local stations: one as a macula 12
    //! station is (macula-pqc, an ML-DSA-87 certificate), and the ones a dial
    //! must refuse.

    use std::sync::Arc;

    use quinn::crypto::rustls::QuicServerConfig;
    use rustls::crypto::aws_lc_rs::kx_group as aws;
    use rustls::crypto::CryptoProvider;
    use rustls::pki_types::{CertificateDer, PrivateKeyDer};
    use rustls::{NamedGroup, ServerConfig};

    use super::{dial_target, tls_client_config, DialError, Target, ALPN};
    use crate::profile::Profile;

    /// SecP384r1MLKEM1024, code point 0x11ED.
    const SECP384R1MLKEM1024: NamedGroup = NamedGroup::Unknown(0x11ED);

    fn mldsa_certificate() -> (CertificateDer<'static>, PrivateKeyDer<'static>) {
        let (certificate, key) =
            macula_pqc::self_signed_certificate(&[7u8; 32], vec!["localhost".to_string()])
                .expect("a certificate");
        (certificate, key.into())
    }

    fn classical_certificate() -> (CertificateDer<'static>, PrivateKeyDer<'static>) {
        let key_pair = rcgen::KeyPair::generate().expect("a key pair");
        let certificate = rcgen::CertificateParams::new(vec!["localhost".to_string()])
            .expect("certificate params")
            .self_signed(&key_pair)
            .expect("a certificate");
        (
            certificate.der().clone(),
            PrivateKeyDer::Pkcs8(key_pair.serialize_der().into()),
        )
    }

    /// A station on 127.0.0.1, serving `config` over QUIC; its endpoint and
    /// port.
    fn station(mut config: ServerConfig, alpn: &[u8]) -> (quinn::Endpoint, u16) {
        config.alpn_protocols = vec![alpn.to_vec()];
        let quic =
            QuicServerConfig::with_initial(Arc::new(config), macula_pqc::quic_initial_suite())
                .expect("a QUIC server config");
        let endpoint = quinn::Endpoint::server(
            quinn::ServerConfig::with_crypto(Arc::new(quic)),
            ([127, 0, 0, 1], 0).into(),
        )
        .expect("an endpoint");
        let port = endpoint.local_addr().expect("an address").port();
        let accepting = endpoint.clone();
        tokio::spawn(hold_connections(accepting));
        (endpoint, port)
    }

    /// Accepts every connection the station's endpoint receives, holding each
    /// until it closes.
    async fn hold_connections(accepting: quinn::Endpoint) {
        while let Some(incoming) = accepting.accept().await {
            tokio::spawn(hold_until_closed(incoming));
        }
    }

    /// Holds one incoming connection, once established, until it closes.
    async fn hold_until_closed(incoming: quinn::Incoming) {
        if let Ok(connection) = incoming.await {
            connection.closed().await;
        }
    }

    fn macula_station() -> (quinn::Endpoint, u16, CertificateDer<'static>) {
        let (certificate, key) = mldsa_certificate();
        let config = macula_pqc::server_builder()
            .with_no_client_auth()
            .with_single_cert(vec![certificate.clone()], key)
            .expect("a station configuration");
        let (endpoint, port) = station(config, ALPN);
        (endpoint, port, certificate)
    }

    fn target(port: u16) -> Target {
        Target {
            host: "127.0.0.1".to_string(),
            port,
            profile: Profile::PqHybrid,
            expected_node_id: [1u8; 32],
        }
    }

    #[test]
    fn a_dial_offers_secp384r1mlkem1024_alone() {
        let config = tls_client_config().expect("a configuration");
        let offered: Vec<NamedGroup> = config
            .crypto_provider()
            .kx_groups
            .iter()
            .map(|g| g.name())
            .collect();
        assert_eq!(offered, vec![SECP384R1MLKEM1024]);
    }

    /// A station that offers SecP256r1MLKEM768 alone, signing with ML-DSA-87
    /// as a macula station does: everything about it is acceptable except
    /// its key exchange group.
    fn station_on_group(group: NamedGroup) -> (quinn::Endpoint, u16) {
        let (certificate, key) = mldsa_certificate();
        let pqc = macula_pqc::server_builder();
        let provider = CryptoProvider {
            kx_groups: pqc
                .crypto_provider()
                .kx_groups
                .iter()
                .copied()
                .filter(|g| g.name() == group)
                .collect(),
            ..(**pqc.crypto_provider()).clone()
        };
        assert_eq!(provider.kx_groups.len(), 1, "macula-pqc offers {group:?}");
        let config = ServerConfig::builder_with_provider(Arc::new(provider))
            .with_protocol_versions(&[&rustls::version::TLS13])
            .expect("TLS 1.3")
            .with_no_client_auth()
            .with_single_cert(vec![certificate], key)
            .expect("a station configuration");
        station(config, ALPN)
    }

    #[tokio::test]
    async fn a_station_that_offers_only_secp256r1mlkem768_is_refused() {
        let (_station, port) = station_on_group(NamedGroup::secp256r1MLKEM768);
        assert!(matches!(
            dial_target(&target(port)).await,
            Err(DialError::Connection(_))
        ));
    }

    #[tokio::test]
    async fn a_station_that_offers_only_secp384r1mlkem1024_is_reached() {
        let (_station, port) = station_on_group(SECP384R1MLKEM1024);
        let dialed = dial_target(&target(port))
            .await
            .expect("the station is reached");
        dialed.connection.close(0u32.into(), b"done");
    }

    #[tokio::test]
    async fn a_macula_12_station_is_reached_and_its_leaf_handed_back() {
        let (_station, port, certificate) = macula_station();
        let dialed = dial_target(&target(port))
            .await
            .expect("the station is reached");
        assert_eq!(dialed.leaf, certificate.to_vec());
        dialed.connection.close(0u32.into(), b"done");
    }

    #[tokio::test]
    async fn a_target_without_an_expected_node_id_is_refused_before_dialing() {
        let mut unpinned = target(9);
        unpinned.expected_node_id = [0u8; 32];
        assert!(matches!(
            dial_target(&unpinned).await,
            Err(DialError::NoExpectedNodeId)
        ));
    }

    #[tokio::test]
    async fn a_station_with_a_classical_certificate_is_refused() {
        let (certificate, key) = classical_certificate();
        let provider = CryptoProvider {
            kx_groups: vec![aws::SECP256R1MLKEM768],
            ..rustls::crypto::aws_lc_rs::default_provider()
        };
        let config = ServerConfig::builder_with_provider(Arc::new(provider))
            .with_protocol_versions(&[&rustls::version::TLS13])
            .expect("TLS 1.3")
            .with_no_client_auth()
            .with_single_cert(vec![certificate], key)
            .expect("a station configuration");
        let (_station, port) = station(config, ALPN);
        assert!(matches!(
            dial_target(&target(port)).await,
            Err(DialError::Connection(_))
        ));
    }

    #[tokio::test]
    async fn a_classical_only_station_is_refused() {
        let (certificate, key) = classical_certificate();
        let provider = CryptoProvider {
            kx_groups: vec![aws::X25519, aws::SECP256R1, aws::SECP384R1],
            ..rustls::crypto::aws_lc_rs::default_provider()
        };
        let config = ServerConfig::builder_with_provider(Arc::new(provider))
            .with_protocol_versions(&[&rustls::version::TLS13])
            .expect("TLS 1.3")
            .with_no_client_auth()
            .with_single_cert(vec![certificate], key)
            .expect("a station configuration");
        let (_station, port) = station(config, ALPN);
        assert!(matches!(
            dial_target(&target(port)).await,
            Err(DialError::Connection(_))
        ));
    }

    #[tokio::test]
    async fn a_station_that_does_not_speak_macula_is_refused() {
        let (certificate, key) = mldsa_certificate();
        let config = macula_pqc::server_builder()
            .with_no_client_auth()
            .with_single_cert(vec![certificate], key)
            .expect("a station configuration");
        let (_station, port) = station(config, b"h3");
        assert!(matches!(
            dial_target(&target(port)).await,
            Err(DialError::Connection(_))
        ));
    }
}