use std::path::Path;
use rcgen::{
BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair,
KeyUsagePurpose, SanType,
};
use crate::error::{Error, Result};
const CA_CN: &str = "isb local registry CA";
const CA_YEARS: i64 = 10;
const LEAF_DAYS: i64 = 825;
pub struct Material {
pub ca_cert: String,
pub cert: String,
pub key: String,
}
fn ca_params() -> CertificateParams {
let mut p = CertificateParams::default();
p.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
p.distinguished_name = rcgen::DistinguishedName::new();
p.distinguished_name.push(DnType::CommonName, CA_CN);
p.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::CrlSign,
KeyUsagePurpose::DigitalSignature,
];
p
}
fn tls_err(step: &str, e: rcgen::Error) -> Error {
Error::invalid(format!("registry TLS: {step}: {e}"))
}
fn write_private(path: &Path, text: &str) -> Result<()> {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let tmp = path.with_extension("tmp");
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&tmp)?;
f.write_all(text.as_bytes())?;
f.sync_all()?;
std::fs::rename(&tmp, path)?;
Ok(())
}
pub fn ensure(dir: &Path, ip: std::net::IpAddr, renew: bool) -> Result<Material> {
std::fs::create_dir_all(dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
}
let (ca_key_path, ca_path) = (dir.join("ca.key"), dir.join("ca.crt"));
let (key_path, cert_path, for_path) = (
dir.join("tls.key"),
dir.join("tls.crt"),
dir.join("tls.for"),
);
let now = time::OffsetDateTime::now_utc();
let ca_key = match std::fs::read_to_string(&ca_key_path) {
Ok(pem) => KeyPair::from_pem(&pem).map_err(|e| tls_err("read the CA key", e))?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let k = KeyPair::generate().map_err(|e| tls_err("generate the CA key", e))?;
let mut p = ca_params();
p.not_before = now - time::Duration::days(1);
p.not_after = now + time::Duration::days(365 * CA_YEARS);
let cert = p
.self_signed(&k)
.map_err(|e| tls_err("sign the CA certificate", e))?;
write_private(&ca_key_path, &k.serialize_pem())?;
std::fs::write(&ca_path, cert.pem())?;
let _ = std::fs::remove_file(&cert_path);
k
}
Err(e) => return Err(e.into()),
};
let ca_cert = std::fs::read_to_string(&ca_path)?;
let want_for = ip.to_string();
let current = std::fs::read_to_string(&for_path).ok();
let have_leaf = cert_path.exists() && key_path.exists();
if !have_leaf || current.as_deref().map(str::trim) != Some(want_for.as_str()) || renew {
let issuer = Issuer::new(ca_params(), &ca_key);
let k = KeyPair::generate().map_err(|e| tls_err("generate the registry key", e))?;
let mut p = CertificateParams::default();
p.distinguished_name = rcgen::DistinguishedName::new();
p.distinguished_name
.push(DnType::CommonName, "isb local registry");
p.subject_alt_names = vec![SanType::IpAddress(ip)];
p.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
p.key_usages = vec![KeyUsagePurpose::DigitalSignature];
p.not_before = now - time::Duration::days(1);
p.not_after = now + time::Duration::days(LEAF_DAYS);
let cert = p
.signed_by(&k, &issuer)
.map_err(|e| tls_err("sign the registry certificate", e))?;
write_private(&key_path, &k.serialize_pem())?;
std::fs::write(&cert_path, cert.pem())?;
std::fs::write(&for_path, &want_for)?;
}
Ok(Material {
ca_cert,
cert: std::fs::read_to_string(&cert_path)?,
key: std::fs::read_to_string(&key_path)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ca_and_leaf_are_made_once_and_kept_private() {
let d = tempfile::tempdir().unwrap();
let ip: std::net::IpAddr = "127.0.0.1".parse().unwrap();
let a = ensure(d.path(), ip, false).unwrap();
assert!(a.ca_cert.starts_with("-----BEGIN CERTIFICATE-----"));
assert!(a.key.contains("PRIVATE KEY"));
let b = ensure(d.path(), ip, false).unwrap();
assert_eq!(a.cert, b.cert, "an unchanged leaf is kept");
assert_eq!(a.ca_cert, b.ca_cert);
let c = ensure(d.path(), ip, true).unwrap();
assert_ne!(a.cert, c.cert, "renew reissues the leaf");
assert_eq!(a.ca_cert, c.ca_cert, "under the same CA");
use std::os::unix::fs::PermissionsExt;
for f in ["ca.key", "tls.key"] {
let m = std::fs::metadata(d.path().join(f)).unwrap().permissions();
assert_eq!(m.mode() & 0o777, 0o600, "{f}");
}
}
}