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}