Skip to main content

nap_core/server/
cert.rs

1// SPDX-FileCopyrightText: 2026 Digital Creations
2// SPDX-License-Identifier: MIT
3//! Certificate generation for Lore QUIC server
4//!
5//! Uses rcgen to generate self-signed certificates compatible with Lore's QUIC endpoint.
6
7use anyhow::{Context, Result};
8use rcgen::{CertificateParams, DistinguishedName, KeyPair};
9use std::fs;
10use std::path::Path;
11
12/// Generate self-signed certificates for Lore QUIC server
13///
14/// Creates a certificate and private key pair suitable for Lore's QUIC endpoint.
15/// Certificates are persisted across restarts and only regenerated if missing or invalid.
16pub fn generate_certificates(cert_dir: &Path) -> Result<CertificateFiles> {
17    fs::create_dir_all(cert_dir).context("Failed to create certificate directory")?;
18
19    let cert_path = cert_dir.join("cert.pem");
20    let key_path = cert_dir.join("key.pem");
21
22    // Only regenerate if missing
23    if cert_path.exists() && key_path.exists() {
24        tracing::info!("Certificates already exist at {:?}", cert_dir);
25        return Ok(CertificateFiles {
26            cert_path,
27            key_path,
28        });
29    }
30
31    tracing::info!("Generating self-signed certificates for Lore QUIC");
32
33    let mut params = CertificateParams::default();
34
35    // Set distinguished name
36    let mut dn = DistinguishedName::new();
37    dn.push(rcgen::DnType::CommonName, "localhost");
38    dn.push(rcgen::DnType::OrganizationName, "NAP SDK");
39    params.distinguished_name = dn;
40
41    // Set subject alternative names for localhost
42    params.subject_alt_names = vec![
43        rcgen::SanType::DnsName(rcgen::Ia5String::try_from("localhost").unwrap()),
44        rcgen::SanType::IpAddress("127.0.0.1".parse().unwrap()),
45        rcgen::SanType::IpAddress("::1".parse().unwrap()),
46    ];
47
48    // Generate certificate and key
49    let key_pair = KeyPair::generate()?;
50    let cert = params.self_signed(&key_pair)?;
51
52    // Write certificate
53    let cert_pem = cert.pem();
54    fs::write(&cert_path, cert_pem).context("Failed to write certificate file")?;
55
56    // Write private key
57    let key_pem = key_pair.serialize_pem();
58    fs::write(&key_path, key_pem).context("Failed to write private key file")?;
59
60    // Set restrictive permissions on private key
61    #[cfg(unix)]
62    {
63        use std::os::unix::fs::PermissionsExt;
64        let mut perms = fs::metadata(&key_path)
65            .with_context(|| {
66                format!(
67                    "Failed to read key file permissions at {}",
68                    key_path.display()
69                )
70            })?
71            .permissions();
72        perms.set_mode(0o600); // owner read/write only
73        fs::set_permissions(&key_path, perms).with_context(|| {
74            format!(
75                "Failed to set restrictive permissions on key file at {}",
76                key_path.display()
77            )
78        })?;
79        tracing::debug!("Set key file permissions to 0600 (owner-only)");
80    }
81
82    #[cfg(windows)]
83    {
84        // On Windows, mark the key file as hidden and system to discourage
85        // casual access. NTFS ACLs provide stricter protection if needed.
86        use std::os::windows::ffi::OsStrExt;
87        use windows_sys::Win32::Storage::FileSystem::{
88            FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_SYSTEM, SetFileAttributesW,
89        };
90
91        let key_wide: Vec<u16> = key_path
92            .as_os_str()
93            .encode_wide()
94            .chain(std::iter::once(0))
95            .collect();
96        unsafe {
97            let result = SetFileAttributesW(
98                key_wide.as_ptr(),
99                FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM,
100            );
101            if result == 0 {
102                tracing::warn!(
103                    "Failed to set hidden/system attributes on key file at {}. \
104                     The key file may be visible in Explorer.",
105                    key_path.display()
106                );
107            } else {
108                tracing::debug!("Set key file attributes to hidden+system (Windows)");
109            }
110        }
111    }
112
113    tracing::info!("Certificates generated successfully at {:?}", cert_dir);
114
115    Ok(CertificateFiles {
116        cert_path,
117        key_path,
118    })
119}
120
121/// Paths to generated certificate files
122#[derive(Debug, Clone)]
123pub struct CertificateFiles {
124    pub cert_path: std::path::PathBuf,
125    pub key_path: std::path::PathBuf,
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use tempfile::TempDir;
132
133    #[test]
134    fn test_generate_certificates() {
135        let temp_dir = TempDir::new().unwrap();
136        let cert_dir = temp_dir.path();
137
138        let files = generate_certificates(cert_dir).unwrap();
139
140        assert!(files.cert_path.exists());
141        assert!(files.key_path.exists());
142
143        // Verify we can regenerate without error (should skip if exists)
144        let files2 = generate_certificates(cert_dir).unwrap();
145        assert_eq!(files.cert_path, files2.cert_path);
146        assert_eq!(files.key_path, files2.key_path);
147    }
148}