1use 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";
28pub const CLIENT_CN: &str = "isb-control-plane";
30const CA_YEARS: i64 = 10;
31pub 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#[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#[derive(Debug, Clone)]
71pub struct Leaf {
72 pub cert: String,
73 pub key: String,
74}
75
76impl Leaf {
77 pub fn fingerprint(&self) -> Result<String> {
79 fingerprint_pem(&self.cert)
80 }
81}
82
83pub 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
97pub 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 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 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 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 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 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 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
240pub 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
252pub 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
271pub const AGENT_CA: &str = "ca.crt";
273pub const AGENT_CERT: &str = "tls.crt";
274pub const AGENT_KEY: &str = "tls.key";
275
276pub 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}