1use 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#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum TrustError {
54 Unauthenticated(ByteString),
56 Internal(ByteString),
58}
59
60pub struct CaCerts {
62 anchors: Vec<Certificate>,
63 store: backend::Store,
64}
65
66impl CaCerts {
67 #[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 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 #[must_use]
114 pub fn len(&self) -> usize {
115 self.anchors.len()
116 }
117
118 #[must_use]
120 pub fn is_empty(&self) -> bool {
121 self.anchors.is_empty()
122 }
123
124 #[must_use]
126 pub fn certs(&self) -> &[Certificate] {
127 &self.anchors
128 }
129
130 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 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 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 #[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 #[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 #[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 #[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 let peer = PeerCerts::new(c("leaf"), vec![]);
260 assert!(matches!(
261 store.is_trusted(&peer, year(NOW)),
262 Err(TrustError::Unauthenticated(_))
263 ));
264
265 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 #[test]
279 fn partial_chain() {
280 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}