@@ -18,14 +18,14 @@ use std::collections::HashMap;
1818use std:: fmt;
1919use std:: sync:: Arc ;
2020
21- use anyhow:: { Context , Result } ;
21+ use anyhow:: { bail , Context , Result } ;
2222use arc_swap:: { ArcSwap , Guard } ;
2323use or_panic:: ResultOrPanic ;
2424use rustls:: pki_types:: pem:: PemObject ;
2525use rustls:: pki_types:: { CertificateDer , PrivateKeyDer } ;
2626use rustls:: server:: { ClientHello , ResolvesServerCert } ;
2727use rustls:: sign:: CertifiedKey ;
28- use tracing:: info;
28+ use tracing:: { info, warn } ;
2929
3030use crate :: kv:: CertData ;
3131
@@ -241,7 +241,23 @@ impl CertStoreBuilder {
241241 . duration_since ( std:: time:: UNIX_EPOCH )
242242 . context ( "system time is before Unix epoch" ) ?
243243 . as_secs ( ) ;
244- anyhow:: ensure!( data. not_after > now, "certificate is expired" ) ;
244+ // `not_after` is a replicated field; the chain is what gets served.
245+ if data. not_after <= now {
246+ bail ! ( "certificate record for {domain} is expired" ) ;
247+ }
248+ let cert_not_after = get_cert_expiry ( & data. cert_pem ) . with_context ( || {
249+ format ! ( "failed to read the expiry of the certificate for {domain}" )
250+ } ) ?;
251+ if cert_not_after <= now {
252+ bail ! ( "certificate for {domain} is expired" ) ;
253+ }
254+ if cert_not_after < data. not_after {
255+ // Renewal is scheduled from the record, so this delays it.
256+ warn ! (
257+ "cert_store: the record for {domain} outlives its certificate by {} seconds" ,
258+ data. not_after - cert_not_after
259+ ) ;
260+ }
245261
246262 let certified_key = parse_certified_key ( & data. cert_pem , & data. key_pem )
247263 . with_context ( || format ! ( "failed to parse certificate for {}" , domain) ) ?;
@@ -255,7 +271,7 @@ impl CertStoreBuilder {
255271 info ! (
256272 "cert_store: prepared wildcard certificate for *.{} (expires: {})" ,
257273 domain,
258- format_expiry( data . not_after )
274+ format_expiry( cert_not_after )
259275 ) ;
260276
261277 // Store metadata
@@ -280,6 +296,14 @@ impl Default for CertStoreBuilder {
280296 }
281297}
282298
299+ /// The `notAfter` of the first certificate in a PEM chain, as a Unix timestamp.
300+ pub ( crate ) fn get_cert_expiry ( cert_pem : & str ) -> Option < u64 > {
301+ use x509_parser:: prelude:: * ;
302+ let pem = Pem :: iter_from_buffer ( cert_pem. as_bytes ( ) ) . next ( ) ?. ok ( ) ?;
303+ let cert = pem. parse_x509 ( ) . ok ( ) ?;
304+ u64:: try_from ( cert. validity ( ) . not_after . timestamp ( ) ) . ok ( )
305+ }
306+
283307/// Parse certificate and private key PEM strings into a CertifiedKey
284308fn parse_certified_key ( cert_pem : & str , key_pem : & str ) -> Result < CertifiedKey > {
285309 let certs = CertificateDer :: pem_slice_iter ( cert_pem. as_bytes ( ) )
@@ -445,6 +469,42 @@ mod tests {
445469 assert ! ( !store. has_cert_for_sni( "example.org" ) ) ;
446470 }
447471
472+ /// A certificate whose chain has already expired, described by a record
473+ /// that says otherwise.
474+ fn make_expired_cert_data ( ) -> CertData {
475+ use ra_tls:: rcgen:: { self , CertificateParams , KeyPair } ;
476+
477+ let key_pair = KeyPair :: generate ( ) . expect ( "failed to generate key pair" ) ;
478+ let mut params = CertificateParams :: new ( vec ! [ "test.example.com" . to_string( ) ] )
479+ . expect ( "failed to create cert params" ) ;
480+ params. not_before = rcgen:: date_time_ymd ( 2019 , 1 , 1 ) ;
481+ params. not_after = rcgen:: date_time_ymd ( 2020 , 1 , 1 ) ;
482+ let cert = params
483+ . self_signed ( & key_pair)
484+ . expect ( "failed to generate self-signed cert" ) ;
485+
486+ CertData {
487+ cert_pem : cert. pem ( ) ,
488+ key_pem : key_pair. serialize_pem ( ) ,
489+ not_after : u64:: MAX ,
490+ issued_by : 1 ,
491+ issued_at : 0 ,
492+ }
493+ }
494+
495+ #[ test]
496+ fn an_expired_chain_is_rejected_however_the_record_dates_it ( ) {
497+ let mut builder = CertStoreBuilder :: new ( ) ;
498+ let err = builder
499+ . add_cert ( "example.com" , & make_expired_cert_data ( ) )
500+ . expect_err ( "an expired certificate chain must be rejected" ) ;
501+ assert ! (
502+ err. to_string( ) . contains( "expired" ) ,
503+ "unexpected rejection reason: {err:#}"
504+ ) ;
505+ assert ! ( !builder. build( ) . has_cert( "example.com" ) ) ;
506+ }
507+
448508 #[ test]
449509 fn mismatched_key_update_retains_previous_certificate ( ) {
450510 let original = make_test_cert_data ( ) ;
0 commit comments