nikau/network/
certs.rs

1use std::fs;
2use std::io::{self, prelude::*};
3use std::os::unix::fs::PermissionsExt;
4use std::path::PathBuf;
5
6use anyhow::{bail, Context, Result};
7use sha2::{Digest, Sha256};
8use tracing::{info, warn};
9
10pub fn load_known_certs(config_dir: &PathBuf) -> Result<Vec<rustls_pki_types::CertificateDer<'static>>> {
11    let mut certs = vec![];
12    for path in fs::read_dir(init_known_certs_dir(config_dir)?)? {
13        let path = path?;
14        let filetype = path.file_type()?;
15        if !filetype.is_file() {
16            continue;
17        }
18        certs.push(load_cert(path.path())?);
19    }
20    Ok(certs)
21}
22
23fn splash(label: &str, fingerprint: &str) {
24    println!(
25        r"
26 \\ //
27  \V/
28   U
29   | nikau {}
30   | {}
31",
32        label, fingerprint
33    );
34}
35
36pub fn load_keypair<'a>(
37    splash_label: &str,
38    config_dir: &PathBuf,
39) -> Result<(rustls_pki_types::CertificateDer<'a>, rustls_pki_types::PrivateKeyDer<'a>)> {
40    let file_path = config_dir.join("private.pem");
41    if file_path.is_file() {
42        read_existing_keypair(splash_label, &file_path)
43    } else {
44        write_new_keypair(splash_label, &file_path)
45    }
46}
47
48fn read_existing_keypair<'a>(
49    splash_label: &str,
50    file_path: &PathBuf,
51) -> Result<(rustls_pki_types::CertificateDer<'a>, rustls_pki_types::PrivateKeyDer<'a>)> {
52    let mut reader =
53        io::BufReader::new(fs::File::open(&file_path).with_context(|| {
54            format!("Failed to open keypair file: {}", file_path.display())
55        })?);
56    let mut cert: Option<rustls_pki_types::CertificateDer> = None;
57    let mut key: Option<rustls_pki_types::PrivateKeyDer> = None;
58    for item in rustls_pemfile::read_all(&mut reader) {
59        match item.with_context(|| format!("Failed to read keypair file: {}", file_path.display()))? {
60            rustls_pemfile::Item::X509Certificate(filecert) => {
61                cert = Some(rustls_pki_types::CertificateDer::from(filecert));
62            }
63            rustls_pemfile::Item::Pkcs8Key(filekey) => {
64                key = Some(rustls_pki_types::PrivateKeyDer::from(filekey));
65            }
66            _ => {
67                // Avoid logging the content in case its a privkey
68                warn!("Unexpected item in {}", file_path.display());
69            }
70        }
71    }
72    if let (Some(cert), Some(key)) = (cert, key) {
73        splash(splash_label, &fingerprint(&cert));
74        info!("Using keypair from {}", file_path.display());
75        Ok((cert, key))
76    } else {
77        bail!("Incomplete cert/key content in {}", file_path.display());
78    }
79}
80
81fn write_new_keypair<'a>(
82    splash_label: &str,
83    file_path: &PathBuf,
84) -> Result<(rustls_pki_types::CertificateDer<'a>, rustls_pki_types::PrivateKeyDer<'a>)> {
85    let pair = rcgen::generate_simple_self_signed(vec![])
86        .context("Failed to generate self-signed cert")?;
87
88    info!("Writing a new keypair to {}", file_path.display());
89    let mut outfile = fs::File::create(&file_path).with_context(|| {
90        format!(
91            "Failed to open keypair file for writing: {}",
92            file_path.display()
93        )
94    })?;
95    ensure_permissions(&file_path, 0o600).with_context(|| {
96        format!(
97            "Failed to set permissions on keypair file: {}",
98            file_path.display()
99        )
100    })?;
101    outfile
102        .write_all(pair.cert.pem().as_bytes())
103        .with_context(|| format!("Failed to write public key to file: {}", file_path.display()))?;
104    outfile
105        .write_all(pair.signing_key.serialize_pem().as_bytes())
106        .with_context(|| format!("Failed to write private key to file: {}", file_path.display()))?;
107
108    let rustls_cert = rustls_pki_types::CertificateDer::from(pair.cert.der().to_vec());
109    splash(splash_label, &fingerprint(&rustls_cert));
110    Ok((
111        rustls_cert,
112        rustls_pki_types::PrivateKeyDer::from(rustls_pki_types::PrivatePkcs8KeyDer::from(pair.signing_key.serialize_der())),
113    ))
114}
115
116/// Returns the sha256 fingerprint of this certificate.
117/// We use this for cert filenames and for comparing certs in confirmation prompts.
118/// This should match the output of "openssl x509 -in <filename> -noout -sha256 -fingerprint"
119pub fn fingerprint(cert: &rustls_pki_types::CertificateDer) -> String {
120    format!("{:x}", Sha256::digest(cert))
121}
122
123pub fn write_approved_cert(
124    cert: &rustls_pki_types::CertificateDer,
125    fingerprint: &str,
126    config_dir: &PathBuf,
127) -> Result<()> {
128    let file_path = init_known_certs_dir(config_dir)
129        .context("Failed to init known_certs dir")?
130        .join(format!("{}.pem", fingerprint));
131    let content = pem::encode_config(
132        &pem::Pem::new("CERTIFICATE", cert.as_ref()),
133        pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
134    );
135    let mut outfile = fs::File::create(&file_path).with_context(|| {
136        format!(
137            "Failed to open known cert file for writing: {}",
138            file_path.display()
139        )
140    })?;
141    ensure_permissions(&file_path, 0o644).with_context(|| {
142        format!(
143            "Failed to set permissions on known cert file: {}",
144            file_path.display()
145        )
146    })?;
147    outfile.write_all(content.as_bytes()).with_context(|| {
148        format!(
149            "Failed to write known cert to file: {}",
150            file_path.display()
151        )
152    })?;
153    info!("Wrote approved cert to {}", file_path.display());
154    Ok(())
155}
156
157fn load_cert<'a>(file_path: PathBuf) -> Result<rustls_pki_types::CertificateDer<'a>> {
158    let mut reader = io::BufReader::new(
159        fs::File::open(&file_path)
160            .with_context(|| format!("Failed to open cert file: {}", file_path.display()))?,
161    );
162    if let Some(rustls_pemfile::Item::X509Certificate(filecert)) =
163        rustls_pemfile::read_one(&mut reader)
164            .with_context(|| format!("Failed to read cert file: {}", file_path.display()))?
165    {
166        Ok(rustls_pki_types::CertificateDer::from(filecert))
167    } else {
168        bail!("Public certificate not found in {}", file_path.display());
169    }
170}
171
172fn init_known_certs_dir(config_dir: &PathBuf) -> Result<PathBuf> {
173    let dir_path = config_dir.join("known_certs");
174    fs::create_dir_all(&dir_path)
175        .with_context(|| format!("Failed to ensure certs dir exists: {}", dir_path.display()))?;
176    ensure_permissions(&dir_path, 0o755).with_context(|| {
177        format!(
178            "Failed to set permissions on certs dir: {}",
179            dir_path.display()
180        )
181    })?;
182    Ok(dir_path)
183}
184
185fn ensure_permissions(path: &PathBuf, perms: u32) -> Result<()> {
186    let mut permissions = fs::metadata(path)
187        .with_context(|| format!("Failed to read file metadata: {}", path.display()))?
188        .permissions();
189    if permissions.mode() != perms {
190        permissions.set_mode(perms);
191    }
192    Ok(())
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use tempfile;
199
200    #[test]
201    fn can_write_read_keys() {
202        let dir = tempfile::tempdir().unwrap();
203        // This should automatically write a new keypair
204        let (cert1, privkey1) = load_keypair("foo", &dir.path().to_path_buf()).expect("couldn't load");
205        // This should read the existing keypair
206        let (cert2, privkey2) = load_keypair("foo", &dir.path().to_path_buf()).expect("couldn't load");
207        // The results should match
208        assert!(fingerprint(&cert1) == fingerprint(&cert2));
209        assert!(cert1 == cert2);
210        assert!(privkey1 == privkey2);
211    }
212}