mod apd;
mod b64;
mod discover;
mod key;
mod ps;
mod sig;
pub use ::mcp::http::RequestSigner;
pub use apd::{ApdClient, ApdConfig};
pub use key::AgentKey;
pub fn verify_ed25519(public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<(), String> {
ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, public_key)
.verify(msg, signature)
.map_err(|_| "aauth: signature verification failed".into())
}
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
static INSTALLED: OnceLock<AAuthClient> = OnceLock::new();
pub fn install(client: AAuthClient) {
let _ = INSTALLED.set(client);
}
pub fn signer() -> Option<Arc<dyn ::mcp::http::RequestSigner>> {
INSTALLED
.get()
.map(|c| Arc::new(c.clone()) as Arc<dyn ::mcp::http::RequestSigner>)
}
pub fn installed() -> Option<&'static AAuthClient> {
INSTALLED.get()
}
pub fn setup(settings: &crate::config::AAuthSettings, timeout: Duration) -> Result<(), String> {
if installed().is_some() {
return Ok(());
}
let key = AgentKey::load_or_create(std::path::Path::new(&settings.key_file))?;
let enrollment_token = match &settings.enrollment_token {
Some(tmpl) => Some(crate::sec::secret::resolve(tmpl, &|k| {
std::env::var(k).ok()
})?),
None => None,
};
let config = ApdConfig {
base_url: settings.provider.clone(),
enrollment_token,
enroll_assertion_file: settings.enroll_assertion_file.clone(),
person_server: settings.person_server.clone(),
platform: "workload".into(),
};
install(AAuthClient::new(key, config, timeout));
Ok(())
}
#[derive(Default)]
struct AuthorityState {
access: std::collections::HashMap<String, String>,
auth_token: std::collections::HashMap<String, String>,
wants_digest: std::collections::HashMap<String, bool>,
}
#[derive(Clone)]
pub struct AAuthClient {
apd: Arc<ApdClient>,
person_server: Option<String>,
timeout: Duration,
state: Arc<Mutex<AuthorityState>>,
}
impl AAuthClient {
pub fn new(key: AgentKey, apd: ApdConfig, timeout: Duration) -> AAuthClient {
let person_server = apd.person_server.clone();
AAuthClient {
apd: Arc::new(ApdClient::new(apd, key, timeout)),
person_server,
timeout,
state: Arc::new(Mutex::new(AuthorityState::default())),
}
}
pub fn prime(&self) -> Result<String, String> {
self.apd.verify_provider_metadata()?;
self.apd.token()?;
Ok(self.apd.agent_id().unwrap_or_default())
}
pub fn agent_id(&self) -> Option<String> {
self.apd.agent_id()
}
pub fn discover(&self, authority: &str, endpoint: &str) {
if let Some(meta) = discover::fetch(endpoint, self.timeout) {
self.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.wants_digest
.insert(authority.to_string(), meta.content_digest);
}
}
pub fn adopt_access(&self, authority: &str, token: &str) {
self.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.access
.insert(authority.to_string(), token.to_string());
}
}
impl ::mcp::http::RequestSigner for AAuthClient {
fn sign(
&self,
method: &str,
authority: &str,
path: &str,
body: &[u8],
) -> Vec<(String, String)> {
let Ok(agent_token) = self.apd.token() else {
return Vec::new();
};
let st = self.state.lock().unwrap_or_else(|e| e.into_inner());
let key_id = st.auth_token.get(authority).cloned().unwrap_or(agent_token);
let want_digest = *st.wants_digest.get(authority).unwrap_or(&false);
let access = st.access.get(authority).cloned();
drop(st);
let digest = want_digest.then(|| sig::content_digest(body));
let mut headers = sig::sign_request(
self.apd.key(),
method,
authority,
path,
sig::SigKey::Jwt(&key_id),
sig::now_secs(),
digest.as_deref(),
);
if let Some(a) = access {
headers.push(("Authorization".into(), format!("AAuth {a}")));
}
headers
}
fn on_response(&self, resp: &::mcp::http::AuthResponse, authority: &str) -> bool {
if let Some(access) = &resp.access {
self.adopt_access(authority, access);
return true;
}
let Some(requirement) = &resp.requirement else {
return false;
};
if ps::wants_auth_token(requirement) {
let already = self
.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.auth_token
.contains_key(authority);
if already {
return false; }
let (Some(ps_url), Some(resource_token), Ok(agent_token)) = (
self.person_server.as_deref(),
ps::resource_token(requirement),
self.apd.token(),
) else {
return false; };
let justification = format!(
"agent {} requests access to {authority}",
self.agent_id().unwrap_or_default()
);
match ps::exchange(
ps_url,
self.apd.key(),
&agent_token,
&resource_token,
&justification,
self.timeout,
) {
Ok(auth_token) => {
self.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.auth_token
.insert(authority.to_string(), auth_token);
return true; }
Err(_) => return false, }
}
false
}
fn wants_content_digest(&self, authority: &str) -> bool {
*self
.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.wants_digest
.get(authority)
.unwrap_or(&false)
}
}