Skip to main content

ntex_tls/x509/trust/
mod.rs

1//! Client certificate trust verification against a set of registered CA certificates.
2//!
3//! [`CaCerts::is_trusted`] semantics (all targets):
4//! - an empty store never trusts anything;
5//! - every registered certificate is a trust anchor, it does not have to be self-signed
6//!   (OpenSSL `X509_V_FLAG_PARTIAL_CHAIN`); in particular a registered leaf certificate
7//!   pins that leaf;
8//! - certificates sent by the peer ([`PeerCerts::chain`]) are untrusted intermediates;
9//! - validity periods of all certificates on the verified path, including the anchor, are
10//!   checked against `now`;
11//! - on success the earliest `not_after` of the verified path (leaf, intermediates and
12//!   anchor) is returned, the session must not outlive it;
13//! - no revocation checking.
14//!
15//! # Backends
16//!
17//! | Target  | Backend                                                         |
18//! |---------|-----------------------------------------------------------------|
19//! | rustls, non-Windows | `WebPKI` with the rustls crypto provider |
20//! | OpenSSL only, non-Windows | OpenSSL `X509Store` with `PARTIAL_CHAIN`, default purpose checks |
21//! | windows | Crypt32 private chain engine (exclusive roots)                  |
22//!
23//! The Crypt32 backend (Windows) differs from OpenSSL in these accepted ways:
24//! - a certificate with an extended key usage extension that does not contain
25//!   `clientAuth` is rejected (OpenSSL ignores EKU of intermediates);
26//! - weak signature algorithms (e.g. SHA-1) follow the Windows weak crypto policy;
27//! - basic constraints, X.509 v1 and critical extension handling follow Crypt32 rules;
28//! - a pinned leaf (leaf DER equal to a registered certificate) is trusted after a validity
29//!   check only, the chain engine does not support end-entity trust anchors;
30//! - error strings in [`TrustError`] differ.
31//!
32//! `WebPKI` also enforces client-authentication EKU and its own encoding/algorithm policy.
33//! It does not support verification times before 1970. SHA-1 thumbprints remain supported
34//! even when the crypto provider rejects SHA-1 certificate signatures.
35use std::{fmt, time::SystemTime};
36
37use ntex_bytes::ByteString;
38
39use crate::x509::{Certificate, PeerCerts, digest};
40
41#[cfg(all(not(windows), not(feature = "rustls"), feature = "openssl"))]
42#[path = "openssl.rs"]
43mod backend;
44#[cfg(all(not(windows), feature = "rustls"))]
45#[path = "rustls.rs"]
46mod backend;
47#[cfg(windows)]
48#[path = "crypt32.rs"]
49mod backend;
50
51/// Trust verification failure.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum TrustError {
54    /// Peer certificate is not trusted, or no CA certificates are registered.
55    Unauthenticated(ByteString),
56    /// Verification could not be performed.
57    Internal(ByteString),
58}
59
60/// Registered CA certificates (trust anchors).
61pub struct CaCerts {
62    anchors: Vec<Certificate>,
63    store: backend::Store,
64}
65
66impl CaCerts {
67    /// Builds a trust store from certificates.
68    ///
69    /// Certificates the backend refuses are logged (with the SHA-1 thumbprint) and skipped.
70    #[must_use]
71    pub fn from_certs<I, C>(certs: I) -> Self
72    where
73        I: IntoIterator<Item = C>,
74        C: Into<Certificate>,
75    {
76        Self::from_certs_with(certs.into_iter().map(|c| {
77            let c = c.into();
78            let sha1 = c.thumbprint(digest::Algorithm::Sha1);
79            (c, move |e: &str| {
80                log::error!("Unable to add cert '{sha1}' (SHA1 thumbprint) into X509Store: {e}");
81            })
82        }))
83    }
84
85    /// Builds a trust store from certificates, each paired with an error handler.
86    ///
87    /// The handler is called with the backend error if the certificate is refused; the
88    /// certificate is then skipped. Duplicates are added once and their handler is not called.
89    /// Lets callers report refusals with their own context (e.g. a configured name).
90    pub fn from_certs_with<I, F>(certs: I) -> Self
91    where
92        I: IntoIterator<Item = (Certificate, F)>,
93        F: FnOnce(&str),
94    {
95        let mut builder = backend::Builder::new();
96        let mut anchors: Vec<Certificate> = Vec::new();
97        for (cert, on_error) in certs {
98            if anchors.contains(&cert) {
99                continue;
100            }
101            match builder.add(&cert) {
102                Ok(()) => anchors.push(cert),
103                Err(e) => on_error(&e),
104            }
105        }
106        CaCerts {
107            anchors,
108            store: builder.build(),
109        }
110    }
111
112    /// Number of registered certificates (duplicates are counted once).
113    #[must_use]
114    pub fn len(&self) -> usize {
115        self.anchors.len()
116    }
117
118    /// Returns `true` if no certificates are registered.
119    #[must_use]
120    pub fn is_empty(&self) -> bool {
121        self.anchors.is_empty()
122    }
123
124    /// Registered certificates.
125    #[must_use]
126    pub fn certs(&self) -> &[Certificate] {
127        &self.anchors
128    }
129
130    /// Verifies the peer certificates against the registered CA certificates at time `now`.
131    ///
132    /// Returns the earliest `not_after` of the verified path, see the module docs.
133    ///
134    /// # Errors
135    /// - [`TrustError::Unauthenticated`] if no CA certificates are registered or the peer
136    ///   certificate is rejected;
137    /// - [`TrustError::Internal`] if the verification could not be performed.
138    pub fn is_trusted(&self, peer: &PeerCerts, now: SystemTime) -> Result<SystemTime, TrustError> {
139        if self.is_empty() {
140            return Err(TrustError::Unauthenticated(
141                "No CA certificates registered".into(),
142            ));
143        }
144        self.store.verify(peer, now)
145    }
146}
147
148impl Default for CaCerts {
149    fn default() -> Self {
150        Self::from_certs_with(std::iter::empty::<(Certificate, fn(&str))>())
151    }
152}
153
154impl fmt::Debug for CaCerts {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        f.debug_tuple("CaCerts").field(&self.anchors).finish()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use std::time::{Duration, UNIX_EPOCH};
163
164    use super::*;
165    use crate::x509::testgen::{self, San};
166
167    /// Fixed-date fixtures (P-256, SHA-256) so the tests run on every backend:
168    /// - `root` 2020..2120 -> `inter` (CA) 2021..2110 -> `leaf` 2022..2100;
169    /// - `other` 2020..2120, unrelated self-signed CA;
170    /// - `shortroot` 2020..2060 -> `shortleaf` 2022..2100.
171    fn c(name: &str) -> Certificate {
172        crate::x509::test_provider();
173        let pem: &[u8] = match name {
174            "root" => include_bytes!("../../../tests/testdata/root.pem"),
175            "inter" => include_bytes!("../../../tests/testdata/inter.pem"),
176            "leaf" => include_bytes!("../../../tests/testdata/leaf.pem"),
177            "other" => include_bytes!("../../../tests/testdata/other.pem"),
178            "shortroot" => include_bytes!("../../../tests/testdata/shortroot.pem"),
179            "shortleaf" => include_bytes!("../../../tests/testdata/shortleaf.pem"),
180            _ => unreachable!(),
181        };
182        Certificate::from_pem(pem).unwrap()
183    }
184
185    /// January 1st of `year`, 00:00 UTC.
186    fn year(year: u64) -> SystemTime {
187        let days: u64 = (1970..year)
188            .map(|y| {
189                if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
190                    366
191                } else {
192                    365
193                }
194            })
195            .sum();
196        UNIX_EPOCH + Duration::from_secs(days * 86400)
197    }
198
199    const NOW: u64 = 2050;
200
201    /// STATEMENT: Trust store diagnostics list registered certificates once without exposing backend internals.
202    ///
203    /// HOW:
204    /// - Format an empty store and a store constructed with a duplicate root.
205    /// - Compare the output with the single retained certificate's representation.
206    #[test]
207    fn debug_ca_certs() {
208        let root = c("root");
209        assert_eq!(format!("{:?}", CaCerts::default()), "CaCerts([])");
210        let store = CaCerts::from_certs([root.clone(), root.clone()]);
211        assert_eq!(format!("{store:?}"), format!("CaCerts([{root:?}])"));
212    }
213
214    /// STATEMENT: Generated EC and RSA private keys are redacted in normal and pretty diagnostics.
215    ///
216    /// HOW:
217    /// - Generate both supported key kinds and check both formatting modes against the redacted label.
218    #[test]
219    fn debug_generated_keys_are_redacted() {
220        for key in [testgen::key(), testgen::rsa_key()] {
221            assert_eq!(format!("{key:?}"), "Key(<redacted>)");
222            assert_eq!(format!("{key:#?}"), "Key(<redacted>)");
223        }
224    }
225
226    /// STATEMENT: Empty stores reject every peer.
227    ///
228    /// HOW:
229    /// - Verify the fixed leaf/intermediate against an empty store and check its error.
230    #[test]
231    fn empty_store() {
232        let peer = PeerCerts::new(c("leaf"), vec![c("inter")]);
233        let store = CaCerts::default();
234        assert!(store.is_empty());
235        assert_eq!(
236            store.is_trusted(&peer, year(NOW)),
237            Err(TrustError::Unauthenticated(
238                "No CA certificates registered".into()
239            ))
240        );
241    }
242
243    /// STATEMENT: A root trusts its complete chain but not missing or unrelated issuers.
244    ///
245    /// HOW:
246    /// - Register duplicate roots and verify the fixed chain, then remove or replace its issuer.
247    #[test]
248    fn chain_to_root() {
249        let store = CaCerts::from_certs([c("root"), c("root")]);
250        assert_eq!(store.len(), 1);
251
252        let peer = PeerCerts::new(c("leaf"), vec![c("inter")]);
253        assert_eq!(
254            store.is_trusted(&peer, year(NOW)).unwrap(),
255            c("leaf").not_after()
256        );
257
258        // missing intermediate
259        let peer = PeerCerts::new(c("leaf"), vec![]);
260        assert!(matches!(
261            store.is_trusted(&peer, year(NOW)),
262            Err(TrustError::Unauthenticated(_))
263        ));
264
265        // untrusted root
266        let store = CaCerts::from_certs([c("other")]);
267        let peer = PeerCerts::new(c("leaf"), vec![c("inter")]);
268        assert!(matches!(
269            store.is_trusted(&peer, year(NOW)),
270            Err(TrustError::Unauthenticated(_))
271        ));
272    }
273
274    /// STATEMENT: Intermediates and exact leaves can be registered as anchors.
275    ///
276    /// HOW:
277    /// - Verify a leaf against its intermediate and itself; reject its expired pin.
278    #[test]
279    fn partial_chain() {
280        // intermediate as anchor, expiry is min(leaf, inter)
281        let store = CaCerts::from_certs([c("inter")]);
282        let peer = PeerCerts::new(c("leaf"), vec![]);
283        assert_eq!(
284            store.is_trusted(&peer, year(NOW)).unwrap(),
285            c("leaf").not_after()
286        );
287
288        // pinned leaf
289        let store = CaCerts::from_certs([c("leaf")]);
290        assert_eq!(
291            store.is_trusted(&peer, year(NOW)).unwrap(),
292            c("leaf").not_after()
293        );
294        assert!(matches!(
295            store.is_trusted(&peer, year(2101)),
296            Err(TrustError::Unauthenticated(_))
297        ));
298    }
299
300    /// STATEMENT: Returned expiry includes the selected trust anchor.
301    ///
302    /// HOW:
303    /// - Verify a leaf whose root expires first and assert the root's expiry is returned.
304    #[test]
305    fn expiry_is_min_of_path() {
306        let store = CaCerts::from_certs([c("shortroot")]);
307        let peer = PeerCerts::new(c("shortleaf"), vec![]);
308        assert_eq!(c("shortroot").not_after(), year(2060));
309        assert_eq!(store.is_trusted(&peer, year(NOW)).unwrap(), year(2060));
310    }
311
312    /// STATEMENT: Verification uses the supplied time instead of the current clock.
313    ///
314    /// HOW:
315    /// - Verify the fixed chain before, within and after its validity interval.
316    #[test]
317    fn uses_now() {
318        let store = CaCerts::from_certs([c("root")]);
319        let peer = PeerCerts::new(c("leaf"), vec![c("inter")]);
320        assert!(store.is_trusted(&peer, year(2101)).is_err());
321        assert!(store.is_trusted(&peer, year(2021)).is_err());
322        assert!(store.is_trusted(&peer, year(2099)).is_ok());
323    }
324
325    /// STATEMENT: Unused certificates cannot shorten a verified path's lifetime.
326    ///
327    /// HOW:
328    /// - Add an unrelated shorter-lived root to both anchors and peer intermediates.
329    /// - Check the real chain still expires with its leaf.
330    #[test]
331    fn unrelated_expiry() {
332        let store = CaCerts::from_certs([c("shortroot"), c("root")]);
333        let peer = PeerCerts::new(c("leaf"), vec![c("shortroot"), c("inter"), c("other")]);
334        assert_eq!(
335            store.is_trusted(&peer, year(NOW)).unwrap(),
336            c("leaf").not_after()
337        );
338    }
339
340    /// STATEMENT: An expired anchor cannot authenticate an otherwise valid leaf.
341    ///
342    /// HOW:
343    /// - Verify the short-root fixture after the root expires but before its leaf expires.
344    #[test]
345    fn expired_anchor() {
346        let store = CaCerts::from_certs([c("shortroot")]);
347        let peer = PeerCerts::new(c("shortleaf"), vec![]);
348        assert!(matches!(
349            store.is_trusted(&peer, year(2070)),
350            Err(TrustError::Unauthenticated(_))
351        ));
352    }
353
354    /// STATEMENT: `WebPKI` reports unsupported historical verification times explicitly.
355    ///
356    /// HOW:
357    /// - Attempt verification one second before the Unix epoch and check the internal error.
358    #[cfg(all(not(windows), feature = "rustls"))]
359    #[test]
360    fn before_unix_epoch() {
361        let store = CaCerts::from_certs([c("root")]);
362        let peer = PeerCerts::new(c("leaf"), vec![c("inter")]);
363        assert!(matches!(
364            store.is_trusted(&peer, UNIX_EPOCH - Duration::from_secs(1)),
365            Err(TrustError::Internal(_))
366        ));
367    }
368
369    /// STATEMENT: Trust expires with an intermediate even when the leaf and root outlive it.
370    ///
371    /// HOW:
372    /// - Generate a chain whose intermediate expires before its leaf and root.
373    /// - Require the intermediate's expiry now and reject the chain a day after it.
374    #[test]
375    fn intermediate_expiry() {
376        let san = San::default();
377        let (rk, ik) = (testgen::key(), testgen::key());
378        let root = testgen::cert("root", true, None, &rk, &san, (-1, 3000));
379        let inter = testgen::cert("inter", true, Some((&root, &rk)), &ik, &san, (-1, 1000));
380        let leaf = testgen::cert(
381            "leaf",
382            false,
383            Some((&inter, &ik)),
384            &testgen::key(),
385            &san,
386            (-1, 2000),
387        );
388        let store = CaCerts::from_certs([root]);
389        let peer = PeerCerts::new(leaf, vec![inter.clone()]);
390        let now = SystemTime::now();
391        assert_eq!(store.is_trusted(&peer, now).unwrap(), inter.not_after());
392        let later = inter.not_after() + Duration::from_hours(24);
393        assert!(later < peer.leaf().not_after());
394        assert!(matches!(
395            store.is_trusted(&peer, later),
396            Err(TrustError::Unauthenticated(_))
397        ));
398    }
399
400    /// STATEMENT: CA trust enforces client-auth EKU while exact leaf pinning remains explicit.
401    ///
402    /// HOW:
403    /// - Issue leaves without EKU, with client-auth, with both, and server-auth only.
404    /// - Reject only the server-auth leaf under the CA, but permit each exact leaf pin.
405    /// - Reject a client-auth leaf issued by a server-auth-only intermediate.
406    #[cfg(any(windows, feature = "rustls"))]
407    #[test]
408    fn client_auth_usage() {
409        use crate::x509::testgen::ExtKeyUsage::{ClientAuth, ServerAuth};
410        let san = San::default();
411        let now = SystemTime::now();
412        let ck = testgen::key();
413        let ca = testgen::cert("ca", true, None, &ck, &san, (-1, 10));
414        let store = CaCerts::from_certs([ca.clone()]);
415        let issue = |issuer, ca, eku| {
416            testgen::cert_with_eku("x", ca, Some(issuer), &testgen::key(), &san, (-1, 10), eku)
417        };
418        for (eku, accepted) in [
419            (&[][..], true),
420            (&[ClientAuth][..], true),
421            (&[ServerAuth, ClientAuth][..], true),
422            (&[ServerAuth][..], false),
423        ] {
424            let peer = PeerCerts::new(issue((&ca, &ck), false, eku), vec![]);
425            assert_eq!(store.is_trusted(&peer, now).is_ok(), accepted, "{eku:?}");
426            let pinned = CaCerts::from_certs([peer.leaf().clone()]);
427            assert_eq!(
428                pinned.is_trusted(&peer, now).unwrap(),
429                peer.leaf().not_after()
430            );
431        }
432
433        let ik = testgen::key();
434        let inter = testgen::cert_with_eku(
435            "inter",
436            true,
437            Some((&ca, &ck)),
438            &ik,
439            &san,
440            (-1, 10),
441            &[ServerAuth],
442        );
443        let peer = PeerCerts::new(issue((&inter, &ik), false, &[ClientAuth]), vec![inter]);
444        assert!(matches!(
445            store.is_trusted(&peer, now),
446            Err(TrustError::Unauthenticated(_))
447        ));
448    }
449
450    /// STATEMENT: Rejected anchors invoke their callback and do not displace valid deduplicated anchors.
451    ///
452    /// HOW:
453    /// - Supply a CA with duplicate basic constraints between two copies of a valid root.
454    /// - Check its callback fires once and the remaining store still authenticates the fixture chain.
455    #[cfg(all(not(windows), feature = "rustls"))]
456    #[test]
457    fn rejected_anchor_callback() {
458        use rcgen::{BasicConstraints, CertificateParams, CustomExtension, IsCa, KeyPair};
459        use std::cell::RefCell;
460        let mut bad = CertificateParams::default();
461        bad.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
462        bad.custom_extensions = vec![CustomExtension::from_oid_content(
463            &[2, 5, 29, 19],
464            vec![0x30, 0],
465        )];
466        let bad = bad.self_signed(&KeyPair::generate().unwrap()).unwrap();
467        let bad = Certificate::from_der(bad.der().to_vec()).unwrap();
468        let rejected = RefCell::new(Vec::new());
469        let store =
470            CaCerts::from_certs_with([c("root"), bad, c("root")].into_iter().enumerate().map(
471                |(index, cert)| {
472                    let rejected = &rejected;
473                    (cert, move |error: &str| {
474                        rejected.borrow_mut().push((index, error.to_owned()));
475                    })
476                },
477            ));
478        assert_eq!(rejected.borrow().len(), 1);
479        assert_eq!(rejected.borrow()[0].0, 1);
480        assert_ne!(rejected.borrow()[0].1, "");
481        assert_eq!(store.certs(), &[c("root")]);
482        let peer = PeerCerts::new(c("leaf"), vec![c("inter")]);
483        assert_eq!(
484            store.is_trusted(&peer, year(NOW)).unwrap(),
485            peer.leaf().not_after()
486        );
487    }
488}