//! A client certificate's validity window, read from the PEM itself. //! //! The window is the certificate's own `notBefore`/`notAfter`, the same two //! timestamps the store checks at login, so a caller that asks "how much of //! its life is left" and the store never disagree about the answer. use std::path::Path; use x509_cert::der::DecodePem as _; use crate::{Error, client::ENV_CLIENT_CERT}; /// The gauge a service exports for its client certificate's `notBefore`, unix /// seconds. The alert rule `hh-bao-client-cert-lifetime` selects this name. pub const NOT_BEFORE_METRIC: &str = "bao.client_cert.not_before"; /// The gauge for the certificate's `notAfter`, unix seconds. pub const NOT_AFTER_METRIC: &str = "bao.client_cert.not_after"; /// A certificate's validity window, `(notBefore, notAfter)` in unix seconds. /// /// # Errors /// [`Error::Certificate`] when `pem` is not one PEM-encoded X.509 certificate. pub fn validity(pem: &str) -> Result<(i64, i64), Error> { let cert = x509_cert::Certificate::from_pem(pem.as_bytes()) .map_err(|e| Error::Certificate(e.to_string()))?; let validity = &cert.tbs_certificate.validity; let secs = |t: x509_cert::time::Time| { i64::try_from(t.to_unix_duration().as_secs()).unwrap_or(i64::MAX) }; Ok((secs(validity.not_before), secs(validity.not_after))) } /// [`validity`] of the client certificate file at `path` (the file /// [`ENV_CLIENT_CERT`] names). /// /// # Errors /// [`Error::Identity`] when the file cannot be read, [`Error::Certificate`] /// when it is not a certificate. pub fn file_validity(path: &Path) -> Result<(i64, i64), Error> { let pem = std::fs::read_to_string(path).map_err(|source| Error::Identity { var: ENV_CLIENT_CERT, path: path.display().to_string(), source, })?; validity(&pem) } #[cfg(test)] pub(crate) mod tests { use super::*; /// The window `certificate_pem` is built with: 2026-01-01T00:00:00Z to /// 2026-04-01T00:00:00Z. pub(crate) const NOT_BEFORE: i64 = 1_767_225_600; pub(crate) const NOT_AFTER: i64 = 1_775_001_600; /// A certificate valid from `not_before` to `not_after` (unix seconds), /// PEM-encoded. The name, key and signature are placeholders: [`validity`] /// reads the window and verifies nothing. pub(crate) fn certificate_pem(not_before: i64, not_after: i64) -> String { use x509_cert::der::{ EncodePem as _, asn1::{BitString, ObjectIdentifier, UtcTime}, pem::LineEnding, }; use x509_cert::{ Certificate, TbsCertificate, Version, name::Name, serial_number::SerialNumber, spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}, time::{Time, Validity}, }; let time = |secs: i64| { let secs = u64::try_from(secs).expect("a date after 1970"); Time::UtcTime( UtcTime::from_unix_duration(std::time::Duration::from_secs(secs)) .expect("a date UTCTime can hold"), ) }; let algorithm = AlgorithmIdentifierOwned { oid: ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2"), parameters: None, }; let cert = Certificate { tbs_certificate: TbsCertificate { version: Version::V3, serial_number: SerialNumber::new(&[1]).expect("a serial number"), signature: algorithm.clone(), issuer: Name::default(), validity: Validity { not_before: time(not_before), not_after: time(not_after), }, subject: Name::default(), subject_public_key_info: SubjectPublicKeyInfoOwned { algorithm: algorithm.clone(), subject_public_key: BitString::from_bytes(&[0]).expect("a bit string"), }, issuer_unique_id: None, subject_unique_id: None, extensions: None, }, signature_algorithm: algorithm, signature: BitString::from_bytes(&[0]).expect("a bit string"), }; cert.to_pem(LineEnding::LF).expect("encodes") } #[test] fn the_window_is_read_from_the_certificate() { assert_eq!( validity(&certificate_pem(NOT_BEFORE, NOT_AFTER)).expect("a well-formed certificate"), (NOT_BEFORE, NOT_AFTER) ); } #[test] fn something_that_is_not_a_certificate_is_an_error() { assert!(matches!( validity("not a certificate"), Err(Error::Certificate(_)) )); assert!(validity("").is_err()); } #[test] fn a_file_is_read_through_its_path() { let dir = std::env::temp_dir().join(format!("swarm-secret-client-cert-{}", std::process::id())); std::fs::create_dir_all(&dir).expect("scratch dir"); let path = dir.join("cert.pem"); std::fs::write(&path, certificate_pem(NOT_BEFORE, NOT_AFTER)).expect("write"); assert_eq!( file_validity(&path).expect("readable"), (NOT_BEFORE, NOT_AFTER) ); std::fs::remove_dir_all(&dir).expect("cleanup"); } #[test] fn a_missing_file_names_the_variable_and_path() { let err = file_validity(Path::new("/nonexistent/cert.pem")).expect_err("no such file"); assert!( matches!(&err, Error::Identity { var, path, .. } if *var == ENV_CLIENT_CERT && path == "/nonexistent/cert.pem"), "{err}" ); } }