use super::TlsMetadata;
use anyhow::{Result, anyhow};
use chrono::Utc;
use rustls::{
DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
client::{
WebPkiServerVerifier,
danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
},
pki_types::{CertificateDer, ServerName, UnixTime},
};
use std::{
fmt,
sync::{Arc, Mutex},
};
use x509_parser::prelude::{FromDer, X509Certificate};
#[derive(Debug, Clone, Default)]
pub struct CapturedCertMetadata {
pub subject: Option<String>,
pub issuer: Option<String>,
pub expiry_days: Option<i64>,
}
impl From<CapturedCertMetadata> for TlsMetadata {
fn from(captured: CapturedCertMetadata) -> Self {
Self {
cert_subject: captured.subject,
cert_issuer: captured.issuer,
cert_expiry_days: captured.expiry_days,
..Default::default()
}
}
}
#[derive(Clone)]
pub struct CertCapturingVerifier {
captured: Arc<Mutex<Option<CapturedCertMetadata>>>,
inner_verifier: Arc<WebPkiServerVerifier>,
}
impl fmt::Debug for CertCapturingVerifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CertCapturingVerifier")
.field("captured", &self.captured)
.field("inner_verifier", &"WebPkiServerVerifier")
.finish()
}
}
impl CertCapturingVerifier {
pub fn new() -> Result<Self> {
let root_store: RootCertStore = webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect();
let inner_verifier = WebPkiServerVerifier::builder(Arc::new(root_store))
.build()
.map_err(|e| anyhow!("failed to build WebPKI verifier: {e}"))?;
Ok(Self {
captured: Arc::new(Mutex::new(None)),
inner_verifier,
})
}
pub fn with_root_certificates(root_store: RootCertStore) -> Result<Self> {
let inner_verifier = WebPkiServerVerifier::builder(Arc::new(root_store))
.build()
.map_err(|e| anyhow!("failed to build WebPKI verifier: {e}"))?;
Ok(Self {
captured: Arc::new(Mutex::new(None)),
inner_verifier,
})
}
#[must_use]
pub fn get_captured(&self) -> Option<CapturedCertMetadata> {
self.captured.lock().ok()?.clone()
}
fn extract_metadata(cert_der: &[u8]) -> Result<CapturedCertMetadata> {
let (_, cert) = X509Certificate::from_der(cert_der)
.map_err(|e| anyhow!("failed to parse certificate: {e}"))?;
let subject = Some(cert.subject().to_string());
let issuer = Some(cert.issuer().to_string());
let raw = cert.validity().not_after.to_datetime();
let not_after =
chrono::DateTime::<Utc>::from_timestamp(raw.unix_timestamp(), raw.nanosecond())
.ok_or_else(|| anyhow!("invalid certificate expiry timestamp"))?;
let remaining = not_after - Utc::now();
let expiry_days = Some(remaining.num_days());
Ok(CapturedCertMetadata {
subject,
issuer,
expiry_days,
})
}
}
impl ServerCertVerifier for CertCapturingVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, TlsError> {
if let Ok(metadata) = Self::extract_metadata(end_entity.as_ref())
&& let Ok(mut captured) = self.captured.lock()
{
*captured = Some(metadata);
}
self.inner_verifier.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
)
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
self.inner_verifier
.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
self.inner_verifier
.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.inner_verifier.supported_verify_schemes()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use rustls::crypto::ring::default_provider;
fn ensure_crypto_provider() {
let _ = rustls::crypto::CryptoProvider::install_default(default_provider());
}
#[test]
fn test_verifier_creation() {
ensure_crypto_provider();
let verifier = CertCapturingVerifier::new();
assert!(verifier.is_ok());
}
#[test]
fn test_captured_initially_none() {
ensure_crypto_provider();
let verifier = CertCapturingVerifier::new().unwrap();
assert!(verifier.get_captured().is_none());
}
#[test]
fn test_metadata_conversion() {
let captured = CapturedCertMetadata {
subject: Some("CN=example.com".to_string()),
issuer: Some("CN=CA".to_string()),
expiry_days: Some(90),
};
let tls_metadata: TlsMetadata = captured.clone().into();
assert_eq!(tls_metadata.cert_subject, captured.subject);
assert_eq!(tls_metadata.cert_issuer, captured.issuer);
assert_eq!(tls_metadata.cert_expiry_days, captured.expiry_days);
}
}