Skip to main content

sipx_transport/
tls.rs

1//! SIP over TLS (RFC 3261 §26, RFC 5922).
2//!
3//! A TLS connection differs from a TCP one in its bytes, not in its transaction handling — so
4//! this crate reuses [`crate::tcp`]'s framing and pool wholesale and adds only the handshake
5//! and the verification around it.
6//!
7//! The verification is the point, and `docs/specs/sip-tls.md` settles what it is. Two decisions
8//! from there govern this file:
9//!
10//! **There is no way to turn it off.** No `insecure` flag, no `danger_accept_invalid_certs`.
11//! Code that needs to trust a fixture CA adds that CA as a trust anchor — a different operation
12//! with a different shape, saying *what* to trust rather than *that anything goes*. Every stack
13//! that ships the other kind of flag eventually finds it in production.
14//!
15//! **The name checked is the one sipx set out to reach**, not the name a SRV record led to.
16//! Checking the resolved name would let whoever can influence DNS choose which certificate is
17//! acceptable, and the verification becomes decorative.
18//!
19//! **The version floor is the library's, not ours** (§3.5, RFC 8996). Both configurations below
20//! are built from `rustls`'s default version set, and neither this file nor anything above it
21//! names a version — so 1.0 and 1.1 are excluded because the library has nothing older than 1.2
22//! to select, not because sipx refuses them. That makes the floor a *dependency* property: a
23//! backend that still spoke 1.0 would move it without a line changing here, which is why RFC
24//! 8996's registry row cites `tests/tls_versions.rs` — the refusal observed on the wire, plus the
25//! version set asserted — rather than the sentence in the spec.
26
27use std::sync::Arc;
28
29use rustls_pki_types::pem::PemObject as _;
30use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
31use tokio_rustls::rustls::{ClientConfig, RootCertStore, ServerConfig};
32use tokio_rustls::{TlsAcceptor, TlsConnector};
33
34/// What can go wrong establishing TLS.
35///
36/// The variants are separate because expired, wrong-host and unknown-issuer are three different
37/// operational problems with three different fixes. Collapsing them into "handshake failed"
38/// costs an engineer an afternoon.
39#[derive(Debug, thiserror::Error)]
40#[non_exhaustive]
41pub enum TlsError {
42    /// The name to verify against is not a valid DNS name.
43    #[error("{0} is not a name a certificate can be checked against")]
44    UnusableName(String),
45    /// A certificate or key could not be read.
46    #[error("reading {what}: {detail}")]
47    Material {
48        /// Which file or blob.
49        what: String,
50        /// What was wrong with it.
51        detail: String,
52    },
53    /// The configuration itself is invalid.
54    #[error("tls configuration: {0}")]
55    Config(String),
56    /// The handshake failed — including every verification failure, which rustls reports as an
57    /// alert with its reason attached.
58    #[error("tls handshake with {peer}: {detail}")]
59    Handshake {
60        /// Who we were talking to.
61        peer: String,
62        /// What went wrong, as reported by the TLS library.
63        detail: String,
64    },
65}
66
67/// How sipx behaves as a TLS client.
68#[derive(Clone)]
69pub struct ClientTls {
70    config: Arc<ClientConfig>,
71}
72
73impl std::fmt::Debug for ClientTls {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        // The config holds keys; printing it would put them in a log.
76        f.write_str("ClientTls { .. }")
77    }
78}
79
80/// Which certificates to trust.
81#[derive(Debug, Clone, Default)]
82pub struct TrustAnchors {
83    /// Additional roots, on top of or instead of the system's.
84    extra: Vec<CertificateDer<'static>>,
85    /// Whether to include the platform's own roots.
86    system: bool,
87}
88
89impl TrustAnchors {
90    /// Whatever the platform trusts.
91    ///
92    /// The *platform's* store, not a copy of one vendor's list compiled in. The difference
93    /// matters twice: an operator who adds a corporate CA expects sipx to honour it, and a
94    /// root that is distrusted after a compromise stops being trusted when the OS says so
95    /// rather than when someone remembers to bump a dependency.
96    #[must_use]
97    pub fn system() -> Self {
98        Self {
99            extra: Vec::new(),
100            system: true,
101        }
102    }
103
104    /// Trust only what is added here.
105    ///
106    /// This is what a test uses. Note the shape: it names the CA to trust rather than
107    /// disabling the check, so a mistake produces a *failed* handshake rather than a silently
108    /// accepted one.
109    #[must_use]
110    pub fn only() -> Self {
111        Self {
112            extra: Vec::new(),
113            system: false,
114        }
115    }
116
117    /// Add a PEM-encoded certificate as a trust anchor.
118    pub fn add_pem(&mut self, pem: &[u8]) -> Result<(), TlsError> {
119        let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(pem)
120            .collect::<Result<_, _>>()
121            .map_err(|error| TlsError::Material {
122                what: "trust anchor".to_owned(),
123                detail: error.to_string(),
124            })?;
125        if certs.is_empty() {
126            return Err(TlsError::Material {
127                what: "trust anchor".to_owned(),
128                detail: "no certificate found in the PEM data".to_owned(),
129            });
130        }
131        self.extra.extend(certs);
132        Ok(())
133    }
134
135    fn store(&self) -> Result<RootCertStore, TlsError> {
136        let mut store = RootCertStore::empty();
137        if self.system {
138            let loaded = rustls_native_certs::load_native_certs();
139            for error in &loaded.errors {
140                // Reported rather than swallowed: a partially loaded store fails handshakes
141                // that ought to succeed, and the reason is otherwise invisible.
142                tracing::warn!(%error, "could not read part of the platform trust store");
143            }
144            for cert in loaded.certs {
145                // A platform store may hold a certificate rustls will not parse. Skipping it
146                // is right — one unusable root must not cost the other two hundred.
147                if let Err(error) = store.add(cert) {
148                    tracing::debug!(%error, "skipping an unusable root from the platform store");
149                }
150            }
151        }
152        for cert in &self.extra {
153            store
154                .add(cert.clone())
155                .map_err(|error| TlsError::Config(error.to_string()))?;
156        }
157        if store.is_empty() {
158            return Err(TlsError::Config(
159                "no trust anchors: every certificate would be refused".to_owned(),
160            ));
161        }
162        Ok(store)
163    }
164}
165
166impl ClientTls {
167    /// A client that verifies against these anchors.
168    pub fn new(anchors: &TrustAnchors) -> Result<Self, TlsError> {
169        Self::with_identity(anchors, None)
170    }
171
172    /// A client that also presents a certificate of its own (mutual TLS).
173    ///
174    /// When a server asks for one and none is configured, the handshake proceeds without it and
175    /// the server decides. sipx does not pre-emptively fail, because plenty of servers ask
176    /// optionally.
177    pub fn with_identity(
178        anchors: &TrustAnchors,
179        identity: Option<Identity>,
180    ) -> Result<Self, TlsError> {
181        let roots = anchors.store()?;
182        let builder = ClientConfig::builder().with_root_certificates(roots);
183
184        let config = match identity {
185            Some(identity) => builder
186                .with_client_auth_cert(identity.chain, identity.key)
187                .map_err(|error| TlsError::Config(error.to_string()))?,
188            None => builder.with_no_client_auth(),
189        };
190
191        Ok(Self {
192            config: Arc::new(config),
193        })
194    }
195
196    /// A connector for one peer.
197    #[must_use]
198    pub fn connector(&self) -> TlsConnector {
199        TlsConnector::from(Arc::clone(&self.config))
200    }
201
202    /// Reuse this exact trust and identity policy for a QUIC connection.
203    #[cfg(feature = "quic")]
204    pub(crate) fn quic_config(&self) -> Result<quinn::ClientConfig, TlsError> {
205        let config = self.quic_rustls_config();
206        let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(config)
207            .map_err(|error| TlsError::Config(error.to_string()))?;
208        Ok(quinn::ClientConfig::new(Arc::new(crypto)))
209    }
210
211    #[cfg(feature = "quic")]
212    fn quic_rustls_config(&self) -> ClientConfig {
213        let mut config = (*self.config).clone();
214        config.alpn_protocols = vec![b"sip/2".to_vec()];
215        config.enable_early_data = false;
216        config
217    }
218}
219
220/// A certificate and key sipx presents.
221pub struct Identity {
222    chain: Vec<CertificateDer<'static>>,
223    key: PrivateKeyDer<'static>,
224}
225
226impl std::fmt::Debug for Identity {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        // The key is deliberately opaque in every diagnostic, including a refused reload.
229        f.write_str("Identity { .. }")
230    }
231}
232
233impl Identity {
234    /// Read a certificate chain and key from PEM.
235    pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> Result<Self, TlsError> {
236        let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(cert_pem)
237            .collect::<Result<_, _>>()
238            .map_err(|error| TlsError::Material {
239                what: "certificate".to_owned(),
240                detail: error.to_string(),
241            })?;
242        if chain.is_empty() {
243            return Err(TlsError::Material {
244                what: "certificate".to_owned(),
245                detail: "no certificate found in the PEM data".to_owned(),
246            });
247        }
248
249        let key = PrivateKeyDer::from_pem_slice(key_pem).map_err(|error| TlsError::Material {
250            what: "private key".to_owned(),
251            detail: error.to_string(),
252        })?;
253
254        Ok(Self { chain, key })
255    }
256
257    /// Prove that every supplied issuer certificate belongs to the server chain.
258    ///
259    /// A server normally omits its trust root, so the last supplied certificate is the path's
260    /// provisional anchor. With a leaf alone there is no issuer material to validate here; the
261    /// peer still validates that leaf against its own anchors during the handshake. With two or
262    /// more certificates, every certificate after the leaf must be consumed, in the supplied
263    /// order, by one valid server-authentication path. That rejects both malformed certificates
264    /// and harmless-looking unrelated extras before a listener can publish them.
265    fn validate_server_chain(&self) -> Result<(), TlsError> {
266        let Some((leaf, issuers)) = self.chain.split_first() else {
267            return Err(TlsError::Config(
268                "server certificate chain has no leaf".to_owned(),
269            ));
270        };
271        let end_entity = webpki::EndEntityCert::try_from(leaf).map_err(|error| {
272            TlsError::Config(format!("invalid server certificate leaf: {error}"))
273        })?;
274        let Some((anchor_certificate, intermediates)) = issuers.split_last() else {
275            return Ok(());
276        };
277        let anchor = webpki::anchor_from_trusted_cert(anchor_certificate).map_err(|error| {
278            TlsError::Config(format!("invalid server certificate chain anchor: {error}"))
279        })?;
280        let provider = tokio_rustls::rustls::crypto::ring::default_provider();
281        let anchors = [anchor];
282        let verified = end_entity
283            .verify_for_usage(
284                provider.signature_verification_algorithms.all,
285                &anchors,
286                intermediates,
287                UnixTime::now(),
288                webpki::KeyUsage::server_auth(),
289                None,
290                None,
291            )
292            .map_err(|error| {
293                TlsError::Config(format!("invalid server certificate chain: {error}"))
294            })?;
295        let supplied_in_order = verified
296            .intermediate_certificates()
297            .map(webpki::Cert::der)
298            .eq(intermediates.iter().cloned());
299        if !supplied_in_order {
300            return Err(TlsError::Config(
301                "server certificate chain contains an unrelated or out-of-order certificate"
302                    .to_owned(),
303            ));
304        }
305        Ok(())
306    }
307}
308
309/// How sipx behaves as a TLS server.
310#[derive(Clone)]
311pub struct ServerTls {
312    config: Arc<ServerConfig>,
313}
314
315impl std::fmt::Debug for ServerTls {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        f.write_str("ServerTls { .. }")
318    }
319}
320
321impl ServerTls {
322    /// A server presenting this identity, not asking for a client certificate.
323    pub fn new(identity: Identity) -> Result<Self, TlsError> {
324        identity.validate_server_chain()?;
325        let config = ServerConfig::builder()
326            .with_no_client_auth()
327            .with_single_cert(identity.chain, identity.key)
328            .map_err(|error| TlsError::Config(error.to_string()))?;
329        Ok(Self {
330            config: Arc::new(config),
331        })
332    }
333
334    /// An acceptor for incoming connections.
335    #[must_use]
336    pub fn acceptor(&self) -> TlsAcceptor {
337        TlsAcceptor::from(Arc::clone(&self.config))
338    }
339
340    /// Reuse this exact identity policy for the QUIC handshake.
341    #[cfg(feature = "quic")]
342    pub(crate) fn quic_config(&self) -> Result<quinn::ServerConfig, TlsError> {
343        let config = self.quic_rustls_config();
344        let crypto = quinn::crypto::rustls::QuicServerConfig::try_from(config)
345            .map_err(|error| TlsError::Config(error.to_string()))?;
346        Ok(quinn::ServerConfig::with_crypto(Arc::new(crypto)))
347    }
348
349    #[cfg(feature = "quic")]
350    fn quic_rustls_config(&self) -> ServerConfig {
351        let mut config = (*self.config).clone();
352        config.alpn_protocols = vec![b"sip/2".to_vec()];
353        config.max_early_data_size = 0;
354        config
355    }
356}
357
358/// The name a certificate is checked against.
359///
360/// **The host from the URI sipx set out to reach**, not the name resolution produced. If the
361/// resolved name were used, anyone who can influence DNS would choose which certificate is
362/// acceptable — the handshake would still succeed, the check would still appear to run, and it
363/// would mean nothing.
364pub fn verification_name(uri_host: &str) -> Result<ServerName<'static>, TlsError> {
365    ServerName::try_from(uri_host.to_owned())
366        .map_err(|_| TlsError::UnusableName(uri_host.to_owned()))
367}
368
369#[cfg(test)]
370#[allow(
371    clippy::unwrap_used,
372    clippy::expect_used,
373    clippy::panic,
374    clippy::indexing_slicing
375)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn a_hostname_is_a_usable_verification_name() {
381        assert!(verification_name("sip.example.com").is_ok());
382        assert!(verification_name("example.com").is_ok());
383    }
384
385    /// An IP address is a usable server name in TLS, but a SIP URI naming one has no domain
386    /// identity to check — the caller has to decide what that means rather than have this
387    /// function guess.
388    #[test]
389    fn an_address_is_accepted_as_a_name() {
390        assert!(verification_name("192.0.2.1").is_ok());
391    }
392
393    #[test]
394    fn something_that_is_not_a_name_is_refused_by_name() {
395        let error = verification_name("not a hostname!").expect_err("refused");
396        assert!(error.to_string().contains("not a hostname!"), "{error}");
397    }
398
399    /// Trusting nothing is a configuration error rather than a silent refusal of everything:
400    /// the second would look like a network problem at every call site.
401    #[test]
402    fn a_client_with_no_anchors_is_refused_at_construction() {
403        let error = ClientTls::new(&TrustAnchors::only()).expect_err("refused");
404        assert!(error.to_string().contains("no trust anchors"), "{error}");
405    }
406
407    #[test]
408    fn the_system_anchors_are_enough_to_build_a_client() {
409        assert!(ClientTls::new(&TrustAnchors::system()).is_ok());
410    }
411
412    #[test]
413    fn pem_that_holds_no_certificate_is_refused_by_name() {
414        let mut anchors = TrustAnchors::only();
415        let error = anchors.add_pem(b"not a certificate").expect_err("refused");
416        assert!(error.to_string().contains("no certificate"), "{error}");
417    }
418
419    #[test]
420    fn an_identity_needs_both_halves() {
421        let error = Identity::from_pem(b"", b"").expect_err("refused");
422        assert!(error.to_string().contains("certificate"), "{error}");
423    }
424
425    /// The configuration holds private keys. A `Debug` that printed them would put them in
426    /// whatever log the caller writes.
427    #[test]
428    fn debug_output_does_not_leak_key_material() {
429        let client = ClientTls::new(&TrustAnchors::system()).expect("builds");
430        let printed = format!("{client:?}");
431        assert_eq!(printed, "ClientTls { .. }");
432
433        let ca = sipx_testkit::certs::Ca::new();
434        let (certificate, key) = ca.issue_for("localhost");
435        let identity =
436            Identity::from_pem(certificate.as_bytes(), key.as_bytes()).expect("identity");
437        assert_eq!(format!("{identity:?}"), "Identity { .. }");
438    }
439
440    #[cfg(feature = "quic")]
441    #[test]
442    fn quic_requires_sip2_and_refuses_early_data_in_both_directions() {
443        let client = ClientTls::new(&TrustAnchors::system()).expect("client");
444        let client = client.quic_rustls_config();
445        assert_eq!(client.alpn_protocols, [b"sip/2".to_vec()]);
446        assert!(!client.enable_early_data);
447
448        let ca = sipx_testkit::certs::Ca::new();
449        let (certificate, key) = ca.issue_for("localhost");
450        let identity =
451            Identity::from_pem(certificate.as_bytes(), key.as_bytes()).expect("identity");
452        let server = ServerTls::new(identity)
453            .expect("server")
454            .quic_rustls_config();
455        assert_eq!(server.alpn_protocols, [b"sip/2".to_vec()]);
456        assert_eq!(server.max_early_data_size, 0);
457    }
458
459    /// Q13: even a client holding early-data-capable resumption state cannot deliver a request
460    /// before sipx's server handshake completes; its retry is exposed only as a 1-RTT stream.
461    #[cfg(feature = "quic")]
462    #[tokio::test]
463    async fn a_resumed_client_cannot_deliver_early_data_to_a_sipx_server() {
464        let ca = sipx_testkit::certs::Ca::new();
465        let (certificate, key) = ca.issue_for("localhost");
466        let identity =
467            Identity::from_pem(certificate.as_bytes(), key.as_bytes()).expect("identity");
468        let server_policy = ServerTls::new(identity).expect("server policy");
469
470        // The first handshake deliberately issues an early-data-capable ticket. The second
471        // configuration comes through sipx's production conversion and shares the underlying
472        // rustls ticket machinery through the cloned policy.
473        let mut permissive = server_policy.quic_rustls_config();
474        permissive.max_early_data_size = u32::MAX;
475        let permissive = quinn::crypto::rustls::QuicServerConfig::try_from(permissive)
476            .map(|crypto| quinn::ServerConfig::with_crypto(Arc::new(crypto)))
477            .expect("permissive ticket server");
478        let rejecting = server_policy.quic_config().expect("sipx QUIC server");
479        let server =
480            quinn::Endpoint::server(permissive, "127.0.0.1:0".parse().expect("server address"))
481                .expect("server endpoint");
482        let server_addr = server.local_addr().expect("server address");
483
484        let mut anchors = TrustAnchors::only();
485        anchors
486            .add_pem(ca.pem().as_bytes())
487            .expect("test authority");
488        let mut client_tls = ClientTls::new(&anchors)
489            .expect("client policy")
490            .quic_rustls_config();
491        client_tls.enable_early_data = true;
492        let client_crypto = quinn::crypto::rustls::QuicClientConfig::try_from(client_tls)
493            .expect("early-data client");
494        let mut client_config = quinn::ClientConfig::new(Arc::new(client_crypto));
495        client_config.transport_config(crate::quic::transport_config());
496        let mut client = quinn::Endpoint::client("127.0.0.1:0".parse().expect("client address"))
497            .expect("client endpoint");
498        client.set_default_client_config(client_config);
499
500        let (ready, configured) = tokio::sync::oneshot::channel();
501        let server_task = tokio::spawn(async move {
502            let first = server
503                .accept()
504                .await
505                .expect("first connection")
506                .await
507                .expect("first handshake");
508            let (mut marker, _unused) = first.open_bi().await.expect("1-RTT marker stream");
509            marker.write_all(b"ready").await.expect("1-RTT marker");
510            marker.finish().expect("1-RTT marker finishes");
511
512            server.set_server_config(Some(rejecting));
513            ready.send(()).expect("client waits for configuration");
514            let second = server
515                .accept()
516                .await
517                .expect("resumed connection")
518                .await
519                .expect("resumption handshake");
520            let (_reply, mut request) = second.accept_bi().await.expect("request stream");
521            let was_early = request.is_0rtt();
522            let bytes = request.read_to_end(1024).await.expect("request bytes");
523            (was_early, bytes)
524        });
525
526        let first = client
527            .connect(server_addr, "localhost")
528            .expect("first connect starts")
529            .await
530            .expect("first connect");
531        let (_unused, mut marker) = first.accept_bi().await.expect("server marker");
532        assert_eq!(
533            marker.read_to_end(16).await.expect("marker bytes"),
534            b"ready"
535        );
536        drop(first);
537        configured.await.expect("rejecting server installed");
538
539        let (resumed, accepted) = client
540            .connect(server_addr, "localhost")
541            .expect("resumption starts")
542            .into_0rtt()
543            .expect("client has early-data keys");
544        let (mut request, _reply) = resumed.open_bi().await.expect("early request stream");
545        request
546            .write_all(b"SIP request attempted as early data")
547            .await
548            .expect("early write is queued");
549        request.finish().expect("request finishes");
550        assert!(!accepted.await, "sipx accepted replayable early data");
551        let (mut request, _reply) = resumed.open_bi().await.expect("1-RTT request stream");
552        request
553            .write_all(b"SIP request attempted as early data")
554            .await
555            .expect("1-RTT retry writes");
556        request.finish().expect("1-RTT retry finishes");
557        let (was_early, bytes) = server_task.await.expect("server task");
558        assert!(!was_early, "the server exposed a 0-RTT request stream");
559        assert_eq!(bytes, b"SIP request attempted as early data");
560    }
561}