use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CertFailure {
Expired,
NotYetValid,
NameMismatch {
presented: Vec<String>,
},
UnknownIssuer,
Revoked,
BadSignature,
PinMismatch,
Malformed,
Other(String),
}
impl fmt::Display for CertFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Expired => write!(f, "certificate has expired"),
Self::NotYetValid => write!(f, "certificate is not yet valid"),
Self::NameMismatch { presented } => {
write!(f, "certificate is not valid for this SIP domain")?;
if !presented.is_empty() {
write!(f, " (presented: {})", presented.join(", "))?;
}
Ok(())
}
Self::UnknownIssuer => write!(f, "certificate is not signed by a trusted issuer"),
Self::Revoked => write!(f, "certificate has been revoked"),
Self::BadSignature => write!(f, "certificate signature does not verify"),
Self::PinMismatch => write!(f, "certificate does not match the pinned fingerprint"),
Self::Malformed => write!(f, "certificate could not be parsed"),
Self::Other(msg) => write!(f, "TLS failure: {msg}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UntrustedCertificate {
pub sha256: [u8; 32],
pub reason: CertFailure,
}
impl UntrustedCertificate {
pub fn fingerprint_hex(&self) -> String {
self.sha256.iter().map(|b| format!("{b:02x}")).collect()
}
}
impl fmt::Display for UntrustedCertificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (sha256:{})", self.reason, self.fingerprint_hex())
}
}
impl std::error::Error for UntrustedCertificate {}
pub fn untrusted_certificate<'a>(
err: &'a (dyn std::error::Error + 'static),
) -> Option<&'a UntrustedCertificate> {
let mut cur = Some(err);
while let Some(e) = cur {
if let Some(u) = e.downcast_ref::<UntrustedCertificate>() {
return Some(u);
}
if let Some(io) = e.downcast_ref::<std::io::Error>() {
if let Some(inner) = io.get_ref() {
if let Some(u) = inner.downcast_ref::<UntrustedCertificate>() {
return Some(u);
}
}
}
cur = e.source();
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn untrusted() -> UntrustedCertificate {
UntrustedCertificate {
sha256: [0xab; 32],
reason: CertFailure::UnknownIssuer,
}
}
#[test]
fn fingerprint_is_lowercase_hex_of_the_full_digest() {
let hex = untrusted().fingerprint_hex();
assert_eq!(hex.len(), 64);
assert!(hex.starts_with("abab"));
}
#[test]
fn display_carries_the_reason_and_the_fingerprint() {
let s = untrusted().to_string();
assert!(s.contains("trusted issuer"), "{s}");
assert!(s.contains("sha256:abab"), "{s}");
}
#[test]
fn name_mismatch_lists_what_was_presented() {
let f = CertFailure::NameMismatch {
presented: vec!["edge-3.example.net".to_string()],
};
assert!(f.to_string().contains("edge-3.example.net"));
}
#[test]
fn found_through_an_io_error_wrapper() {
let io = std::io::Error::new(std::io::ErrorKind::InvalidData, untrusted());
let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(io);
let found = untrusted_certificate(boxed.as_ref()).expect("found");
assert_eq!(found.reason, CertFailure::UnknownIssuer);
}
#[test]
fn absent_from_an_unrelated_error() {
let io = std::io::Error::other("something else");
assert!(untrusted_certificate(&io).is_none());
}
}