Skip to main content

lanflare_core/crypto/
verifier.rs

1//! Custom certificate verifier for fingerprint pinning without CA trust roots.
2
3use crate::crypto::cert::{fingerprint_from_cert_der, verify_cert_from_der};
4use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
5use rustls::client::WebPkiServerVerifier;
6use rustls::pki_types::pem::PemObject;
7use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
8use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
9use rustls::server::WebPkiClientVerifier;
10use rustls::{
11    CertificateError, DigitallySignedStruct, DistinguishedName, Error, OtherError, RootCertStore,
12    SignatureScheme,
13};
14use std::fmt::{Debug, Display, Formatter};
15use std::sync::Arc;
16
17/// The reason a peer certificate was rejected, preserving a descriptive error chain.
18#[derive(Clone, Eq, PartialEq)]
19pub struct RejectedCert(pub String);
20
21impl Display for RejectedCert {
22    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23        write!(f, "{}", self.0)
24    }
25}
26
27impl Debug for RejectedCert {
28    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33impl std::error::Error for RejectedCert {}
34
35fn rejected(reason: String) -> Error {
36    tracing::warn!("{reason}");
37    Error::InvalidCertificate(CertificateError::Other(OtherError(Arc::new(RejectedCert(
38        reason,
39    )))))
40}
41
42/// Verifies server certificates against an optional pinned SHA-256 fingerprint.
43///
44/// LocalSend peers use self-signed certificates without PKI CAs. Peer identity is defined
45/// by the uppercase-hex SHA-256 fingerprint of the DER certificate.
46///
47/// When `expected_fingerprint` is `Some`, any certificate whose fingerprint differs is
48/// immediately rejected with a descriptive error.
49/// When `expected_fingerprint` is `None`, any cryptographically valid self-signed / peer
50/// certificate is accepted (Trust On First Use, for discovery probes).
51#[derive(Debug)]
52pub struct PinnedServerCertVerifier {
53    inner: Arc<dyn ServerCertVerifier>,
54    expected_fingerprint: Option<String>,
55}
56
57impl PinnedServerCertVerifier {
58    /// Creates a new pinned server certificate verifier.
59    ///
60    /// `own_cert_pem` is added to a local dummy root store so that the underlying signature
61    /// validator initializes properly without an empty-root error. It is never used as a trust CA.
62    pub fn new(own_cert_pem: &str, expected_fingerprint: Option<String>) -> anyhow::Result<Self> {
63        let mut root_store = RootCertStore::empty();
64        root_store.add(CertificateDer::from_pem_slice(own_cert_pem.as_bytes())?)?;
65
66        let inner = WebPkiServerVerifier::builder(Arc::new(root_store)).build()?;
67        Ok(Self {
68            inner,
69            expected_fingerprint: expected_fingerprint.map(|f| f.to_ascii_uppercase()),
70        })
71    }
72
73    /// Access the pinned fingerprint filter (if any).
74    pub fn expected_fingerprint(&self) -> Option<&str> {
75        self.expected_fingerprint.as_deref()
76    }
77}
78
79impl ServerCertVerifier for PinnedServerCertVerifier {
80    fn verify_server_cert(
81        &self,
82        end_entity: &CertificateDer<'_>,
83        _intermediates: &[CertificateDer<'_>],
84        _server_name: &ServerName<'_>,
85        _ocsp_response: &[u8],
86        _now: UnixTime,
87    ) -> Result<ServerCertVerified, Error> {
88        // LocalSend peers are identified by IP without matching SANs; check crypto & validity
89        verify_cert_from_der(end_entity.as_ref(), None)
90            .map_err(|e| rejected(format!("Server certificate is not valid: {e:#}")))?;
91
92        if let Some(expected) = &self.expected_fingerprint {
93            let actual = fingerprint_from_cert_der(end_entity.as_ref());
94            if actual != *expected {
95                return Err(rejected(format!(
96                    "Server certificate fingerprint mismatch: expected {expected}, got {actual}"
97                )));
98            }
99        }
100
101        Ok(ServerCertVerified::assertion())
102    }
103
104    fn verify_tls12_signature(
105        &self,
106        message: &[u8],
107        cert: &CertificateDer<'_>,
108        dss: &DigitallySignedStruct,
109    ) -> Result<HandshakeSignatureValid, Error> {
110        self.inner.verify_tls12_signature(message, cert, dss)
111    }
112
113    fn verify_tls13_signature(
114        &self,
115        message: &[u8],
116        cert: &CertificateDer<'_>,
117        dss: &DigitallySignedStruct,
118    ) -> Result<HandshakeSignatureValid, Error> {
119        self.inner.verify_tls13_signature(message, cert, dss)
120    }
121
122    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
123        self.inner.supported_verify_schemes()
124    }
125}
126
127/// Verifies client certificates on incoming TLS connections.
128///
129/// If `mandatory` is true, connections without client certificates are rejected.
130/// If `expected_fingerprint` is `Some`, the client certificate fingerprint must match it.
131#[derive(Debug)]
132pub struct PinnedClientCertVerifier {
133    inner: Arc<dyn ClientCertVerifier>,
134    mandatory: bool,
135    expected_fingerprint: Option<String>,
136}
137
138impl PinnedClientCertVerifier {
139    /// Creates a new client certificate verifier.
140    ///
141    /// `own_cert_pem` is used to initialize the inner verifier's signature schemes.
142    pub fn new(
143        own_cert_pem: &str,
144        mandatory: bool,
145        expected_fingerprint: Option<String>,
146    ) -> anyhow::Result<Self> {
147        let mut root_store = RootCertStore::empty();
148        root_store.add(CertificateDer::from_pem_slice(own_cert_pem.as_bytes())?)?;
149
150        let inner = WebPkiClientVerifier::builder(Arc::new(root_store)).build()?;
151        Ok(Self {
152            inner,
153            mandatory,
154            expected_fingerprint: expected_fingerprint.map(|f| f.to_ascii_uppercase()),
155        })
156    }
157
158    /// Access whether client auth is strictly required.
159    pub fn is_mandatory(&self) -> bool {
160        self.mandatory
161    }
162
163    /// Access the pinned client fingerprint filter (if any).
164    pub fn expected_fingerprint(&self) -> Option<&str> {
165        self.expected_fingerprint.as_deref()
166    }
167}
168
169impl ClientCertVerifier for PinnedClientCertVerifier {
170    fn offer_client_auth(&self) -> bool {
171        true
172    }
173
174    fn client_auth_mandatory(&self) -> bool {
175        self.mandatory
176    }
177
178    fn root_hint_subjects(&self) -> &[DistinguishedName] {
179        self.inner.root_hint_subjects()
180    }
181
182    fn verify_client_cert(
183        &self,
184        end_entity: &CertificateDer<'_>,
185        _intermediates: &[CertificateDer<'_>],
186        _now: UnixTime,
187    ) -> Result<ClientCertVerified, Error> {
188        verify_cert_from_der(end_entity.as_ref(), None)
189            .map_err(|e| rejected(format!("Client certificate is not valid: {e:#}")))?;
190
191        if let Some(expected) = &self.expected_fingerprint {
192            let actual = fingerprint_from_cert_der(end_entity.as_ref());
193            if actual != *expected {
194                return Err(rejected(format!(
195                    "Client certificate fingerprint mismatch: expected {expected}, got {actual}"
196                )));
197            }
198        }
199
200        Ok(ClientCertVerified::assertion())
201    }
202
203    fn verify_tls12_signature(
204        &self,
205        message: &[u8],
206        cert: &CertificateDer<'_>,
207        dss: &DigitallySignedStruct,
208    ) -> Result<HandshakeSignatureValid, Error> {
209        self.inner.verify_tls12_signature(message, cert, dss)
210    }
211
212    fn verify_tls13_signature(
213        &self,
214        message: &[u8],
215        cert: &CertificateDer<'_>,
216        dss: &DigitallySignedStruct,
217    ) -> Result<HandshakeSignatureValid, Error> {
218        self.inner.verify_tls13_signature(message, cert, dss)
219    }
220
221    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
222        self.inner.supported_verify_schemes()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use crate::crypto::cert::generate_self_signed;
230
231    static SPEC_CERT: &str = "-----BEGIN CERTIFICATE-----\n\
232MIIDGTCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQsFADBQMRcwFQYDVQQDEw5Mb2Nh\n\
233bFNlbmQgVXNlcjEJMAcGA1UEChMAMQkwBwYDVQQLEwAxCTAHBgNVBAcTADEJMAcG\n\
234A1UECBMAMQkwBwYDVQQGEwAwHhcNMjUwMjA5MDAwMzE0WhcNMzUwMjA3MDAwMzE0\n\
235WjBQMRcwFQYDVQQDEw5Mb2NhbFNlbmQgVXNlcjEJMAcGA1UEChMAMQkwBwYDVQQL\n\
236EwAxCTAHBgNVBAcTADEJMAcGA1UECBMAMQkwBwYDVQQGEwAwggEiMA0GCSqGSIb3\n\
237DQEBAQUAA4IBDwAwggEKAoIBAQCL24MxhGfrdJm0Q8ZGiBkZ27ldcEChB4w7rSbJ\n\
238yiKeosoNbJl2kyj5dZjfBhWGgDLGDMM5w+Mh/5SrWgTL/QrhbB+lsrxILLznWqBi\n\
239R8wJP0P2YW9fBahQskJQcUXt/3jsCsMTWea4rWc3HZGh03bAkJfLM+PDSOfTpvAZ\n\
2406DQSp9QLzC9bgVNnq3W0SvOZGpF0xRa4InCyTUgxsNsV4+GIrmN5w4EbRFVVYu7D\n\
2415OS5fxNSCukiS0fb6oQzUp0vIAycvvWHHbAy8T6UMoUor2nfvNcryiaOX5WBMLyh\n\
242yMZ5gMOyXjdm1bT1XSlvtXPYUzxvsGAzTqS8mXjw8h7mm5htAgMBAAEwDQYJKoZI\n\
243hvcNAQELBQADggEBABZ+I7D6wkeSrsi1NBLP2zoZ5oGh+INNcGTravfOQHs4Fbas\n\
244/CysaUYjsD3fmaDh4MxgWEqAmWnnBiojfpGX2SGuFqRBKyT9DgitBt0L7Ezg1k3h\n\
245bfSiFW4hXWp75grVO8xfML7ZcWMlhKrOsOMUGiy1qs3qsyJ3w7B2Tz78HhXGO5dd\n\
246jyPmZarhixKO92UpEvKGxjO0E/3UUNUzxKTAAgFfhKpuwHUgIijM/EppZtA8OcSh\n\
247fEztiV0xKfcPVx4d6dqRt/NMElK1Ivw2vUuxTymphZkkFOzht9m73/kyKaeFp8Ij\n\
248VRus1zGVD8IVpIdPMyz01WJyS7M0fWaHXKWo+Bo=\n\
249-----END CERTIFICATE-----";
250
251    const SPEC_FINGERPRINT: &str =
252        "4BADDE53A7F7CDEEED93189FD898E02BF6B4806CA4C05DE0ACE08319B86552FA";
253
254    fn init_provider() {
255        let _ = rustls::crypto::ring::default_provider().install_default();
256    }
257
258    #[test]
259    fn test_server_verifier_accepts_matching_fingerprint() {
260        init_provider();
261        let verifier =
262            PinnedServerCertVerifier::new(SPEC_CERT, Some(SPEC_FINGERPRINT.to_string())).unwrap();
263
264        let cert_der = CertificateDer::from_pem_slice(SPEC_CERT.as_bytes()).unwrap();
265        let server_name = ServerName::try_from("192.168.1.50").unwrap();
266
267        let res = verifier.verify_server_cert(
268            &cert_der,
269            &[],
270            &server_name,
271            &[],
272            UnixTime::now(),
273        );
274        assert!(res.is_ok());
275    }
276
277    #[test]
278    fn test_server_verifier_accepts_case_insensitive_fingerprint() {
279        init_provider();
280        let lowercase_fp = SPEC_FINGERPRINT.to_ascii_lowercase();
281        let verifier =
282            PinnedServerCertVerifier::new(SPEC_CERT, Some(lowercase_fp)).unwrap();
283
284        let cert_der = CertificateDer::from_pem_slice(SPEC_CERT.as_bytes()).unwrap();
285        let server_name = ServerName::try_from("192.168.1.50").unwrap();
286
287        let res = verifier.verify_server_cert(
288            &cert_der,
289            &[],
290            &server_name,
291            &[],
292            UnixTime::now(),
293        );
294        assert!(res.is_ok());
295    }
296
297    #[test]
298    fn test_server_verifier_rejects_mismatching_fingerprint() {
299        init_provider();
300        let rogue_fp = "0".repeat(64);
301        let verifier =
302            PinnedServerCertVerifier::new(SPEC_CERT, Some(rogue_fp.clone())).unwrap();
303
304        let cert_der = CertificateDer::from_pem_slice(SPEC_CERT.as_bytes()).unwrap();
305        let server_name = ServerName::try_from("192.168.1.50").unwrap();
306
307        let err = verifier
308            .verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now())
309            .unwrap_err();
310        let msg = format!("{err:?}");
311        assert!(msg.contains("fingerprint mismatch"), "{msg}");
312        assert!(msg.contains(&rogue_fp), "{msg}");
313        assert!(msg.contains(SPEC_FINGERPRINT), "{msg}");
314    }
315
316    #[test]
317    fn test_server_verifier_accepts_any_valid_cert_when_unpinned() {
318        init_provider();
319        let verifier = PinnedServerCertVerifier::new(SPEC_CERT, None).unwrap();
320
321        let cert_der = CertificateDer::from_pem_slice(SPEC_CERT.as_bytes()).unwrap();
322        let server_name = ServerName::try_from("192.168.1.50").unwrap();
323
324        let res = verifier.verify_server_cert(
325            &cert_der,
326            &[],
327            &server_name,
328            &[],
329            UnixTime::now(),
330        );
331        assert!(res.is_ok());
332    }
333
334    #[test]
335    fn test_client_verifier_accepts_matching_fingerprint() {
336        init_provider();
337        let verifier =
338            PinnedClientCertVerifier::new(SPEC_CERT, true, Some(SPEC_FINGERPRINT.to_string()))
339                .unwrap();
340
341        assert!(verifier.is_mandatory());
342        let cert_der = CertificateDer::from_pem_slice(SPEC_CERT.as_bytes()).unwrap();
343
344        let res = verifier.verify_client_cert(&cert_der, &[], UnixTime::now());
345        assert!(res.is_ok());
346    }
347
348    #[test]
349    fn test_client_verifier_rejects_mismatched_fingerprint() {
350        init_provider();
351        let verifier =
352            PinnedClientCertVerifier::new(SPEC_CERT, true, Some("A".repeat(64))).unwrap();
353
354        let cert_der = CertificateDer::from_pem_slice(SPEC_CERT.as_bytes()).unwrap();
355        let err = verifier.verify_client_cert(&cert_der, &[], UnixTime::now()).unwrap_err();
356        let msg = format!("{err:?}");
357        assert!(msg.contains("fingerprint mismatch"), "{msg}");
358    }
359
360    #[test]
361    fn test_client_verifier_with_newly_generated_identity() {
362        init_provider();
363        let identity = generate_self_signed().unwrap();
364        let verifier = PinnedClientCertVerifier::new(
365            &identity.cert_pem,
366            false,
367            Some(identity.fingerprint.clone()),
368        )
369        .unwrap();
370
371        assert!(!verifier.is_mandatory());
372        let cert_der = CertificateDer::from_pem_slice(identity.cert_pem.as_bytes()).unwrap();
373        assert!(verifier.verify_client_cert(&cert_der, &[], UnixTime::now()).is_ok());
374    }
375}