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}