Skip to main content

wavekat_sip/
tls_error.rs

1//! What we report about a certificate we refused.
2//!
3//! A rejected certificate drops the connection — there is no continue-anyway
4//! path at any layer. What a consumer gets instead is the fingerprint it would
5//! be pinning and the reason the chain failed, which is what a
6//! trust-on-first-use prompt needs in order to be honest.
7
8use std::fmt;
9
10/// Why a server's certificate was refused.
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum CertFailure {
14    /// The certificate's validity period has ended.
15    Expired,
16    /// The certificate's validity period has not begun.
17    NotYetValid,
18    /// The certificate is valid, but for other names. RFC 5922 §7.3 requires
19    /// the account's SIP domain, not the host an SRV record named.
20    NameMismatch {
21        /// The names the certificate did present.
22        presented: Vec<String>,
23    },
24    /// The chain did not reach a trusted root.
25    UnknownIssuer,
26    /// The issuer has revoked the certificate.
27    Revoked,
28    /// The certificate's signature does not verify against its issuer's key.
29    BadSignature,
30    /// The chain was fine, but the leaf is not the certificate pinned by
31    /// [`TlsPolicy::Pinned`](crate::TlsPolicy::Pinned).
32    PinMismatch,
33    /// The certificate could not be parsed.
34    ///
35    /// Usually still carries a fingerprint: the verifier records the leaf's
36    /// SHA-256 over the raw wire bytes before delegating to the policy
37    /// verifier that raises this failure, so under
38    /// [`TlsPolicy::SystemRoots`](crate::TlsPolicy::SystemRoots) a malformed
39    /// leaf still reaches you as a typed [`UntrustedCertificate`] with a
40    /// usable fingerprint — this variant exists for the narrower case where
41    /// parsing fails before that point. Under
42    /// [`TlsPolicy::Pinned`](crate::TlsPolicy::Pinned) it cannot arise at
43    /// all: that verifier compares raw bytes and never parses X.509.
44    Malformed,
45    /// A TLS failure this enum has no dedicated variant for. This can still
46    /// be a certificate problem — one of the less common
47    /// `rustls::CertificateError` variants this crate has not given its own
48    /// case — in which case the message is prefixed `"certificate: "`, or it
49    /// can be a non-certificate TLS failure (a protocol error, no shared
50    /// cipher suite) reported as-is.
51    Other(String),
52}
53
54impl fmt::Display for CertFailure {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::Expired => write!(f, "certificate has expired"),
58            Self::NotYetValid => write!(f, "certificate is not yet valid"),
59            Self::NameMismatch { presented } => {
60                write!(f, "certificate is not valid for this SIP domain")?;
61                if !presented.is_empty() {
62                    write!(f, " (presented: {})", presented.join(", "))?;
63                }
64                Ok(())
65            }
66            Self::UnknownIssuer => write!(f, "certificate is not signed by a trusted issuer"),
67            Self::Revoked => write!(f, "certificate has been revoked"),
68            Self::BadSignature => write!(f, "certificate signature does not verify"),
69            Self::PinMismatch => write!(f, "certificate does not match the pinned fingerprint"),
70            Self::Malformed => write!(f, "certificate could not be parsed"),
71            Self::Other(msg) => write!(f, "TLS failure: {msg}"),
72        }
73    }
74}
75
76/// A certificate we refused, and enough about it for a consumer to decide what
77/// to do next.
78///
79/// The fingerprint is the SHA-256 of the leaf's DER encoding — the same value
80/// [`TlsPolicy::Pinned`](crate::TlsPolicy::Pinned) takes, so a consumer can
81/// offer to pin exactly what it just saw. A consumer that does so is making a
82/// trust-on-first-use decision, and its own UI should say plainly that an
83/// attacker present at that moment is the thing being pinned.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct UntrustedCertificate {
86    /// SHA-256 of the leaf certificate's DER encoding.
87    pub sha256: [u8; 32],
88    /// Why it was refused.
89    pub reason: CertFailure,
90}
91
92impl UntrustedCertificate {
93    /// The fingerprint as lowercase hex, the form a user is shown and compares.
94    pub fn fingerprint_hex(&self) -> String {
95        self.sha256.iter().map(|b| format!("{b:02x}")).collect()
96    }
97}
98
99impl fmt::Display for UntrustedCertificate {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "{} (sha256:{})", self.reason, self.fingerprint_hex())
102    }
103}
104
105impl std::error::Error for UntrustedCertificate {}
106
107/// Find an [`UntrustedCertificate`] inside an error returned by this crate.
108///
109/// TLS failures surface as `io::Error`s wrapping this type, which in turn reach
110/// a consumer inside a boxed error. Walking the `source()` chain by hand to find
111/// it is fiddly enough that the crate should just do it.
112pub fn untrusted_certificate<'a>(
113    err: &'a (dyn std::error::Error + 'static),
114) -> Option<&'a UntrustedCertificate> {
115    let mut cur = Some(err);
116    while let Some(e) = cur {
117        if let Some(u) = e.downcast_ref::<UntrustedCertificate>() {
118            return Some(u);
119        }
120        if let Some(io) = e.downcast_ref::<std::io::Error>() {
121            if let Some(inner) = io.get_ref() {
122                if let Some(u) = inner.downcast_ref::<UntrustedCertificate>() {
123                    return Some(u);
124                }
125            }
126        }
127        cur = e.source();
128    }
129    None
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    fn untrusted() -> UntrustedCertificate {
137        UntrustedCertificate {
138            sha256: [0xab; 32],
139            reason: CertFailure::UnknownIssuer,
140        }
141    }
142
143    #[test]
144    fn fingerprint_is_lowercase_hex_of_the_full_digest() {
145        let hex = untrusted().fingerprint_hex();
146        assert_eq!(hex.len(), 64);
147        assert!(hex.starts_with("abab"));
148    }
149
150    #[test]
151    fn display_carries_the_reason_and_the_fingerprint() {
152        let s = untrusted().to_string();
153        assert!(s.contains("trusted issuer"), "{s}");
154        assert!(s.contains("sha256:abab"), "{s}");
155    }
156
157    #[test]
158    fn name_mismatch_lists_what_was_presented() {
159        let f = CertFailure::NameMismatch {
160            presented: vec!["edge-3.example.net".to_string()],
161        };
162        assert!(f.to_string().contains("edge-3.example.net"));
163    }
164
165    #[test]
166    fn found_through_an_io_error_wrapper() {
167        let io = std::io::Error::new(std::io::ErrorKind::InvalidData, untrusted());
168        let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(io);
169        let found = untrusted_certificate(boxed.as_ref()).expect("found");
170        assert_eq!(found.reason, CertFailure::UnknownIssuer);
171    }
172
173    #[test]
174    fn absent_from_an_unrelated_error() {
175        let io = std::io::Error::other("something else");
176        assert!(untrusted_certificate(&io).is_none());
177    }
178}