1+ use std:: borrow:: Cow ;
12use std:: cmp:: Ordering ;
23use std:: collections:: { BTreeMap , HashSet } ;
34use std:: ops:: Add ;
45
5- use chrono:: { Duration , NaiveDate , Utc } ;
6+ use chrono:: { Datelike , Duration , NaiveDate , Utc } ;
67use num_bigint:: BigUint ;
78use pki_types:: pem:: PemObject ;
89use pki_types:: CertificateDer ;
9- use serde:: Deserialize ;
10+ use serde:: de:: DeserializeOwned ;
11+ use serde:: { Deserialize , Serialize } ;
12+ use url:: Url ;
1013
1114// Fetch root certificate data from the CCADB server.
1215//
@@ -259,6 +262,224 @@ impl From<&str> for TrustBits {
259262 }
260263}
261264
265+ pub async fn crl_hosts ( store : RootStore ) -> Result < HashSet < String > , Box < dyn core:: error:: Error > > {
266+ let ccadb_url = "https://ccadb.my.site.com/services/apexrest/v1/allcertificaterecords" ;
267+ let client = build_client ( ISRG_ROOT_X2 ) ?;
268+
269+ let mut records = HashSet :: default ( ) ;
270+ let mut page_number = 1 ;
271+ let mut decade = 2000 ;
272+ let last_decade = ( Utc :: now ( ) . year ( ) / 10 * 10 ) as u16 ;
273+ let today = Utc :: now ( ) . naive_utc ( ) . date ( ) ;
274+ loop {
275+ let input = AllCertificateRecordsRequest {
276+ filters : Some ( AllCertificateRecordsRequestFilters {
277+ not_before_decade : Some ( decade) ,
278+ page_number,
279+ } ) ,
280+ field_sets : vec ! [
281+ AllCertificateRecordsRequestFieldSet :: PertainingToCertificatesIssued ,
282+ AllCertificateRecordsRequestFieldSet :: Capabilities ,
283+ ] ,
284+ } ;
285+
286+ let req = client. post ( ccadb_url) . json ( & input) . build ( ) ?;
287+ let rsp = client. execute ( req) . await ?. error_for_status ( ) ?;
288+ let json = rsp. text ( ) . await ?;
289+ let data = serde_json:: from_str :: < AllCertificateRecordsResponse > ( & json) ?;
290+ for info in data. data {
291+ if info. certificate_data . valid_to < today
292+ || !matches ! (
293+ info. trusted( store) ,
294+ StoreStatus :: Trusted | StoreStatus :: Included
295+ )
296+ || !info. for_tls ( )
297+ {
298+ continue ;
299+ }
300+
301+ let Some ( pertaining) = info. pertaining_to_certificates_issued else {
302+ continue ;
303+ } ;
304+
305+ let iter = pertaining
306+ . all_full_crl_urls
307+ . iter ( )
308+ . chain ( pertaining. partitioned_crls . iter ( ) ) ;
309+ for url in iter {
310+ if url. trim ( ) . is_empty ( ) {
311+ continue ;
312+ }
313+
314+ let Ok ( url) = Url :: parse ( url) else {
315+ println ! ( "invalid URL: {url}" ) ;
316+ continue ;
317+ } ;
318+
319+ if let Some ( host) = url. host_str ( ) {
320+ records. insert ( host. to_string ( ) ) ;
321+ }
322+ }
323+ }
324+
325+ page_number += 1 ;
326+ if data. meta . pagination . current_page_number < data. meta . pagination . total_pages {
327+ continue ;
328+ }
329+
330+ if decade >= last_decade {
331+ break ;
332+ }
333+
334+ decade += 10 ;
335+ page_number = 1 ;
336+ }
337+
338+ Ok ( records)
339+ }
340+
341+ #[ derive( Debug , Deserialize ) ]
342+ #[ serde( rename_all = "PascalCase" ) ]
343+ struct AllCertificateRecordsResponse {
344+ meta : AllCertificateRecordsResponseMeta ,
345+ data : Vec < RecordData > ,
346+ }
347+
348+ #[ derive( Debug , Deserialize ) ]
349+ #[ serde( rename_all = "PascalCase" ) ]
350+ struct RecordData {
351+ root_store_status : RootStoreStatus ,
352+ certificate_data : CertificateData ,
353+ pertaining_to_certificates_issued : Option < PertainingToCertificatesIssued > ,
354+ capabilities : Option < Capabilities > ,
355+ }
356+
357+ impl RecordData {
358+ fn trusted ( & self , store : RootStore ) -> StoreStatus {
359+ match store {
360+ RootStore :: Apple => self . root_store_status . apple_status ,
361+ RootStore :: Chrome => self . root_store_status . chrome_status ,
362+ RootStore :: Microsoft => self . root_store_status . microsoft_status ,
363+ RootStore :: Mozilla => self . root_store_status . mozilla_status ,
364+ }
365+ }
366+
367+ fn for_tls ( & self ) -> bool {
368+ match & self . capabilities {
369+ Some ( capabilities) => capabilities. tls_capable || capabilities. tls_ev_capable ,
370+ None => false ,
371+ }
372+ }
373+ }
374+
375+ #[ derive( Debug , Deserialize ) ]
376+ #[ serde( rename_all = "PascalCase" ) ]
377+ struct RootStoreStatus {
378+ apple_status : StoreStatus ,
379+ chrome_status : StoreStatus ,
380+ microsoft_status : StoreStatus ,
381+ mozilla_status : StoreStatus ,
382+ }
383+
384+ #[ derive( Clone , Copy , Debug , Deserialize , Eq , PartialEq ) ]
385+ enum StoreStatus {
386+ Trusted ,
387+ #[ serde( rename = "Not Trusted" ) ]
388+ NotTrusted ,
389+ #[ serde( rename = "Not Included" ) ]
390+ NotIncluded ,
391+ #[ serde( rename = "Not Yet Included" ) ]
392+ NotYetIncluded ,
393+ Disabled ,
394+ Included ,
395+ NotBefore ,
396+ Removed ,
397+ Blocked ,
398+ }
399+
400+ #[ derive( Debug , Deserialize ) ]
401+ #[ serde( rename_all = "PascalCase" ) ]
402+ struct CertificateData {
403+ valid_to : NaiveDate ,
404+ }
405+
406+ #[ derive( Debug , Deserialize ) ]
407+ struct PertainingToCertificatesIssued {
408+ #[ serde( rename = "JSONArrayOfAllFullCRLURLs" , deserialize_with = "json_string" ) ]
409+ all_full_crl_urls : Vec < String > ,
410+ #[ serde(
411+ rename = "JSONArrayOfPartitionedCRLs" ,
412+ deserialize_with = "json_string"
413+ ) ]
414+ partitioned_crls : Vec < String > ,
415+ }
416+
417+ fn json_string < ' de , D : serde:: Deserializer < ' de > , T : DeserializeOwned + Default > (
418+ deserializer : D ,
419+ ) -> Result < T , D :: Error > {
420+ let s = <Cow < ' de , str > >:: deserialize ( deserializer) ?;
421+ if s. is_empty ( ) || s == "\" \" " {
422+ return Ok ( T :: default ( ) ) ;
423+ }
424+
425+ serde_json:: from_str ( s. as_ref ( ) ) . map_err ( serde:: de:: Error :: custom)
426+ }
427+
428+ #[ derive( Debug , Deserialize ) ]
429+ #[ serde( rename_all = "PascalCase" ) ]
430+ struct Capabilities {
431+ #[ serde( rename = "TLSCapable" ) ]
432+ tls_capable : bool ,
433+ #[ serde( rename = "TLSEVCapable" ) ]
434+ tls_ev_capable : bool ,
435+ }
436+
437+ #[ derive( Debug , Deserialize ) ]
438+ #[ serde( rename_all = "PascalCase" ) ]
439+ struct AllCertificateRecordsResponseMeta {
440+ pagination : AllCertificateRecordsResponsePagination ,
441+ }
442+
443+ #[ derive( Debug , Deserialize ) ]
444+ #[ serde( rename_all = "PascalCase" ) ]
445+ struct AllCertificateRecordsResponsePagination {
446+ total_pages : u16 ,
447+ current_page_number : u16 ,
448+ }
449+
450+ #[ derive( Debug , Serialize ) ]
451+ struct AllCertificateRecordsRequest {
452+ filters : Option < AllCertificateRecordsRequestFilters > ,
453+ #[ serde( rename = "fieldSets" ) ]
454+ field_sets : Vec < AllCertificateRecordsRequestFieldSet > ,
455+ }
456+
457+ #[ derive( Debug , Serialize ) ]
458+ struct AllCertificateRecordsRequestFilters {
459+ #[ serde( rename = "notBeforeDecade" ) ]
460+ not_before_decade : Option < u16 > ,
461+ #[ serde( rename = "PageNumber" ) ]
462+ page_number : u16 ,
463+ }
464+
465+ /// Field sets that can be requested as part of an [`AllCertificateRecordsRequest`].
466+ ///
467+ /// https://github.com/mozilla/CCADB-Tools/blob/master/API_AllCertificateRecords/README.md#3-dynamic-field-sets
468+ #[ derive( Debug , Serialize ) ]
469+ enum AllCertificateRecordsRequestFieldSet {
470+ Capabilities ,
471+ PertainingToCertificatesIssued ,
472+ }
473+
474+ #[ non_exhaustive]
475+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
476+ pub enum RootStore {
477+ Apple ,
478+ Chrome ,
479+ Microsoft ,
480+ Mozilla ,
481+ }
482+
262483/// Build a reqwest client that only trusts the CA certificate authenticating the CCADB server
263484fn build_client ( root_pem : & str ) -> Result < reqwest:: Client , reqwest:: Error > {
264485 // If we see Unknown CA TLS validation failures from the Reqwest client in the future it
@@ -280,6 +501,8 @@ fn build_client(root_pem: &str) -> Result<reqwest::Client, reqwest::Error> {
280501 . build ( )
281502}
282503
504+ // https://letsencrypt.org/certs/isrg-root-x2-cross-signed.pem
505+ const ISRG_ROOT_X2 : & str = include_str ! ( "data/isrg-root-x2-cross-signed.pem" ) ;
283506const DIGI_CERT_GLOBAL_ROOT_G2 : & str = include_str ! ( "data/DigiCertGlobalRootG2.pem" ) ;
284507
285508static EXCLUDED_FINGERPRINTS : & [ & str ] = & [
@@ -294,6 +517,15 @@ static EXCLUDED_FINGERPRINTS: &[&str] = &[
294517mod tests {
295518 use super :: * ;
296519
520+ #[ tokio:: test]
521+ async fn test_crl_hosts ( ) {
522+ let hosts = crl_hosts ( RootStore :: Chrome ) . await . unwrap ( ) ;
523+ dbg ! ( & hosts) ;
524+ assert ! ( hosts. contains( "x2.c.lencr.org" ) ) ;
525+ assert ! ( hosts. contains( "crl.apple.com" ) ) ;
526+ assert ! ( hosts. contains( "crl.pki.goog" ) ) ;
527+ }
528+
297529 #[ test]
298530 fn test_trusted_for_tls ( ) {
299531 let mut metadata = CertificateMetadata {
0 commit comments