use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StateTransitionStatus {
Uninitialized,
Pending {
created_at_tick: u64,
ttl_ticks: u64,
},
Consumed {
consumed_at_tick: u64,
},
Expired {
expired_at_tick: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelStoredEntry {
pub state_id: String,
pub client_id: String,
pub created_at_tick: u64,
pub ttl_ticks: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SingleStateModel {
pub status: StateTransitionStatus,
pub consumption_count: usize,
}
impl Default for SingleStateModel {
fn default() -> Self {
Self::new()
}
}
impl SingleStateModel {
#[must_use]
pub const fn new() -> Self {
Self {
status: StateTransitionStatus::Uninitialized,
consumption_count: 0,
}
}
pub fn insert(&mut self, ttl_ticks: u64, current_tick: u64) -> bool {
match self.status {
StateTransitionStatus::Uninitialized => {
self.status = StateTransitionStatus::Pending {
created_at_tick: current_tick,
ttl_ticks,
};
true
}
_ => false,
}
}
pub fn take_state(&mut self, current_tick: u64) -> Option<u64> {
match self.status {
StateTransitionStatus::Pending {
created_at_tick,
ttl_ticks,
} => {
if current_tick.saturating_sub(created_at_tick) >= ttl_ticks {
self.status = StateTransitionStatus::Expired {
expired_at_tick: current_tick,
};
None
} else {
self.status = StateTransitionStatus::Consumed {
consumed_at_tick: current_tick,
};
self.consumption_count = self.consumption_count.saturating_add(1);
Some(created_at_tick)
}
}
_ => None,
}
}
#[must_use]
pub const fn verify_single_use_invariant(&self) -> bool {
self.consumption_count <= 1
}
}
#[derive(Debug, Clone, Default)]
pub struct OAuthStateTransitionModel {
pub states: HashMap<String, StateTransitionStatus>,
pub payloads: HashMap<String, ModelStoredEntry>,
pub consumption_counts: HashMap<String, usize>,
}
impl OAuthStateTransitionModel {
#[must_use]
pub fn new() -> Self {
Self {
states: HashMap::new(),
payloads: HashMap::new(),
consumption_counts: HashMap::new(),
}
}
pub fn insert(
&mut self,
state_id: &str,
client_id: &str,
ttl_ticks: u64,
current_tick: u64,
) -> bool {
if state_id.is_empty() || ttl_ticks == 0 {
return false;
}
let entry = ModelStoredEntry {
state_id: state_id.to_string(),
client_id: client_id.to_string(),
created_at_tick: current_tick,
ttl_ticks,
};
self.states.insert(
state_id.to_string(),
StateTransitionStatus::Pending {
created_at_tick: current_tick,
ttl_ticks,
},
);
self.payloads.insert(state_id.to_string(), entry);
self.consumption_counts.remove(state_id);
true
}
pub fn take_state(&mut self, state_id: &str, current_tick: u64) -> Option<ModelStoredEntry> {
let current_status = self
.states
.get(state_id)
.copied()
.unwrap_or(StateTransitionStatus::Uninitialized);
match current_status {
StateTransitionStatus::Pending {
created_at_tick,
ttl_ticks,
} => {
let is_expired = current_tick.saturating_sub(created_at_tick) >= ttl_ticks;
if is_expired {
self.states.insert(
state_id.to_string(),
StateTransitionStatus::Expired {
expired_at_tick: current_tick,
},
);
self.payloads.remove(state_id);
None
} else {
self.states.insert(
state_id.to_string(),
StateTransitionStatus::Consumed {
consumed_at_tick: current_tick,
},
);
let entry = self.payloads.remove(state_id);
let count = self
.consumption_counts
.entry(state_id.to_string())
.or_insert(0);
*count = count.saturating_add(1);
entry
}
}
StateTransitionStatus::Consumed { .. }
| StateTransitionStatus::Expired { .. }
| StateTransitionStatus::Uninitialized => None,
}
}
pub fn prune(&mut self, current_tick: u64) -> usize {
let mut expired_keys = Vec::new();
for (state_id, status) in &self.states {
if let StateTransitionStatus::Pending {
created_at_tick,
ttl_ticks,
} = status
{
if current_tick.saturating_sub(*created_at_tick) >= *ttl_ticks {
expired_keys.push(state_id.clone());
}
}
}
let pruned_count = expired_keys.len();
for state_id in expired_keys {
self.states.insert(
state_id.clone(),
StateTransitionStatus::Expired {
expired_at_tick: current_tick,
},
);
self.payloads.remove(&state_id);
}
pruned_count
}
#[must_use]
pub fn verify_single_use_invariant(&self, state_id: &str) -> bool {
let count = self.consumption_counts.get(state_id).copied().unwrap_or(0);
count <= 1
}
#[must_use]
pub fn verify_global_store_invariants(&self) -> bool {
for (state_id, count) in &self.consumption_counts {
if *count > 1 {
return false;
}
if let Some(status) = self.states.get(state_id) {
if *count == 1 && !matches!(status, StateTransitionStatus::Consumed { .. }) {
return false;
}
}
}
true
}
pub fn simulate_concurrent_consumption_race(
&mut self,
state_id: &str,
num_racers: usize,
current_tick: u64,
) -> (usize, usize) {
let mut success_count: usize = 0;
let mut failure_count: usize = 0;
for _ in 0..num_racers {
if self.take_state(state_id, current_tick).is_some() {
success_count = success_count.saturating_add(1);
} else {
failure_count = failure_count.saturating_add(1);
}
}
(success_count, failure_count)
}
}
pub struct PkceFormalSpec;
impl PkceFormalSpec {
#[must_use]
pub const fn is_unreserved_char(byte: u8) -> bool {
matches!(byte,
b'a'..=b'z' |
b'A'..=b'Z' |
b'0'..=b'9' |
b'-' | b'.' | b'_' | b'~'
)
}
#[must_use]
pub const fn is_valid_verifier_len(len: usize) -> bool {
len >= 43 && len <= 128
}
#[must_use]
pub fn spec_validate_verifier(verifier: &[u8]) -> bool {
if !Self::is_valid_verifier_len(verifier.len()) {
return false;
}
for &b in verifier {
if !Self::is_unreserved_char(b) {
return false;
}
}
true
}
#[must_use]
pub const fn spec_s256_challenge_len() -> usize {
43
}
}
pub struct ConstantTimeEqSpec;
impl ConstantTimeEqSpec {
#[must_use]
pub fn spec_slice_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
for i in 0..a.len() {
if a[i] != b[i] {
return false;
}
}
true
}
#[must_use]
pub fn spec_constant_time_eq_model(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff: u8 = 0;
for i in 0..a.len() {
diff |= a[i] ^ b[i];
}
diff == 0
}
#[must_use]
pub fn verify_soundness(a: &[u8], b: &[u8]) -> bool {
Self::spec_constant_time_eq_model(a, b) == Self::spec_slice_eq(a, b)
}
}
pub struct SsrfFormalSpec;
impl SsrfFormalSpec {
#[must_use]
pub fn spec_is_restricted_ipv4(ip: &Ipv4Addr) -> bool {
let octets = ip.octets();
let o0 = octets[0];
let o1 = octets[1];
let o2 = octets[2];
o0 == 0
|| o0 == 10
|| (o0 == 100 && (o1 & 0xC0) == 64)
|| o0 == 127
|| (o0 == 169 && o1 == 254)
|| (o0 == 172 && (16..=31).contains(&o1))
|| (o0 == 192 && o1 == 0 && o2 == 0)
|| (o0 == 192 && o1 == 0 && o2 == 2)
|| (o0 == 192 && o1 == 88 && o2 == 99)
|| (o0 == 192 && o1 == 168)
|| (o0 == 198 && (o1 == 18 || o1 == 19))
|| (o0 == 198 && o1 == 51 && o2 == 100)
|| (o0 == 203 && o1 == 0 && o2 == 113)
|| (224..=239).contains(&o0)
|| o0 >= 240
}
#[must_use]
pub fn spec_is_restricted_ipv6(ip: &Ipv6Addr) -> bool {
let seg = ip.segments();
ip.is_unspecified()
|| ip.is_loopback()
|| if let Some(mapped) = ip.to_ipv4_mapped() {
Self::spec_is_restricted_ipv4(&mapped)
} else {
false
}
|| (seg[0] == 0 && seg[1] == 0 && seg[2] == 0 && seg[3] == 0 && seg[4] == 0xffff && seg[5] == 0)
|| (seg[0] == 0x0064 && seg[1] == 0xff9b && seg[2] == 0 && seg[3] == 0 && seg[4] == 0 && seg[5] == 0)
|| (seg[0] == 0x2001 && seg[1] == 0x0db8)
|| (seg[0] == 0x2001 && seg[1] == 0)
|| (seg[0] == 0x2002 && Self::spec_is_restricted_ipv4(&Ipv4Addr::new(
((seg[1] >> 8) & 0xff) as u8,
(seg[1] & 0xff) as u8,
((seg[2] >> 8) & 0xff) as u8,
(seg[2] & 0xff) as u8,
)))
|| ((seg[0] & 0xfe00) == 0xfc00)
|| ((seg[0] & 0xffc0) == 0xfe80)
|| ((seg[0] & 0xffc0) == 0xfec0)
|| ((seg[0] & 0xff00) == 0xff00)
}
#[must_use]
pub fn spec_is_restricted_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => Self::spec_is_restricted_ipv4(&v4),
IpAddr::V6(v6) => Self::spec_is_restricted_ipv6(&v6),
}
}
}
pub struct DPoPHtuFormalSpec;
impl DPoPHtuFormalSpec {
#[must_use]
pub fn spec_has_no_query(normalized_uri: &str) -> bool {
!normalized_uri.contains('?')
}
#[must_use]
pub fn spec_has_no_fragment(normalized_uri: &str) -> bool {
!normalized_uri.contains('#')
}
#[must_use]
pub fn spec_valid_scheme(normalized_uri: &str) -> bool {
normalized_uri.starts_with("https://") || normalized_uri.starts_with("http://")
}
#[must_use]
pub fn spec_no_default_ports(normalized_uri: &str) -> bool {
let is_https = normalized_uri.starts_with("https://");
let is_http = normalized_uri.starts_with("http://");
if is_https {
!normalized_uri.contains(":443/") && !normalized_uri.ends_with(":443")
} else if is_http {
!normalized_uri.contains(":80/") && !normalized_uri.ends_with(":80")
} else {
true
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, missing_docs)]
mod tests {
use super::*;
#[test]
fn test_state_transition_single_use_model() {
let mut model = OAuthStateTransitionModel::new();
let state = "formal_state_123";
assert!(model.insert(state, "client_1", 100, 10));
assert!(model.verify_global_store_invariants());
assert!(model.insert(state, "client_1", 100, 15));
let entry = model.take_state(state, 20);
assert!(entry.is_some());
assert_eq!(entry.unwrap().state_id, state);
assert!(model.verify_single_use_invariant(state));
assert!(model.verify_global_store_invariants());
assert!(model.take_state(state, 25).is_none());
assert!(model.verify_single_use_invariant(state));
assert!(model.verify_global_store_invariants());
}
#[test]
fn test_state_transition_expiration_model() {
let mut model = OAuthStateTransitionModel::new();
let state = "expired_state_formal";
assert!(model.insert(state, "client_1", 50, 10));
assert!(model.take_state(state, 70).is_none());
assert!(matches!(
model.states.get(state),
Some(StateTransitionStatus::Expired { .. })
));
assert!(model.verify_single_use_invariant(state));
}
#[test]
fn test_concurrent_consumption_simulation() {
let mut model = OAuthStateTransitionModel::new();
let state = "race_state";
assert!(model.insert(state, "client_1", 100, 0));
let (success, failure) = model.simulate_concurrent_consumption_race(state, 100, 10);
assert_eq!(success, 1);
assert_eq!(failure, 99);
assert!(model.verify_single_use_invariant(state));
}
#[test]
fn test_pkce_formal_spec_bounds() {
assert!(PkceFormalSpec::is_valid_verifier_len(43));
assert!(PkceFormalSpec::is_valid_verifier_len(128));
assert!(!PkceFormalSpec::is_valid_verifier_len(42));
assert!(!PkceFormalSpec::is_valid_verifier_len(129));
assert!(PkceFormalSpec::is_unreserved_char(b'a'));
assert!(PkceFormalSpec::is_unreserved_char(b'Z'));
assert!(PkceFormalSpec::is_unreserved_char(b'9'));
assert!(PkceFormalSpec::is_unreserved_char(b'-'));
assert!(PkceFormalSpec::is_unreserved_char(b'.'));
assert!(PkceFormalSpec::is_unreserved_char(b'_'));
assert!(PkceFormalSpec::is_unreserved_char(b'~'));
assert!(!PkceFormalSpec::is_unreserved_char(b' '));
assert!(!PkceFormalSpec::is_unreserved_char(b'+'));
assert!(!PkceFormalSpec::is_unreserved_char(b'='));
}
#[test]
fn test_constant_time_eq_soundness() {
let a = b"token_alpha_1234";
let b = b"token_alpha_1234";
let c = b"token_beta__5678";
assert!(ConstantTimeEqSpec::verify_soundness(a, b));
assert!(ConstantTimeEqSpec::verify_soundness(a, c));
assert!(ConstantTimeEqSpec::verify_soundness(b"", b""));
assert!(ConstantTimeEqSpec::verify_soundness(a, b"short"));
}
#[test]
fn test_ssrf_formal_spec_rejection() {
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&Ipv4Addr::new(
10, 0, 0, 1
)));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&Ipv4Addr::new(
127, 0, 0, 1
)));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&Ipv4Addr::new(
169, 254, 169, 254
)));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&Ipv4Addr::new(
172, 16, 0, 1
)));
assert!(SsrfFormalSpec::spec_is_restricted_ipv4(&Ipv4Addr::new(
192, 168, 1, 1
)));
assert!(!SsrfFormalSpec::spec_is_restricted_ipv4(&Ipv4Addr::new(
1, 1, 1, 1
)));
assert!(!SsrfFormalSpec::spec_is_restricted_ipv4(&Ipv4Addr::new(
8, 8, 8, 8
)));
}
#[test]
fn test_ssrf_formal_spec_6to4_embedded_ipv4() {
let v6 = Ipv6Addr::new(0x2002, 0x0a00, 0x0001, 0, 0, 0, 0, 1);
assert!(SsrfFormalSpec::spec_is_restricted_ipv6(&v6));
let public_6to4 = Ipv6Addr::new(0x2002, 0xc68c, 0x2174, 0, 0, 0, 0, 1);
assert!(!SsrfFormalSpec::spec_is_restricted_ipv6(&public_6to4));
assert_eq!(
crate::ssrf::is_restricted_ipv6(&v6),
SsrfFormalSpec::spec_is_restricted_ipv6(&v6)
);
assert_eq!(
crate::ssrf::is_restricted_ipv6(&public_6to4),
SsrfFormalSpec::spec_is_restricted_ipv6(&public_6to4)
);
}
}