1use std::path::Path;
10
11use rcgen::{
12 BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair,
13 KeyUsagePurpose, SanType,
14};
15
16use crate::error::{Error, Result};
17
18const CA_CN: &str = "isb local registry CA";
19const CA_YEARS: i64 = 10;
23const LEAF_DAYS: i64 = 825;
24
25pub struct Material {
27 pub ca_cert: String,
28 pub cert: String,
29 pub key: String,
30}
31
32fn ca_params() -> CertificateParams {
33 let mut p = CertificateParams::default();
34 p.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
35 p.distinguished_name = rcgen::DistinguishedName::new();
36 p.distinguished_name.push(DnType::CommonName, CA_CN);
37 p.key_usages = vec![
38 KeyUsagePurpose::KeyCertSign,
39 KeyUsagePurpose::CrlSign,
40 KeyUsagePurpose::DigitalSignature,
41 ];
42 p
43}
44
45fn tls_err(step: &str, e: rcgen::Error) -> Error {
46 Error::invalid(format!("registry TLS: {step}: {e}"))
47}
48
49fn write_private(path: &Path, text: &str) -> Result<()> {
50 use std::io::Write;
51 use std::os::unix::fs::OpenOptionsExt;
52 let tmp = path.with_extension("tmp");
53 let mut f = std::fs::OpenOptions::new()
54 .write(true)
55 .create(true)
56 .truncate(true)
57 .mode(0o600)
58 .open(&tmp)?;
59 f.write_all(text.as_bytes())?;
60 f.sync_all()?;
61 std::fs::rename(&tmp, path)?;
62 Ok(())
63}
64
65pub fn ensure(dir: &Path, ip: std::net::IpAddr, renew: bool) -> Result<Material> {
68 std::fs::create_dir_all(dir)?;
69 #[cfg(unix)]
70 {
71 use std::os::unix::fs::PermissionsExt;
72 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
73 }
74 let (ca_key_path, ca_path) = (dir.join("ca.key"), dir.join("ca.crt"));
75 let (key_path, cert_path, for_path) = (
76 dir.join("tls.key"),
77 dir.join("tls.crt"),
78 dir.join("tls.for"),
79 );
80 let now = time::OffsetDateTime::now_utc();
81
82 let ca_key = match std::fs::read_to_string(&ca_key_path) {
83 Ok(pem) => KeyPair::from_pem(&pem).map_err(|e| tls_err("read the CA key", e))?,
84 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
85 let k = KeyPair::generate().map_err(|e| tls_err("generate the CA key", e))?;
86 let mut p = ca_params();
87 p.not_before = now - time::Duration::days(1);
88 p.not_after = now + time::Duration::days(365 * CA_YEARS);
89 let cert = p
90 .self_signed(&k)
91 .map_err(|e| tls_err("sign the CA certificate", e))?;
92 write_private(&ca_key_path, &k.serialize_pem())?;
93 std::fs::write(&ca_path, cert.pem())?;
94 let _ = std::fs::remove_file(&cert_path);
96 k
97 }
98 Err(e) => return Err(e.into()),
99 };
100 let ca_cert = std::fs::read_to_string(&ca_path)?;
101
102 let want_for = ip.to_string();
103 let current = std::fs::read_to_string(&for_path).ok();
104 let have_leaf = cert_path.exists() && key_path.exists();
105 if !have_leaf || current.as_deref().map(str::trim) != Some(want_for.as_str()) || renew {
106 let issuer = Issuer::new(ca_params(), &ca_key);
107 let k = KeyPair::generate().map_err(|e| tls_err("generate the registry key", e))?;
108 let mut p = CertificateParams::default();
109 p.distinguished_name = rcgen::DistinguishedName::new();
110 p.distinguished_name
111 .push(DnType::CommonName, "isb local registry");
112 p.subject_alt_names = vec![SanType::IpAddress(ip)];
113 p.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
114 p.key_usages = vec![KeyUsagePurpose::DigitalSignature];
115 p.not_before = now - time::Duration::days(1);
116 p.not_after = now + time::Duration::days(LEAF_DAYS);
117 let cert = p
118 .signed_by(&k, &issuer)
119 .map_err(|e| tls_err("sign the registry certificate", e))?;
120 write_private(&key_path, &k.serialize_pem())?;
121 std::fs::write(&cert_path, cert.pem())?;
122 std::fs::write(&for_path, &want_for)?;
123 }
124 Ok(Material {
125 ca_cert,
126 cert: std::fs::read_to_string(&cert_path)?,
127 key: std::fs::read_to_string(&key_path)?,
128 })
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn ca_and_leaf_are_made_once_and_kept_private() {
137 let d = tempfile::tempdir().unwrap();
138 let ip: std::net::IpAddr = "127.0.0.1".parse().unwrap();
139 let a = ensure(d.path(), ip, false).unwrap();
140 assert!(a.ca_cert.starts_with("-----BEGIN CERTIFICATE-----"));
141 assert!(a.key.contains("PRIVATE KEY"));
142 let b = ensure(d.path(), ip, false).unwrap();
143 assert_eq!(a.cert, b.cert, "an unchanged leaf is kept");
144 assert_eq!(a.ca_cert, b.ca_cert);
145 let c = ensure(d.path(), ip, true).unwrap();
146 assert_ne!(a.cert, c.cert, "renew reissues the leaf");
147 assert_eq!(a.ca_cert, c.ca_cert, "under the same CA");
148 use std::os::unix::fs::PermissionsExt;
149 for f in ["ca.key", "tls.key"] {
150 let m = std::fs::metadata(d.path().join(f)).unwrap().permissions();
151 assert_eq!(m.mode() & 0o777, 0o600, "{f}");
152 }
153 }
154}