Skip to main content

lanflare_core/crypto/
cert.rs

1//! Self-signed certificate and key generation.
2
3use std::io::Cursor;
4use sha2::{Digest, Sha256};
5use x509_parser::asn1_rs::FromDer;
6use x509_parser::certificate::X509Certificate;
7use x509_parser::pem::Pem;
8use x509_parser::x509::SubjectPublicKeyInfo;
9
10/// Generated self-signed TLS identity.
11#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12pub struct TlsIdentity {
13    /// PEM-encoded self-signed X.509 certificate.
14    pub cert_pem: String,
15    /// PEM-encoded private key (PKCS#8).
16    pub key_pem: String,
17    /// Uppercase hex SHA-256 fingerprint of the DER certificate bytes.
18    pub fingerprint: String,
19}
20
21/// Loads a persisted TLS identity from a JSON file, or generates and saves a new one if not found or corrupted.
22pub fn load_or_generate_identity(path: &std::path::Path) -> anyhow::Result<TlsIdentity> {
23    if let Ok(content) = std::fs::read_to_string(path)
24        && let Ok(ident) = serde_json::from_str::<TlsIdentity>(&content)
25    {
26        // Verify identity validity
27        if !ident.cert_pem.trim().is_empty()
28            && !ident.key_pem.trim().is_empty()
29            && !ident.fingerprint.trim().is_empty()
30        {
31            return Ok(ident);
32        }
33    }
34
35    let identity = generate_self_signed()?;
36    let _ = save_identity(path, &identity);
37    Ok(identity)
38}
39
40/// Saves a TLS identity to the specified JSON path.
41pub fn save_identity(path: &std::path::Path, identity: &TlsIdentity) -> std::io::Result<()> {
42    if let Some(parent) = path.parent() {
43        let _ = std::fs::create_dir_all(parent);
44    }
45    let data = serde_json::to_string_pretty(identity)
46        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
47    std::fs::write(path, data)
48}
49
50/// Generates a new self-signed RSA-2048 device identity.
51///
52/// LocalSend v2.2 specification standard:
53/// - RSA-2048 key pair
54/// - `CN=LocalSend User`
55/// - Valid from 1975 to 4096 (rcgen default, effectively non-expiring)
56/// - Device identity is uniquely identified by the uppercase SHA-256 hex fingerprint
57pub fn generate_self_signed() -> anyhow::Result<TlsIdentity> {
58    generate_self_signed_rsa(2048)
59}
60
61/// Generates a self-signed RSA TLS identity with the specified bit size (e.g. 2048).
62pub fn generate_self_signed_rsa(bits: usize) -> anyhow::Result<TlsIdentity> {
63    use rsa::pkcs8::{EncodePrivateKey, LineEnding};
64
65    let mut rng = rsa::rand_core::OsRng;
66    let private_key = rsa::RsaPrivateKey::new(&mut rng, bits)?;
67    let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF)?;
68    let key_pair = rcgen::KeyPair::try_from(private_key.to_pkcs8_der()?.as_bytes())?;
69
70    let mut params = rcgen::CertificateParams::default();
71    params.distinguished_name = rcgen::DistinguishedName::new();
72    params
73        .distinguished_name
74        .push(rcgen::DnType::CommonName, "LocalSend User");
75
76    let cert = params.self_signed(&key_pair)?;
77    let cert_pem = cert.pem();
78    let fingerprint = fingerprint_from_cert_der(cert.der());
79
80    Ok(TlsIdentity {
81        cert_pem,
82        key_pem: private_key_pem.to_string(),
83        fingerprint,
84    })
85}
86
87/// Generates a self-signed Ed25519 TLS identity.
88///
89/// Supported for high-performance LanFlare-native peer connections.
90pub fn generate_self_signed_ed25519() -> anyhow::Result<TlsIdentity> {
91    let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)?;
92    let private_key_pem = key_pair.serialize_pem();
93
94    let mut params = rcgen::CertificateParams::default();
95    params.distinguished_name = rcgen::DistinguishedName::new();
96    params
97        .distinguished_name
98        .push(rcgen::DnType::CommonName, "LocalSend User");
99
100    let cert = params.self_signed(&key_pair)?;
101    let cert_pem = cert.pem();
102    let fingerprint = fingerprint_from_cert_der(cert.der());
103
104    Ok(TlsIdentity {
105        cert_pem,
106        key_pem: private_key_pem,
107        fingerprint,
108    })
109}
110
111/// Computes the SHA-256 fingerprint of a certificate in DER format.
112///
113/// Formatted as an uppercase hex string (64 chars), adhering strictly to
114/// LocalSend v2.2 wire specification format.
115pub fn fingerprint_from_cert_der(cert_der: &[u8]) -> String {
116    let mut hasher = Sha256::new();
117    hasher.update(cert_der);
118    let hash = hasher.finalize();
119    hash.iter().map(|b| format!("{b:02X}")).collect()
120}
121
122/// Computes the SHA-256 fingerprint of a PEM-encoded certificate.
123pub fn fingerprint_from_cert_pem(cert_pem: &str) -> anyhow::Result<String> {
124    let (pem, _) = Pem::read(Cursor::new(cert_pem.as_bytes()))?;
125    Ok(fingerprint_from_cert_der(&pem.contents))
126}
127
128/// Verifies whether an X.509 certificate parsed from PEM is valid:
129/// - Cryptographic signature check (self-signed or against `expected_public_key`)
130/// - Validity timeframe (not expired and already active)
131/// - Optional public key matching (SPKI equality)
132pub fn verify_cert_from_pem(
133    cert_pem: &str,
134    expected_public_key_pem: Option<&str>,
135) -> anyhow::Result<()> {
136    let (pem, _) = Pem::read(Cursor::new(cert_pem.as_bytes()))?;
137    let parsed_cert = pem.parse_x509()?;
138    verify_cert_from_cert(&parsed_cert, expected_public_key_pem)
139}
140
141/// Verifies whether an X.509 certificate parsed from DER is valid:
142/// - Cryptographic signature check (self-signed or against `expected_public_key`)
143/// - Validity timeframe (not expired and already active)
144/// - Optional public key matching (SPKI equality)
145pub fn verify_cert_from_der(
146    cert_der: &[u8],
147    expected_public_key_pem: Option<&str>,
148) -> anyhow::Result<()> {
149    let (_, parsed_cert) = X509Certificate::from_der(cert_der)?;
150    verify_cert_from_cert(&parsed_cert, expected_public_key_pem)
151}
152
153/// Internal helper to verify an `X509Certificate`.
154fn verify_cert_from_cert(
155    cert: &X509Certificate<'_>,
156    expected_public_key_pem: Option<&str>,
157) -> anyhow::Result<()> {
158    if !cert.validity.is_valid() {
159        anyhow::bail!("Certificate validity period check failed");
160    }
161
162    if let Some(pub_key_pem) = expected_public_key_pem {
163        let cert_spki_raw = cert.tbs_certificate.subject_pki.raw;
164        let (expected_pem, _) = Pem::read(Cursor::new(pub_key_pem.as_bytes()))?;
165        if cert_spki_raw != expected_pem.contents.as_slice() {
166            // Also attempt SubjectPublicKeyInfo parsed comparison for formatting differences
167            let (_, expected_spki) = SubjectPublicKeyInfo::from_der(&expected_pem.contents)?;
168            let cert_spki = cert.tbs_certificate.subject_pki.parsed()?;
169            let expected_parsed = expected_spki.parsed()?;
170            if cert_spki != expected_parsed {
171                anyhow::bail!("Certificate public key mismatch");
172            }
173        }
174    }
175
176    cert.verify_signature(None)
177        .map_err(|e| anyhow::anyhow!("Signature verification error: {e:?}"))?;
178
179    Ok(())
180}
181
182/// Extracts the public key from DER certificate bytes and encodes it as PEM.
183pub fn public_key_from_cert_der(cert_der: &[u8]) -> anyhow::Result<String> {
184    let (_, cert) = X509Certificate::from_der(cert_der)?;
185    public_key_from_cert(&cert)
186}
187
188/// Extracts the public key from an `X509Certificate` and encodes it as PEM (SPKI).
189pub fn public_key_from_cert(cert: &X509Certificate<'_>) -> anyhow::Result<String> {
190    let cert_public_key = cert.tbs_certificate.subject_pki.raw;
191    let pem_block = pem::Pem::new("PUBLIC KEY", cert_public_key.to_vec());
192    let config = pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF);
193    Ok(pem::encode_config(&pem_block, config))
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    static SPEC_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----\n\
201MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi9uDMYRn63SZtEPGRogZ\n\
202Gdu5XXBAoQeMO60mycoinqLKDWyZdpMo+XWY3wYVhoAyxgzDOcPjIf+Uq1oEy/0K\n\
2034WwfpbK8SCy851qgYkfMCT9D9mFvXwWoULJCUHFF7f947ArDE1nmuK1nNx2RodN2\n\
204wJCXyzPjw0jn06bwGeg0EqfUC8wvW4FTZ6t1tErzmRqRdMUWuCJwsk1IMbDbFePh\n\
205iK5jecOBG0RVVWLuw+TkuX8TUgrpIktH2+qEM1KdLyAMnL71hx2wMvE+lDKFKK9p\n\
20637zXK8omjl+VgTC8ocjGeYDDsl43ZtW09V0pb7Vz2FM8b7BgM06kvJl48PIe5puY\n\
207bQIDAQAB\n\
208-----END PUBLIC KEY-----";
209
210    static SPEC_CERT: &str = "-----BEGIN CERTIFICATE-----\n\
211MIIDGTCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQsFADBQMRcwFQYDVQQDEw5Mb2Nh\n\
212bFNlbmQgVXNlcjEJMAcGA1UEChMAMQkwBwYDVQQLEwAxCTAHBgNVBAcTADEJMAcG\n\
213A1UECBMAMQkwBwYDVQQGEwAwHhcNMjUwMjA5MDAwMzE0WhcNMzUwMjA3MDAwMzE0\n\
214WjBQMRcwFQYDVQQDEw5Mb2NhbFNlbmQgVXNlcjEJMAcGA1UEChMAMQkwBwYDVQQL\n\
215EwAxCTAHBgNVBAcTADEJMAcGA1UECBMAMQkwBwYDVQQGEwAwggEiMA0GCSqGSIb3\n\
216DQEBAQUAA4IBDwAwggEKAoIBAQCL24MxhGfrdJm0Q8ZGiBkZ27ldcEChB4w7rSbJ\n\
217yiKeosoNbJl2kyj5dZjfBhWGgDLGDMM5w+Mh/5SrWgTL/QrhbB+lsrxILLznWqBi\n\
218R8wJP0P2YW9fBahQskJQcUXt/3jsCsMTWea4rWc3HZGh03bAkJfLM+PDSOfTpvAZ\n\
2196DQSp9QLzC9bgVNnq3W0SvOZGpF0xRa4InCyTUgxsNsV4+GIrmN5w4EbRFVVYu7D\n\
2205OS5fxNSCukiS0fb6oQzUp0vIAycvvWHHbAy8T6UMoUor2nfvNcryiaOX5WBMLyh\n\
221yMZ5gMOyXjdm1bT1XSlvtXPYUzxvsGAzTqS8mXjw8h7mm5htAgMBAAEwDQYJKoZI\n\
222hvcNAQELBQADggEBABZ+I7D6wkeSrsi1NBLP2zoZ5oGh+INNcGTravfOQHs4Fbas\n\
223/CysaUYjsD3fmaDh4MxgWEqAmWnnBiojfpGX2SGuFqRBKyT9DgitBt0L7Ezg1k3h\n\
224bfSiFW4hXWp75grVO8xfML7ZcWMlhKrOsOMUGiy1qs3qsyJ3w7B2Tz78HhXGO5dd\n\
225jyPmZarhixKO92UpEvKGxjO0E/3UUNUzxKTAAgFfhKpuwHUgIijM/EppZtA8OcSh\n\
226fEztiV0xKfcPVx4d6dqRt/NMElK1Ivw2vUuxTymphZkkFOzht9m73/kyKaeFp8Ij\n\
227VRus1zGVD8IVpIdPMyz01WJyS7M0fWaHXKWo+Bo=\n\
228-----END CERTIFICATE-----";
229
230    const SPEC_FINGERPRINT: &str =
231        "4BADDE53A7F7CDEEED93189FD898E02BF6B4806CA4C05DE0ACE08319B86552FA";
232
233    #[test]
234    fn test_spec_fingerprint_der_matches() {
235        let (pem, _) = Pem::read(Cursor::new(SPEC_CERT.as_bytes())).unwrap();
236        let computed = fingerprint_from_cert_der(&pem.contents);
237        assert_eq!(computed, SPEC_FINGERPRINT);
238    }
239
240    #[test]
241    fn test_spec_fingerprint_pem_matches() {
242        let computed = fingerprint_from_cert_pem(SPEC_CERT).unwrap();
243        assert_eq!(computed, SPEC_FINGERPRINT);
244    }
245
246    #[test]
247    fn test_spec_cert_verification() {
248        verify_cert_from_pem(SPEC_CERT, Some(SPEC_PUBLIC_KEY)).unwrap();
249    }
250
251    #[test]
252    fn test_spec_cert_extract_public_key() {
253        let (pem, _) = Pem::read(Cursor::new(SPEC_CERT.as_bytes())).unwrap();
254        let extracted = public_key_from_cert_der(&pem.contents).unwrap();
255        assert_eq!(
256            extracted.trim(),
257            SPEC_PUBLIC_KEY.trim()
258        );
259    }
260
261    #[test]
262    fn test_generate_self_signed_rsa_and_verify() {
263        let identity = generate_self_signed().unwrap();
264        assert!(identity.cert_pem.contains("-----BEGIN CERTIFICATE-----"));
265        assert!(identity.key_pem.contains("-----BEGIN PRIVATE KEY-----"));
266        assert_eq!(identity.fingerprint.len(), 64);
267
268        // Verify fingerprint matches re-extracted PEM
269        let recomputed_fp = fingerprint_from_cert_pem(&identity.cert_pem).unwrap();
270        assert_eq!(identity.fingerprint, recomputed_fp);
271
272        // Verify self-signed certificate signature & validity
273        verify_cert_from_pem(&identity.cert_pem, None).unwrap();
274
275        // Parse with x509-parser to verify subject CN
276        let (pem, _) = Pem::read(Cursor::new(identity.cert_pem.as_bytes())).unwrap();
277        let (_, cert) = X509Certificate::from_der(&pem.contents).unwrap();
278        let cn = cert.subject().iter_common_name().next().unwrap();
279        assert_eq!(cn.as_str().unwrap(), "LocalSend User");
280    }
281
282    #[test]
283    fn test_generate_self_signed_ed25519_and_verify() {
284        let identity = generate_self_signed_ed25519().unwrap();
285        assert!(identity.cert_pem.contains("-----BEGIN CERTIFICATE-----"));
286        assert!(identity.key_pem.contains("-----BEGIN PRIVATE KEY-----"));
287        assert_eq!(identity.fingerprint.len(), 64);
288
289        // Verify self-signed certificate signature & validity
290        verify_cert_from_pem(&identity.cert_pem, None).unwrap();
291
292        let recomputed_fp = fingerprint_from_cert_pem(&identity.cert_pem).unwrap();
293        assert_eq!(identity.fingerprint, recomputed_fp);
294    }
295
296    #[test]
297    fn test_corrupted_cert_fails_verification() {
298        // Corrupt signature bytes of the spec cert
299        let mut corrupted = SPEC_CERT.to_string();
300        corrupted = corrupted.replace("VRus1zGVD8IVpIdPMyz01WJyS7M0fWaHXKWo+Bo=", "VRus1zGVD8IVpIdPMyz01WJySwAAAAAAAAAAAAA=");
301        let err = verify_cert_from_pem(&corrupted, Some(SPEC_PUBLIC_KEY));
302        assert!(err.is_err(), "Corrupted signature must fail verification");
303    }
304
305    #[test]
306    fn test_mismatched_public_key_fails_verification() {
307        let identity1 = generate_self_signed().unwrap();
308        let identity2 = generate_self_signed().unwrap();
309
310        let (pem2, _) = Pem::read(Cursor::new(identity2.cert_pem.as_bytes())).unwrap();
311        let pubkey2 = public_key_from_cert_der(&pem2.contents).unwrap();
312
313        let err = verify_cert_from_pem(&identity1.cert_pem, Some(&pubkey2));
314        assert!(err.is_err(), "Mismatched public key must fail verification");
315    }
316
317    #[test]
318    fn test_identity_persistence_and_load() {
319        let unique = format!("lanflare_test_ident_{}", rand::random::<u64>());
320        let temp_dir = std::env::temp_dir().join(unique);
321        std::fs::create_dir_all(&temp_dir).unwrap();
322        let path = temp_dir.join("identity.json");
323
324        // 1. Initial generation
325        let id1 = load_or_generate_identity(&path).unwrap();
326        assert_eq!(id1.fingerprint.len(), 64);
327        assert!(path.exists());
328
329        // 2. Load existing
330        let id2 = load_or_generate_identity(&path).unwrap();
331        assert_eq!(id1, id2, "Loaded identity must match previously saved identity");
332
333        // 3. Corrupt file -> should regenerate cleanly
334        std::fs::write(&path, "invalid json").unwrap();
335        let id3 = load_or_generate_identity(&path).unwrap();
336        assert_eq!(id3.fingerprint.len(), 64);
337        assert_ne!(id1.fingerprint, id3.fingerprint);
338
339        let _ = std::fs::remove_dir_all(&temp_dir);
340    }
341}