use std::{
error::Error,
panic::{self, AssertUnwindSafe},
sync::{Arc, Mutex, PoisonError},
};
#[cfg(target_vendor = "apple")]
pub mod apple;
pub trait CertVerifier: Send + Sync + 'static {
fn verify(&self, peer: &PeerCertificates<'_>) -> Result<(), CertRejected>;
}
impl<F> CertVerifier for F
where
F: Fn(&PeerCertificates<'_>) -> Result<(), CertRejected> + Send + Sync + 'static,
{
fn verify(&self, peer: &PeerCertificates<'_>) -> Result<(), CertRejected> {
self(peer)
}
}
#[derive(Clone, Copy, Debug)]
pub struct PeerCertificates<'a> {
chain: &'a [&'a [u8]],
server_name: Option<&'a str>,
}
impl<'a> PeerCertificates<'a> {
#[must_use]
pub fn new(chain: &'a [&'a [u8]], server_name: Option<&'a str>) -> Self {
Self { chain, server_name }
}
#[must_use]
pub fn chain(&self) -> &'a [&'a [u8]] {
self.chain
}
#[must_use]
pub fn server_name(&self) -> Option<&'a str> {
self.server_name
}
}
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct CertRejected {
message: String,
#[source]
source: Option<Box<dyn Error + Send + Sync>>,
}
impl CertRejected {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
#[must_use]
pub fn with_source(mut self, source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct RejectionReport(Arc<Mutex<Option<CertRejected>>>);
impl RejectionReport {
fn record(&self, rejected: CertRejected) {
let mut slot = self.0.lock().unwrap_or_else(PoisonError::into_inner);
if slot.is_none() {
*slot = Some(rejected);
}
}
pub(crate) fn take(&self) -> Option<CertRejected> {
self.0.lock().unwrap_or_else(PoisonError::into_inner).take()
}
}
pub(crate) fn to_squiche_verifier(
verifier: &Arc<dyn CertVerifier>,
report: Option<RejectionReport>,
) -> impl Fn(&[&[u8]], Option<&str>) -> squiche::CertificateVerdict + Send + Sync + 'static {
let verifier = Arc::clone(verifier);
move |chain, server_name| {
let peer = PeerCertificates::new(chain, server_name);
let verdict = panic::catch_unwind(AssertUnwindSafe(|| verifier.verify(&peer)));
let rejected = match verdict {
Ok(Ok(())) => return squiche::CertificateVerdict::Trusted,
Ok(Err(rejected)) => rejected,
Err(_) => CertRejected::new("the certificate verifier panicked"),
};
if let Some(report) = &report {
report.record(rejected);
}
squiche::CertificateVerdict::Untrusted
}
}
#[cfg(test)]
mod tests {
use super::*;
fn verdict_of(
verifier: impl Fn(&[&[u8]], Option<&str>) -> squiche::CertificateVerdict,
) -> squiche::CertificateVerdict {
let chain: [&[u8]; 1] = [b"der"];
verifier(&chain, Some("localhost"))
}
#[test]
fn closure_implements_the_trait() {
let verifier: Arc<dyn CertVerifier> = Arc::new(|peer: &PeerCertificates<'_>| {
match peer.server_name() {
Some("trusted") => Ok(()),
_ => Err(CertRejected::new("unexpected name")),
}
});
let chain: [&[u8]; 1] = [b"der"];
assert!(
verifier
.verify(&PeerCertificates::new(&chain, Some("trusted")))
.is_ok()
);
assert_eq!(
verifier
.verify(&PeerCertificates::new(&chain, Some("other")))
.unwrap_err()
.message(),
"unexpected name"
);
}
#[test]
fn rejection_keeps_its_source() {
let rejected = CertRejected::new("chain is not trusted")
.with_source(std::io::Error::other("errSecNotTrusted"));
assert_eq!(rejected.to_string(), "chain is not trusted");
assert_eq!(
rejected.source().map(ToString::to_string),
Some("errSecNotTrusted".to_string())
);
}
#[test]
fn report_keeps_the_first_rejection() {
let report = RejectionReport::default();
report.record(CertRejected::new("first"));
report.record(CertRejected::new("second"));
assert_eq!(
report.take().map(|r| r.message().to_string()),
Some("first".to_string())
);
assert!(report.take().is_none());
}
#[test]
fn a_panicking_verifier_rejects_with_a_reason() {
let verifier: Arc<dyn CertVerifier> =
Arc::new(|_: &PeerCertificates<'_>| -> Result<(), CertRejected> {
panic!("verifier is broken")
});
let report = RejectionReport::default();
let squiche_verifier = to_squiche_verifier(&verifier, Some(report.clone()));
let hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let verdict = verdict_of(squiche_verifier);
panic::set_hook(hook);
assert_eq!(verdict, squiche::CertificateVerdict::Untrusted);
assert_eq!(
report.take().map(|r| r.message().to_string()),
Some("the certificate verifier panicked".to_string())
);
}
}