Skip to main content

toole_core/
file_certif.rs

1use crate::ToolError;
2use directories::ProjectDirs;
3use rcgen::{date_time_ymd, CertificateParams, DistinguishedName, DnType, KeyPair, SanType};
4use sha2::{Digest, Sha256};
5use std::collections::HashMap;
6use std::net::IpAddr;
7use std::path::PathBuf;
8use tokio::fs;
9
10/// vérificateur de certificat par empreinte épinglée (TOFU) : au premier
11/// contact je n'attends aucune empreinte (expected = None) et j'accepte pour
12/// la mémoriser ensuite ; aux contacts suivants je refuse tout certificat dont
13/// l'empreinte diffère de celle épinglée pour ce pair (identité changée =
14/// attaque de l'homme du milieu probable)
15#[derive(Debug)]
16pub struct PinnedServerVerifier {
17    pub expected: Option<String>,
18}
19
20impl rustls::client::danger::ServerCertVerifier for PinnedServerVerifier {
21    fn verify_server_cert(
22        &self,
23        end_entity: &rustls::pki_types::CertificateDer<'_>,
24        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
25        _server_name: &rustls::pki_types::ServerName<'_>,
26        _ocsp_response: &[u8],
27        _now: rustls::pki_types::UnixTime,
28    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
29        let fp = fingerprint(end_entity.as_ref());
30        if let Some(expected) = &self.expected {
31            if *expected != fp {
32                return Err(rustls::Error::General(format!(
33                    "empreinte du pair ({fp}) differente de celle epinguee ({expected}) : attaque MITM ?"
34                )));
35            }
36        }
37        Ok(rustls::client::danger::ServerCertVerified::assertion())
38    }
39
40    fn verify_tls12_signature(
41        &self,
42        _message: &[u8],
43        _cert: &rustls::pki_types::CertificateDer<'_>,
44        _dss: &rustls::DigitallySignedStruct,
45    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
46        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
47    }
48
49    fn verify_tls13_signature(
50        &self,
51        _message: &[u8],
52        _cert: &rustls::pki_types::CertificateDer<'_>,
53        _dss: &rustls::DigitallySignedStruct,
54    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
55        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
56    }
57
58    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
59        rustls::crypto::ring::default_provider()
60            .signature_verification_algorithms
61            .supported_schemes()
62    }
63}
64
65/// empreinte SHA-256 (hex) du certificat : elle identifie la clé du pair tant
66/// que le certificat persisté est réutilisé (c'est le cas : certificat() relit
67/// cert.pem au démarrage). Je hache le DER complet, pas le SPKI, pour rester
68/// sur la donnée déjà exposée par le handshake rustls
69pub fn fingerprint(cert_der: &[u8]) -> String {
70    let mut hasher = Sha256::new();
71    hasher.update(cert_der);
72    hasher
73        .finalize()
74        .iter()
75        .map(|b| format!("{b:02x}"))
76        .collect()
77}
78
79/// empreinte du certificat présenté par le pair sur une connexion QUIC
80/// établie : le handshake ayant réussi, le cert est celui que le pair a servi
81pub fn peer_fingerprint(connection: &quinn::Connection) -> Option<String> {
82    let identity = connection.peer_identity()?;
83    let certs = identity
84        .downcast::<Vec<rustls::pki_types::CertificateDer<'static>>>()
85        .ok()?;
86    let cert = certs.first()?;
87    Some(fingerprint(cert.as_ref()))
88}
89
90/// empreinte épinglée pour un device_id, si elle existe (None = premier
91/// contact). Un fichier absent ou illisible retombe sur None : au pire on
92/// re-pinne, c'est dégradé mais jamais bloquant
93pub fn pin_for(device_id: &str) -> Option<String> {
94    let raw = std::fs::read_to_string(pins_path().ok()?).ok()?;
95    let pins: HashMap<String, String> = serde_json::from_str(&raw).ok()?;
96    pins.get(device_id).cloned()
97}
98
99/// j'épingle (ou je ré-épingle) l'empreinte d'un device_id dans le fichier
100/// d'empreintes persisté
101pub fn save_pin(device_id: &str, fingerprint: &str) -> Result<(), ToolError> {
102    let path = pins_path()?;
103    let mut pins: HashMap<String, String> = match std::fs::read_to_string(&path) {
104        Ok(raw) => serde_json::from_str(&raw).unwrap_or_default(),
105        Err(_) => HashMap::new(),
106    };
107    pins.insert(device_id.to_string(), fingerprint.to_string());
108    let raw = serde_json::to_string_pretty(&pins)?;
109    std::fs::write(path, raw)?;
110    Ok(())
111}
112
113fn pins_path() -> Result<PathBuf, ToolError> {
114    Ok(data_dir()?.join("pins.json"))
115}
116
117pub async fn certificat() -> Result<(String, String), ToolError> {
118    let (key_file, cert_file) = data_file()?;
119
120    if key_file.exists() && cert_file.exists() {
121        let cert_pem = fs::read_to_string(&cert_file).await?;
122        let key_pem = fs::read_to_string(&key_file).await?;
123        return Ok((cert_pem, key_pem));
124    }
125
126    let my_local_ip = local_ip_address::local_ip().unwrap_or(IpAddr::from([127, 0, 0, 1]));
127
128    let mut params: CertificateParams = Default::default();
129    params.not_before = date_time_ymd(2026, 1, 1);
130    params.not_after = date_time_ymd(2036, 1, 1);
131
132    params.distinguished_name = DistinguishedName::new();
133    params
134        .distinguished_name
135        .push(DnType::OrganizationName, "Toolé");
136    params
137        .distinguished_name
138        .push(DnType::CommonName, "Toolé P2P Server");
139
140    params.subject_alt_names = vec![
141        SanType::DnsName("localhost".try_into().map_err(|_| {
142            ToolError::IoError(std::io::Error::new(
143                std::io::ErrorKind::InvalidData,
144                "invalid DNS name",
145            ))
146        })?),
147        SanType::IpAddress(my_local_ip),
148    ];
149
150    let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).map_err(|e| {
151        ToolError::IoError(std::io::Error::other(format!("key generation failed: {e}")))
152    })?;
153
154    let cert = params
155        .self_signed(&key_pair)
156        .map_err(|e| ToolError::IoError(std::io::Error::other(format!("self-sign failed: {e}"))))?;
157
158    let cert_pem = cert.pem();
159    let key_pem = key_pair.serialize_pem();
160
161    fs::write(&cert_file, cert_pem.as_bytes()).await?;
162    fs::write(&key_file, key_pem.as_bytes()).await?;
163
164    // je restreins l'accès à la clé privée (lecture seule pour l'utilisateur
165    // courant) : un autre compte local ne doit pas pouvoir la récupérer
166    #[cfg(unix)]
167    {
168        use std::os::unix::fs::PermissionsExt;
169        let _ = fs::set_permissions(&key_file, std::fs::Permissions::from_mode(0o600)).await;
170    }
171
172    Ok((cert_pem, key_pem))
173}
174
175fn data_dir() -> Result<PathBuf, ToolError> {
176    let proj_dirs =
177        ProjectDirs::from("com", "Tiligre Open Space", "Toole").ok_or(ToolError::AppDirError)?;
178    let data_dir = proj_dirs.data_dir();
179    std::fs::create_dir_all(data_dir)?;
180    Ok(data_dir.to_path_buf())
181}
182
183fn data_file() -> Result<(PathBuf, PathBuf), ToolError> {
184    let data_dir = data_dir()?;
185
186    // je créé aussi le sous-dossier certs
187    let cert_dir = data_dir.join("certs");
188    std::fs::create_dir_all(&cert_dir)?;
189
190    Ok((cert_dir.join("key.pem"), cert_dir.join("cert.pem")))
191}