Skip to main content

isb_server/servers/
pki.rs

1//! The control plane's CA for its agents (P5.1).
2//!
3//! One CA under `<state>/servers/pki/` (key 0600) issues two kinds of leaf:
4//! - a **server** certificate per agent (EKU serverAuth only, SAN = the
5//!   address the control plane dials), with its key, both sent to the agent
6//!   at bootstrap and on rotation;
7//! - one **client** certificate for the control plane itself (EKU
8//!   clientAuth only, CN [`CLIENT_CN`]).
9//!
10//! The agent trusts this CA alone and checks the clientAuth EKU (webpki
11//! does), so another agent's server certificate cannot call an agent, and
12//! nothing the control plane did not issue can.
13
14use std::net::IpAddr;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18use rcgen::{
19    BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair,
20    KeyUsagePurpose, SanType,
21};
22use rustls::pki_types::pem::PemObject;
23use rustls::pki_types::{CertificateDer, PrivateKeyDer};
24
25use crate::error::{Error, Result};
26
27const CA_CN: &str = "isb control plane CA";
28/// The control plane's client certificate's common name.
29pub const CLIENT_CN: &str = "isb-control-plane";
30const CA_YEARS: i64 = 10;
31/// Agent and client leaves; `isb server rotate-cert` reissues an agent's.
32pub const LEAF_DAYS: i64 = 397;
33
34fn err(step: &str, e: impl std::fmt::Display) -> Error {
35    Error::invalid(format!("servers TLS: {step}: {e}"))
36}
37
38fn ca_params() -> CertificateParams {
39    let mut p = CertificateParams::default();
40    p.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
41    p.distinguished_name = rcgen::DistinguishedName::new();
42    p.distinguished_name.push(DnType::CommonName, CA_CN);
43    p.key_usages = vec![
44        KeyUsagePurpose::KeyCertSign,
45        KeyUsagePurpose::CrlSign,
46        KeyUsagePurpose::DigitalSignature,
47    ];
48    p
49}
50
51/// Write `text` to `path` with mode 0600, atomically.
52#[doc(hidden)]
53pub fn write_private(path: &Path, text: &str) -> Result<()> {
54    use std::io::Write;
55    use std::os::unix::fs::OpenOptionsExt;
56    let tmp = path.with_extension("tmp");
57    let mut f = std::fs::OpenOptions::new()
58        .write(true)
59        .create(true)
60        .truncate(true)
61        .mode(0o600)
62        .open(&tmp)?;
63    f.write_all(text.as_bytes())?;
64    f.sync_all()?;
65    std::fs::rename(&tmp, path)?;
66    Ok(())
67}
68
69/// A certificate and its key, PEM.
70#[derive(Debug, Clone)]
71pub struct Leaf {
72    pub cert: String,
73    pub key: String,
74}
75
76impl Leaf {
77    /// SHA-256 of the certificate's DER, hex.
78    pub fn fingerprint(&self) -> Result<String> {
79        fingerprint_pem(&self.cert)
80    }
81}
82
83/// SHA-256 of a PEM certificate's DER, hex.
84pub fn fingerprint_pem(pem: &str) -> Result<String> {
85    let der = CertificateDer::from_pem_slice(pem.as_bytes()).map_err(|e| err("read a cert", e))?;
86    Ok(hex(ring::digest::digest(
87        &ring::digest::SHA256,
88        der.as_ref(),
89    )
90    .as_ref()))
91}
92
93pub(crate) fn hex(b: &[u8]) -> String {
94    b.iter().map(|x| format!("{x:02x}")).collect()
95}
96
97/// The control plane's CA, loaded or made on first use.
98pub struct Ca {
99    dir: PathBuf,
100    key: KeyPair,
101    pub cert_pem: String,
102}
103
104impl std::fmt::Debug for Ca {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("Ca").field("dir", &self.dir).finish()
107    }
108}
109
110impl Ca {
111    /// The CA in `dir` (made with key 0600 in a 0700 directory if missing),
112    /// and the control plane's client leaf beside it.
113    pub fn open(dir: &Path) -> Result<Ca> {
114        std::fs::create_dir_all(dir)?;
115        {
116            use std::os::unix::fs::PermissionsExt;
117            std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
118        }
119        let (key_path, cert_path) = (dir.join("ca.key"), dir.join("ca.crt"));
120        let now = time::OffsetDateTime::now_utc();
121        let key = match std::fs::read_to_string(&key_path) {
122            Ok(pem) => KeyPair::from_pem(&pem).map_err(|e| err("read the CA key", e))?,
123            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
124                let k = KeyPair::generate().map_err(|e| err("generate the CA key", e))?;
125                let mut p = ca_params();
126                p.not_before = now - time::Duration::days(1);
127                p.not_after = now + time::Duration::days(365 * CA_YEARS);
128                let c = p
129                    .self_signed(&k)
130                    .map_err(|e| err("sign the CA certificate", e))?;
131                write_private(&key_path, &k.serialize_pem())?;
132                std::fs::write(&cert_path, c.pem())?;
133                // A new CA invalidates the old client leaf.
134                let _ = std::fs::remove_file(dir.join("client.crt"));
135                k
136            }
137            Err(e) => return Err(e.into()),
138        };
139        let cert_pem = std::fs::read_to_string(&cert_path)?;
140        let ca = Ca {
141            dir: dir.to_path_buf(),
142            key,
143            cert_pem,
144        };
145        if !(dir.join("client.crt").exists() && dir.join("client.key").exists()) {
146            let l = ca.issue_client()?;
147            write_private(&dir.join("client.key"), &l.key)?;
148            std::fs::write(dir.join("client.crt"), &l.cert)?;
149        }
150        Ok(ca)
151    }
152
153    fn leaf(&self, params: CertificateParams, what: &str) -> Result<Leaf> {
154        let issuer = Issuer::new(ca_params(), &self.key);
155        let k = KeyPair::generate().map_err(|e| err(&format!("generate the {what} key"), e))?;
156        let c = params
157            .signed_by(&k, &issuer)
158            .map_err(|e| err(&format!("sign the {what} certificate"), e))?;
159        Ok(Leaf {
160            cert: c.pem(),
161            key: k.serialize_pem(),
162        })
163    }
164
165    fn leaf_params(cn: &str) -> CertificateParams {
166        let now = time::OffsetDateTime::now_utc();
167        let mut p = CertificateParams::default();
168        p.distinguished_name = rcgen::DistinguishedName::new();
169        p.distinguished_name.push(DnType::CommonName, cn);
170        p.key_usages = vec![KeyUsagePurpose::DigitalSignature];
171        p.not_before = now - time::Duration::days(1);
172        p.not_after = now + time::Duration::days(LEAF_DAYS);
173        p
174    }
175
176    /// An agent's server certificate for `address` (an IP or a DNS name).
177    pub fn issue_server(&self, name: &str, address: &str) -> Result<Leaf> {
178        let mut p = Self::leaf_params(&format!("isb agent {name}"));
179        p.subject_alt_names = vec![match address.parse::<IpAddr>() {
180            Ok(ip) => SanType::IpAddress(ip),
181            Err(_) => SanType::DnsName(
182                address
183                    .to_string()
184                    .try_into()
185                    .map_err(|e| err("the address as a DNS name", e))?,
186            ),
187        }];
188        p.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
189        self.leaf(p, "agent")
190    }
191
192    /// A client certificate (the control plane's).
193    pub fn issue_client(&self) -> Result<Leaf> {
194        let mut p = Self::leaf_params(CLIENT_CN);
195        p.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
196        self.leaf(p, "client")
197    }
198
199    /// The control plane's client leaf.
200    pub fn client(&self) -> Result<Leaf> {
201        Ok(Leaf {
202            cert: std::fs::read_to_string(self.dir.join("client.crt"))?,
203            key: std::fs::read_to_string(self.dir.join("client.key"))?,
204        })
205    }
206
207    /// What the control plane dials agents with: this CA as the only root,
208    /// and its client certificate.
209    pub fn client_config(&self) -> Result<Arc<rustls::ClientConfig>> {
210        client_config(&self.cert_pem, &self.client()?)
211    }
212}
213
214fn provider() -> Arc<rustls::crypto::CryptoProvider> {
215    Arc::new(rustls::crypto::ring::default_provider())
216}
217
218fn certs(pem: &str) -> Result<Vec<CertificateDer<'static>>> {
219    let v: Vec<_> = CertificateDer::pem_slice_iter(pem.as_bytes())
220        .collect::<std::result::Result<_, _>>()
221        .map_err(|e| err("read certificates", e))?;
222    if v.is_empty() {
223        return Err(err("read certificates", "no certificate in the PEM"));
224    }
225    Ok(v)
226}
227
228fn key(pem: &str) -> Result<PrivateKeyDer<'static>> {
229    PrivateKeyDer::from_pem_slice(pem.as_bytes()).map_err(|e| err("read a private key", e))
230}
231
232fn roots(ca_pem: &str) -> Result<rustls::RootCertStore> {
233    let mut r = rustls::RootCertStore::empty();
234    for c in certs(ca_pem)? {
235        r.add(c).map_err(|e| err("add the CA", e))?;
236    }
237    Ok(r)
238}
239
240/// A client config trusting only `ca_pem`, presenting `leaf`.
241pub fn client_config(ca_pem: &str, leaf: &Leaf) -> Result<Arc<rustls::ClientConfig>> {
242    Ok(Arc::new(
243        rustls::ClientConfig::builder_with_provider(provider())
244            .with_safe_default_protocol_versions()
245            .map_err(|e| err("TLS versions", e))?
246            .with_root_certificates(roots(ca_pem)?)
247            .with_client_auth_cert(certs(&leaf.cert)?, key(&leaf.key)?)
248            .map_err(|e| err("the client certificate", e))?,
249    ))
250}
251
252/// The agent's server config: `leaf` as its identity, and only clients with
253/// a clientAuth certificate from `ca_pem`.
254pub fn server_config(ca_pem: &str, leaf: &Leaf) -> Result<Arc<rustls::ServerConfig>> {
255    let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
256        Arc::new(roots(ca_pem)?),
257        provider(),
258    )
259    .build()
260    .map_err(|e| err("the client verifier", e))?;
261    Ok(Arc::new(
262        rustls::ServerConfig::builder_with_provider(provider())
263            .with_safe_default_protocol_versions()
264            .map_err(|e| err("TLS versions", e))?
265            .with_client_cert_verifier(verifier)
266            .with_single_cert(certs(&leaf.cert)?, key(&leaf.key)?)
267            .map_err(|e| err("the server certificate", e))?,
268    ))
269}
270
271/// The files an agent keeps in its TLS directory.
272pub const AGENT_CA: &str = "ca.crt";
273pub const AGENT_CERT: &str = "tls.crt";
274pub const AGENT_KEY: &str = "tls.key";
275
276/// The agent's server config from its TLS directory.
277pub fn agent_server_config(dir: &Path) -> Result<Arc<rustls::ServerConfig>> {
278    let read = |f: &str| {
279        std::fs::read_to_string(dir.join(f))
280            .map_err(|e| Error::invalid(format!("{}: {e}", dir.join(f).display())))
281    };
282    server_config(
283        &read(AGENT_CA)?,
284        &Leaf {
285            cert: read(AGENT_CERT)?,
286            key: read(AGENT_KEY)?,
287        },
288    )
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn ca_is_made_once_and_kept_private() {
297        let d = tempfile::tempdir().unwrap();
298        let a = Ca::open(d.path()).unwrap();
299        let b = Ca::open(d.path()).unwrap();
300        assert_eq!(a.cert_pem, b.cert_pem);
301        assert_eq!(a.client().unwrap().cert, b.client().unwrap().cert);
302        use std::os::unix::fs::PermissionsExt;
303        for f in ["ca.key", "client.key"] {
304            let m = std::fs::metadata(d.path().join(f)).unwrap().permissions();
305            assert_eq!(m.mode() & 0o777, 0o600, "{f}");
306        }
307        let m = std::fs::metadata(d.path()).unwrap().permissions();
308        assert_eq!(m.mode() & 0o777, 0o700);
309    }
310
311    #[test]
312    fn leaves_chain_to_the_ca_and_build_configs() {
313        let d = tempfile::tempdir().unwrap();
314        let ca = Ca::open(d.path()).unwrap();
315        let s = ca.issue_server("box", "203.0.113.7").unwrap();
316        let n = ca.issue_server("box", "agent.example.com").unwrap();
317        assert_ne!(s.fingerprint().unwrap(), n.fingerprint().unwrap());
318        server_config(&ca.cert_pem, &s).unwrap();
319        ca.client_config().unwrap();
320    }
321}