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}