use std::sync::Arc;
use aex_core::{
AgentId, Capability, CapabilitySet, Error, IdScheme, IdentityProvider, Result, Signature,
SignatureAlgorithm,
};
use aex_jws::{Algorithm as JwsAlgorithm, VerifierKey};
use async_trait::async_trait;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
const ED25519_MULTICODEC_PREFIX: [u8; 2] = [0xed, 0x01];
pub struct DidWebProvider {
agent_id: AgentId,
signing_key: SigningKey,
peers: Arc<RwLock<std::collections::HashMap<AgentId, VerifyingKey>>>,
component_name: String,
}
impl DidWebProvider {
pub fn new(
agent_id: AgentId,
signing_key: SigningKey,
component_name: impl Into<String>,
) -> Result<Self> {
if agent_id.scheme() != IdScheme::DidWeb {
return Err(Error::InvalidAgentId(format!(
"DidWebProvider requires a did:web agent_id, got {}",
agent_id.as_str()
)));
}
Ok(Self {
agent_id,
signing_key,
peers: Arc::new(RwLock::new(Default::default())),
component_name: component_name.into(),
})
}
pub async fn register_peer(&self, peer_id: AgentId, pubkey: VerifyingKey) {
self.peers.write().await.insert(peer_id, pubkey);
}
pub fn well_known_url(agent_id: &AgentId) -> Result<String> {
let uri = agent_id.as_did_uri().ok_or_else(|| {
Error::InvalidAgentId(format!(
"did:web id is not a valid DID URI: {}",
agent_id.as_str()
))
})?;
if uri.method != "web" {
return Err(Error::InvalidAgentId(format!(
"expected did:web, got did:{}",
uri.method
)));
}
let authority = uri.method_specific_id.split(':').next().unwrap_or("");
if authority.is_empty() {
return Err(Error::InvalidAgentId("did:web authority is empty".into()));
}
if authority.contains('/') || authority.contains('?') || authority.contains('#') {
return Err(Error::InvalidAgentId(format!(
"did:web authority contains URL-reserved chars: {}",
authority
)));
}
Ok(format!("https://{}/.well-known/agent-card.json", authority))
}
pub async fn fetch_and_verify_card(
&self,
peer_id: &AgentId,
) -> Result<(VerifyingKey, AgentCardPayload)> {
let url = Self::well_known_url(peer_id)?;
let resp = aex_net::safe_get(&url, &self.component_name)
.await
.map_err(|e| Error::NotFound(format!("did:web fetch failed for {}: {}", peer_id, e)))?;
let jws = std::str::from_utf8(&resp.body)
.map_err(|e| Error::Crypto(format!("agent card not UTF-8: {}", e)))?;
let verified = aex_jws::verify(jws.trim(), |kid| {
let payload_b64 = jws
.trim()
.split('.')
.nth(1)
.ok_or(aex_jws::JwsError::InvalidStructure)?;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let payload_bytes = URL_SAFE_NO_PAD
.decode(payload_b64)
.map_err(|e| aex_jws::JwsError::Base64Decode(format!("payload: {}", e)))?;
let payload: AgentCardPayload = serde_json::from_slice(&payload_bytes)
.map_err(|e| aex_jws::JwsError::InvalidHeader(format!("payload parse: {}", e)))?;
if payload.agent_id != kid {
return Err(aex_jws::JwsError::KidAlgMismatch {
kid: kid.into(),
header_alg: "EdDSA".into(),
key_alg: format!("payload claims {}", payload.agent_id),
});
}
let vk = decode_did_key_multibase(&payload.public_key.public_key_multibase)
.map_err(|e| aex_jws::JwsError::InvalidHeader(format!("public_key: {}", e)))?;
Ok(Some(VerifierKey::Ed25519(vk)))
})
.map_err(|e| Error::Crypto(format!("agent card JWS verify failed: {}", e)))?;
if verified.header.alg != JwsAlgorithm::EdDsa {
return Err(Error::Crypto(format!(
"did:web cards must use EdDSA for now; got {:?}",
verified.header.alg
)));
}
let payload: AgentCardPayload = serde_json::from_slice(&verified.payload)
.map_err(|e| Error::Crypto(format!("agent card payload parse: {}", e)))?;
let vk = decode_did_key_multibase(&payload.public_key.public_key_multibase)?;
Ok((vk, payload))
}
}
#[async_trait]
impl IdentityProvider for DidWebProvider {
fn agent_id(&self) -> &AgentId {
&self.agent_id
}
async fn sign(&self, message: &[u8]) -> Result<Signature> {
let sig = self.signing_key.sign(message);
Ok(Signature {
algorithm: SignatureAlgorithm::Ed25519,
bytes: sig.to_bytes().to_vec(),
})
}
async fn verify_peer(
&self,
peer_id: &AgentId,
message: &[u8],
signature: &Signature,
) -> Result<()> {
if signature.algorithm != SignatureAlgorithm::Ed25519 {
return Err(Error::SignatureFormat(format!(
"did:web (Ed25519 keys) requires Ed25519 signature, got {:?}",
signature.algorithm
)));
}
let cached = self.peers.read().await.get(peer_id).copied();
let pubkey = match cached {
Some(k) => k,
None => {
let (vk, _payload) = self.fetch_and_verify_card(peer_id).await?;
self.peers.write().await.insert(peer_id.clone(), vk);
vk
}
};
use ed25519_dalek::Verifier;
let sig_bytes: [u8; 64] = signature.bytes.as_slice().try_into().map_err(|_| {
Error::SignatureFormat(format!(
"Ed25519 signature must be 64 bytes, got {}",
signature.bytes.len()
))
})?;
let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
pubkey
.verify(message, &sig)
.map_err(|_| Error::SignatureInvalid)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCardPayload {
pub iss: String,
pub sub: String,
pub iat: i64,
pub exp: i64,
pub agent_id: String,
pub public_key: PublicKeyDeclaration,
#[serde(default)]
pub capabilities: CapabilitySet,
#[serde(default)]
pub endpoints: Endpoints,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicKeyDeclaration {
#[serde(rename = "type")]
pub key_type: String,
#[serde(rename = "publicKeyMultibase")]
pub public_key_multibase: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Endpoints {
pub control_plane: Option<String>,
#[serde(default)]
pub data_planes: Vec<String>,
}
impl AgentCardPayload {
pub fn has_capability(&self, cap: Capability) -> bool {
self.capabilities.has(cap)
}
}
fn decode_did_key_multibase(s: &str) -> Result<VerifyingKey> {
let after_z = s
.strip_prefix('z')
.ok_or_else(|| Error::Crypto(format!("multibase must start with 'z', got '{}'", s)))?;
let bytes = bs58::decode(after_z)
.into_vec()
.map_err(|e| Error::Crypto(format!("base58 decode: {}", e)))?;
if bytes.len() != ED25519_MULTICODEC_PREFIX.len() + 32 {
return Err(Error::Crypto(format!(
"multibase length mismatch: {} bytes",
bytes.len()
)));
}
if bytes[..2] != ED25519_MULTICODEC_PREFIX {
return Err(Error::Crypto(format!(
"multicodec prefix mismatch: {:02x?}",
&bytes[..2]
)));
}
let pk: [u8; 32] = bytes[2..].try_into().expect("length checked above");
VerifyingKey::from_bytes(&pk).map_err(|e| Error::Crypto(format!("Ed25519 key: {}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
fn fixed_did_web_provider() -> DidWebProvider {
let sk = SigningKey::from_bytes(&[3u8; 32]);
let id = AgentId::new("did:web:acme.com#agent-vendite").unwrap();
DidWebProvider::new(id, sk, "test").unwrap()
}
#[test]
fn well_known_url_simple() {
let id = AgentId::new("did:web:acme.com#fatture").unwrap();
let url = DidWebProvider::well_known_url(&id).unwrap();
assert_eq!(url, "https://acme.com/.well-known/agent-card.json");
}
#[test]
fn well_known_url_strips_fragment() {
let id = AgentId::new("did:web:studio-rossi.it#clienti").unwrap();
let url = DidWebProvider::well_known_url(&id).unwrap();
assert!(!url.contains("clienti"));
assert_eq!(url, "https://studio-rossi.it/.well-known/agent-card.json");
}
#[test]
fn well_known_url_takes_authority_root() {
let id = AgentId::new("did:web:example.com:agents:bob").unwrap();
let url = DidWebProvider::well_known_url(&id).unwrap();
assert_eq!(url, "https://example.com/.well-known/agent-card.json");
}
#[test]
fn well_known_url_rejects_non_web() {
let id = AgentId::new("did:key:zabc").unwrap();
let err = DidWebProvider::well_known_url(&id).unwrap_err();
assert!(matches!(err, Error::InvalidAgentId(_)));
}
#[test]
fn well_known_url_rejects_authority_with_slash() {
let id = AgentId::new("did:web:evil.com/path").unwrap();
let err = DidWebProvider::well_known_url(&id).unwrap_err();
assert!(matches!(err, Error::InvalidAgentId(_)));
}
#[test]
fn constructor_rejects_non_did_web_id() {
let sk = SigningKey::from_bytes(&[1u8; 32]);
let id = AgentId::new("did:key:zabc").unwrap();
match DidWebProvider::new(id, sk, "test") {
Err(Error::InvalidAgentId(_)) => {}
Err(other) => panic!("wrong error variant: {other:?}"),
Ok(_) => panic!("expected rejection of non-did:web agent_id"),
}
}
#[test]
fn agent_id_returns_did_web() {
let p = fixed_did_web_provider();
assert_eq!(p.agent_id().scheme(), IdScheme::DidWeb);
assert_eq!(p.agent_id().as_str(), "did:web:acme.com#agent-vendite");
}
#[tokio::test]
async fn sign_and_verify_self_with_registered_key() {
let p = fixed_did_web_provider();
let vk = p.signing_key.verifying_key();
p.register_peer(p.agent_id().clone(), vk).await;
let sig = p.sign(b"hi").await.unwrap();
p.verify_peer(p.agent_id(), b"hi", &sig).await.unwrap();
}
#[tokio::test]
async fn rejects_wrong_signature_algorithm() {
let p = fixed_did_web_provider();
let bogus = Signature {
algorithm: SignatureAlgorithm::EcdsaSecp256k1,
bytes: vec![0u8; 64],
};
let err = p.verify_peer(p.agent_id(), b"x", &bogus).await.unwrap_err();
assert!(matches!(err, Error::SignatureFormat(_)));
}
#[tokio::test]
async fn rejects_tampered_signature() {
let p = fixed_did_web_provider();
let vk = p.signing_key.verifying_key();
p.register_peer(p.agent_id().clone(), vk).await;
let mut sig = p.sign(b"x").await.unwrap();
sig.bytes[0] ^= 0xff;
let err = p.verify_peer(p.agent_id(), b"x", &sig).await.unwrap_err();
assert!(matches!(err, Error::SignatureInvalid));
}
#[test]
fn agent_card_payload_serde_roundtrip() {
let card = AgentCardPayload {
iss: "did:web:acme.com".into(),
sub: "did:web:acme.com#fatture".into(),
iat: 1_716_100_000,
exp: 1_716_186_400,
agent_id: "did:web:acme.com#fatture".into(),
public_key: PublicKeyDeclaration {
key_type: "Ed25519VerificationKey2020".into(),
public_key_multibase: "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV".into(),
},
capabilities: CapabilitySet::empty()
.with(Capability::WireV2)
.with(Capability::JwsAgentCard),
endpoints: Endpoints {
control_plane: Some("https://acme.com/aex".into()),
data_planes: vec!["https://data.acme.com".into()],
},
};
let json = serde_json::to_string(&card).unwrap();
let back: AgentCardPayload = serde_json::from_str(&json).unwrap();
assert_eq!(card.agent_id, back.agent_id);
assert!(back.has_capability(Capability::WireV2));
assert!(back.has_capability(Capability::JwsAgentCard));
assert!(!back.has_capability(Capability::A2ABridge));
}
#[test]
fn decode_did_key_multibase_roundtrip() {
let sk = SigningKey::from_bytes(&[5u8; 32]);
let vk = sk.verifying_key();
let mut buf: Vec<u8> = ED25519_MULTICODEC_PREFIX.to_vec();
buf.extend_from_slice(vk.as_bytes());
let encoded = format!("z{}", bs58::encode(buf).into_string());
let decoded = decode_did_key_multibase(&encoded).unwrap();
assert_eq!(decoded.as_bytes(), vk.as_bytes());
}
#[test]
fn decode_rejects_missing_z_prefix() {
let err = decode_did_key_multibase("ab12cd").unwrap_err();
assert!(matches!(err, Error::Crypto(_)));
}
#[test]
fn decode_rejects_bad_multicodec() {
let mut buf: Vec<u8> = vec![0x12, 0x20];
buf.extend_from_slice(&[0u8; 32]);
let s = format!("z{}", bs58::encode(buf).into_string());
let err = decode_did_key_multibase(&s).unwrap_err();
assert!(matches!(err, Error::Crypto(_)));
}
}