lanflare_core/crypto/
cert.rs1use 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#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12pub struct TlsIdentity {
13 pub cert_pem: String,
15 pub key_pem: String,
17 pub fingerprint: String,
19}
20
21pub 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 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
40pub 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
50pub fn generate_self_signed() -> anyhow::Result<TlsIdentity> {
58 generate_self_signed_rsa(2048)
59}
60
61pub 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
87pub 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
111pub 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
122pub 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
128pub 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
141pub 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
153fn 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 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
182pub 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
188pub 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 let recomputed_fp = fingerprint_from_cert_pem(&identity.cert_pem).unwrap();
270 assert_eq!(identity.fingerprint, recomputed_fp);
271
272 verify_cert_from_pem(&identity.cert_pem, None).unwrap();
274
275 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_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 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 let id1 = load_or_generate_identity(&path).unwrap();
326 assert_eq!(id1.fingerprint.len(), 64);
327 assert!(path.exists());
328
329 let id2 = load_or_generate_identity(&path).unwrap();
331 assert_eq!(id1, id2, "Loaded identity must match previously saved identity");
332
333 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}