sidedns_core/certs/trust/
system.rs1use 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#[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#[cfg(target_os = "linux")]
88fn is_installed_impl(cert_path: &Path) -> bool {
89 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 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 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#[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 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#[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#[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}