1pub use crate::error::{Error, Result};
2
3use std::collections::HashSet;
4use std::fs;
5use std::os::unix;
6use std::path::{Path, PathBuf};
7
8use log::warn;
9use openssl::hash::MessageDigest;
10use openssl::x509::X509;
11use regex::Regex;
12
13mod error;
14
15pub fn rehash(dir: impl AsRef<Path>) -> Result<()> {
33 let mut seen_fingerprints: HashSet<Vec<u8>> = HashSet::new();
34
35 for entry in clean_dir(dir.as_ref())? {
36 if let Ok(Some(certificate)) = read_single_certificate(&entry) {
37 let fingerprint = certificate.digest(MessageDigest::sha1())?;
38
39 if !seen_fingerprints.contains(&*fingerprint) {
40 seen_fingerprints.insert(fingerprint.to_vec());
41 hash_link(&entry, certificate.subject_name_hash())?;
42 } else {
43 let path_display = entry.display();
44 warn!("rehash: skipping duplicate certificate in {path_display}");
45 }
46 } else {
47 let path_display = entry.display();
48 warn!("rehash: skipping {path_display}, it does not contain exactly one certificate");
49 }
50 }
51
52 Ok(())
53}
54
55fn clean_dir(dir: impl AsRef<Path>) -> std::io::Result<Vec<PathBuf>> {
57 let mut entries: Vec<PathBuf> = vec![];
58 let regex = Regex::new(r"^[[:xdigit:]]{8}\.\d+$").unwrap();
59
60 for entry in fs::read_dir(dir.as_ref())? {
61 let entry = entry?;
62 let path = entry.path();
63 if path.is_symlink() {
64 if let Ok(false) = path.try_exists() {
65 fs::remove_file(&path)?;
66 } else if regex.is_match(&entry.file_name().to_string_lossy()) {
67 fs::remove_file(&path)?;
68 } else {
69 entries.push(path);
70 }
71 } else {
72 entries.push(path);
73 }
74 }
75
76 entries.sort();
77
78 Ok(entries)
79}
80
81fn read_single_certificate(path: impl AsRef<Path>) -> Result<Option<X509>> {
82 let data = std::fs::read(path)?;
83 match X509::stack_from_pem(&data) {
84 Ok(x509) if x509.len() == 1 => Ok(Some(x509[0].clone())),
85 _ => Ok(Some(X509::from_der(&data)?)),
87 }
88}
89
90fn hash_link(target_path: impl AsRef<Path>, hash: u32) -> Result<()> {
96 let target_path = target_path.as_ref();
97 let parent_dir = target_path.parent().unwrap();
98 let link_name = parent_dir.join(hash_link_stem(hash));
99 let mut count = 0;
100
101 loop {
102 let link_path = link_name.with_extension(format!("{count}"));
103
104 if link_path.is_symlink() {
105 if link_path.try_exists()? {
106 if link_path.read_link()? == target_path {
107 return Ok(());
108 } else {
109 count += 1;
110 }
111 } else {
112 fs::remove_file(&link_path)?;
113 }
114 } else {
115 unix::fs::symlink(target_path.file_name().unwrap(), &link_path)?;
116 break;
117 }
118 }
119
120 Ok(())
121}
122
123fn hash_link_stem(hash: u32) -> String {
124 format!("{:08x}", hash)
125}
126
127#[cfg(test)]
128mod test {
129 use std::fs::{self, File};
130 use std::io::Write;
131 use std::os::unix;
132 use std::path::Path;
133
134 use insta::assert_debug_snapshot;
135 use openssl::{
136 asn1::Asn1Time,
137 bn::{BigNum, MsbOption},
138 hash::MessageDigest,
139 nid::Nid,
140 pkey::PKey,
141 rsa::Rsa,
142 x509::{X509Name, X509},
143 };
144 use tempfile::{tempdir, NamedTempFile};
145
146 use super::*;
147
148 #[test]
156 fn test_rehash() {
157 let cert = build_x509("foo");
158 let hash = format!("{:x}", cert.subject_name_hash());
159 let mut cert_file = NamedTempFile::new().unwrap();
160 cert_file.write_all(&cert.to_pem().unwrap()).unwrap();
161 let temp_dir = tempdir().unwrap();
162 let cert_dir = temp_dir.path().to_owned();
163 let hash_link = cert_dir.join(hash).with_extension("0");
164 let cert_link_0 = cert_dir.join("cert-link_0.crt");
165 unix::fs::symlink(cert_file.path(), &cert_link_0).unwrap();
166 let cert_link_1 = cert_dir.join("cert-link_1.crt");
167 unix::fs::symlink(cert_file.path(), cert_link_1).unwrap();
168 let mut cert_bundle = File::create(cert_dir.join("ca-certificates.crt")).unwrap();
169 cert_bundle
170 .write_all(&build_x509("bar").to_pem().unwrap())
171 .unwrap();
172 cert_bundle
173 .write_all(&build_x509("baz").to_pem().unwrap())
174 .unwrap();
175
176 rehash(&cert_dir).unwrap();
177
178 let mut dir_entries: Vec<String> = vec![];
179 for entry in fs::read_dir(&cert_dir).unwrap() {
180 let entry = entry.unwrap();
181 dir_entries.push(entry.file_name().to_string_lossy().into());
182 }
183 dir_entries.sort();
184 assert_debug_snapshot!(&dir_entries);
185
186 assert_eq!(
187 hash_link.read_link().unwrap(),
188 Path::new(cert_link_0.file_name().unwrap())
189 );
190 }
191
192 #[test]
193 fn test_clean_links_on_empty_dir() {
194 let tempdir = tempdir().unwrap();
195 let cert_dir = tempdir.path().to_owned();
196
197 let result = clean_dir(cert_dir).unwrap();
198
199 assert!(result.is_empty());
200 }
201
202 #[test]
203 fn test_clean_links_removes_hash_links() {
204 let cert_file = NamedTempFile::new().unwrap();
207 let temp_dir = tempdir().unwrap();
208 let cert_dir = temp_dir.path().to_owned();
209 let cert_link_0 = cert_dir.join("cert-link_0.crt");
210 let cert_link_1 = cert_dir.join("cert-link_1.crt");
211 unix::fs::symlink(cert_file.path(), &cert_link_0).unwrap();
212 unix::fs::symlink(cert_file.path(), &cert_link_1).unwrap();
213 let hash_link_0 = cert_dir.join("12345678.0");
214 let hash_link_1 = cert_dir.join("12345678.1");
215 unix::fs::symlink(&cert_link_0, &hash_link_0).unwrap();
216 unix::fs::symlink(&cert_link_1, &hash_link_1).unwrap();
217
218 clean_dir(cert_dir).unwrap();
219
220 assert!(!hash_link_0.exists() && !hash_link_1.exists());
221 }
222
223 #[test]
224 fn test_clean_links_removes_broken_links() {
225 let cert_file = NamedTempFile::new().unwrap();
228 let temp_dir = tempdir().unwrap();
229 let cert_dir = temp_dir.path().to_owned();
230 let broken_link_0 = cert_dir.join("broken-link_0.crt");
231 let broken_link_1 = cert_dir.join("broken-link_1.crt");
232 unix::fs::symlink(cert_file.path(), &broken_link_0).unwrap();
233 unix::fs::symlink(cert_file.path(), &broken_link_1).unwrap();
234 fs::remove_file(cert_file.path()).unwrap();
236
237 clean_dir(cert_dir).unwrap();
238
239 assert!(!broken_link_0.exists() && !broken_link_1.exists());
240 }
241
242 #[test]
243 fn test_clean_links_keeps_unbroken_links() {
244 let temp_file = NamedTempFile::new().unwrap();
246 let temp_dir = tempdir().unwrap();
247 let cert_dir = temp_dir.path().to_owned();
248 let cert_link = cert_dir.join("cert-link.crt");
249 unix::fs::symlink(temp_file.path(), &cert_link).unwrap();
250
251 clean_dir(cert_dir).unwrap();
252
253 assert!(cert_link.exists());
254 }
255
256 #[test]
257 fn test_hash_link() {
258 let temp_file = NamedTempFile::new().unwrap();
260 let temp_dir = tempdir().unwrap();
261 let cert_dir = temp_dir.path().to_owned();
262 let cert_link = cert_dir.join("cert-link.crt");
263 unix::fs::symlink(temp_file.path(), &cert_link).unwrap();
264 let hash: u32 = 12345678;
265 let hash_link_stem = hash_link_stem(hash);
266 let hash_link_path = cert_dir.join(hash_link_stem).with_extension("0");
267
268 hash_link(&cert_link, hash).unwrap();
269
270 assert_eq!(
271 hash_link_path.read_link().unwrap(),
272 Path::new(cert_link.file_name().unwrap())
273 );
274 }
275
276 #[test]
277 fn test_hash_link_does_not_duplicate() {
278 let temp_file = NamedTempFile::new().unwrap();
281 let temp_dir = tempdir().unwrap();
282 let cert_dir = temp_dir.path().to_owned();
283 let cert_link = cert_dir.join("cert-link.crt");
284 unix::fs::symlink(temp_file.path(), &cert_link).unwrap();
285 let hash: u32 = 12345678;
286 let hash_link_stem = hash_link_stem(hash);
287 let hash_link_0 = cert_dir.join(hash_link_stem).with_extension("0");
288
289 unix::fs::symlink(&cert_link, &hash_link_0).unwrap();
290
291 let hash_link_1 = hash_link_0.with_extension("1");
292
293 hash_link(&cert_link, hash).unwrap();
294
295 assert!(!hash_link_1.exists());
296 }
297
298 #[test]
299 fn test_hash_link_resolves_collision() {
300 let temp_file = NamedTempFile::new().unwrap();
304 let temp_dir = tempdir().unwrap();
305 let cert_dir = temp_dir.path().to_owned();
306 let cert_link_0 = cert_dir.join("cert-link_0.crt");
307 unix::fs::symlink(temp_file.path(), &cert_link_0).unwrap();
308 let cert_link_1 = cert_dir.join("cert-link_1.crt");
309 unix::fs::symlink(temp_file.path(), &cert_link_1).unwrap();
310 let hash: u32 = 12345678;
311 let hash_link_stem = hash_link_stem(hash);
312 let hash_link_0 = cert_dir.join(hash_link_stem).with_extension("0");
313 unix::fs::symlink(&cert_link_0, &hash_link_0).unwrap();
314 let hash_link_1 = hash_link_0.with_extension("1");
315
316 hash_link(&cert_link_1, hash).unwrap();
317
318 assert_eq!(
319 hash_link_1.read_link().unwrap(),
320 Path::new(cert_link_1.file_name().unwrap())
321 );
322 }
323
324 fn build_x509(cn: &str) -> X509 {
325 let rsa = Rsa::generate(2048).unwrap();
326 let pkey = PKey::from_rsa(rsa).unwrap();
327
328 let mut name = X509Name::builder().unwrap();
329 name.append_entry_by_nid(Nid::COMMONNAME, cn).unwrap();
330 let name = name.build();
331
332 let mut builder = X509::builder().unwrap();
333 builder.set_version(2).unwrap();
334 builder.set_subject_name(&name).unwrap();
335 builder.set_issuer_name(&name).unwrap();
336 builder
337 .set_not_before(&Asn1Time::days_from_now(0).unwrap())
338 .unwrap();
339 builder
340 .set_not_after(&Asn1Time::days_from_now(365).unwrap())
341 .unwrap();
342 builder.set_pubkey(&pkey).unwrap();
343
344 let mut serial = BigNum::new().unwrap();
345 serial.rand(128, MsbOption::MAYBE_ZERO, false).unwrap();
346 builder
347 .set_serial_number(&serial.to_asn1_integer().unwrap())
348 .unwrap();
349
350 builder.sign(&pkey, MessageDigest::sha256()).unwrap();
351
352 builder.build()
353 }
354}