1use std::sync::Arc;
17
18use aex_core::{
19 AgentId, Capability, CapabilitySet, Error, IdScheme, IdentityProvider, Result, Signature,
20 SignatureAlgorithm,
21};
22use aex_jws::{Algorithm as JwsAlgorithm, VerifierKey};
23use async_trait::async_trait;
24use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
25use serde::{Deserialize, Serialize};
26use tokio::sync::RwLock;
27
28const ED25519_MULTICODEC_PREFIX: [u8; 2] = [0xed, 0x01];
30
31pub struct DidWebProvider {
41 agent_id: AgentId,
42 signing_key: SigningKey,
43 peers: Arc<RwLock<std::collections::HashMap<AgentId, VerifyingKey>>>,
44 component_name: String,
46}
47
48impl DidWebProvider {
49 pub fn new(
54 agent_id: AgentId,
55 signing_key: SigningKey,
56 component_name: impl Into<String>,
57 ) -> Result<Self> {
58 if agent_id.scheme() != IdScheme::DidWeb {
59 return Err(Error::InvalidAgentId(format!(
60 "DidWebProvider requires a did:web agent_id, got {}",
61 agent_id.as_str()
62 )));
63 }
64 Ok(Self {
65 agent_id,
66 signing_key,
67 peers: Arc::new(RwLock::new(Default::default())),
68 component_name: component_name.into(),
69 })
70 }
71
72 pub async fn register_peer(&self, peer_id: AgentId, pubkey: VerifyingKey) {
75 self.peers.write().await.insert(peer_id, pubkey);
76 }
77
78 pub fn well_known_url(agent_id: &AgentId) -> Result<String> {
85 let uri = agent_id.as_did_uri().ok_or_else(|| {
86 Error::InvalidAgentId(format!(
87 "did:web id is not a valid DID URI: {}",
88 agent_id.as_str()
89 ))
90 })?;
91 if uri.method != "web" {
92 return Err(Error::InvalidAgentId(format!(
93 "expected did:web, got did:{}",
94 uri.method
95 )));
96 }
97 let authority = uri.method_specific_id.split(':').next().unwrap_or("");
103 if authority.is_empty() {
104 return Err(Error::InvalidAgentId("did:web authority is empty".into()));
105 }
106 if authority.contains('/') || authority.contains('?') || authority.contains('#') {
110 return Err(Error::InvalidAgentId(format!(
111 "did:web authority contains URL-reserved chars: {}",
112 authority
113 )));
114 }
115 Ok(format!("https://{}/.well-known/agent-card.json", authority))
116 }
117
118 pub async fn fetch_and_verify_card(
121 &self,
122 peer_id: &AgentId,
123 ) -> Result<(VerifyingKey, AgentCardPayload)> {
124 let url = Self::well_known_url(peer_id)?;
125 let resp = aex_net::safe_get(&url, &self.component_name)
126 .await
127 .map_err(|e| Error::NotFound(format!("did:web fetch failed for {}: {}", peer_id, e)))?;
128
129 let jws = std::str::from_utf8(&resp.body)
130 .map_err(|e| Error::Crypto(format!("agent card not UTF-8: {}", e)))?;
131
132 let verified = aex_jws::verify(jws.trim(), |kid| {
138 let payload_b64 = jws
150 .trim()
151 .split('.')
152 .nth(1)
153 .ok_or(aex_jws::JwsError::InvalidStructure)?;
154 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
155 use base64::Engine;
156 let payload_bytes = URL_SAFE_NO_PAD
157 .decode(payload_b64)
158 .map_err(|e| aex_jws::JwsError::Base64Decode(format!("payload: {}", e)))?;
159 let payload: AgentCardPayload = serde_json::from_slice(&payload_bytes)
160 .map_err(|e| aex_jws::JwsError::InvalidHeader(format!("payload parse: {}", e)))?;
161
162 if payload.agent_id != kid {
165 return Err(aex_jws::JwsError::KidAlgMismatch {
166 kid: kid.into(),
167 header_alg: "EdDSA".into(),
168 key_alg: format!("payload claims {}", payload.agent_id),
169 });
170 }
171 let vk = decode_did_key_multibase(&payload.public_key.public_key_multibase)
172 .map_err(|e| aex_jws::JwsError::InvalidHeader(format!("public_key: {}", e)))?;
173 Ok(Some(VerifierKey::Ed25519(vk)))
174 })
175 .map_err(|e| Error::Crypto(format!("agent card JWS verify failed: {}", e)))?;
176
177 if verified.header.alg != JwsAlgorithm::EdDsa {
178 return Err(Error::Crypto(format!(
179 "did:web cards must use EdDSA for now; got {:?}",
180 verified.header.alg
181 )));
182 }
183
184 let payload: AgentCardPayload = serde_json::from_slice(&verified.payload)
185 .map_err(|e| Error::Crypto(format!("agent card payload parse: {}", e)))?;
186 let vk = decode_did_key_multibase(&payload.public_key.public_key_multibase)?;
187
188 Ok((vk, payload))
189 }
190}
191
192#[async_trait]
193impl IdentityProvider for DidWebProvider {
194 fn agent_id(&self) -> &AgentId {
195 &self.agent_id
196 }
197
198 async fn sign(&self, message: &[u8]) -> Result<Signature> {
199 let sig = self.signing_key.sign(message);
200 Ok(Signature {
201 algorithm: SignatureAlgorithm::Ed25519,
202 bytes: sig.to_bytes().to_vec(),
203 })
204 }
205
206 async fn verify_peer(
207 &self,
208 peer_id: &AgentId,
209 message: &[u8],
210 signature: &Signature,
211 ) -> Result<()> {
212 if signature.algorithm != SignatureAlgorithm::Ed25519 {
213 return Err(Error::SignatureFormat(format!(
214 "did:web (Ed25519 keys) requires Ed25519 signature, got {:?}",
215 signature.algorithm
216 )));
217 }
218
219 let cached = self.peers.read().await.get(peer_id).copied();
221 let pubkey = match cached {
222 Some(k) => k,
223 None => {
224 let (vk, _payload) = self.fetch_and_verify_card(peer_id).await?;
225 self.peers.write().await.insert(peer_id.clone(), vk);
226 vk
227 }
228 };
229
230 use ed25519_dalek::Verifier;
231 let sig_bytes: [u8; 64] = signature.bytes.as_slice().try_into().map_err(|_| {
232 Error::SignatureFormat(format!(
233 "Ed25519 signature must be 64 bytes, got {}",
234 signature.bytes.len()
235 ))
236 })?;
237 let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
238 pubkey
239 .verify(message, &sig)
240 .map_err(|_| Error::SignatureInvalid)
241 }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct AgentCardPayload {
250 pub iss: String,
252 pub sub: String,
254 pub iat: i64,
256 pub exp: i64,
258 pub agent_id: String,
260 pub public_key: PublicKeyDeclaration,
262 #[serde(default)]
264 pub capabilities: CapabilitySet,
265 #[serde(default)]
267 pub endpoints: Endpoints,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct PublicKeyDeclaration {
273 #[serde(rename = "type")]
275 pub key_type: String,
276 #[serde(rename = "publicKeyMultibase")]
278 pub public_key_multibase: String,
279}
280
281#[derive(Debug, Clone, Default, Serialize, Deserialize)]
283pub struct Endpoints {
284 pub control_plane: Option<String>,
286 #[serde(default)]
288 pub data_planes: Vec<String>,
289}
290
291impl AgentCardPayload {
292 pub fn has_capability(&self, cap: Capability) -> bool {
294 self.capabilities.has(cap)
295 }
296}
297
298fn decode_did_key_multibase(s: &str) -> Result<VerifyingKey> {
304 let after_z = s
305 .strip_prefix('z')
306 .ok_or_else(|| Error::Crypto(format!("multibase must start with 'z', got '{}'", s)))?;
307 let bytes = bs58::decode(after_z)
308 .into_vec()
309 .map_err(|e| Error::Crypto(format!("base58 decode: {}", e)))?;
310 if bytes.len() != ED25519_MULTICODEC_PREFIX.len() + 32 {
311 return Err(Error::Crypto(format!(
312 "multibase length mismatch: {} bytes",
313 bytes.len()
314 )));
315 }
316 if bytes[..2] != ED25519_MULTICODEC_PREFIX {
317 return Err(Error::Crypto(format!(
318 "multicodec prefix mismatch: {:02x?}",
319 &bytes[..2]
320 )));
321 }
322 let pk: [u8; 32] = bytes[2..].try_into().expect("length checked above");
323 VerifyingKey::from_bytes(&pk).map_err(|e| Error::Crypto(format!("Ed25519 key: {}", e)))
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn fixed_did_web_provider() -> DidWebProvider {
331 let sk = SigningKey::from_bytes(&[3u8; 32]);
332 let id = AgentId::new("did:web:acme.com#agent-vendite").unwrap();
333 DidWebProvider::new(id, sk, "test").unwrap()
334 }
335
336 #[test]
337 fn well_known_url_simple() {
338 let id = AgentId::new("did:web:acme.com#fatture").unwrap();
339 let url = DidWebProvider::well_known_url(&id).unwrap();
340 assert_eq!(url, "https://acme.com/.well-known/agent-card.json");
341 }
342
343 #[test]
344 fn well_known_url_strips_fragment() {
345 let id = AgentId::new("did:web:studio-rossi.it#clienti").unwrap();
346 let url = DidWebProvider::well_known_url(&id).unwrap();
347 assert!(!url.contains("clienti"));
348 assert_eq!(url, "https://studio-rossi.it/.well-known/agent-card.json");
349 }
350
351 #[test]
352 fn well_known_url_takes_authority_root() {
353 let id = AgentId::new("did:web:example.com:agents:bob").unwrap();
356 let url = DidWebProvider::well_known_url(&id).unwrap();
357 assert_eq!(url, "https://example.com/.well-known/agent-card.json");
358 }
359
360 #[test]
361 fn well_known_url_rejects_non_web() {
362 let id = AgentId::new("did:key:zabc").unwrap();
363 let err = DidWebProvider::well_known_url(&id).unwrap_err();
364 assert!(matches!(err, Error::InvalidAgentId(_)));
365 }
366
367 #[test]
368 fn well_known_url_rejects_authority_with_slash() {
369 let id = AgentId::new("did:web:evil.com/path").unwrap();
373 let err = DidWebProvider::well_known_url(&id).unwrap_err();
374 assert!(matches!(err, Error::InvalidAgentId(_)));
375 }
376
377 #[test]
378 fn constructor_rejects_non_did_web_id() {
379 let sk = SigningKey::from_bytes(&[1u8; 32]);
380 let id = AgentId::new("did:key:zabc").unwrap();
381 match DidWebProvider::new(id, sk, "test") {
382 Err(Error::InvalidAgentId(_)) => {}
383 Err(other) => panic!("wrong error variant: {other:?}"),
384 Ok(_) => panic!("expected rejection of non-did:web agent_id"),
385 }
386 }
387
388 #[test]
389 fn agent_id_returns_did_web() {
390 let p = fixed_did_web_provider();
391 assert_eq!(p.agent_id().scheme(), IdScheme::DidWeb);
392 assert_eq!(p.agent_id().as_str(), "did:web:acme.com#agent-vendite");
393 }
394
395 #[tokio::test]
396 async fn sign_and_verify_self_with_registered_key() {
397 let p = fixed_did_web_provider();
398 let vk = p.signing_key.verifying_key();
399 p.register_peer(p.agent_id().clone(), vk).await;
402 let sig = p.sign(b"hi").await.unwrap();
403 p.verify_peer(p.agent_id(), b"hi", &sig).await.unwrap();
404 }
405
406 #[tokio::test]
407 async fn rejects_wrong_signature_algorithm() {
408 let p = fixed_did_web_provider();
409 let bogus = Signature {
410 algorithm: SignatureAlgorithm::EcdsaSecp256k1,
411 bytes: vec![0u8; 64],
412 };
413 let err = p.verify_peer(p.agent_id(), b"x", &bogus).await.unwrap_err();
414 assert!(matches!(err, Error::SignatureFormat(_)));
415 }
416
417 #[tokio::test]
418 async fn rejects_tampered_signature() {
419 let p = fixed_did_web_provider();
420 let vk = p.signing_key.verifying_key();
421 p.register_peer(p.agent_id().clone(), vk).await;
422 let mut sig = p.sign(b"x").await.unwrap();
423 sig.bytes[0] ^= 0xff;
424 let err = p.verify_peer(p.agent_id(), b"x", &sig).await.unwrap_err();
425 assert!(matches!(err, Error::SignatureInvalid));
426 }
427
428 #[test]
429 fn agent_card_payload_serde_roundtrip() {
430 let card = AgentCardPayload {
431 iss: "did:web:acme.com".into(),
432 sub: "did:web:acme.com#fatture".into(),
433 iat: 1_716_100_000,
434 exp: 1_716_186_400,
435 agent_id: "did:web:acme.com#fatture".into(),
436 public_key: PublicKeyDeclaration {
437 key_type: "Ed25519VerificationKey2020".into(),
438 public_key_multibase: "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV".into(),
439 },
440 capabilities: CapabilitySet::empty()
441 .with(Capability::WireV2)
442 .with(Capability::JwsAgentCard),
443 endpoints: Endpoints {
444 control_plane: Some("https://acme.com/aex".into()),
445 data_planes: vec!["https://data.acme.com".into()],
446 },
447 };
448 let json = serde_json::to_string(&card).unwrap();
449 let back: AgentCardPayload = serde_json::from_str(&json).unwrap();
450 assert_eq!(card.agent_id, back.agent_id);
451 assert!(back.has_capability(Capability::WireV2));
452 assert!(back.has_capability(Capability::JwsAgentCard));
453 assert!(!back.has_capability(Capability::A2ABridge));
454 }
455
456 #[test]
457 fn decode_did_key_multibase_roundtrip() {
458 let sk = SigningKey::from_bytes(&[5u8; 32]);
459 let vk = sk.verifying_key();
460 let mut buf: Vec<u8> = ED25519_MULTICODEC_PREFIX.to_vec();
461 buf.extend_from_slice(vk.as_bytes());
462 let encoded = format!("z{}", bs58::encode(buf).into_string());
463 let decoded = decode_did_key_multibase(&encoded).unwrap();
464 assert_eq!(decoded.as_bytes(), vk.as_bytes());
465 }
466
467 #[test]
468 fn decode_rejects_missing_z_prefix() {
469 let err = decode_did_key_multibase("ab12cd").unwrap_err();
470 assert!(matches!(err, Error::Crypto(_)));
471 }
472
473 #[test]
474 fn decode_rejects_bad_multicodec() {
475 let mut buf: Vec<u8> = vec![0x12, 0x20];
477 buf.extend_from_slice(&[0u8; 32]);
478 let s = format!("z{}", bs58::encode(buf).into_string());
479 let err = decode_did_key_multibase(&s).unwrap_err();
480 assert!(matches!(err, Error::Crypto(_)));
481 }
482}