1use std::collections::BTreeMap;
2
3use base64::Engine as _;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7
8use crate::types::JsonObject;
9
10use super::security::{SecurityRequirement, SecurityScheme};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct AgentCard {
16 pub name: String,
18 pub description: String,
20 pub supported_interfaces: Vec<AgentInterface>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub provider: Option<AgentProvider>,
25 pub version: String,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub documentation_url: Option<String>,
30 pub capabilities: AgentCapabilities,
32 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33 pub security_schemes: BTreeMap<String, SecurityScheme>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub security_requirements: Vec<SecurityRequirement>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
39 pub default_input_modes: Vec<String>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub default_output_modes: Vec<String>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub skills: Vec<AgentSkill>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub signatures: Vec<AgentCardSignature>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub icon_url: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct AgentInterface {
59 pub url: String,
61 pub protocol_binding: String,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub tenant: Option<String>,
66 pub protocol_version: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct AgentProvider {
74 pub url: String,
76 pub organization: String,
78}
79
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct AgentCapabilities {
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub streaming: Option<bool>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub push_notifications: Option<bool>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub extensions: Vec<AgentExtension>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub extended_agent_card: Option<bool>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct AgentExtension {
102 pub uri: String,
104 #[serde(default, skip_serializing_if = "String::is_empty")]
105 pub description: String,
107 #[serde(default, skip_serializing_if = "crate::types::is_false")]
108 pub required: bool,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub params: Option<JsonObject>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct AgentSkill {
119 pub id: String,
121 pub name: String,
123 pub description: String,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
126 pub tags: Vec<String>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
129 pub examples: Vec<String>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
132 pub input_modes: Vec<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub output_modes: Vec<String>,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
138 pub security_requirements: Vec<SecurityRequirement>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct AgentCardSignature {
146 pub protected: String,
148 pub signature: String,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub header: Option<JsonObject>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct JwsProtectedHeader {
159 pub alg: String,
161 pub kid: String,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub typ: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub jku: Option<String>,
169 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
170 pub extra: BTreeMap<String, Value>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct AgentCardSignatureVerificationInput {
177 pub protected_header: JwsProtectedHeader,
179 pub protected_segment: String,
181 pub signature: Vec<u8>,
183 pub signing_input: Vec<u8>,
185 pub unprotected_header: Option<JsonObject>,
187}
188
189#[derive(Debug, Error)]
191pub enum AgentCardSignatureError {
192 #[error("agent card does not contain any signatures")]
194 MissingSignatures,
195 #[error("invalid protected header encoding: {0}")]
197 InvalidProtectedEncoding(String),
198 #[error("invalid signature encoding: {0}")]
200 InvalidSignatureEncoding(String),
201 #[error("invalid protected header JSON: {0}")]
203 InvalidProtectedHeader(#[source] serde_json::Error),
204 #[error("no agent-card signature matched the supported algorithms")]
206 UnsupportedAlgorithm,
207 #[error("agent-card signature verification failed")]
209 VerificationFailed,
210 #[error("agent-card serialization failed: {0}")]
212 Serialization(#[from] serde_json::Error),
213}
214
215impl AgentCard {
216 pub fn unsigned_clone(&self) -> Self {
218 let mut card = self.clone();
219 card.signatures.clear();
220 card
221 }
222
223 pub fn canonical_signing_payload(&self) -> Result<String, AgentCardSignatureError> {
225 canonicalize_json(&serde_json::to_value(self.unsigned_clone())?)
226 }
227
228 pub fn verify_signatures<F>(
234 &self,
235 supported_algorithms: &[&str],
236 mut verifier: F,
237 ) -> Result<(), AgentCardSignatureError>
238 where
239 F: FnMut(&AgentCardSignatureVerificationInput) -> Result<bool, AgentCardSignatureError>,
240 {
241 if self.signatures.is_empty() {
242 return Err(AgentCardSignatureError::MissingSignatures);
243 }
244
245 let mut matched_algorithm = false;
246 for signature in &self.signatures {
247 let input = signature.verification_input(self)?;
248 if !supported_algorithms.is_empty()
249 && !supported_algorithms.iter().any(|algorithm| {
250 algorithm.eq_ignore_ascii_case(input.protected_header.alg.as_str())
251 })
252 {
253 continue;
254 }
255
256 matched_algorithm = true;
257 if verifier(&input)? {
258 return Ok(());
259 }
260 }
261
262 if !matched_algorithm {
263 return Err(AgentCardSignatureError::UnsupportedAlgorithm);
264 }
265
266 Err(AgentCardSignatureError::VerificationFailed)
267 }
268}
269
270impl AgentCardSignature {
271 pub fn protected_header(&self) -> Result<JwsProtectedHeader, AgentCardSignatureError> {
273 let bytes = base64_url_engine()
274 .decode(self.protected.as_bytes())
275 .map_err(|error| {
276 AgentCardSignatureError::InvalidProtectedEncoding(error.to_string())
277 })?;
278
279 serde_json::from_slice(&bytes).map_err(AgentCardSignatureError::InvalidProtectedHeader)
280 }
281
282 pub fn signature_bytes(&self) -> Result<Vec<u8>, AgentCardSignatureError> {
284 base64_url_engine()
285 .decode(self.signature.as_bytes())
286 .map_err(|error| AgentCardSignatureError::InvalidSignatureEncoding(error.to_string()))
287 }
288
289 pub fn verification_input(
291 &self,
292 card: &AgentCard,
293 ) -> Result<AgentCardSignatureVerificationInput, AgentCardSignatureError> {
294 let protected_header = self.protected_header()?;
295 let signature = self.signature_bytes()?;
296 let payload = card.canonical_signing_payload()?;
297 let payload_segment = base64_url_engine().encode(payload.as_bytes());
298 let signing_input = format!("{}.{}", self.protected, payload_segment).into_bytes();
299
300 Ok(AgentCardSignatureVerificationInput {
301 protected_header,
302 protected_segment: self.protected.clone(),
303 signature,
304 signing_input,
305 unprotected_header: self.header.clone(),
306 })
307 }
308}
309
310fn canonicalize_json(value: &Value) -> Result<String, AgentCardSignatureError> {
311 match value {
312 Value::Null => Ok("null".to_owned()),
313 Value::Bool(value) => Ok(if *value { "true" } else { "false" }.to_owned()),
314 Value::Number(value) => Ok(value.to_string()),
315 Value::String(value) => serde_json::to_string(value).map_err(AgentCardSignatureError::from),
316 Value::Array(values) => {
317 let mut json = String::from("[");
318 for (index, value) in values.iter().enumerate() {
319 if index > 0 {
320 json.push(',');
321 }
322 json.push_str(&canonicalize_json(value)?);
323 }
324 json.push(']');
325 Ok(json)
326 }
327 Value::Object(values) => {
328 let mut keys = values.keys().collect::<Vec<_>>();
329 keys.sort_unstable();
330
331 let mut json = String::from("{");
332 for (index, key) in keys.into_iter().enumerate() {
333 if index > 0 {
334 json.push(',');
335 }
336 json.push_str(&serde_json::to_string(key)?);
337 json.push(':');
338 json.push_str(&canonicalize_json(&values[key])?);
339 }
340 json.push('}');
341 Ok(json)
342 }
343 }
344}
345
346fn base64_url_engine() -> &'static base64::engine::GeneralPurpose {
347 &base64::engine::general_purpose::URL_SAFE_NO_PAD
348}
349
350#[cfg(test)]
351mod tests {
352 use std::collections::BTreeMap;
353
354 use super::{
355 AgentCapabilities, AgentCard, AgentCardSignature, AgentCardSignatureError, AgentExtension,
356 AgentInterface, AgentSkill, JwsProtectedHeader,
357 };
358 use base64::Engine as _;
359 use serde_json::json;
360
361 #[test]
362 fn agent_card_round_trip_serialization() {
363 let card = AgentCard {
364 name: "Echo Agent".to_owned(),
365 description: "Replies with the same text".to_owned(),
366 supported_interfaces: vec![AgentInterface {
367 url: "https://example.com/rpc".to_owned(),
368 protocol_binding: "JSONRPC".to_owned(),
369 tenant: None,
370 protocol_version: "1.0".to_owned(),
371 }],
372 provider: None,
373 version: "0.1.0".to_owned(),
374 documentation_url: None,
375 capabilities: AgentCapabilities {
376 streaming: Some(true),
377 push_notifications: Some(false),
378 extensions: vec![AgentExtension {
379 uri: "https://example.com/ext/streaming".to_owned(),
380 description: "Streaming support".to_owned(),
381 required: false,
382 params: None,
383 }],
384 extended_agent_card: Some(false),
385 },
386 security_schemes: BTreeMap::new(),
387 security_requirements: Vec::new(),
388 default_input_modes: vec!["text/plain".to_owned()],
389 default_output_modes: vec!["text/plain".to_owned()],
390 skills: vec![AgentSkill {
391 id: "echo".to_owned(),
392 name: "Echo".to_owned(),
393 description: "Echo back user input".to_owned(),
394 tags: vec!["utility".to_owned()],
395 examples: vec!["echo hello".to_owned()],
396 input_modes: vec!["text/plain".to_owned()],
397 output_modes: vec!["text/plain".to_owned()],
398 security_requirements: Vec::new(),
399 }],
400 signatures: Vec::new(),
401 icon_url: None,
402 };
403
404 let json = serde_json::to_string(&card).expect("card should serialize");
405 let round_trip: AgentCard = serde_json::from_str(&json).expect("card should deserialize");
406
407 assert_eq!(round_trip.name, "Echo Agent");
408 assert_eq!(
409 round_trip.supported_interfaces[0].protocol_binding,
410 "JSONRPC"
411 );
412 assert_eq!(
413 round_trip.capabilities.extensions[0].description,
414 "Streaming support"
415 );
416 assert!(!round_trip.capabilities.extensions[0].required);
417 assert_eq!(round_trip.skills[0].id, "echo");
418 }
419
420 #[test]
421 fn signature_helper_decodes_protected_header() {
422 let protected = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
423 serde_json::to_vec(&json!({
424 "alg": "ES256",
425 "kid": "key-1",
426 "typ": "JOSE",
427 }))
428 .expect("header should serialize"),
429 );
430 let signature = AgentCardSignature {
431 protected,
432 signature: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([1_u8, 2, 3]),
433 header: None,
434 };
435
436 let header = signature
437 .protected_header()
438 .expect("protected header should decode");
439 assert_eq!(
440 header,
441 JwsProtectedHeader {
442 alg: "ES256".to_owned(),
443 kid: "key-1".to_owned(),
444 typ: Some("JOSE".to_owned()),
445 jku: None,
446 extra: BTreeMap::new(),
447 }
448 );
449 }
450
451 #[test]
452 fn canonical_signing_payload_omits_signatures() {
453 let mut card = sample_card();
454 card.signatures.push(sample_signature());
455
456 let payload = card
457 .canonical_signing_payload()
458 .expect("payload should canonicalize");
459
460 assert!(!payload.contains("\"signatures\""));
461 assert!(payload.starts_with("{\"capabilities\""));
462 }
463
464 #[test]
465 fn verify_signatures_builds_detached_jws_input() {
466 let mut card = sample_card();
467 let signature = sample_signature();
468 let protected = signature.protected.clone();
469 card.signatures.push(signature);
470
471 let payload_segment = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
472 card.canonical_signing_payload()
473 .expect("payload should canonicalize"),
474 );
475 let expected_input = format!("{protected}.{payload_segment}");
476
477 card.verify_signatures(&["ES256"], |input| {
478 assert_eq!(input.protected_header.alg, "ES256");
479 assert_eq!(input.protected_header.kid, "key-1");
480 assert_eq!(input.signature, vec![1_u8, 2, 3]);
481 assert_eq!(input.signing_input, expected_input.as_bytes());
482 Ok(true)
483 })
484 .expect("verification should succeed");
485 }
486
487 #[test]
488 fn verify_signatures_rejects_cards_without_supported_algorithms() {
489 let mut card = sample_card();
490 card.signatures.push(sample_signature());
491
492 let error = card
493 .verify_signatures(&["RS256"], |_input| Ok(true))
494 .expect_err("unsupported algorithms should fail");
495
496 assert!(matches!(
497 error,
498 AgentCardSignatureError::UnsupportedAlgorithm
499 ));
500 }
501
502 fn sample_card() -> AgentCard {
503 AgentCard {
504 name: "Echo Agent".to_owned(),
505 description: "Replies with the same text".to_owned(),
506 supported_interfaces: vec![AgentInterface {
507 url: "https://example.com/rpc".to_owned(),
508 protocol_binding: "JSONRPC".to_owned(),
509 tenant: None,
510 protocol_version: "1.0".to_owned(),
511 }],
512 provider: None,
513 version: "0.1.0".to_owned(),
514 documentation_url: None,
515 capabilities: AgentCapabilities {
516 streaming: Some(true),
517 push_notifications: Some(false),
518 extensions: vec![AgentExtension {
519 uri: "https://example.com/ext/streaming".to_owned(),
520 description: "Streaming support".to_owned(),
521 required: false,
522 params: None,
523 }],
524 extended_agent_card: Some(false),
525 },
526 security_schemes: BTreeMap::new(),
527 security_requirements: Vec::new(),
528 default_input_modes: vec!["text/plain".to_owned()],
529 default_output_modes: vec!["text/plain".to_owned()],
530 skills: vec![AgentSkill {
531 id: "echo".to_owned(),
532 name: "Echo".to_owned(),
533 description: "Echo back user input".to_owned(),
534 tags: vec!["utility".to_owned()],
535 examples: vec!["echo hello".to_owned()],
536 input_modes: vec!["text/plain".to_owned()],
537 output_modes: vec!["text/plain".to_owned()],
538 security_requirements: Vec::new(),
539 }],
540 signatures: Vec::new(),
541 icon_url: None,
542 }
543 }
544
545 fn sample_signature() -> AgentCardSignature {
546 AgentCardSignature {
547 protected: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
548 serde_json::to_vec(&json!({
549 "alg": "ES256",
550 "kid": "key-1",
551 "typ": "JOSE",
552 }))
553 .expect("header should serialize"),
554 ),
555 signature: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([1_u8, 2, 3]),
556 header: None,
557 }
558 }
559}