use crate::tools::time::TimeMillis;
use rustls_pki_types::{CertificateDer, ServerName, UnixTime};
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
use webpki::EndEntityCert;
pub fn is_cert_valid(chain_der: &[Vec<u8>], announced_address: &str, now: TimeMillis) -> bool {
let Some((leaf_der_vec, intermediate_der_vecs)) = chain_der.split_first() else {
return false;
};
let leaf_der: CertificateDer<'_> = CertificateDer::from(leaf_der_vec.as_slice());
let leaf_cert: EndEntityCert<'_> = match EndEntityCert::try_from(&leaf_der) {
Ok(c) => c,
Err(_) => return false,
};
let intermediate_ders: Vec<CertificateDer<'_>> = intermediate_der_vecs.iter().map(|d| CertificateDer::from(d.as_slice())).collect();
let now_unix: UnixTime = match time_millis_to_unix_time(now) {
Some(t) => t,
None => return false,
};
let trust_anchors: &[rustls_pki_types::TrustAnchor<'_>] = webpki_roots::TLS_SERVER_ROOTS;
if leaf_cert
.verify_for_usage(
webpki::ALL_VERIFICATION_ALGS,
trust_anchors,
&intermediate_ders,
now_unix,
webpki::KeyUsage::server_auth(),
None,
None,
)
.is_err()
{
return false;
}
let Some(ip_str) = strip_port_from_address(announced_address) else {
return false;
};
let server_name: ServerName<'_> = match build_ip_server_name(&ip_str) {
Some(name) => name,
None => return false,
};
leaf_cert.verify_is_valid_for_subject_name(&server_name).is_ok()
}
fn time_millis_to_unix_time(time_millis: TimeMillis) -> Option<UnixTime> {
let millis: i64 = time_millis.0;
if millis < 0 {
return None;
}
Some(UnixTime::since_unix_epoch(Duration::from_millis(millis as u64)))
}
fn strip_port_from_address(announced_address: &str) -> Option<String> {
let trimmed: &str = announced_address.trim();
if trimmed.is_empty() {
return None;
}
if let Some(rest_after_open_bracket) = trimmed.strip_prefix('[') {
let close_bracket_pos: usize = rest_after_open_bracket.find(']')?;
let ipv6_body: &str = &rest_after_open_bracket[..close_bracket_pos];
let suffix: &str = &rest_after_open_bracket[close_bracket_pos + 1..];
if !suffix.is_empty() {
let port_str: &str = suffix.strip_prefix(':')?;
if port_str.is_empty() || !port_str.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
}
return Some(ipv6_body.to_string());
}
match trimmed.rsplit_once(':') {
Some((host_part, port_part)) if !host_part.contains(':') => {
if port_part.is_empty() || !port_part.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some(host_part.to_string())
}
_ => Some(trimmed.to_string()),
}
}
fn build_ip_server_name(host_str: &str) -> Option<ServerName<'static>> {
let ip_addr: IpAddr = IpAddr::from_str(host_str).ok()?;
let ip_addr_typed: rustls_pki_types::IpAddr = match ip_addr {
IpAddr::V4(v4) => rustls_pki_types::IpAddr::V4(rustls_pki_types::Ipv4Addr::from(v4.octets())),
IpAddr::V6(v6) => rustls_pki_types::IpAddr::V6(rustls_pki_types::Ipv6Addr::from(v6.segments())),
};
Some(ServerName::IpAddress(ip_addr_typed))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_chain_is_invalid() {
let now: TimeMillis = TimeMillis(1_700_000_000_000);
assert!(!is_cert_valid(&[], "127.0.0.1:8080", now));
}
#[test]
fn garbage_der_in_leaf_is_invalid() {
let chain_der: Vec<Vec<u8>> = vec![vec![0xff, 0xfe, 0xfd, 0xfc]];
let now: TimeMillis = TimeMillis(1_700_000_000_000);
assert!(!is_cert_valid(&chain_der, "127.0.0.1:8080", now));
}
#[test]
fn empty_address_is_invalid() {
let chain_der: Vec<Vec<u8>> = vec![vec![0xff]];
let now: TimeMillis = TimeMillis(1_700_000_000_000);
assert!(!is_cert_valid(&chain_der, "", now));
}
#[test]
fn negative_time_is_invalid() {
let chain_der: Vec<Vec<u8>> = vec![vec![0xff]];
let now: TimeMillis = TimeMillis(-1);
assert!(!is_cert_valid(&chain_der, "127.0.0.1:8080", now));
}
#[test]
fn strip_port_ipv4_with_port() {
assert_eq!(strip_port_from_address("1.2.3.4:8080"), Some("1.2.3.4".to_string()));
}
#[test]
fn strip_port_ipv4_without_port() {
assert_eq!(strip_port_from_address("1.2.3.4"), Some("1.2.3.4".to_string()));
}
#[test]
fn strip_port_ipv6_bracketed_with_port() {
assert_eq!(strip_port_from_address("[::1]:8080"), Some("::1".to_string()));
}
#[test]
fn strip_port_ipv6_bracketed_without_port() {
assert_eq!(strip_port_from_address("[2001:db8::1]"), Some("2001:db8::1".to_string()));
}
#[test]
fn strip_port_bare_ipv6_no_port() {
assert_eq!(strip_port_from_address("2001:db8::1"), Some("2001:db8::1".to_string()));
}
#[test]
fn strip_port_empty() {
assert_eq!(strip_port_from_address(""), None);
}
#[test]
fn strip_port_whitespace_trimmed() {
assert_eq!(strip_port_from_address(" 1.2.3.4:8080 "), Some("1.2.3.4".to_string()));
}
#[test]
fn strip_port_rejects_non_numeric_port_on_ipv4() {
assert_eq!(strip_port_from_address("1.2.3.4:notaport"), None);
}
#[test]
fn strip_port_rejects_empty_port_on_ipv4() {
assert_eq!(strip_port_from_address("1.2.3.4:"), None);
}
#[test]
fn strip_port_rejects_bracketed_with_junk_suffix() {
assert_eq!(strip_port_from_address("[::1]garbage"), None);
}
#[test]
fn strip_port_rejects_bracketed_with_non_numeric_port() {
assert_eq!(strip_port_from_address("[::1]:notaport"), None);
}
#[test]
fn strip_port_rejects_bracketed_with_empty_port() {
assert_eq!(strip_port_from_address("[::1]:"), None);
}
#[test]
fn strip_port_rejects_unbalanced_bracket() {
assert_eq!(strip_port_from_address("[::1"), None);
}
#[test]
fn build_server_name_for_ipv4() {
let server_name: Option<ServerName<'static>> = build_ip_server_name("1.2.3.4");
assert!(server_name.is_some());
assert!(matches!(server_name.unwrap(), ServerName::IpAddress(_)));
}
#[test]
fn build_server_name_for_ipv6() {
let server_name: Option<ServerName<'static>> = build_ip_server_name("::1");
assert!(server_name.is_some());
assert!(matches!(server_name.unwrap(), ServerName::IpAddress(_)));
}
#[test]
fn build_server_name_rejects_dns() {
let server_name: Option<ServerName<'static>> = build_ip_server_name("example.com");
assert!(server_name.is_none());
}
#[test]
fn fuzz_is_cert_valid_never_panics() {
bolero::check!()
.with_type::<(Vec<Vec<u8>>, String, i64)>()
.for_each(|(chain_der, announced_address, now_millis)| {
let _ = is_cert_valid(chain_der, announced_address, TimeMillis(*now_millis));
});
}
#[test]
fn fuzz_strip_port_from_address_never_panics() {
bolero::check!()
.with_type::<String>()
.for_each(|input| {
let _ = strip_port_from_address(input);
});
}
}