hashiverse_lib/tools/
cert_validation.rs1use crate::tools::time::TimeMillis;
25use rustls_pki_types::{CertificateDer, ServerName, UnixTime};
26use std::net::IpAddr;
27use std::str::FromStr;
28use std::time::Duration;
29use webpki::EndEntityCert;
30
31pub fn is_cert_valid(chain_der: &[Vec<u8>], announced_address: &str, now: TimeMillis) -> bool {
34 let Some((leaf_der_vec, intermediate_der_vecs)) = chain_der.split_first() else {
35 return false;
36 };
37
38 let leaf_der: CertificateDer<'_> = CertificateDer::from(leaf_der_vec.as_slice());
39 let leaf_cert: EndEntityCert<'_> = match EndEntityCert::try_from(&leaf_der) {
40 Ok(c) => c,
41 Err(_) => return false,
42 };
43
44 let intermediate_ders: Vec<CertificateDer<'_>> = intermediate_der_vecs.iter().map(|d| CertificateDer::from(d.as_slice())).collect();
45
46 let now_unix: UnixTime = match time_millis_to_unix_time(now) {
47 Some(t) => t,
48 None => return false,
49 };
50
51 let trust_anchors: &[rustls_pki_types::TrustAnchor<'_>] = webpki_roots::TLS_SERVER_ROOTS;
52
53 if leaf_cert
54 .verify_for_usage(
55 webpki::ALL_VERIFICATION_ALGS,
56 trust_anchors,
57 &intermediate_ders,
58 now_unix,
59 webpki::KeyUsage::server_auth(),
60 None,
61 None,
62 )
63 .is_err()
64 {
65 return false;
66 }
67
68 let Some(ip_str) = strip_port_from_address(announced_address) else {
69 return false;
70 };
71
72 let server_name: ServerName<'_> = match build_ip_server_name(&ip_str) {
73 Some(name) => name,
74 None => return false,
75 };
76
77 leaf_cert.verify_is_valid_for_subject_name(&server_name).is_ok()
78}
79
80fn time_millis_to_unix_time(time_millis: TimeMillis) -> Option<UnixTime> {
81 let millis: i64 = time_millis.0;
82 if millis < 0 {
83 return None;
84 }
85 Some(UnixTime::since_unix_epoch(Duration::from_millis(millis as u64)))
86}
87
88fn strip_port_from_address(announced_address: &str) -> Option<String> {
93 let trimmed: &str = announced_address.trim();
94 if trimmed.is_empty() {
95 return None;
96 }
97
98 if let Some(rest_after_open_bracket) = trimmed.strip_prefix('[') {
99 let close_bracket_pos: usize = rest_after_open_bracket.find(']')?;
100 let ipv6_body: &str = &rest_after_open_bracket[..close_bracket_pos];
101 let suffix: &str = &rest_after_open_bracket[close_bracket_pos + 1..];
102 if !suffix.is_empty() {
107 let port_str: &str = suffix.strip_prefix(':')?;
108 if port_str.is_empty() || !port_str.bytes().all(|b| b.is_ascii_digit()) {
109 return None;
110 }
111 }
112 return Some(ipv6_body.to_string());
113 }
114
115 match trimmed.rsplit_once(':') {
116 Some((host_part, port_part)) if !host_part.contains(':') => {
117 if port_part.is_empty() || !port_part.bytes().all(|b| b.is_ascii_digit()) {
120 return None;
121 }
122 Some(host_part.to_string())
123 }
124 _ => Some(trimmed.to_string()),
128 }
129}
130
131fn build_ip_server_name(host_str: &str) -> Option<ServerName<'static>> {
132 let ip_addr: IpAddr = IpAddr::from_str(host_str).ok()?;
133 let ip_addr_typed: rustls_pki_types::IpAddr = match ip_addr {
134 IpAddr::V4(v4) => rustls_pki_types::IpAddr::V4(rustls_pki_types::Ipv4Addr::from(v4.octets())),
135 IpAddr::V6(v6) => rustls_pki_types::IpAddr::V6(rustls_pki_types::Ipv6Addr::from(v6.segments())),
136 };
137 Some(ServerName::IpAddress(ip_addr_typed))
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn empty_chain_is_invalid() {
146 let now: TimeMillis = TimeMillis(1_700_000_000_000);
147 assert!(!is_cert_valid(&[], "127.0.0.1:8080", now));
148 }
149
150 #[test]
151 fn garbage_der_in_leaf_is_invalid() {
152 let chain_der: Vec<Vec<u8>> = vec![vec![0xff, 0xfe, 0xfd, 0xfc]];
153 let now: TimeMillis = TimeMillis(1_700_000_000_000);
154 assert!(!is_cert_valid(&chain_der, "127.0.0.1:8080", now));
155 }
156
157 #[test]
158 fn empty_address_is_invalid() {
159 let chain_der: Vec<Vec<u8>> = vec![vec![0xff]];
160 let now: TimeMillis = TimeMillis(1_700_000_000_000);
161 assert!(!is_cert_valid(&chain_der, "", now));
162 }
163
164 #[test]
165 fn negative_time_is_invalid() {
166 let chain_der: Vec<Vec<u8>> = vec![vec![0xff]];
167 let now: TimeMillis = TimeMillis(-1);
168 assert!(!is_cert_valid(&chain_der, "127.0.0.1:8080", now));
169 }
170
171 #[test]
172 fn strip_port_ipv4_with_port() {
173 assert_eq!(strip_port_from_address("1.2.3.4:8080"), Some("1.2.3.4".to_string()));
174 }
175
176 #[test]
177 fn strip_port_ipv4_without_port() {
178 assert_eq!(strip_port_from_address("1.2.3.4"), Some("1.2.3.4".to_string()));
179 }
180
181 #[test]
182 fn strip_port_ipv6_bracketed_with_port() {
183 assert_eq!(strip_port_from_address("[::1]:8080"), Some("::1".to_string()));
184 }
185
186 #[test]
187 fn strip_port_ipv6_bracketed_without_port() {
188 assert_eq!(strip_port_from_address("[2001:db8::1]"), Some("2001:db8::1".to_string()));
189 }
190
191 #[test]
192 fn strip_port_bare_ipv6_no_port() {
193 assert_eq!(strip_port_from_address("2001:db8::1"), Some("2001:db8::1".to_string()));
194 }
195
196 #[test]
197 fn strip_port_empty() {
198 assert_eq!(strip_port_from_address(""), None);
199 }
200
201 #[test]
202 fn strip_port_whitespace_trimmed() {
203 assert_eq!(strip_port_from_address(" 1.2.3.4:8080 "), Some("1.2.3.4".to_string()));
204 }
205
206 #[test]
207 fn strip_port_rejects_non_numeric_port_on_ipv4() {
208 assert_eq!(strip_port_from_address("1.2.3.4:notaport"), None);
209 }
210
211 #[test]
212 fn strip_port_rejects_empty_port_on_ipv4() {
213 assert_eq!(strip_port_from_address("1.2.3.4:"), None);
214 }
215
216 #[test]
217 fn strip_port_rejects_bracketed_with_junk_suffix() {
218 assert_eq!(strip_port_from_address("[::1]garbage"), None);
219 }
220
221 #[test]
222 fn strip_port_rejects_bracketed_with_non_numeric_port() {
223 assert_eq!(strip_port_from_address("[::1]:notaport"), None);
224 }
225
226 #[test]
227 fn strip_port_rejects_bracketed_with_empty_port() {
228 assert_eq!(strip_port_from_address("[::1]:"), None);
229 }
230
231 #[test]
232 fn strip_port_rejects_unbalanced_bracket() {
233 assert_eq!(strip_port_from_address("[::1"), None);
234 }
235
236 #[test]
237 fn build_server_name_for_ipv4() {
238 let server_name: Option<ServerName<'static>> = build_ip_server_name("1.2.3.4");
239 assert!(server_name.is_some());
240 assert!(matches!(server_name.unwrap(), ServerName::IpAddress(_)));
241 }
242
243 #[test]
244 fn build_server_name_for_ipv6() {
245 let server_name: Option<ServerName<'static>> = build_ip_server_name("::1");
246 assert!(server_name.is_some());
247 assert!(matches!(server_name.unwrap(), ServerName::IpAddress(_)));
248 }
249
250 #[test]
251 fn build_server_name_rejects_dns() {
252 let server_name: Option<ServerName<'static>> = build_ip_server_name("example.com");
253 assert!(server_name.is_none());
254 }
255
256 #[test]
262 fn fuzz_is_cert_valid_never_panics() {
263 bolero::check!()
264 .with_type::<(Vec<Vec<u8>>, String, i64)>()
265 .for_each(|(chain_der, announced_address, now_millis)| {
266 let _ = is_cert_valid(chain_der, announced_address, TimeMillis(*now_millis));
267 });
268 }
269
270 #[test]
271 fn fuzz_strip_port_from_address_never_panics() {
272 bolero::check!()
273 .with_type::<String>()
274 .for_each(|input| {
275 let _ = strip_port_from_address(input);
276 });
277 }
278}