#[cfg(not(kani))]
mod ctx_not_kani {
pub(crate) use crate::dpop::normalize_htu;
pub(crate) use crate::pkce::{derive_s256_challenge, validate_verifier};
pub(crate) use crate::ssrf::{is_restricted_ip, is_restricted_ipv6};
pub(crate) use crate::verification::formal_models::{
DPoPHtuFormalSpec, OAuthStateTransitionModel,
};
pub(crate) use std::net::Ipv6Addr;
}
#[cfg(not(kani))]
use ctx_not_kani::*;
use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::crypto::constant_time_eq;
use crate::ssrf::{is_restricted_ipv4, SsrfFilter};
use crate::verification::formal_models::ConstantTimeEqSpec;
use crate::verification::formal_models::PkceFormalSpec;
use crate::verification::formal_models::SsrfFormalSpec;
#[derive(Debug, Default)]
pub struct AntiVacuityCoverage {
hit_points: std::sync::RwLock<HashSet<String>>,
total_assertions: AtomicUsize,
}
impl AntiVacuityCoverage {
#[must_use]
pub fn new() -> Self {
Self {
hit_points: std::sync::RwLock::new(HashSet::new()),
total_assertions: AtomicUsize::new(0),
}
}
pub fn cover(&self, tag: &str, condition: bool) {
if condition {
if let Ok(mut guard) = self.hit_points.write() {
guard.insert(tag.to_string());
}
}
self.total_assertions.fetch_add(1, Ordering::Relaxed);
}
#[allow(clippy::panic)]
pub fn assert_all_covered(&self, required_tags: &[&str]) {
let guard = match self.hit_points.read() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
for tag in required_tags {
assert!(
guard.contains(*tag),
"ANTI-VACUITY VIOLATION: Required cover point '{tag}' was never reached! Proof is vacuous."
);
}
}
#[must_use]
pub fn covered_count(&self) -> usize {
let guard = match self.hit_points.read() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
guard.len()
}
}
static GLOBAL_COVERAGE: std::sync::OnceLock<AntiVacuityCoverage> = std::sync::OnceLock::new();
#[must_use]
pub fn global_coverage() -> &'static AntiVacuityCoverage {
GLOBAL_COVERAGE.get_or_init(AntiVacuityCoverage::new)
}
#[macro_export]
macro_rules! anti_vacuity_cover {
($tag:expr, $cond:expr) => {
let cond_val = $cond;
$crate::verification::kani_harnesses::global_coverage().cover($tag, cond_val);
#[cfg(kani)]
kani::cover!(cond_val, $tag);
};
}
#[cfg_attr(kani, kani::proof)]
#[cfg_attr(kani, kani::unwind(10))]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
pub fn proof_single_use_state_consumption() {
#[cfg(kani)]
{
let mut model = crate::verification::formal_models::SingleStateModel::new();
let ttl_ticks: u64 = kani::any();
kani::assume(ttl_ticks > 0 && ttl_ticks <= 1000);
let insert_tick: u64 = kani::any();
kani::assume(insert_tick <= 100);
let initial_take = model.take_state(0);
assert!(initial_take.is_none());
let inserted = model.insert(ttl_ticks, insert_tick);
assert!(inserted);
let take1_tick: u64 = kani::any();
kani::assume(take1_tick >= insert_tick && take1_tick <= insert_tick + 2000);
let take1 = model.take_state(take1_tick);
if take1_tick < insert_tick.saturating_add(ttl_ticks) {
assert!(take1.is_some());
let take2_tick: u64 = kani::any();
kani::assume(take2_tick >= take1_tick && take2_tick <= take1_tick + 2000);
let take2 = model.take_state(take2_tick);
assert!(take2.is_none(), "Consumed state cannot be consumed again");
kani::cover!(take1.is_some(), "first_take_success");
} else {
assert!(take1.is_none(), "Expired state returns None");
kani::cover!(take1.is_none(), "expired_state_rejected");
}
assert!(model.verify_single_use_invariant());
}
#[cfg(not(kani))]
{
let mut model = OAuthStateTransitionModel::new();
let state_token = "symbolic_state_token_123";
let client_id = "https://app.example.com/client-metadata.json";
let ttl_ticks = 100u64;
let initial_take = model.take_state(state_token, 0);
assert!(initial_take.is_none());
anti_vacuity_cover!("uninitialized_state_rejected", initial_take.is_none());
let inserted = model.insert(state_token, client_id, ttl_ticks, 10);
assert!(inserted);
anti_vacuity_cover!("state_inserted", inserted);
assert!(model.verify_global_store_invariants());
let first_take = model.take_state(state_token, 20);
assert!(first_take.is_some());
if let Some(entry) = &first_take {
assert_eq!(entry.state_id, state_token);
assert_eq!(entry.client_id, client_id);
}
anti_vacuity_cover!("first_take_success", first_take.is_some());
assert!(model.verify_single_use_invariant(state_token));
let second_take = model.take_state(state_token, 25);
assert!(second_take.is_none());
anti_vacuity_cover!("second_take_rejected", second_take.is_none());
assert!(model.verify_single_use_invariant(state_token));
let third_take = model.take_state(state_token, 30);
assert!(third_take.is_none());
assert!(model.verify_single_use_invariant(state_token));
let expired_token = "symbolic_expired_state";
let exp_inserted = model.insert(expired_token, client_id, 50, 0);
assert!(exp_inserted);
let exp_take = model.take_state(expired_token, 60);
assert!(exp_take.is_none());
anti_vacuity_cover!("expired_state_rejected", exp_take.is_none());
let race_token = "symbolic_race_state";
assert!(model.insert(race_token, client_id, 100, 0));
let (winners, losers) = model.simulate_concurrent_consumption_race(race_token, 50, 10);
assert_eq!(winners, 1);
assert_eq!(losers, 49);
anti_vacuity_cover!(
"concurrent_race_single_winner",
winners == 1 && losers == 49
);
assert!(model.verify_single_use_invariant(race_token));
assert!(model.verify_global_store_invariants());
}
}
#[cfg_attr(kani, kani::proof)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
pub fn proof_ssrf_restricted_ip_rejection() {
#[cfg(kani)]
{
let octets: [u8; 4] = kani::any();
let ip = Ipv4Addr::from(octets);
let filter = SsrfFilter::new(false);
let is_10 = octets[0] == 10;
let is_172 = octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31);
let is_192 = octets[0] == 192 && octets[1] == 168;
let is_loop = octets[0] == 127;
let is_meta = octets[0] == 169 && octets[1] == 254;
let is_cgnat = octets[0] == 100 && (octets[1] >= 64 && octets[1] <= 127);
let is_multi = octets[0] >= 224 && octets[0] <= 239;
let is_res = octets[0] >= 240;
let should_block =
is_10 || is_172 || is_192 || is_loop || is_meta || is_cgnat || is_multi || is_res;
assert_eq!(
is_restricted_ipv4(&ip),
SsrfFormalSpec::spec_is_restricted_ipv4(&ip),
"production must agree with the formal spec for every IPv4 address"
);
if should_block {
assert!(is_restricted_ipv4(&ip));
assert!(filter.validate_ip(IpAddr::V4(ip)).is_err());
}
if !SsrfFormalSpec::spec_is_restricted_ipv4(&ip) {
assert!(
filter.validate_ip(IpAddr::V4(ip)).is_ok(),
"permitted symbolic IPv4 must be accepted by SsrfFilter::validate_ip"
);
}
kani::cover!(is_10, "rfc1918_10_blocked");
kani::cover!(is_172, "rfc1918_172_blocked");
kani::cover!(is_192, "rfc1918_192_blocked");
kani::cover!(is_meta, "cloud_metadata_169_254_blocked");
kani::cover!(is_loop, "loopback_127_blocked");
kani::cover!(!should_block, "public_ip_allowed");
}
#[cfg(not(kani))]
{
let filter = SsrfFilter::new(false);
let ip_10 = Ipv4Addr::new(10, 254, 1, 2);
assert!(is_restricted_ip(IpAddr::V4(ip_10)));
assert!(is_restricted_ipv4(&ip_10));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&ip_10));
assert!(filter.validate_ip(IpAddr::V4(ip_10)).is_err());
anti_vacuity_cover!("rfc1918_10_blocked", is_restricted_ipv4(&ip_10));
let ip_172 = Ipv4Addr::new(172, 31, 255, 254);
assert!(is_restricted_ip(IpAddr::V4(ip_172)));
assert!(is_restricted_ipv4(&ip_172));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&ip_172));
assert!(filter.validate_ip(IpAddr::V4(ip_172)).is_err());
anti_vacuity_cover!("rfc1918_172_blocked", is_restricted_ipv4(&ip_172));
let ip_192 = Ipv4Addr::new(192, 168, 100, 1);
assert!(is_restricted_ip(IpAddr::V4(ip_192)));
assert!(is_restricted_ipv4(&ip_192));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&ip_192));
assert!(filter.validate_ip(IpAddr::V4(ip_192)).is_err());
anti_vacuity_cover!("rfc1918_192_blocked", is_restricted_ipv4(&ip_192));
let ip_meta = Ipv4Addr::new(169, 254, 169, 254);
assert!(is_restricted_ip(IpAddr::V4(ip_meta)));
assert!(is_restricted_ipv4(&ip_meta));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&ip_meta));
assert!(filter.validate_ip(IpAddr::V4(ip_meta)).is_err());
anti_vacuity_cover!(
"cloud_metadata_169_254_blocked",
is_restricted_ipv4(&ip_meta)
);
let ip_loop = Ipv4Addr::new(127, 0, 0, 1);
assert!(is_restricted_ip(IpAddr::V4(ip_loop)));
assert!(is_restricted_ipv4(&ip_loop));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&ip_loop));
assert!(filter.validate_ip(IpAddr::V4(ip_loop)).is_err());
anti_vacuity_cover!("loopback_127_blocked", is_restricted_ipv4(&ip_loop));
let ip_cgnat = Ipv4Addr::new(100, 64, 0, 1);
assert!(is_restricted_ip(IpAddr::V4(ip_cgnat)));
assert!(is_restricted_ipv4(&ip_cgnat));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&ip_cgnat));
assert!(filter.validate_ip(IpAddr::V4(ip_cgnat)).is_err());
anti_vacuity_cover!("cgnat_100_64_blocked", is_restricted_ipv4(&ip_cgnat));
let ip_ula = Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1);
assert!(is_restricted_ip(IpAddr::V6(ip_ula)));
assert!(is_restricted_ipv6(&ip_ula));
assert!(SsrfFormalSpec::spec_is_restricted_ipv6(&ip_ula));
assert!(filter.validate_ip(IpAddr::V6(ip_ula)).is_err());
anti_vacuity_cover!("ipv6_ula_fc00_blocked", is_restricted_ipv6(&ip_ula));
let ip_fe80 = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
assert!(is_restricted_ip(IpAddr::V6(ip_fe80)));
assert!(is_restricted_ipv6(&ip_fe80));
assert!(SsrfFormalSpec::spec_is_restricted_ipv6(&ip_fe80));
assert!(filter.validate_ip(IpAddr::V6(ip_fe80)).is_err());
anti_vacuity_cover!("ipv6_link_local_fe80_blocked", is_restricted_ipv6(&ip_fe80));
let mapped_priv = Ipv4Addr::new(10, 0, 0, 1).to_ipv6_mapped();
assert!(is_restricted_ip(IpAddr::V6(mapped_priv)));
assert!(is_restricted_ipv6(&mapped_priv));
assert!(SsrfFormalSpec::spec_is_restricted_ipv6(&mapped_priv));
assert!(filter.validate_ip(IpAddr::V6(mapped_priv)).is_err());
anti_vacuity_cover!("ipv4_mapped_ipv6_blocked", is_restricted_ipv6(&mapped_priv));
let ip_pub = Ipv4Addr::new(8, 8, 8, 8);
assert!(!is_restricted_ip(IpAddr::V4(ip_pub)));
assert!(!is_restricted_ipv4(&ip_pub));
assert!(!SsrfFormalSpec::spec_is_restricted_ipv4(&ip_pub));
assert!(filter.validate_ip(IpAddr::V4(ip_pub)).is_ok());
anti_vacuity_cover!("public_ip_allowed", !is_restricted_ipv4(&ip_pub));
}
}
#[cfg_attr(kani, kani::proof)]
#[cfg_attr(kani, kani::unwind(129))]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
pub fn proof_pkce_s256_verifier_bounds() {
#[cfg(kani)]
{
let min_bytes: [u8; 43] = kani::any();
let max_bytes: [u8; 128] = kani::any();
let min_len_ok = PkceFormalSpec::is_valid_verifier_len(43);
let min_charset_ok = min_bytes
.iter()
.all(|&b| PkceFormalSpec::is_unreserved_char(b));
let min_prod_ok = min_len_ok
&& min_bytes.iter().all(|&b| {
b.is_ascii_alphanumeric() || b == b'-' || b == b'.' || b == b'_' || b == b'~'
});
assert_eq!(
min_prod_ok, min_charset_ok,
"production byte logic must equal the formal spec at length 43"
);
let max_len_ok = PkceFormalSpec::is_valid_verifier_len(128);
let max_charset_ok = max_bytes
.iter()
.all(|&b| PkceFormalSpec::is_unreserved_char(b));
let max_prod_ok = max_len_ok
&& max_bytes.iter().all(|&b| {
b.is_ascii_alphanumeric() || b == b'-' || b == b'.' || b == b'_' || b == b'~'
});
assert_eq!(
max_prod_ok, max_charset_ok,
"production byte logic must equal the formal spec at length 128"
);
assert_eq!(PkceFormalSpec::spec_s256_challenge_len(), 43);
let byte: u8 = kani::any();
assert_eq!(
PkceFormalSpec::is_unreserved_char(byte),
byte.is_ascii_alphanumeric()
|| byte == b'-'
|| byte == b'.'
|| byte == b'_'
|| byte == b'~'
);
kani::cover!(min_charset_ok, "valid_min_length_43_verifier");
kani::cover!(max_charset_ok, "valid_max_length_128_verifier");
}
#[cfg(not(kani))]
{
let min_verifier = "a".repeat(43);
assert!(validate_verifier(&min_verifier).is_ok());
assert!(PkceFormalSpec::spec_validate_verifier(
min_verifier.as_bytes()
));
let ch_min = derive_s256_challenge(&min_verifier);
assert_eq!(ch_min.len(), 43);
anti_vacuity_cover!(
"valid_min_length_43_verifier",
validate_verifier(&min_verifier).is_ok()
);
let max_verifier = "z".repeat(128);
assert!(validate_verifier(&max_verifier).is_ok());
assert!(PkceFormalSpec::spec_validate_verifier(
max_verifier.as_bytes()
));
let ch_max = derive_s256_challenge(&max_verifier);
assert_eq!(ch_max.len(), 43);
anti_vacuity_cover!(
"valid_max_length_128_verifier",
validate_verifier(&max_verifier).is_ok()
);
let mid_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.test~example-pkce-1234567";
let mid_verifier = &mid_verifier[..64];
assert!(validate_verifier(mid_verifier).is_ok());
assert!(PkceFormalSpec::spec_validate_verifier(
mid_verifier.as_bytes()
));
anti_vacuity_cover!(
"valid_mid_length_verifier",
validate_verifier(mid_verifier).is_ok()
);
let short_verifier = "a".repeat(42);
assert!(validate_verifier(&short_verifier).is_err());
assert!(!PkceFormalSpec::spec_validate_verifier(
short_verifier.as_bytes()
));
anti_vacuity_cover!(
"invalid_short_length_rejected",
validate_verifier(&short_verifier).is_err()
);
let long_verifier = "a".repeat(129);
assert!(validate_verifier(&long_verifier).is_err());
assert!(!PkceFormalSpec::spec_validate_verifier(
long_verifier.as_bytes()
));
anti_vacuity_cover!(
"invalid_long_length_rejected",
validate_verifier(&long_verifier).is_err()
);
let illegal_space = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEj k";
assert!(validate_verifier(illegal_space).is_err());
assert!(!PkceFormalSpec::spec_validate_verifier(
illegal_space.as_bytes()
));
let illegal_plus = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEj+k";
assert!(validate_verifier(illegal_plus).is_err());
anti_vacuity_cover!(
"invalid_character_rejected",
validate_verifier(illegal_space).is_err()
);
anti_vacuity_cover!(
"challenge_length_is_43",
ch_min.len() == 43 && ch_max.len() == 43
);
}
}
#[cfg_attr(kani, kani::proof)]
#[cfg_attr(kani, kani::unwind(17))]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
pub fn proof_constant_time_eq_soundness() {
#[cfg(kani)]
{
let a_full: [u8; 8] = kani::any();
let b_full: [u8; 8] = kani::any();
assert_eq!(
constant_time_eq(&a_full, &b_full),
ConstantTimeEqSpec::spec_constant_time_eq_model(&a_full, &b_full),
"constant_time_eq must agree with the element-wise spec model"
);
assert_eq!(constant_time_eq(&a_full, &b_full), a_full == b_full);
let a_len: usize = kani::any();
kani::assume((1..=8).contains(&a_len));
let b_len: usize = kani::any();
kani::assume((1..=8).contains(&b_len));
let a: [u8; 8] = kani::any();
let b: [u8; 8] = kani::any();
let res = constant_time_eq(&a[..a_len], &b[..b_len]);
assert_eq!(
res,
ConstantTimeEqSpec::spec_constant_time_eq_model(&a[..a_len], &b[..b_len]),
"constant_time_eq must agree with the element-wise spec model"
);
assert_eq!(res, a[..a_len] == b[..b_len]);
kani::cover!(a_full == b_full, "equal_non_empty_slices_true");
kani::cover!(a_full != b_full, "differing_first_byte_false");
kani::cover!(a_len != b_len, "mismatched_length_false");
}
#[cfg(not(kani))]
{
let s1 = b"cryptographic_secret_token_1234";
let s2 = b"cryptographic_secret_token_1234";
assert!(constant_time_eq(s1, s2));
assert!(ConstantTimeEqSpec::verify_soundness(s1, s2));
anti_vacuity_cover!("equal_non_empty_slices_true", constant_time_eq(s1, s2));
let mut diff_first = *s1;
diff_first[0] ^= 0x01;
assert!(!constant_time_eq(s1, &diff_first));
assert!(ConstantTimeEqSpec::verify_soundness(s1, &diff_first));
anti_vacuity_cover!(
"differing_first_byte_false",
!constant_time_eq(s1, &diff_first)
);
let mut diff_last = *s1;
diff_last[s1.len() - 1] ^= 0x01;
assert!(!constant_time_eq(s1, &diff_last));
assert!(ConstantTimeEqSpec::verify_soundness(s1, &diff_last));
anti_vacuity_cover!(
"differing_last_byte_false",
!constant_time_eq(s1, &diff_last)
);
let mut diff_mid = *s1;
diff_mid[s1.len() / 2] ^= 0x01;
assert!(!constant_time_eq(s1, &diff_mid));
assert!(ConstantTimeEqSpec::verify_soundness(s1, &diff_mid));
anti_vacuity_cover!(
"differing_middle_byte_false",
!constant_time_eq(s1, &diff_mid)
);
let short = &s1[..16];
assert!(!constant_time_eq(s1, short));
assert!(ConstantTimeEqSpec::verify_soundness(s1, short));
anti_vacuity_cover!("mismatched_length_false", !constant_time_eq(s1, short));
assert!(constant_time_eq(b"", b""));
assert!(ConstantTimeEqSpec::verify_soundness(b"", b""));
anti_vacuity_cover!("empty_slices_true", constant_time_eq(b"", b""));
}
}
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
pub fn proof_dpop_htu_normalization_invariants() {
#[cfg(any())]
{
let port: u16 = kani::any();
let is_https: bool = kani::any();
let scheme = if is_https { "https" } else { "http" };
let url = format!("{scheme}://example.com:{port}/oauth/token");
if let Ok(res) = normalize_htu(&url) {
let is_default_port = (is_https && port == 443) || (!is_https && port == 80);
if is_default_port {
assert!(!res.contains(&format!(":{port}")));
} else if port != 0 {
assert!(res.contains(&format!(":{port}")));
}
assert!(DPoPHtuFormalSpec::spec_valid_scheme(&res));
assert!(DPoPHtuFormalSpec::spec_has_no_query(&res));
assert!(DPoPHtuFormalSpec::spec_has_no_fragment(&res));
}
kani::cover!(port == 443, "port_443_stripped_success");
kani::cover!(port == 80, "port_80_stripped_success");
kani::cover!(port == 8443, "custom_port_preserved_success");
}
#[cfg(not(kani))]
{
if let Ok(res_query) = normalize_htu("https://example.com/oauth/token?grant_type=code") {
assert_eq!(res_query, "https://example.com/oauth/token");
assert!(DPoPHtuFormalSpec::spec_has_no_query(&res_query));
anti_vacuity_cover!(
"query_stripped_success",
DPoPHtuFormalSpec::spec_has_no_query(&res_query)
);
}
if let Ok(res_frag) = normalize_htu("https://example.com/oauth/token#frag") {
assert_eq!(res_frag, "https://example.com/oauth/token");
assert!(DPoPHtuFormalSpec::spec_has_no_fragment(&res_frag));
anti_vacuity_cover!(
"fragment_stripped_success",
DPoPHtuFormalSpec::spec_has_no_fragment(&res_frag)
);
}
if let Ok(res_443) = normalize_htu("https://example.com:443/oauth/token") {
assert_eq!(res_443, "https://example.com/oauth/token");
assert!(DPoPHtuFormalSpec::spec_no_default_ports(&res_443));
anti_vacuity_cover!(
"port_443_stripped_success",
DPoPHtuFormalSpec::spec_no_default_ports(&res_443)
);
}
if let Ok(res_80) = normalize_htu("http://example.com:80/oauth/token") {
assert_eq!(res_80, "http://example.com/oauth/token");
assert!(DPoPHtuFormalSpec::spec_no_default_ports(&res_80));
anti_vacuity_cover!(
"port_80_stripped_success",
DPoPHtuFormalSpec::spec_no_default_ports(&res_80)
);
}
if let Ok(res_custom) = normalize_htu("https://example.com:8443/oauth/token") {
assert_eq!(res_custom, "https://example.com:8443/oauth/token");
anti_vacuity_cover!(
"custom_port_preserved_success",
res_custom.contains(":8443")
);
}
if let Ok(res_case) = normalize_htu("https://AUTH.EXAMPLE.COM/Token/Path") {
assert_eq!(res_case, "https://auth.example.com/Token/Path");
assert!(DPoPHtuFormalSpec::spec_valid_scheme(&res_case));
anti_vacuity_cover!(
"uppercase_host_lowercased_success",
res_case.starts_with("https://auth.example.com")
);
}
let res_ftp = normalize_htu("ftp://example.com/token");
assert!(res_ftp.is_err());
anti_vacuity_cover!("invalid_scheme_rejected", res_ftp.is_err());
}
}