use crate::muragent::MuragentError;
use crate::muragent::dsse;
use crate::muragent::executable_ban;
use crate::muragent::jcs_canonical;
use crate::muragent::manifest::MuragentManifest;
use crate::muragent::reader::MuragentArchive;
use crate::muragent::statement::{InTotoStatement, verify_subjects};
pub struct ValidationResult {
pub manifest: MuragentManifest,
pub author_pubkey: [u8; 32],
pub keyid: String,
}
impl std::fmt::Debug for ValidationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ValidationResult")
.field("manifest_schema", &self.manifest.schema)
.field("agent_slug", &self.manifest.agent.slug)
.field("keyid", &self.keyid)
.finish()
}
}
pub fn validate(archive: &MuragentArchive) -> Result<ValidationResult, MuragentError> {
for path in archive.files.keys() {
executable_ban::check_extension(path).map_err(MuragentError::ExecutableContent)?;
}
let manifest_yaml = archive.get_str("manifest.yaml")?;
let manifest: MuragentManifest = serde_yaml_ng::from_str(manifest_yaml)
.map_err(|e| MuragentError::ManifestParse(e.to_string()))?;
for mcp in &manifest.mcp_servers {
executable_ban::check_mcp_command(&mcp.command_basename, &[])
.map_err(MuragentError::ForbiddenMcpCommand)?;
}
if !manifest.is_v2() {
return Err(MuragentError::SchemaMismatch(manifest.schema.clone()));
}
manifest
.validate_bundle_id()
.map_err(MuragentError::ManifestParse)?;
let embedded_signed_json = archive
.get("manifest.signed.json")
.ok_or_else(|| MuragentError::Other("missing manifest.signed.json".into()))?;
let rederived = jcs_canonical::derive_signed_json(manifest_yaml)?;
if embedded_signed_json != rederived.as_slice() {
return Err(MuragentError::SignedJsonMismatch);
}
let signatures_json = archive.get_str("signatures.json")?;
let envelope: dsse::DsseEnvelope = serde_json::from_str(signatures_json)
.map_err(|e| MuragentError::DsseError(format!("signatures.json parse: {e}")))?;
use base64::{Engine, engine::general_purpose::STANDARD as B64};
let payload_bytes = B64
.decode(&envelope.payload)
.map_err(|e| MuragentError::DsseError(format!("payload base64: {e}")))?;
let statement: InTotoStatement = serde_json::from_slice(&payload_bytes)
.map_err(|e| MuragentError::DsseError(format!("statement parse: {e}")))?;
if statement.type_ != "https://in-toto.io/Statement/v1" {
return Err(MuragentError::DsseError(format!(
"unexpected statement _type: {}",
statement.type_
)));
}
if statement.predicate_type != "https://mur.run/agent-manifest/v1" {
return Err(MuragentError::DsseError(format!(
"unexpected predicateType: {}",
statement.predicate_type
)));
}
let actual_manifest_sha256 = {
use sha2::Digest;
hex::encode(sha2::Sha256::digest(embedded_signed_json))
};
if statement.predicate.manifest_sha256 != actual_manifest_sha256 {
return Err(MuragentError::DsseError(format!(
"manifest_sha256 mismatch: expected {}, got {}",
actual_manifest_sha256, statement.predicate.manifest_sha256
)));
}
dsse::verify(&envelope, "application/vnd.in-toto+json")?;
verify_subjects(&statement, &archive.files_as_vec())?;
let pubkey_bytes = B64
.decode(&envelope.signatures[0].public_key)
.map_err(|e| MuragentError::DsseError(format!("pubkey b64: {e}")))?;
let pubkey_arr: [u8; 32] = pubkey_bytes
.try_into()
.map_err(|_| MuragentError::DsseError("pubkey not 32 bytes".into()))?;
Ok(ValidationResult {
manifest,
author_pubkey: pubkey_arr,
keyid: envelope.signatures[0].keyid.clone(),
})
}