use std::fs;
use async_trait::async_trait;
use agentpin::resolver::{
ChainResolver, DiscoveryResolver, LocalFileResolver, TrustBundleResolver,
};
use agentpin::types::bundle::TrustBundle;
use agentpin::verification::{VerificationResult as ApVerificationResult, VerifierConfig};
use super::discovery::DiscoveryCache;
use super::key_store::AgentPinKeyStore;
use super::types::{AgentPinConfig, AgentPinError, AgentVerificationResult, DiscoveryMode};
#[async_trait]
pub trait AgentPinVerifier: Send + Sync {
async fn verify_credential(&self, jwt: &str) -> Result<AgentVerificationResult, AgentPinError>;
}
pub struct DefaultAgentPinVerifier {
config: AgentPinConfig,
key_store: AgentPinKeyStore,
sync_resolver: Option<Box<dyn DiscoveryResolver>>,
}
impl DefaultAgentPinVerifier {
pub fn new(config: AgentPinConfig) -> Result<Self, AgentPinError> {
let key_store = AgentPinKeyStore::new(&config.key_store_path).map_err(|e| {
AgentPinError::KeyStoreError {
reason: e.to_string(),
}
})?;
let sync_resolver = Self::build_sync_resolver(&config)?;
Ok(Self {
config,
key_store,
sync_resolver,
})
}
fn build_sync_resolver(
config: &AgentPinConfig,
) -> Result<Option<Box<dyn DiscoveryResolver>>, AgentPinError> {
match config.discovery_mode {
DiscoveryMode::Bundle => {
let path = config.trust_bundle_path.as_ref().ok_or_else(|| {
AgentPinError::ConfigError {
reason: "trust_bundle_path required for bundle mode".to_string(),
}
})?;
let json = fs::read_to_string(path).map_err(|e| AgentPinError::IoError {
reason: format!("Failed to read trust bundle: {}", e),
})?;
let bundle: TrustBundle =
serde_json::from_str(&json).map_err(|e| AgentPinError::ConfigError {
reason: format!("Invalid trust bundle JSON: {}", e),
})?;
Ok(Some(Box::new(TrustBundleResolver::new(&bundle))))
}
DiscoveryMode::Local => {
let dir = config.local_discovery_dir.as_ref().ok_or_else(|| {
AgentPinError::ConfigError {
reason: "local_discovery_dir required for local mode".to_string(),
}
})?;
Ok(Some(Box::new(LocalFileResolver::new(
dir,
config.local_revocation_dir.as_deref(),
))))
}
DiscoveryMode::Chain => {
let mut resolvers: Vec<Box<dyn DiscoveryResolver>> = Vec::new();
if let Some(ref path) = config.trust_bundle_path {
if let Ok(json) = fs::read_to_string(path) {
if let Ok(bundle) = serde_json::from_str::<TrustBundle>(&json) {
resolvers.push(Box::new(TrustBundleResolver::new(&bundle)));
}
}
}
if let Some(ref dir) = config.local_discovery_dir {
resolvers.push(Box::new(LocalFileResolver::new(
dir,
config.local_revocation_dir.as_deref(),
)));
}
if resolvers.is_empty() {
Ok(None)
} else {
Ok(Some(Box::new(ChainResolver::new(resolvers))))
}
}
DiscoveryMode::WellKnown => Ok(None),
}
}
fn convert_result(result: &ApVerificationResult) -> AgentVerificationResult {
if result.valid {
let capabilities = result
.capabilities
.as_ref()
.map(|caps| caps.iter().map(|c| c.to_string()).collect())
.unwrap_or_default();
AgentVerificationResult {
valid: true,
agent_id: result.agent_id.clone(),
issuer: result.issuer.clone(),
capabilities,
delegation_verified: result.delegation_verified,
error_message: None,
warnings: result.warnings.clone(),
}
} else {
AgentVerificationResult {
valid: false,
agent_id: result.agent_id.clone(),
issuer: result.issuer.clone(),
capabilities: vec![],
delegation_verified: None,
error_message: result.error_message.clone(),
warnings: result.warnings.clone(),
}
}
}
}
#[async_trait]
impl AgentPinVerifier for DefaultAgentPinVerifier {
async fn verify_credential(&self, jwt: &str) -> Result<AgentVerificationResult, AgentPinError> {
let verifier_config = VerifierConfig {
clock_skew_secs: self.config.clock_skew_secs,
max_ttl_secs: self.config.max_ttl_secs,
};
let audience = self.config.audience.as_deref();
let mut pin_store = self.key_store.load_pin_store()?;
let result = if let Some(ref resolver) = self.sync_resolver {
agentpin::verification::verify_credential_with_resolver(
jwt,
resolver.as_ref(),
&mut pin_store,
audience,
&verifier_config,
)
} else {
agentpin::verification::verify_credential(
jwt,
&mut pin_store,
audience,
&verifier_config,
)
.await
};
if let Err(e) = self.key_store.save_pin_store(&pin_store) {
tracing::warn!("Failed to persist AgentPin key store: {}", e);
}
Ok(Self::convert_result(&result))
}
}
pub struct CachingResolver<R: DiscoveryResolver> {
inner: R,
cache: DiscoveryCache,
}
impl<R: DiscoveryResolver> CachingResolver<R> {
pub fn new(inner: R, cache: DiscoveryCache) -> Self {
Self { inner, cache }
}
}
impl<R: DiscoveryResolver> DiscoveryResolver for CachingResolver<R> {
fn resolve_discovery(
&self,
domain: &str,
) -> Result<agentpin::types::discovery::DiscoveryDocument, agentpin::error::Error> {
if let Some(cached) = self.cache.get(domain) {
return Ok(cached);
}
let doc = self.inner.resolve_discovery(domain)?;
let _ = self.cache.put(domain, &doc);
Ok(doc)
}
fn resolve_revocation(
&self,
domain: &str,
discovery: &agentpin::types::discovery::DiscoveryDocument,
) -> Result<Option<agentpin::types::revocation::RevocationDocument>, agentpin::error::Error>
{
self.inner.resolve_revocation(domain, discovery)
}
}
pub struct MockAgentPinVerifier {
should_succeed: bool,
mock_agent_id: String,
mock_issuer: String,
mock_capabilities: Vec<String>,
}
impl MockAgentPinVerifier {
pub fn new_success() -> Self {
Self {
should_succeed: true,
mock_agent_id: "mock-agent-001".to_string(),
mock_issuer: "mock.example.com".to_string(),
mock_capabilities: vec!["execute:*".to_string()],
}
}
pub fn new_failure() -> Self {
Self {
should_succeed: false,
mock_agent_id: String::new(),
mock_issuer: String::new(),
mock_capabilities: vec![],
}
}
pub fn with_identity(agent_id: String, issuer: String, capabilities: Vec<String>) -> Self {
Self {
should_succeed: true,
mock_agent_id: agent_id,
mock_issuer: issuer,
mock_capabilities: capabilities,
}
}
}
#[async_trait]
impl AgentPinVerifier for MockAgentPinVerifier {
async fn verify_credential(
&self,
_jwt: &str,
) -> Result<AgentVerificationResult, AgentPinError> {
if self.should_succeed {
Ok(AgentVerificationResult::success(
self.mock_agent_id.clone(),
self.mock_issuer.clone(),
self.mock_capabilities.clone(),
))
} else {
Ok(AgentVerificationResult::failure(
"Mock verification failed".to_string(),
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_verifier_success() {
let verifier = MockAgentPinVerifier::new_success();
let result = verifier.verify_credential("dummy.jwt.token").await.unwrap();
assert!(result.valid);
assert_eq!(result.agent_id, Some("mock-agent-001".to_string()));
assert_eq!(result.issuer, Some("mock.example.com".to_string()));
assert!(!result.capabilities.is_empty());
}
#[tokio::test]
async fn test_mock_verifier_failure() {
let verifier = MockAgentPinVerifier::new_failure();
let result = verifier.verify_credential("dummy.jwt.token").await.unwrap();
assert!(!result.valid);
assert!(result.error_message.is_some());
}
#[tokio::test]
async fn test_mock_verifier_custom_identity() {
let verifier = MockAgentPinVerifier::with_identity(
"custom-agent".to_string(),
"custom.example.com".to_string(),
vec!["read:data".to_string(), "write:data".to_string()],
);
let result = verifier.verify_credential("dummy.jwt.token").await.unwrap();
assert!(result.valid);
assert_eq!(result.agent_id, Some("custom-agent".to_string()));
assert_eq!(result.issuer, Some("custom.example.com".to_string()));
assert_eq!(result.capabilities.len(), 2);
}
#[test]
fn test_caching_resolver() {
use agentpin::types::bundle::TrustBundle;
let temp_dir = tempfile::tempdir().unwrap();
let cache = DiscoveryCache::new(temp_dir.path(), 3600).unwrap();
let doc = agentpin::discovery::build_discovery_document(
"cached.example.com",
agentpin::types::discovery::EntityType::Maker,
vec![agentpin::jwk::Jwk {
kid: "k1".to_string(),
kty: "EC".to_string(),
crv: "P-256".to_string(),
x: "x".to_string(),
y: "y".to_string(),
use_: "sig".to_string(),
key_ops: None,
exp: None,
}],
vec![],
2,
"2026-02-10T00:00:00Z",
);
let bundle = TrustBundle {
agentpin_bundle_version: "0.1".to_string(),
created_at: "2026-02-10T00:00:00Z".to_string(),
documents: vec![doc],
revocations: vec![],
};
let inner = TrustBundleResolver::new(&bundle);
let resolver = CachingResolver::new(inner, cache);
let resolved = resolver.resolve_discovery("cached.example.com").unwrap();
assert_eq!(resolved.entity, "cached.example.com");
let resolved2 = resolver.resolve_discovery("cached.example.com").unwrap();
assert_eq!(resolved2.entity, "cached.example.com");
}
}