mur_common/muragent/
validator.rs1use crate::muragent::MuragentError;
6use crate::muragent::dsse;
7use crate::muragent::executable_ban;
8use crate::muragent::jcs_canonical;
9use crate::muragent::manifest::MuragentManifest;
10use crate::muragent::reader::MuragentArchive;
11use crate::muragent::statement::{InTotoStatement, verify_subjects};
12
13pub struct ValidationResult {
14 pub manifest: MuragentManifest,
15 pub author_pubkey: [u8; 32],
16 pub keyid: String,
17}
18
19impl std::fmt::Debug for ValidationResult {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 f.debug_struct("ValidationResult")
22 .field("manifest_schema", &self.manifest.schema)
23 .field("agent_slug", &self.manifest.agent.slug)
24 .field("keyid", &self.keyid)
25 .finish()
26 }
27}
28
29pub fn validate(archive: &MuragentArchive) -> Result<ValidationResult, MuragentError> {
31 for path in archive.files.keys() {
35 executable_ban::check_extension(path).map_err(MuragentError::ExecutableContent)?;
36 }
37 let manifest_yaml = archive.get_str("manifest.yaml")?;
38 let manifest: MuragentManifest = serde_yaml_ng::from_str(manifest_yaml)
39 .map_err(|e| MuragentError::ManifestParse(e.to_string()))?;
40 for mcp in &manifest.mcp_servers {
41 executable_ban::check_mcp_command(&mcp.command_basename, &[])
42 .map_err(MuragentError::ForbiddenMcpCommand)?;
43 }
44
45 if !manifest.schema_supported() {
47 return Err(MuragentError::SchemaMismatch(manifest.schema.clone()));
48 }
49 let schema_requires_v3 = manifest.schema == "mur-agent/3";
50 if schema_requires_v3 != manifest.requires_v3() {
51 return Err(MuragentError::ManifestParse(
52 "mur-agent/3 requires model_requirements, and model_requirements require mur-agent/3"
53 .into(),
54 ));
55 }
56 if manifest.requires_v3() && manifest.model_hint.is_some() {
57 return Err(MuragentError::ManifestParse(
58 "model_requirements and model_hint are mutually exclusive".into(),
59 ));
60 }
61
62 manifest
64 .validate_bundle_id()
65 .map_err(MuragentError::ManifestParse)?;
66
67 let embedded_signed_json = archive
71 .get("manifest.signed.json")
72 .ok_or_else(|| MuragentError::Other("missing manifest.signed.json".into()))?;
73 let rederived = jcs_canonical::derive_signed_json(manifest_yaml)?;
74 if embedded_signed_json != rederived.as_slice() {
75 return Err(MuragentError::SignedJsonMismatch);
76 }
77
78 let signatures_json = archive.get_str("signatures.json")?;
80 let envelope: dsse::DsseEnvelope = serde_json::from_str(signatures_json)
81 .map_err(|e| MuragentError::DsseError(format!("signatures.json parse: {e}")))?;
82
83 use base64::{Engine, engine::general_purpose::STANDARD as B64};
85 let payload_bytes = B64
86 .decode(&envelope.payload)
87 .map_err(|e| MuragentError::DsseError(format!("payload base64: {e}")))?;
88 let statement: InTotoStatement = serde_json::from_slice(&payload_bytes)
89 .map_err(|e| MuragentError::DsseError(format!("statement parse: {e}")))?;
90
91 if statement.type_ != "https://in-toto.io/Statement/v1" {
92 return Err(MuragentError::DsseError(format!(
93 "unexpected statement _type: {}",
94 statement.type_
95 )));
96 }
97 if statement.predicate_type != "https://mur.run/agent-manifest/v1" {
98 return Err(MuragentError::DsseError(format!(
99 "unexpected predicateType: {}",
100 statement.predicate_type
101 )));
102 }
103
104 let actual_manifest_sha256 = {
105 use sha2::Digest;
106 hex::encode(sha2::Sha256::digest(embedded_signed_json))
107 };
108 if statement.predicate.manifest_sha256 != actual_manifest_sha256 {
109 return Err(MuragentError::DsseError(format!(
110 "manifest_sha256 mismatch: expected {}, got {}",
111 actual_manifest_sha256, statement.predicate.manifest_sha256
112 )));
113 }
114
115 dsse::verify(&envelope, "application/vnd.in-toto+json")?;
117
118 verify_subjects(&statement, &archive.files_as_vec())?;
120
121 let pubkey_bytes = B64
125 .decode(&envelope.signatures[0].public_key)
126 .map_err(|e| MuragentError::DsseError(format!("pubkey b64: {e}")))?;
127 let pubkey_arr: [u8; 32] = pubkey_bytes
128 .try_into()
129 .map_err(|_| MuragentError::DsseError("pubkey not 32 bytes".into()))?;
130
131 Ok(ValidationResult {
132 manifest,
133 author_pubkey: pubkey_arr,
134 keyid: envelope.signatures[0].keyid.clone(),
135 })
136}