use anyhow::{Context, Result};
use freebird_common::federation::{FederationMetadata, Revocation, TrustPolicy, Vouch};
use serde::Deserialize;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
const MAX_CLOCK_SKEW_SECS: i64 = 300;
const MAX_VOUCHES_PER_ISSUER: usize = 50;
const MAX_REQUESTS_PER_TRAVERSAL: usize = 100;
const MAX_ISSUERS_VISITED: usize = 200;
fn validate_issuer_domain(expected_domain: &str, returned_issuer_id: &str) -> bool {
if expected_domain == returned_issuer_id {
return true;
}
let returned_hostname = if returned_issuer_id.contains(':') {
let parts: Vec<&str> = returned_issuer_id.split(':').collect();
if parts.len() >= 2 {
parts[1]
} else {
returned_issuer_id
}
} else {
returned_issuer_id
};
expected_domain == returned_hostname
}
#[derive(Debug, Clone, Deserialize)]
struct IssuerMetadata {
issuer_id: String,
voprf: VoprfInfo,
}
#[derive(Debug, Clone, Deserialize)]
struct VoprfInfo {
#[allow(dead_code)]
suite: String,
#[allow(dead_code)]
kid: String,
pubkey: String, #[allow(dead_code)]
exp_sec: u64,
}
#[derive(Debug, Clone)]
struct CachedMetadata {
metadata: FederationMetadata,
fetched_at: SystemTime,
}
#[derive(Debug, Clone)]
struct CachedPubkey {
pubkey: Vec<u8>,
fetched_at: SystemTime,
}
pub struct TrustGraph {
policy: TrustPolicy,
cache: Arc<RwLock<HashMap<String, CachedMetadata>>>,
pubkey_cache: Arc<RwLock<HashMap<String, CachedPubkey>>>,
client: reqwest::Client,
}
impl TrustGraph {
pub fn new(policy: TrustPolicy) -> Self {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("Failed to create HTTP client");
Self {
policy,
cache: Arc::new(RwLock::new(HashMap::new())),
pubkey_cache: Arc::new(RwLock::new(HashMap::new())),
client,
}
}
pub async fn is_trusted(&self, issuer_id: &str, issuer_pubkey: &[u8]) -> bool {
if !self.policy.enabled {
debug!("Federation disabled, rejecting issuer {}", issuer_id);
return false;
}
if self.policy.blocked_issuers.contains(&issuer_id.to_string()) {
warn!("Issuer {} is explicitly blocked", issuer_id);
return false;
}
if self.policy.trusted_roots.contains(&issuer_id.to_string()) {
debug!("Issuer {} is a trusted root", issuer_id);
return true;
}
if self.policy.require_direct_trust {
return self.has_direct_trust(issuer_id, issuer_pubkey).await;
}
match self.find_trust_paths(issuer_id, issuer_pubkey).await {
Ok(paths) => {
let count = paths.len();
debug!(
"Found {} trust paths to issuer {} (required: {})",
count, issuer_id, self.policy.min_trust_paths
);
count >= self.policy.min_trust_paths as usize
}
Err(e) => {
warn!("Failed to traverse trust graph for {}: {}", issuer_id, e);
false
}
}
}
async fn has_direct_trust(&self, issuer_id: &str, issuer_pubkey: &[u8]) -> bool {
let now = current_timestamp();
for root_id in &self.policy.trusted_roots {
let metadata = match self.fetch_metadata(root_id).await {
Ok(m) => m,
Err(e) => {
warn!("Failed to fetch metadata for root {}: {}", root_id, e);
continue;
}
};
for vouch in &metadata.vouches {
if vouch.vouched_issuer_id == issuer_id
&& self.is_vouch_valid(vouch, issuer_pubkey, now).await
{
debug!("Found direct vouch from root {} for {}", root_id, issuer_id);
return true;
}
}
}
false
}
async fn find_trust_paths(
&self,
target_issuer_id: &str,
target_pubkey: &[u8],
) -> Result<Vec<Vec<String>>> {
let now = current_timestamp();
let mut paths = Vec::new();
let mut request_count: usize = 0;
let mut queue: VecDeque<(String, Vec<String>, u32)> = VecDeque::new();
for root in &self.policy.trusted_roots {
queue.push_back((root.clone(), vec![root.clone()], 0));
}
let mut visited = HashSet::new();
while let Some((current_id, path, depth)) = queue.pop_front() {
if visited.len() >= MAX_ISSUERS_VISITED {
warn!(
"Trust graph traversal hit issuer limit ({}), stopping",
MAX_ISSUERS_VISITED
);
break;
}
if visited.contains(¤t_id) {
continue;
}
visited.insert(current_id.clone());
if request_count >= MAX_REQUESTS_PER_TRAVERSAL {
warn!(
"Trust graph traversal hit request limit ({}), stopping",
MAX_REQUESTS_PER_TRAVERSAL
);
break;
}
request_count += 1;
let metadata = match self.fetch_metadata(¤t_id).await {
Ok(m) => m,
Err(e) => {
warn!(
"Failed to fetch metadata for {}: {}, skipping",
current_id, e
);
continue;
}
};
let vouches_to_process = if metadata.vouches.len() > MAX_VOUCHES_PER_ISSUER {
warn!(
"Issuer {} has {} vouches, limiting to {}",
current_id,
metadata.vouches.len(),
MAX_VOUCHES_PER_ISSUER
);
&metadata.vouches[..MAX_VOUCHES_PER_ISSUER]
} else {
&metadata.vouches[..]
};
for vouch in vouches_to_process {
if self
.policy
.blocked_issuers
.contains(&vouch.vouched_issuer_id)
{
continue;
}
if request_count >= MAX_REQUESTS_PER_TRAVERSAL {
break;
}
if self
.is_revoked_with_limit(
&vouch.vouched_issuer_id,
&metadata.revocations,
now,
&mut request_count,
)
.await
{
debug!(
"Issuer {} has been revoked by {}, skipping",
vouch.vouched_issuer_id, current_id
);
continue;
}
if vouch.vouched_issuer_id == target_issuer_id {
if request_count >= MAX_REQUESTS_PER_TRAVERSAL {
break;
}
if self
.is_vouch_valid_with_limit(vouch, target_pubkey, now, &mut request_count)
.await
{
let mut final_path = path.clone();
final_path.push(target_issuer_id.to_string());
paths.push(final_path);
debug!("Found trust path: {:?}", paths.last().unwrap());
}
} else if depth < self.policy.max_trust_depth {
if request_count >= MAX_REQUESTS_PER_TRAVERSAL {
break;
}
if !self
.is_vouch_valid_with_limit(
vouch,
&vouch.vouched_pubkey,
now,
&mut request_count,
)
.await
{
debug!(
"Skipping invalid intermediate vouch from {} to {}",
current_id, vouch.vouched_issuer_id
);
continue;
}
let mut new_path = path.clone();
new_path.push(vouch.vouched_issuer_id.clone());
queue.push_back((vouch.vouched_issuer_id.clone(), new_path, depth + 1));
}
}
}
debug!(
"Trust graph traversal complete: {} paths found, {} issuers visited, {} requests made",
paths.len(),
visited.len(),
request_count
);
Ok(paths)
}
async fn fetch_metadata(&self, issuer_id: &str) -> Result<FederationMetadata> {
{
let cache = self.cache.read().await;
if let Some(cached) = cache.get(issuer_id) {
let age = SystemTime::now()
.duration_since(cached.fetched_at)
.unwrap_or(Duration::from_secs(0));
let ttl = Duration::from_secs(self.policy.refresh_interval_secs);
if age < ttl {
debug!("Using cached metadata for {} (age: {:?})", issuer_id, age);
return Ok(cached.metadata.clone());
}
}
}
debug!("Fetching federation metadata for {}", issuer_id);
let url = format!("https://{}/.well-known/federation", issuer_id);
let metadata: FederationMetadata = self
.client
.get(&url)
.send()
.await
.context("Failed to fetch federation metadata")?
.json()
.await
.context("Failed to parse federation metadata")?;
if !validate_issuer_domain(issuer_id, &metadata.issuer_id) {
return Err(anyhow::anyhow!(
"Federation metadata issuer_id mismatch: requested '{}', got '{}'",
issuer_id,
metadata.issuer_id
));
}
{
let mut cache = self.cache.write().await;
cache.insert(
issuer_id.to_string(),
CachedMetadata {
metadata: metadata.clone(),
fetched_at: SystemTime::now(),
},
);
}
info!("Fetched and cached metadata for {}", issuer_id);
Ok(metadata)
}
async fn fetch_pubkey(&self, issuer_id: &str) -> Result<Vec<u8>> {
{
let cache = self.pubkey_cache.read().await;
if let Some(cached) = cache.get(issuer_id) {
let age = SystemTime::now()
.duration_since(cached.fetched_at)
.unwrap_or(Duration::from_secs(0));
let ttl = Duration::from_secs(self.policy.refresh_interval_secs);
if age < ttl {
debug!("Using cached pubkey for {} (age: {:?})", issuer_id, age);
return Ok(cached.pubkey.clone());
}
}
}
debug!("Fetching issuer metadata for {}", issuer_id);
let url = format!("https://{}/.well-known/issuer", issuer_id);
let issuer_metadata: IssuerMetadata = self
.client
.get(&url)
.send()
.await
.context("Failed to fetch issuer metadata")?
.json()
.await
.context("Failed to parse issuer metadata")?;
if !validate_issuer_domain(issuer_id, &issuer_metadata.issuer_id) {
return Err(anyhow::anyhow!(
"Issuer metadata issuer_id mismatch: requested '{}', got '{}'",
issuer_id,
issuer_metadata.issuer_id
));
}
use base64ct::{Base64UrlUnpadded, Encoding};
let pubkey = Base64UrlUnpadded::decode_vec(&issuer_metadata.voprf.pubkey)
.context("Failed to decode public key")?;
{
let mut cache = self.pubkey_cache.write().await;
cache.insert(
issuer_id.to_string(),
CachedPubkey {
pubkey: pubkey.clone(),
fetched_at: SystemTime::now(),
},
);
}
info!("Fetched and cached pubkey for {}", issuer_id);
Ok(pubkey)
}
async fn is_vouch_valid(&self, vouch: &Vouch, vouched_pubkey: &[u8], now: i64) -> bool {
if !vouch.is_valid_at(now, MAX_CLOCK_SKEW_SECS) {
debug!(
"Vouch from {} for {} is expired or invalid time",
vouch.voucher_issuer_id, vouch.vouched_issuer_id
);
return false;
}
if let Some(level) = vouch.trust_level {
if level < self.policy.min_trust_level {
debug!(
"Vouch from {} for {} has insufficient trust level ({} < {})",
vouch.voucher_issuer_id,
vouch.vouched_issuer_id,
level,
self.policy.min_trust_level
);
return false;
}
}
if vouch.vouched_pubkey != vouched_pubkey {
debug!("Vouch public key mismatch for {}", vouch.vouched_issuer_id);
return false;
}
let voucher_pubkey = match self.fetch_pubkey(&vouch.voucher_issuer_id).await {
Ok(pk) => pk,
Err(e) => {
warn!(
"Failed to fetch pubkey for voucher {}: {}",
vouch.voucher_issuer_id, e
);
return false;
}
};
if !vouch.verify(&voucher_pubkey) {
warn!(
"Vouch signature verification failed for {} vouching for {}",
vouch.voucher_issuer_id, vouch.vouched_issuer_id
);
return false;
}
debug!(
"Vouch from {} for {} is valid (verified signature)",
vouch.voucher_issuer_id, vouch.vouched_issuer_id
);
true
}
async fn is_revoked(&self, issuer_id: &str, revocations: &[Revocation], _now: i64) -> bool {
for revocation in revocations {
if revocation.revoked_issuer_id == issuer_id {
let revoker_pubkey = match self.fetch_pubkey(&revocation.revoker_issuer_id).await {
Ok(pk) => pk,
Err(e) => {
warn!(
"Failed to fetch pubkey for revoker {}: {}",
revocation.revoker_issuer_id, e
);
continue; }
};
if !revocation.verify(&revoker_pubkey) {
warn!(
"Revocation signature verification failed for {} revoking {}",
revocation.revoker_issuer_id, revocation.revoked_issuer_id
);
continue; }
debug!(
"Issuer {} has been revoked by {} (reason: {:?}, verified signature)",
issuer_id, revocation.revoker_issuer_id, revocation.reason
);
return true;
}
}
false
}
async fn is_vouch_valid_with_limit(
&self,
vouch: &Vouch,
vouched_pubkey: &[u8],
now: i64,
request_count: &mut usize,
) -> bool {
if !vouch.is_valid_at(now, MAX_CLOCK_SKEW_SECS) {
debug!(
"Vouch from {} for {} is expired or invalid time",
vouch.voucher_issuer_id, vouch.vouched_issuer_id
);
return false;
}
if let Some(level) = vouch.trust_level {
if level < self.policy.min_trust_level {
debug!(
"Vouch from {} for {} has insufficient trust level ({} < {})",
vouch.voucher_issuer_id,
vouch.vouched_issuer_id,
level,
self.policy.min_trust_level
);
return false;
}
}
if vouch.vouched_pubkey != vouched_pubkey {
debug!("Vouch public key mismatch for {}", vouch.vouched_issuer_id);
return false;
}
if *request_count >= MAX_REQUESTS_PER_TRAVERSAL {
warn!("Request limit reached during vouch validation");
return false;
}
*request_count += 1;
let voucher_pubkey = match self.fetch_pubkey(&vouch.voucher_issuer_id).await {
Ok(pk) => pk,
Err(e) => {
warn!(
"Failed to fetch pubkey for voucher {}: {}",
vouch.voucher_issuer_id, e
);
return false;
}
};
if !vouch.verify(&voucher_pubkey) {
warn!(
"Vouch signature verification failed for {} vouching for {}",
vouch.voucher_issuer_id, vouch.vouched_issuer_id
);
return false;
}
debug!(
"Vouch from {} for {} is valid (verified signature)",
vouch.voucher_issuer_id, vouch.vouched_issuer_id
);
true
}
async fn is_revoked_with_limit(
&self,
issuer_id: &str,
revocations: &[Revocation],
_now: i64,
request_count: &mut usize,
) -> bool {
for revocation in revocations {
if revocation.revoked_issuer_id == issuer_id {
if *request_count >= MAX_REQUESTS_PER_TRAVERSAL {
warn!("Request limit reached during revocation check");
return false; }
*request_count += 1;
let revoker_pubkey = match self.fetch_pubkey(&revocation.revoker_issuer_id).await {
Ok(pk) => pk,
Err(e) => {
warn!(
"Failed to fetch pubkey for revoker {}: {}",
revocation.revoker_issuer_id, e
);
continue; }
};
if !revocation.verify(&revoker_pubkey) {
warn!(
"Revocation signature verification failed for {} revoking {}",
revocation.revoker_issuer_id, revocation.revoked_issuer_id
);
continue; }
debug!(
"Issuer {} has been revoked by {} (reason: {:?}, verified signature)",
issuer_id, revocation.revoker_issuer_id, revocation.reason
);
return true;
}
}
false
}
pub async fn clear_cache(&self) {
{
let mut cache = self.cache.write().await;
cache.clear();
}
{
let mut pubkey_cache = self.pubkey_cache.write().await;
pubkey_cache.clear();
}
info!("Federation metadata and public key caches cleared");
}
}
fn current_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trust_policy_default() {
let policy = TrustPolicy::default();
assert!(policy.enabled);
assert_eq!(policy.max_trust_depth, 2);
assert_eq!(policy.min_trust_paths, 1);
}
#[test]
fn test_current_timestamp() {
let now = current_timestamp();
assert!(now > 1700000000); }
#[tokio::test]
async fn test_trust_graph_disabled() {
let mut policy = TrustPolicy::default();
policy.enabled = false;
let graph = TrustGraph::new(policy);
let trusted = graph.is_trusted("issuer:test:v1", &[0u8; 33]).await;
assert!(!trusted, "Should not trust when federation is disabled");
}
#[tokio::test]
async fn test_trust_graph_blocked_issuer() {
let mut policy = TrustPolicy::default();
policy.blocked_issuers = vec!["issuer:bad:v1".to_string()];
let graph = TrustGraph::new(policy);
let trusted = graph.is_trusted("issuer:bad:v1", &[0u8; 33]).await;
assert!(!trusted, "Should not trust blocked issuer");
}
#[tokio::test]
async fn test_trust_graph_trusted_root() {
let mut policy = TrustPolicy::default();
policy.trusted_roots = vec!["issuer:root:v1".to_string()];
let graph = TrustGraph::new(policy);
let trusted = graph.is_trusted("issuer:root:v1", &[0u8; 33]).await;
assert!(trusted, "Should trust root issuer");
}
}