Skip to main content

sidedns_core/certs/trust/
system.rs

1use std::path::Path;
2
3use tokio_rustls::rustls::pki_types::{CertificateDer, pem::PemObject};
4
5use super::TrustStore;
6
7pub struct SystemStore;
8
9impl TrustStore for SystemStore {
10    fn name(&self) -> &str {
11        "system"
12    }
13
14    fn is_available(&self) -> bool {
15        true
16    }
17
18    fn is_installed(&self, cert_path: &Path) -> bool {
19        is_installed_impl(cert_path)
20    }
21
22    fn install(&self, cert_path: &Path) -> anyhow::Result<()> {
23        install_impl(cert_path)
24    }
25
26    fn uninstall(&self, cert_path: &Path) -> anyhow::Result<()> {
27        uninstall_impl(cert_path)
28    }
29}
30
31// ─── macOS ────────────────────────────────────────────────────────────────────
32
33#[cfg(target_os = "macos")]
34fn is_installed_impl(cert_path: &Path) -> bool {
35    let fingerprint = match sha1_fingerprint(cert_path) {
36        Ok(f) => f,
37        Err(_) => return false,
38    };
39    let output = std::process::Command::new("security")
40        .args([
41            "find-certificate",
42            "-a",
43            "-Z",
44            "/Library/Keychains/System.keychain",
45        ])
46        .output();
47    match output {
48        Ok(o) => String::from_utf8_lossy(&o.stdout).contains(&fingerprint),
49        Err(_) => false,
50    }
51}
52
53#[cfg(target_os = "macos")]
54fn install_impl(cert_path: &Path) -> anyhow::Result<()> {
55    let status = std::process::Command::new("sudo")
56        .args([
57            "security",
58            "add-trusted-cert",
59            "-d",
60            "-r",
61            "trustRoot",
62            "-k",
63            "/Library/Keychains/System.keychain",
64            cert_path.to_str().unwrap(),
65        ])
66        .status()?;
67    anyhow::ensure!(status.success(), "security add-trusted-cert failed");
68    Ok(())
69}
70
71#[cfg(target_os = "macos")]
72fn uninstall_impl(cert_path: &Path) -> anyhow::Result<()> {
73    let status = std::process::Command::new("sudo")
74        .args([
75            "security",
76            "remove-trusted-cert",
77            "-d",
78            cert_path.to_str().unwrap(),
79        ])
80        .status()?;
81    anyhow::ensure!(status.success(), "security remove-trusted-cert failed");
82    Ok(())
83}
84
85// ─── Linux ────────────────────────────────────────────────────────────────────
86
87#[cfg(target_os = "linux")]
88fn is_installed_impl(cert_path: &Path) -> bool {
89    // Check if our CA is trusted by the system (probe via openssl if available)
90    let success = std::process::Command::new("openssl")
91        .args([
92            "verify",
93            "-CAfile",
94            "/etc/ssl/certs/ca-certificates.crt",
95            cert_path.to_str().unwrap(),
96        ])
97        .output()
98        .ok()
99        .map(|o| o.status.success())
100        .unwrap_or(false);
101    // Rough heuristic: check if the sidedns cert file exists in the right place
102    success
103        || std::path::Path::new("/usr/local/share/ca-certificates/sidedns.crt").exists()
104        || std::path::Path::new("/etc/pki/ca-trust/source/anchors/sidedns.crt").exists()
105        || std::path::Path::new("/etc/ca-certificates/trust-source/sidedns.pem").exists()
106}
107
108#[cfg(target_os = "linux")]
109fn install_impl(cert_path: &Path) -> anyhow::Result<()> {
110    // Try each supported distribution in order
111    let debian = std::path::Path::new("/usr/local/share/ca-certificates");
112    let fedora = std::path::Path::new("/etc/pki/ca-trust/source/anchors");
113    let arch = std::path::Path::new("/etc/ca-certificates/trust-source");
114
115    if debian.exists() {
116        std::fs::copy(cert_path, debian.join("sidedns.crt"))?;
117        let s = std::process::Command::new("sudo")
118            .arg("update-ca-certificates")
119            .status()?;
120        anyhow::ensure!(s.success(), "update-ca-certificates failed");
121    } else if fedora.exists() {
122        std::fs::copy(cert_path, fedora.join("sidedns.crt"))?;
123        let s = std::process::Command::new("sudo")
124            .args(["update-ca-trust", "extract"])
125            .status()?;
126        anyhow::ensure!(s.success(), "update-ca-trust extract failed");
127    } else if arch.exists() {
128        std::fs::copy(cert_path, arch.join("anchors/sidedns.pem"))?;
129        let s = std::process::Command::new("sudo")
130            .args(["trust", "anchor", "--store", cert_path.to_str().unwrap()])
131            .status()?;
132        anyhow::ensure!(s.success(), "trust anchor --store failed");
133    } else {
134        anyhow::bail!(
135            "unsupported Linux distribution — install manually:\n\
136             copy {:?} to your system CA directory and run the appropriate update command",
137            cert_path
138        );
139    }
140
141    Ok(())
142}
143
144#[cfg(target_os = "linux")]
145fn uninstall_impl(_cert_path: &Path) -> anyhow::Result<()> {
146    let debian_dest = std::path::Path::new("/usr/local/share/ca-certificates/sidedns.crt");
147    let fedora_dest = std::path::Path::new("/etc/pki/ca-trust/source/anchors/sidedns.crt");
148    let arch_dest = std::path::Path::new("/etc/ca-certificates/trust-source/anchors/sidedns.pem");
149
150    if debian_dest.exists() {
151        std::fs::remove_file(debian_dest)?;
152        std::process::Command::new("sudo")
153            .arg("update-ca-certificates")
154            .status()?;
155    } else if fedora_dest.exists() {
156        std::fs::remove_file(fedora_dest)?;
157        std::process::Command::new("sudo")
158            .args(["update-ca-trust", "extract"])
159            .status()?;
160    } else if arch_dest.exists() {
161        std::fs::remove_file(arch_dest)?;
162        std::process::Command::new("sudo")
163            .args(["trust", "anchor", "--remove", arch_dest.to_str().unwrap()])
164            .status()?;
165    }
166
167    Ok(())
168}
169
170// ─── Windows ──────────────────────────────────────────────────────────────────
171
172#[cfg(windows)]
173fn is_installed_impl(_cert_path: &Path) -> bool {
174    let output = std::process::Command::new("certutil")
175        .args(["-store", "Root"])
176        .output();
177    match output {
178        Ok(o) => {
179            let text = String::from_utf8_lossy(&o.stdout);
180            text.contains("SideDNS")
181        },
182        Err(_) => false,
183    }
184}
185
186#[cfg(windows)]
187fn install_impl(cert_path: &Path) -> anyhow::Result<()> {
188    let status = std::process::Command::new("certutil.exe")
189        .args(["-addstore", "-f", "ROOT", cert_path.to_str().unwrap()])
190        .status()?;
191    anyhow::ensure!(status.success(), "certutil -addstore failed");
192    Ok(())
193}
194
195#[cfg(windows)]
196fn uninstall_impl(cert_path: &Path) -> anyhow::Result<()> {
197    // Find by thumbprint then delete
198    let fingerprint = sha1_fingerprint(cert_path)?;
199    let status = std::process::Command::new("certutil")
200        .args(["-delstore", "ROOT", &fingerprint])
201        .status()?;
202    anyhow::ensure!(status.success(), "certutil -delstore failed");
203    Ok(())
204}
205
206// ─── Fallback ─────────────────────────────────────────────────────────────────
207
208#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
209fn is_installed_impl(_cert_path: &Path) -> bool {
210    false
211}
212
213#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
214fn install_impl(cert_path: &Path) -> anyhow::Result<()> {
215    anyhow::bail!("system trust store installation not supported on this platform")
216}
217
218#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
219fn uninstall_impl(_cert_path: &Path) -> anyhow::Result<()> {
220    Ok(())
221}
222
223// ─── Helpers ──────────────────────────────────────────────────────────────────
224
225#[allow(dead_code)]
226fn sha1_fingerprint(cert_path: &Path) -> anyhow::Result<String> {
227    use sha1::{Digest, Sha1};
228    let data = CertificateDer::from_pem_file(cert_path)?;
229    let mut hash = Sha1::new();
230    hash.update(data);
231    let digest = hash.finalize();
232    Ok(digest
233        .iter()
234        .map(|b| format!("{:02X}", b))
235        .collect::<String>())
236}