Skip to main content

isb_core/registry/
tls.rs

1//! The registry's TLS: an isb CA (kept in the daemon's state directory, key
2//! 0600) and a certificate for the registry's loopback address.
3//!
4//! incus pulls OCI images only over https, through skopeo, which trusts a
5//! per-registry CA at `/etc/containers/certs.d/<host:port>/ca.crt`. A CA of
6//! isb's own, rather than a self-signed leaf, lets the leaf be reissued
7//! without touching the host again.
8
9use 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";
19/// How long a certificate lasts. The registry is reachable on loopback only,
20/// so a long life costs little; `isb registry setup` reissues the leaf when
21/// it nears the end.
22const CA_YEARS: i64 = 10;
23const LEAF_DAYS: i64 = 825;
24
25/// PEM files under `<state>/registry/`.
26pub 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
65/// The CA and a leaf for `ip`, created in `dir` when missing (or when the
66/// leaf does not name `ip`, or is due for renewal: `renew`).
67pub 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            // A new CA invalidates any old leaf.
95            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}