Skip to main content

navcore_signalk_client/
trust.rs

1//! Deciding which server is the boat's.
2//!
3//! # Why not the public certificate authorities
4//!
5//! A boat's server has no public name and no route to a public authority:
6//! it answers to `something.local` on a network with no internet behind it,
7//! so nothing like Let's Encrypt can ever vouch for it. The usual reflex is
8//! a self-signed certificate plus a client that accepts any certificate,
9//! and that combination is worse than it looks. It encrypts, so it feels
10//! safe, but it authenticates nothing: a client that accepts anything
11//! cannot tell the boat's server from whatever else answers on that
12//! address, and the first thing it does after connecting is hand over a
13//! bearer token good for a year.
14//!
15//! So the boat runs an authority of its own instead. One key, kept ashore,
16//! signs the server's certificate; every device that should trust the boat
17//! carries the authority's certificate -- public, harmless to copy. A
18//! device then knows it is talking to *this* boat before it sends anything,
19//! and adding a device means installing one file rather than weakening a
20//! check.
21//!
22//! # Why the public roots are left out entirely
23//!
24//! [`Trust::boat_ca`] builds a store containing the boat's authority and
25//! nothing else. Keeping the public roots alongside it would mean any of
26//! several hundred authorities could also vouch for a server here, which is
27//! a much larger surface than the one thing this is trying to establish.
28//!
29//! # Plain text is a decision, not a default
30//!
31//! [`Trust::plaintext`] exists for a server that has no TLS at all, which
32//! is still the common case in the Signal K world. It carries an empty
33//! store rather than a permissive one: point it at a `wss://` URL and the
34//! handshake fails, loudly, instead of quietly accepting a stranger.
35
36use std::sync::Arc;
37
38use crate::ClientError;
39
40/// Which servers this client is willing to believe.
41#[derive(Clone)]
42pub struct Trust {
43    /// The accepted authorities, in DER. Empty for plain text.
44    authorities: Arc<Vec<Vec<u8>>>,
45}
46
47impl Trust {
48    /// No certificate is expected. For `http://` and `ws://` servers.
49    ///
50    /// A TLS server offered to this is refused, whatever it presents.
51    #[must_use]
52    pub fn plaintext() -> Self {
53        Self {
54            authorities: Arc::new(Vec::new()),
55        }
56    }
57
58    /// Trust only servers whose certificate the boat's own authority
59    /// signed.
60    ///
61    /// `pem` is the authority's certificate -- `boat-ca.crt`, the public
62    /// half. Several may be concatenated, which is what makes replacing an
63    /// authority possible without taking every device out of service on the
64    /// same afternoon: carry both for one season, then drop the old one.
65    ///
66    /// # Errors
67    ///
68    /// If the PEM holds no certificate at all. A file that was truncated,
69    /// or a private key handed over by mistake, has to fail here rather
70    /// than produce a client that trusts nothing and blames the network.
71    pub fn boat_ca(pem: &[u8]) -> Result<Self, ClientError> {
72        let mut reader = std::io::BufReader::new(pem);
73        let authorities: Vec<Vec<u8>> = rustls_pemfile::certs(&mut reader)
74            .filter_map(Result::ok)
75            .map(|der| der.to_vec())
76            .collect();
77
78        if authorities.is_empty() {
79            return Err(ClientError::Trust(
80                "no certificate in the authority file".to_owned(),
81            ));
82        }
83
84        Ok(Self {
85            authorities: Arc::new(authorities),
86        })
87    }
88
89    /// Whether anything is trusted, i.e. whether TLS can succeed at all.
90    #[must_use]
91    pub fn is_plaintext(&self) -> bool {
92        self.authorities.is_empty()
93    }
94
95    /// The authorities as rustls wants them, for the WebSocket.
96    pub(crate) fn rustls_config(&self) -> Result<Arc<rustls::ClientConfig>, ClientError> {
97        let mut roots = rustls::RootCertStore::empty();
98        for der in self.authorities.iter() {
99            roots
100                .add(rustls::pki_types::CertificateDer::from(der.clone()))
101                .map_err(|error| ClientError::Trust(error.to_string()))?;
102        }
103
104        // The provider is named rather than taken from process-wide state.
105        // A library that installed a default would be making a decision on
106        // behalf of whatever else links it, and one that read a default
107        // would depend on load order to work.
108        let provider = Arc::new(rustls::crypto::ring::default_provider());
109        let config = rustls::ClientConfig::builder_with_provider(provider)
110            .with_safe_default_protocol_versions()
111            .map_err(|error| ClientError::Trust(error.to_string()))?
112            .with_root_certificates(roots)
113            .with_no_client_auth();
114
115        Ok(Arc::new(config))
116    }
117
118    /// The authorities as ureq wants them, for the REST calls.
119    pub(crate) fn ureq_certificates(&self) -> Vec<ureq::tls::Certificate<'static>> {
120        self.authorities
121            .iter()
122            .map(|der| ureq::tls::Certificate::from_der(der).to_owned())
123            .collect()
124    }
125}
126
127impl std::fmt::Debug for Trust {
128    /// Says how much is trusted, never what. A certificate is not a secret,
129    /// but a log line full of DER helps nobody read a failure.
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        if self.is_plaintext() {
132            write!(f, "Trust(plaintext)")
133        } else {
134            write!(f, "Trust({} authorities)", self.authorities.len())
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    /// A real certificate, so the parsing is tested against the thing it
144    /// will meet rather than against a shape invented here.
145    const CA_PEM: &str = include_str!("../tests/data/test-ca.crt");
146
147    #[test]
148    fn an_authority_is_read_from_pem() {
149        let trust = Trust::boat_ca(CA_PEM.as_bytes()).expect("a certificate");
150        assert!(!trust.is_plaintext());
151        assert_eq!(trust.ureq_certificates().len(), 1);
152        assert!(trust.rustls_config().is_ok());
153    }
154
155    #[test]
156    fn several_authorities_can_be_carried_at_once() {
157        // How an authority is replaced without a flag day: trust both for a
158        // season, then drop the old one.
159        let both = format!("{CA_PEM}{CA_PEM}");
160        let trust = Trust::boat_ca(both.as_bytes()).expect("two certificates");
161        assert_eq!(trust.ureq_certificates().len(), 2);
162    }
163
164    #[test]
165    fn a_file_with_no_certificate_is_an_error() {
166        // A truncated file, or a private key handed over by mistake. Both
167        // would otherwise produce a client that trusts nothing and blames
168        // the network for it.
169        assert!(Trust::boat_ca(b"not a certificate").is_err());
170        assert!(Trust::boat_ca(b"").is_err());
171    }
172
173    #[test]
174    fn plaintext_trusts_nothing_rather_than_everything() {
175        let trust = Trust::plaintext();
176        assert!(trust.is_plaintext());
177        assert!(trust.ureq_certificates().is_empty());
178        // Still builds a config -- with an empty root store, so a TLS
179        // server offered to it is refused instead of accepted.
180        assert!(trust.rustls_config().is_ok());
181    }
182}