1use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9use crate::crypto::{hash, Hash, PublicKey, Sig};
10use crate::error::{Error, Result};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AgentAttestation {
19 agent_id: PublicKey,
21
22 code_hash: Hash,
24
25 config_hash: Hash,
27
28 prompt_hash: Hash,
30
31 tools: Vec<ToolAttestation>,
33
34 runtime: RuntimeAttestation,
36
37 attested_at: i64,
39
40 validity_period_ms: u64,
42
43 authority_signature: Sig,
45
46 authority: PublicKey,
48}
49
50impl AgentAttestation {
51 pub fn builder() -> AgentAttestationBuilder {
53 AgentAttestationBuilder::new()
54 }
55
56 pub fn agent_id(&self) -> &PublicKey {
58 &self.agent_id
59 }
60
61 pub fn code_hash(&self) -> &Hash {
63 &self.code_hash
64 }
65
66 pub fn config_hash(&self) -> &Hash {
68 &self.config_hash
69 }
70
71 pub fn prompt_hash(&self) -> &Hash {
73 &self.prompt_hash
74 }
75
76 pub fn tools(&self) -> &[ToolAttestation] {
78 &self.tools
79 }
80
81 pub fn runtime(&self) -> &RuntimeAttestation {
83 &self.runtime
84 }
85
86 pub fn attested_at(&self) -> i64 {
88 self.attested_at
89 }
90
91 pub fn validity_period(&self) -> Duration {
93 Duration::from_millis(self.validity_period_ms)
94 }
95
96 pub fn authority_signature(&self) -> &Sig {
98 &self.authority_signature
99 }
100
101 pub fn authority(&self) -> &PublicKey {
103 &self.authority
104 }
105
106 pub fn is_valid_at(&self, check_time: i64) -> bool {
111 let expires_at = self
112 .attested_at
113 .saturating_add(self.validity_period_ms as i64);
114 check_time < expires_at
115 }
116
117 pub fn expires_at(&self) -> i64 {
119 self.attested_at
120 .saturating_add(self.validity_period_ms as i64)
121 }
122
123 pub fn canonical_bytes(&self) -> Vec<u8> {
125 let mut data = Vec::new();
127 data.extend_from_slice(&self.agent_id.as_bytes());
128 data.extend_from_slice(self.code_hash.as_bytes());
129 data.extend_from_slice(self.config_hash.as_bytes());
130 data.extend_from_slice(self.prompt_hash.as_bytes());
131 for tool in &self.tools {
132 data.extend_from_slice(tool.tool_id.as_bytes());
133 data.extend_from_slice(tool.version.as_bytes());
134 data.extend_from_slice(tool.implementation_hash.as_bytes());
135 }
136 data.extend_from_slice(self.runtime.runtime_id.as_bytes());
137 data.extend_from_slice(self.runtime.runtime_hash.as_bytes());
138 data.extend_from_slice(&self.attested_at.to_le_bytes());
139 data.extend_from_slice(&self.validity_period_ms.to_le_bytes());
140 data.extend_from_slice(&self.authority.as_bytes());
141 data
142 }
143
144 pub fn hash(&self) -> Hash {
146 hash(&self.canonical_bytes())
147 }
148
149 pub fn verify_signature(&self) -> Result<()> {
151 let bytes = self.canonical_bytes();
152 self.authority
153 .verify(&bytes, &self.authority_signature)
154 .map_err(|_| Error::invalid_input("Attestation signature verification failed"))
155 }
156
157 pub fn has_tool(&self, tool_id: &str) -> bool {
159 self.tools.iter().any(|t| t.tool_id == tool_id)
160 }
161
162 pub fn get_tool(&self, tool_id: &str) -> Option<&ToolAttestation> {
164 self.tools.iter().find(|t| t.tool_id == tool_id)
165 }
166
167 pub fn validate_binding(&self, actor_key: &PublicKey) -> Result<()> {
170 if self.agent_id != *actor_key {
171 return Err(Error::invalid_input(
172 "attestation agent_id does not match event actor",
173 ));
174 }
175 Ok(())
176 }
177
178 pub fn validate_tool(&self, tool_id: &str) -> Result<()> {
181 if !self.has_tool(tool_id) {
182 return Err(Error::invalid_input(format!(
183 "tool '{}' not found in agent attestation",
184 tool_id
185 )));
186 }
187 Ok(())
188 }
189
190 pub fn validate_for_action(&self, actor_key: &PublicKey, action_time: i64) -> Result<()> {
197 if !self.is_valid_at(action_time) {
199 return Err(Error::invalid_input("attestation has expired"));
200 }
201
202 self.verify_signature()?;
204
205 self.validate_binding(actor_key)?;
207
208 Ok(())
209 }
210}
211
212#[derive(Debug, Default)]
214pub struct AgentAttestationBuilder {
215 agent_id: Option<PublicKey>,
216 code_hash: Option<Hash>,
217 config_hash: Option<Hash>,
218 prompt_hash: Option<Hash>,
219 tools: Vec<ToolAttestation>,
220 runtime: Option<RuntimeAttestation>,
221 attested_at: Option<i64>,
222 validity_period_ms: Option<u64>,
223 authority: Option<PublicKey>,
224}
225
226impl AgentAttestationBuilder {
227 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn agent_id(mut self, agent_id: PublicKey) -> Self {
234 self.agent_id = Some(agent_id);
235 self
236 }
237
238 pub fn code_hash(mut self, hash: Hash) -> Self {
240 self.code_hash = Some(hash);
241 self
242 }
243
244 pub fn config_hash(mut self, hash: Hash) -> Self {
246 self.config_hash = Some(hash);
247 self
248 }
249
250 pub fn prompt_hash(mut self, hash: Hash) -> Self {
252 self.prompt_hash = Some(hash);
253 self
254 }
255
256 pub fn tool(mut self, tool: ToolAttestation) -> Self {
258 self.tools.push(tool);
259 self
260 }
261
262 pub fn tools(mut self, tools: Vec<ToolAttestation>) -> Self {
264 self.tools = tools;
265 self
266 }
267
268 pub fn runtime(mut self, runtime: RuntimeAttestation) -> Self {
270 self.runtime = Some(runtime);
271 self
272 }
273
274 pub fn attested_at(mut self, timestamp: i64) -> Self {
276 self.attested_at = Some(timestamp);
277 self
278 }
279
280 pub fn validity_period(mut self, duration: Duration) -> Self {
282 self.validity_period_ms = Some(duration.as_millis() as u64);
283 self
284 }
285
286 pub fn authority(mut self, authority: PublicKey) -> Self {
288 self.authority = Some(authority);
289 self
290 }
291
292 pub fn sign(self, authority_key: &crate::crypto::SecretKey) -> Result<AgentAttestation> {
300 let agent_id = self
301 .agent_id
302 .ok_or_else(|| Error::invalid_input("agent_id is required"))?;
303
304 let code_hash = self
305 .code_hash
306 .ok_or_else(|| Error::invalid_input("code_hash is required"))?;
307
308 if code_hash.as_bytes().iter().all(|&b| b == 0) {
310 return Err(Error::invalid_input("code_hash cannot be empty"));
311 }
312
313 let config_hash = self
314 .config_hash
315 .ok_or_else(|| Error::invalid_input("config_hash is required"))?;
316
317 let prompt_hash = self
318 .prompt_hash
319 .ok_or_else(|| Error::invalid_input("prompt_hash is required"))?;
320
321 let runtime = self
322 .runtime
323 .ok_or_else(|| Error::invalid_input("runtime is required"))?;
324
325 let attested_at = self
326 .attested_at
327 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
328
329 let validity_period_ms = self.validity_period_ms.unwrap_or(3600 * 1000); if validity_period_ms == 0 {
332 return Err(Error::invalid_input("validity_period must be positive"));
333 }
334
335 let authority = self.authority.unwrap_or_else(|| authority_key.public_key());
336
337 let mut attestation = AgentAttestation {
339 agent_id,
340 code_hash,
341 config_hash,
342 prompt_hash,
343 tools: self.tools,
344 runtime,
345 attested_at,
346 validity_period_ms,
347 authority_signature: Sig::empty(), authority,
349 };
350
351 let canonical = attestation.canonical_bytes();
353 attestation.authority_signature = authority_key.sign(&canonical);
354
355 Ok(attestation)
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(tag = "type", rename_all = "snake_case")]
365pub enum RequiredCapability {
366 Read,
368 Write,
370 Delete,
372 Execute,
374 InvokeTool { tool_id: String },
376 SpawnAgent,
378 DelegateCapability,
380 SendMessage { channel: String },
382 ReceiveMessage { channel: String },
384 Spend { currency: String },
386 ModifyPermissions,
388 ViewAuditLog,
390 FileSystem,
392 Network,
394}
395
396impl RequiredCapability {
397 pub fn read() -> Self {
399 Self::Read
400 }
401
402 pub fn write() -> Self {
404 Self::Write
405 }
406
407 pub fn execute() -> Self {
409 Self::Execute
410 }
411
412 pub fn invoke_tool(tool_id: impl Into<String>) -> Self {
414 Self::InvokeTool {
415 tool_id: tool_id.into(),
416 }
417 }
418
419 pub fn file_system() -> Self {
421 Self::FileSystem
422 }
423
424 pub fn network() -> Self {
426 Self::Network
427 }
428}
429
430impl std::fmt::Display for RequiredCapability {
431 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432 match self {
433 RequiredCapability::Read => write!(f, "read"),
434 RequiredCapability::Write => write!(f, "write"),
435 RequiredCapability::Delete => write!(f, "delete"),
436 RequiredCapability::Execute => write!(f, "execute"),
437 RequiredCapability::InvokeTool { tool_id } => write!(f, "invoke_tool:{}", tool_id),
438 RequiredCapability::SpawnAgent => write!(f, "spawn_agent"),
439 RequiredCapability::DelegateCapability => write!(f, "delegate_capability"),
440 RequiredCapability::SendMessage { channel } => write!(f, "send_message:{}", channel),
441 RequiredCapability::ReceiveMessage { channel } => {
442 write!(f, "receive_message:{}", channel)
443 }
444 RequiredCapability::Spend { currency } => write!(f, "spend:{}", currency),
445 RequiredCapability::ModifyPermissions => write!(f, "modify_permissions"),
446 RequiredCapability::ViewAuditLog => write!(f, "view_audit_log"),
447 RequiredCapability::FileSystem => write!(f, "file_system"),
448 RequiredCapability::Network => write!(f, "network"),
449 }
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455pub struct ToolAttestation {
456 pub tool_id: String,
458
459 pub version: String,
461
462 pub implementation_hash: Hash,
464
465 pub required_capabilities: Vec<RequiredCapability>,
467}
468
469impl ToolAttestation {
470 pub fn new(
472 tool_id: impl Into<String>,
473 version: impl Into<String>,
474 implementation_hash: Hash,
475 ) -> Self {
476 Self {
477 tool_id: tool_id.into(),
478 version: version.into(),
479 implementation_hash,
480 required_capabilities: Vec::new(),
481 }
482 }
483
484 pub fn with_capability(mut self, capability: RequiredCapability) -> Self {
486 self.required_capabilities.push(capability);
487 self
488 }
489
490 pub fn with_capabilities(mut self, capabilities: Vec<RequiredCapability>) -> Self {
492 self.required_capabilities = capabilities;
493 self
494 }
495
496 pub fn requires(&self, capability: &RequiredCapability) -> bool {
498 self.required_capabilities.contains(capability)
499 }
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct RuntimeAttestation {
505 pub runtime_id: String,
507
508 pub runtime_hash: Hash,
510
511 pub tee_quote: Option<TeeQuote>,
513
514 pub platform_hash: Option<Hash>,
516}
517
518impl RuntimeAttestation {
519 pub fn new(runtime_id: impl Into<String>, runtime_hash: Hash) -> Self {
521 Self {
522 runtime_id: runtime_id.into(),
523 runtime_hash,
524 tee_quote: None,
525 platform_hash: None,
526 }
527 }
528
529 pub fn with_tee(mut self, quote: TeeQuote) -> Self {
531 self.tee_quote = Some(quote);
532 self
533 }
534
535 pub fn with_platform_hash(mut self, hash: Hash) -> Self {
537 self.platform_hash = Some(hash);
538 self
539 }
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct TeeQuote {
545 pub tee_type: TeeType,
547
548 pub quote: Vec<u8>,
550
551 pub measurements: Vec<Hash>,
553}
554
555impl TeeQuote {
556 pub fn new(tee_type: TeeType, quote: Vec<u8>) -> Self {
558 Self {
559 tee_type,
560 quote,
561 measurements: Vec::new(),
562 }
563 }
564
565 pub fn with_measurement(mut self, hash: Hash) -> Self {
567 self.measurements.push(hash);
568 self
569 }
570}
571
572#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
574#[serde(rename_all = "snake_case")]
575pub enum TeeType {
576 IntelSgx,
578 IntelTdx,
580 AmdSevSnp,
582 ArmCca,
584 Software,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum AttestationError {
591 NotFound,
593 Expired,
595 Revoked,
597 UntrustedAuthority,
599 InvalidSignature,
601 AgentMismatch,
603 ToolNotAttested(String),
605}
606
607impl std::fmt::Display for AttestationError {
608 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609 match self {
610 AttestationError::NotFound => write!(f, "Attestation not found"),
611 AttestationError::Expired => write!(f, "Attestation has expired"),
612 AttestationError::Revoked => write!(f, "Attestation has been revoked"),
613 AttestationError::UntrustedAuthority => write!(f, "Attestation authority not trusted"),
614 AttestationError::InvalidSignature => write!(f, "Attestation signature invalid"),
615 AttestationError::AgentMismatch => write!(f, "Agent ID does not match attestation"),
616 AttestationError::ToolNotAttested(tool) => {
617 write!(f, "Tool '{}' not in agent's attestation", tool)
618 }
619 }
620 }
621}
622
623impl std::error::Error for AttestationError {}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use crate::crypto::{hash, SecretKey};
629
630 fn test_runtime() -> RuntimeAttestation {
631 RuntimeAttestation::new("test-runtime-v1.0.0", hash(b"runtime-binary"))
632 }
633
634 fn test_tool() -> ToolAttestation {
635 ToolAttestation::new("read_file", "1.0.0", hash(b"read-file-impl"))
636 }
637
638 #[test]
641 fn attestation_requires_code_hash() {
642 let authority = SecretKey::generate();
643 let agent = SecretKey::generate();
644
645 let result = AgentAttestation::builder()
646 .agent_id(agent.public_key())
647 .config_hash(hash(b"config"))
649 .prompt_hash(hash(b"prompt"))
650 .runtime(test_runtime())
651 .sign(&authority);
652
653 assert!(result.is_err());
654 }
655
656 #[test]
657 fn attestation_rejects_empty_code_hash() {
658 let authority = SecretKey::generate();
659 let agent = SecretKey::generate();
660
661 let result = AgentAttestation::builder()
662 .agent_id(agent.public_key())
663 .code_hash(Hash::from_bytes([0u8; 32])) .config_hash(hash(b"config"))
665 .prompt_hash(hash(b"prompt"))
666 .runtime(test_runtime())
667 .sign(&authority);
668
669 assert!(result.is_err());
670 assert!(result.unwrap_err().to_string().contains("empty"));
671 }
672
673 #[test]
674 fn attestation_requires_valid_signature() {
675 let authority = SecretKey::generate();
676 let agent = SecretKey::generate();
677
678 let attestation = AgentAttestation::builder()
679 .agent_id(agent.public_key())
680 .code_hash(hash(b"code"))
681 .config_hash(hash(b"config"))
682 .prompt_hash(hash(b"prompt"))
683 .runtime(test_runtime())
684 .sign(&authority)
685 .unwrap();
686
687 assert!(attestation.verify_signature().is_ok());
689
690 let mut tampered = attestation.clone();
692 tampered.attested_at += 1;
693 assert!(tampered.verify_signature().is_err());
694 }
695
696 #[test]
697 fn attestation_validity_period_must_be_positive() {
698 let authority = SecretKey::generate();
699 let agent = SecretKey::generate();
700
701 let result = AgentAttestation::builder()
702 .agent_id(agent.public_key())
703 .code_hash(hash(b"code"))
704 .config_hash(hash(b"config"))
705 .prompt_hash(hash(b"prompt"))
706 .runtime(test_runtime())
707 .validity_period(Duration::from_secs(0)) .sign(&authority);
709
710 assert!(result.is_err());
711 }
712
713 #[test]
716 fn attestation_valid_within_validity_period() {
717 let authority = SecretKey::generate();
718 let agent = SecretKey::generate();
719 let start = 1000000i64;
720
721 let attestation = AgentAttestation::builder()
722 .agent_id(agent.public_key())
723 .code_hash(hash(b"code"))
724 .config_hash(hash(b"config"))
725 .prompt_hash(hash(b"prompt"))
726 .runtime(test_runtime())
727 .attested_at(start)
728 .validity_period(Duration::from_secs(3600)) .sign(&authority)
730 .unwrap();
731
732 assert!(attestation.is_valid_at(start));
734
735 assert!(attestation.is_valid_at(start + 1800 * 1000));
737
738 assert!(attestation.is_valid_at(start + 3540 * 1000));
740 }
741
742 #[test]
743 fn attestation_invalid_after_expiry() {
744 let authority = SecretKey::generate();
745 let agent = SecretKey::generate();
746 let start = 1000000i64;
747
748 let attestation = AgentAttestation::builder()
749 .agent_id(agent.public_key())
750 .code_hash(hash(b"code"))
751 .config_hash(hash(b"config"))
752 .prompt_hash(hash(b"prompt"))
753 .runtime(test_runtime())
754 .attested_at(start)
755 .validity_period(Duration::from_secs(3600)) .sign(&authority)
757 .unwrap();
758
759 assert!(!attestation.is_valid_at(start + 3600 * 1000));
761
762 assert!(!attestation.is_valid_at(start + 3601 * 1000));
764 }
765
766 #[test]
769 fn tool_attestation_includes_version() {
770 let tool = ToolAttestation::new("bash", "5.1.0", hash(b"bash-impl"));
771 assert_eq!(tool.version, "5.1.0");
772 }
773
774 #[test]
775 fn tool_attestation_includes_implementation_hash() {
776 let impl_hash = hash(b"tool-implementation");
777 let tool = ToolAttestation::new("read_file", "1.0.0", impl_hash);
778 assert_eq!(tool.implementation_hash, impl_hash);
779 }
780
781 #[test]
782 fn tool_attestation_with_capabilities() {
783 let tool = ToolAttestation::new("bash", "5.1.0", hash(b"bash-impl"))
784 .with_capability(RequiredCapability::Execute)
785 .with_capability(RequiredCapability::FileSystem);
786
787 assert_eq!(tool.required_capabilities.len(), 2);
788 assert!(tool.requires(&RequiredCapability::Execute));
789 assert!(tool.requires(&RequiredCapability::FileSystem));
790 assert!(!tool.requires(&RequiredCapability::Network));
791 }
792
793 #[test]
794 fn attestation_has_tool_check() {
795 let authority = SecretKey::generate();
796 let agent = SecretKey::generate();
797
798 let attestation = AgentAttestation::builder()
799 .agent_id(agent.public_key())
800 .code_hash(hash(b"code"))
801 .config_hash(hash(b"config"))
802 .prompt_hash(hash(b"prompt"))
803 .runtime(test_runtime())
804 .tool(test_tool())
805 .tool(ToolAttestation::new("bash", "5.1", hash(b"bash")))
806 .sign(&authority)
807 .unwrap();
808
809 assert!(attestation.has_tool("read_file"));
810 assert!(attestation.has_tool("bash"));
811 assert!(!attestation.has_tool("write_file"));
812 }
813
814 #[test]
817 fn runtime_attestation_with_tee() {
818 let runtime = RuntimeAttestation::new("claude-v1", hash(b"runtime"))
819 .with_tee(TeeQuote::new(TeeType::IntelSgx, vec![1, 2, 3, 4]));
820
821 assert!(runtime.tee_quote.is_some());
822 assert_eq!(runtime.tee_quote.unwrap().tee_type, TeeType::IntelSgx);
823 }
824
825 #[test]
826 fn runtime_attestation_with_platform_hash() {
827 let platform = hash(b"platform-measurements");
828 let runtime =
829 RuntimeAttestation::new("claude-v1", hash(b"runtime")).with_platform_hash(platform);
830
831 assert_eq!(runtime.platform_hash, Some(platform));
832 }
833
834 #[test]
837 fn attestation_hash_deterministic() {
838 let authority = SecretKey::generate();
839 let agent = SecretKey::generate();
840
841 let attestation = AgentAttestation::builder()
842 .agent_id(agent.public_key())
843 .code_hash(hash(b"code"))
844 .config_hash(hash(b"config"))
845 .prompt_hash(hash(b"prompt"))
846 .runtime(test_runtime())
847 .attested_at(1000000)
848 .sign(&authority)
849 .unwrap();
850
851 let h1 = attestation.hash();
852 let h2 = attestation.hash();
853 assert_eq!(h1, h2);
854 }
855
856 #[test]
857 fn different_attestations_different_hash() {
858 let authority = SecretKey::generate();
859 let agent = SecretKey::generate();
860
861 let attestation1 = AgentAttestation::builder()
862 .agent_id(agent.public_key())
863 .code_hash(hash(b"code1"))
864 .config_hash(hash(b"config"))
865 .prompt_hash(hash(b"prompt"))
866 .runtime(test_runtime())
867 .attested_at(1000000)
868 .sign(&authority)
869 .unwrap();
870
871 let attestation2 = AgentAttestation::builder()
872 .agent_id(agent.public_key())
873 .code_hash(hash(b"code2")) .config_hash(hash(b"config"))
875 .prompt_hash(hash(b"prompt"))
876 .runtime(test_runtime())
877 .attested_at(1000000)
878 .sign(&authority)
879 .unwrap();
880
881 assert_ne!(attestation1.hash(), attestation2.hash());
882 }
883
884 #[test]
887 fn attestation_getters_work() {
888 let authority = SecretKey::generate();
889 let agent = SecretKey::generate();
890 let code = hash(b"code");
891 let config = hash(b"config");
892 let prompt = hash(b"prompt");
893 let runtime = test_runtime();
894
895 let attestation = AgentAttestation::builder()
896 .agent_id(agent.public_key())
897 .code_hash(code)
898 .config_hash(config)
899 .prompt_hash(prompt)
900 .runtime(runtime.clone())
901 .attested_at(1000000)
902 .validity_period(Duration::from_secs(7200))
903 .sign(&authority)
904 .unwrap();
905
906 assert_eq!(attestation.agent_id(), &agent.public_key());
907 assert_eq!(attestation.code_hash(), &code);
908 assert_eq!(attestation.config_hash(), &config);
909 assert_eq!(attestation.prompt_hash(), &prompt);
910 assert_eq!(attestation.runtime().runtime_id, runtime.runtime_id);
911 assert_eq!(attestation.attested_at(), 1000000);
912 assert_eq!(attestation.validity_period(), Duration::from_secs(7200));
913 assert_eq!(attestation.authority(), &authority.public_key());
914 assert_eq!(attestation.expires_at(), 1000000 + 7200 * 1000);
915 }
916
917 #[test]
920 fn validate_binding_passes_for_matching_agent() {
921 let authority = SecretKey::generate();
922 let agent = SecretKey::generate();
923
924 let attestation = AgentAttestation::builder()
925 .agent_id(agent.public_key())
926 .code_hash(hash(b"code"))
927 .config_hash(hash(b"config"))
928 .prompt_hash(hash(b"prompt"))
929 .runtime(test_runtime())
930 .sign(&authority)
931 .unwrap();
932
933 assert!(attestation.validate_binding(&agent.public_key()).is_ok());
934 }
935
936 #[test]
937 fn validate_binding_fails_for_different_agent() {
938 let authority = SecretKey::generate();
939 let agent = SecretKey::generate();
940 let other = SecretKey::generate();
941
942 let attestation = AgentAttestation::builder()
943 .agent_id(agent.public_key())
944 .code_hash(hash(b"code"))
945 .config_hash(hash(b"config"))
946 .prompt_hash(hash(b"prompt"))
947 .runtime(test_runtime())
948 .sign(&authority)
949 .unwrap();
950
951 let result = attestation.validate_binding(&other.public_key());
952 assert!(result.is_err());
953 assert!(result.unwrap_err().to_string().contains("does not match"));
954 }
955
956 #[test]
959 fn validate_tool_passes_for_attested_tool() {
960 let authority = SecretKey::generate();
961 let agent = SecretKey::generate();
962
963 let attestation = AgentAttestation::builder()
964 .agent_id(agent.public_key())
965 .code_hash(hash(b"code"))
966 .config_hash(hash(b"config"))
967 .prompt_hash(hash(b"prompt"))
968 .runtime(test_runtime())
969 .tool(test_tool())
970 .sign(&authority)
971 .unwrap();
972
973 assert!(attestation.validate_tool("read_file").is_ok());
974 }
975
976 #[test]
977 fn validate_tool_fails_for_unattested_tool() {
978 let authority = SecretKey::generate();
979 let agent = SecretKey::generate();
980
981 let attestation = AgentAttestation::builder()
982 .agent_id(agent.public_key())
983 .code_hash(hash(b"code"))
984 .config_hash(hash(b"config"))
985 .prompt_hash(hash(b"prompt"))
986 .runtime(test_runtime())
987 .tool(test_tool())
988 .sign(&authority)
989 .unwrap();
990
991 let result = attestation.validate_tool("execute_code");
992 assert!(result.is_err());
993 assert!(result.unwrap_err().to_string().contains("not found"));
994 }
995
996 #[test]
999 fn validate_for_action_passes_when_valid() {
1000 let authority = SecretKey::generate();
1001 let agent = SecretKey::generate();
1002
1003 let attestation = AgentAttestation::builder()
1004 .agent_id(agent.public_key())
1005 .code_hash(hash(b"code"))
1006 .config_hash(hash(b"config"))
1007 .prompt_hash(hash(b"prompt"))
1008 .runtime(test_runtime())
1009 .attested_at(1000)
1010 .validity_period(Duration::from_secs(3600))
1011 .sign(&authority)
1012 .unwrap();
1013
1014 assert!(attestation
1016 .validate_for_action(&agent.public_key(), 2000)
1017 .is_ok());
1018 }
1019
1020 #[test]
1021 fn validate_for_action_fails_when_expired() {
1022 let authority = SecretKey::generate();
1023 let agent = SecretKey::generate();
1024
1025 let attestation = AgentAttestation::builder()
1026 .agent_id(agent.public_key())
1027 .code_hash(hash(b"code"))
1028 .config_hash(hash(b"config"))
1029 .prompt_hash(hash(b"prompt"))
1030 .runtime(test_runtime())
1031 .attested_at(1000)
1032 .validity_period(Duration::from_secs(1))
1033 .sign(&authority)
1034 .unwrap();
1035
1036 let result = attestation.validate_for_action(&agent.public_key(), 3000);
1038 assert!(result.is_err());
1039 }
1040
1041 #[test]
1042 fn validate_for_action_fails_for_wrong_agent() {
1043 let authority = SecretKey::generate();
1044 let agent = SecretKey::generate();
1045 let other = SecretKey::generate();
1046
1047 let attestation = AgentAttestation::builder()
1048 .agent_id(agent.public_key())
1049 .code_hash(hash(b"code"))
1050 .config_hash(hash(b"config"))
1051 .prompt_hash(hash(b"prompt"))
1052 .runtime(test_runtime())
1053 .attested_at(1000)
1054 .validity_period(Duration::from_secs(3600))
1055 .sign(&authority)
1056 .unwrap();
1057
1058 let result = attestation.validate_for_action(&other.public_key(), 2000);
1059 assert!(result.is_err());
1060 }
1061}