Skip to main content

pamoja_mqtt/
tls.rs

1//! TLS between the client and the broker.
2//!
3//! The connection runs over rustls with its ring provider, chosen explicitly so the client
4//! works whatever other TLS providers a program links. MQTT over TLS conventionally uses
5//! port 8883, and the broker's certificate is checked against the host name the
6//! configuration connects to.
7
8use std::sync::Arc;
9
10use pamoja_core::{Error, Result};
11use rustls::crypto::ring::default_provider;
12use rustls::{ClientConfig, RootCertStore};
13use rustls_pki_types::pem::PemObject;
14use rustls_pki_types::{CertificateDer, PrivateKeyDer};
15
16/// How a connection to the broker is secured.
17///
18/// Start from [`Tls::system_roots`] for a broker whose certificate a public authority
19/// issued, or [`Tls::with_ca_pem`] for one whose certificate comes from an authority of its
20/// own, and add [`client_certificate`](Tls::client_certificate) when the broker asks each
21/// client to prove who it is.
22///
23/// # Examples
24///
25/// ```
26/// use pamoja_mqtt::{MqttConfig, Tls};
27///
28/// // A hosted broker with a publicly issued certificate, on the MQTT over TLS port.
29/// let config = MqttConfig::new("sensor-1", "broker.example.com", 8883).tls(Tls::system_roots());
30/// assert!(config.uses_tls());
31/// ```
32#[derive(Clone)]
33pub struct Tls {
34    roots: Roots,
35    identity: Option<(Vec<u8>, Vec<u8>)>,
36}
37
38#[derive(Clone)]
39enum Roots {
40    System,
41    Pem(Vec<u8>),
42}
43
44impl Tls {
45    /// Trusts the certificate authorities the operating system trusts.
46    ///
47    /// # Returns
48    ///
49    /// The TLS settings, presenting no client certificate.
50    pub fn system_roots() -> Tls {
51        Tls {
52            roots: Roots::System,
53            identity: None,
54        }
55    }
56
57    /// Trusts only the certificate authorities in a PEM file.
58    ///
59    /// A broker on a private network usually holds a certificate its operator issued, so
60    /// the client trusts that operator's authority and nothing else.
61    ///
62    /// # Arguments
63    ///
64    /// * `pem` - one or more `CERTIFICATE` blocks.
65    ///
66    /// # Returns
67    ///
68    /// The TLS settings, presenting no client certificate.
69    pub fn with_ca_pem(pem: impl Into<Vec<u8>>) -> Tls {
70        Tls {
71            roots: Roots::Pem(pem.into()),
72            identity: None,
73        }
74    }
75
76    /// Presents a client certificate, for a broker that authenticates each client by one.
77    ///
78    /// # Arguments
79    ///
80    /// * `certificate_pem` - the client's certificate, followed by any intermediates.
81    /// * `key_pem` - the certificate's private key, in PKCS #8, PKCS #1, or SEC1 form.
82    ///
83    /// # Returns
84    ///
85    /// The updated settings, for chaining.
86    pub fn client_certificate(
87        mut self,
88        certificate_pem: impl Into<Vec<u8>>,
89        key_pem: impl Into<Vec<u8>>,
90    ) -> Tls {
91        self.identity = Some((certificate_pem.into(), key_pem.into()));
92        self
93    }
94
95    /// Builds the rustls configuration these settings describe.
96    ///
97    /// # Returns
98    ///
99    /// The configuration, shared.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`Error::Transport`] when a PEM file holds no usable certificate or key, the
104    /// system trusts no authority rustls can read, or the key does not match the
105    /// certificate.
106    pub(crate) fn client_config(&self) -> Result<Arc<ClientConfig>> {
107        let mut roots = RootCertStore::empty();
108        match &self.roots {
109            Roots::System => {
110                let found = rustls_native_certs::load_native_certs();
111                for certificate in found.certs {
112                    let _ = roots.add(certificate);
113                }
114                if roots.is_empty() {
115                    return Err(Error::Transport(
116                        "the system trusts no certificate authority this client can read".into(),
117                    ));
118                }
119            }
120            Roots::Pem(pem) => {
121                for certificate in certificates(pem, "the CA file")? {
122                    roots.add(certificate).map_err(|error| {
123                        Error::Transport(format!(
124                            "the CA file holds a certificate rustls refuses: {error}"
125                        ))
126                    })?;
127                }
128            }
129        }
130        let builder = ClientConfig::builder_with_provider(Arc::new(default_provider()))
131            .with_safe_default_protocol_versions()
132            .map_err(|error| Error::Transport(error.to_string()))?
133            .with_root_certificates(roots);
134        let config = match &self.identity {
135            None => builder.with_no_client_auth(),
136            Some((certificate, key)) => {
137                let chain = certificates(certificate, "the client certificate")?;
138                let key = PrivateKeyDer::from_pem_slice(key).map_err(|error| {
139                    Error::Transport(format!("the client key is not a PEM private key: {error}"))
140                })?;
141                builder.with_client_auth_cert(chain, key).map_err(|error| {
142                    Error::Transport(format!("the client certificate cannot be used: {error}"))
143                })?
144            }
145        };
146        Ok(Arc::new(config))
147    }
148}
149
150// Private keys never reach a log or a panic message through this type.
151impl core::fmt::Debug for Tls {
152    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153        let roots = match self.roots {
154            Roots::System => "system",
155            Roots::Pem(_) => "pem",
156        };
157        f.debug_struct("Tls")
158            .field("roots", &roots)
159            .field("client_certificate", &self.identity.is_some())
160            .finish()
161    }
162}
163
164fn certificates(pem: &[u8], what: &str) -> Result<Vec<CertificateDer<'static>>> {
165    let found = CertificateDer::pem_slice_iter(pem)
166        .collect::<core::result::Result<Vec<_>, _>>()
167        .map_err(|error| Error::Transport(format!("{what} is not PEM: {error}")))?;
168    if found.is_empty() {
169        return Err(Error::Transport(format!(
170            "{what} holds no CERTIFICATE block"
171        )));
172    }
173    Ok(found)
174}