Skip to main content

openmoat_proxy/
tls.rs

1//! TLS termination for brokered secrets (ADR-043).
2//!
3//! For a host whose secret says `tls: terminate`, `openmoat run`'s proxy ends
4//! the agent's TLS itself, with a leaf for the host that its own certificate
5//! authority signs, reads and checks each request in the clear, puts the value
6//! in, and opens its own TLS to the host, verified against the system's roots
7//! with the same server name. The agent trusts that CA only inside the run.
8//!
9//! - The CA ([`new_ca`]) is name-constrained to the terminated hosts, so it
10//!   cannot vouch for any other name, and is made by a person (`openmoat proxy
11//!   ca init`); the CLI keeps it where no sandbox lets the agent read and pins
12//!   it in the policy lock.
13//! - [`Tls::new`] checks, for each host, that a leaf the CA signs verifies for
14//!   that host and today, the way a client will check it.
15//! - Only `http/1.1` is offered to the agent, so a client that speaks HTTP/2
16//!   alone fails the handshake instead of sending what the proxy cannot read.
17
18mod ca;
19mod date;
20
21use std::collections::HashMap;
22use std::sync::{Arc, Mutex};
23
24use rustls::client::WebPkiServerVerifier;
25use rustls::client::danger::ServerCertVerifier as _;
26use rustls::crypto::CryptoProvider;
27use rustls::pki_types::pem::PemObject as _;
28use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime};
29use rustls::sign::CertifiedKey;
30use rustls::{ClientConfig, ClientConnection, RootCertStore, ServerConfig, ServerConnection};
31use zeroize::Zeroizing;
32
33pub(crate) use self::date::Day;
34
35/// Rule id of a terminated tunnel refused for its TLS: a handshake that
36/// failed on either side, or an upstream certificate that did not verify.
37pub const RULE_TLS: &str = "proxy-tls";
38
39/// The one application protocol the proxy speaks inside a terminated tunnel.
40const HTTP_1_1: &[u8] = b"http/1.1";
41
42/// Why a CA cannot be made or used, or a terminated connection set up.
43#[derive(Debug, thiserror::Error)]
44#[non_exhaustive]
45pub enum TlsError {
46    /// No host to make a CA for.
47    #[error("no secret sets `tls: terminate`, so there is no host to make a CA for")]
48    NoHosts,
49    /// The clock reads a day no certificate can be written for.
50    #[error("the system clock is before 1970 or past the year 9000")]
51    Clock,
52    /// The CA key file holds no ECDSA P-256 key.
53    #[error("the CA key is not the ECDSA P-256 key `openmoat proxy ca init` writes")]
54    Key,
55    /// The CA certificate file holds no certificate.
56    #[error("the CA certificate file holds no PEM certificate")]
57    Cert,
58    /// A host no certificate can name.
59    #[error("{0} is not a name a certificate can carry")]
60    Host(String),
61    /// A leaf the CA signs for the host does not verify: another key, a host
62    /// outside its name constraints, or a CA that expired.
63    #[error("the OpenMoat proxy CA does not cover {host} today ({why})")]
64    Uncovered {
65        /// The host.
66        host: String,
67        /// What the verification said.
68        why: String,
69    },
70    /// No root certificate to verify upstream hosts with.
71    #[error("no trusted root certificates found on this system to verify upstream hosts")]
72    NoRoots,
73    /// rcgen could not make a certificate.
74    #[error("making a certificate: {0}")]
75    Certificate(String),
76    /// rustls refused a configuration.
77    #[error("TLS configuration: {0}")]
78    Config(String),
79}
80
81impl TlsError {
82    fn rcgen(e: &rcgen::Error) -> Self {
83        Self::Certificate(e.to_string())
84    }
85
86    fn config(e: &rustls::Error) -> Self {
87        Self::Config(e.to_string())
88    }
89}
90
91/// A new CA, as the files `openmoat proxy ca init` writes. The key is zeroed
92/// when dropped and left out of `Debug`.
93pub struct CaFiles {
94    /// The certificate, PEM: what clients inside `openmoat run` trust.
95    pub cert_pem: String,
96    /// The private key, PKCS #8 PEM: only the proxy reads it.
97    pub key_pem: Zeroizing<String>,
98}
99
100impl std::fmt::Debug for CaFiles {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("CaFiles").finish_non_exhaustive()
103    }
104}
105
106/// Make a CA that may sign for `hosts` (and names under them) alone, valid
107/// from yesterday for a year.
108pub fn new_ca(hosts: &[String]) -> Result<CaFiles, TlsError> {
109    let made = ca::generate(hosts, Day::today()?)?;
110    Ok(CaFiles {
111        cert_pem: made.cert,
112        key_pem: made.key,
113    })
114}
115
116/// How long a CA [`new_ca`] makes is valid, in days.
117pub const CA_VALID_DAYS: i64 = ca::CA_DAYS;
118
119/// What upstream certificates are verified against.
120#[derive(Debug, Clone)]
121pub enum Roots {
122    /// The operating system's trust store (`rustls-native-certs`).
123    System,
124    /// These PEM certificates alone: tests, or a private CA.
125    Pem(String),
126}
127
128/// One proxy run's termination: the CA's signer, the leaves made from it and
129/// the configuration upstream connections use.
130pub struct Tls {
131    provider: Arc<CryptoProvider>,
132    signer: ca::Signer,
133    ca: CertificateDer<'static>,
134    upstream: Arc<ClientConfig>,
135    /// Per host, the server configuration holding today's leaf.
136    leaves: Mutex<HashMap<String, (Day, Arc<ServerConfig>)>>,
137}
138
139impl std::fmt::Debug for Tls {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.debug_struct("Tls").finish_non_exhaustive()
142    }
143}
144
145impl Tls {
146    /// Termination with the CA in `cert_pem` and `key_pem`, verifying
147    /// upstreams against `roots`. Refused unless the key signs leaves that
148    /// verify, today, for every one of `hosts`.
149    pub fn new(
150        cert_pem: &str,
151        key_pem: &str,
152        roots: &Roots,
153        hosts: &[String],
154    ) -> Result<Self, TlsError> {
155        let provider = Arc::new(rustls::crypto::ring::default_provider());
156        let ca = CertificateDer::from_pem_slice(cert_pem.as_bytes()).map_err(|_| TlsError::Cert)?;
157        let tls = Self {
158            signer: ca::Signer::new(key_pem)?,
159            upstream: Arc::new(upstream_config(roots, &provider)?),
160            provider,
161            ca,
162            leaves: Mutex::new(HashMap::new()),
163        };
164        for host in hosts {
165            tls.check(host)?;
166        }
167        Ok(tls)
168    }
169
170    /// Whether a leaf for `host` verifies against the CA alone today, the
171    /// check a client makes: chain, validity and name constraints.
172    fn check(&self, host: &str) -> Result<(), TlsError> {
173        let uncovered = |why: String| TlsError::Uncovered {
174            host: host.to_owned(),
175            why,
176        };
177        let mut roots = RootCertStore::empty();
178        roots
179            .add(self.ca.clone())
180            .map_err(|e| uncovered(e.to_string()))?;
181        let verifier =
182            WebPkiServerVerifier::builder_with_provider(Arc::new(roots), self.provider.clone())
183                .build()
184                .map_err(|e| uncovered(e.to_string()))?;
185        let (leaf, _) = self.signer.leaf(host, Day::today()?)?;
186        let name = server_name(host)?;
187        verifier
188            .verify_server_cert(
189                &CertificateDer::from(leaf),
190                &[],
191                &name,
192                &[],
193                UnixTime::now(),
194            )
195            .map(drop)
196            .map_err(|e| uncovered(e.to_string()))
197    }
198
199    /// A server connection presenting today's leaf for `host`, offering
200    /// `http/1.1` only.
201    pub(crate) fn accept(&self, host: &str) -> Result<ServerConnection, TlsError> {
202        let config = self.server_config(host)?;
203        ServerConnection::new(config).map_err(|e| TlsError::config(&e))
204    }
205
206    /// A client connection to `host`, verified against the upstream roots
207    /// with `host` as the server name.
208    pub(crate) fn connect(&self, host: &str) -> Result<ClientConnection, TlsError> {
209        ClientConnection::new(self.upstream.clone(), server_name(host)?)
210            .map_err(|e| TlsError::config(&e))
211    }
212
213    fn server_config(&self, host: &str) -> Result<Arc<ServerConfig>, TlsError> {
214        let today = Day::today()?;
215        // A poisoned cache only lost leaves; they are made again.
216        let mut leaves = self
217            .leaves
218            .lock()
219            .unwrap_or_else(std::sync::PoisonError::into_inner);
220        if let Some((day, config)) = leaves.get(host)
221            && *day == today
222        {
223            return Ok(config.clone());
224        }
225        let (cert, key) = self.signer.leaf(host, today)?;
226        let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.to_vec()));
227        let certified =
228            CertifiedKey::from_der(vec![CertificateDer::from(cert)], key, &self.provider)
229                .map_err(|e| TlsError::config(&e))?;
230        let mut config = ServerConfig::builder_with_provider(self.provider.clone())
231            .with_safe_default_protocol_versions()
232            .map_err(|e| TlsError::config(&e))?
233            .with_no_client_auth()
234            .with_cert_resolver(Arc::new(rustls::sign::SingleCertAndKey::from(certified)));
235        config.alpn_protocols = vec![HTTP_1_1.to_vec()];
236        let config = Arc::new(config);
237        leaves.insert(host.to_owned(), (today, config.clone()));
238        Ok(config)
239    }
240}
241
242fn server_name(host: &str) -> Result<ServerName<'static>, TlsError> {
243    ServerName::try_from(host.to_owned()).map_err(|_| TlsError::Host(host.to_owned()))
244}
245
246fn upstream_config(
247    roots: &Roots,
248    provider: &Arc<CryptoProvider>,
249) -> Result<ClientConfig, TlsError> {
250    let mut store = RootCertStore::empty();
251    match roots {
252        Roots::System => {
253            // Certificates the store holds that rustls cannot use are skipped,
254            // as browsers skip them.
255            let found = rustls_native_certs::load_native_certs();
256            store.add_parsable_certificates(found.certs);
257        }
258        Roots::Pem(pem) => {
259            let certs = CertificateDer::pem_slice_iter(pem.as_bytes())
260                .collect::<Result<Vec<_>, _>>()
261                .map_err(|_| TlsError::Cert)?;
262            store.add_parsable_certificates(certs);
263        }
264    }
265    if store.is_empty() {
266        return Err(TlsError::NoRoots);
267    }
268    let mut config = ClientConfig::builder_with_provider(provider.clone())
269        .with_safe_default_protocol_versions()
270        .map_err(|e| TlsError::config(&e))?
271        .with_root_certificates(store)
272        .with_no_client_auth();
273    config.alpn_protocols = vec![HTTP_1_1.to_vec()];
274    Ok(config)
275}
276
277#[cfg(test)]
278mod tests;