use anyhow::{Context, Result};
use clap::Args;
use std::fs;
use std::path::PathBuf;
use assay_core::mcp::signing::{extract_signature, is_signed, verify_tool, VerifyError};
use assay_core::mcp::trust_policy::{load_public_key_pem, TrustPolicy};
#[derive(Args, Debug)]
pub struct VerifyArgs {
pub tool: PathBuf,
#[arg(long, conflicts_with = "trust_policy")]
pub pubkey: Option<PathBuf>,
#[arg(long, conflicts_with = "pubkey")]
pub trust_policy: Option<PathBuf>,
#[arg(long)]
pub allow_embedded_key: bool,
#[arg(long, short)]
pub quiet: bool,
}
pub fn cmd_verify(args: VerifyArgs) -> i32 {
match run_verify(&args) {
Ok(()) => 0,
Err(e) => {
if !args.quiet {
eprintln!("error: {e:#}");
}
if let Some(verify_err) = e.downcast_ref::<VerifyError>() {
verify_err.exit_code()
} else {
1
}
}
}
}
fn run_verify(args: &VerifyArgs) -> Result<()> {
let tool_json = fs::read_to_string(&args.tool)
.with_context(|| format!("failed to read tool file: {}", args.tool.display()))?;
let tool: serde_json::Value = serde_json::from_str(&tool_json)
.with_context(|| format!("failed to parse tool JSON: {}", args.tool.display()))?;
if !is_signed(&tool) {
if let Some(policy_path) = &args.trust_policy {
let policy = TrustPolicy::from_file(policy_path)?;
if policy.require_signed {
return Err(VerifyError::NoSignature.into());
}
}
if !args.quiet {
println!("Tool is not signed (no x-assay-sig field)");
}
return Ok(());
}
let verifying_key = if let Some(pubkey_path) = &args.pubkey {
load_public_key_pem(pubkey_path)?
} else if let Some(policy_path) = &args.trust_policy {
let policy = TrustPolicy::from_file(policy_path)?;
let sig = extract_signature(&tool).ok_or(VerifyError::NoSignature)?;
if !policy.is_key_trusted(&sig.key_id) {
return Err(VerifyError::KeyNotTrusted {
key_id: sig.key_id.clone(),
}
.into());
}
let loaded = policy.load_keys()?;
loaded
.into_iter()
.find(|k| k.key_id == sig.key_id)
.map(|k| k.key)
.ok_or_else(|| {
anyhow::anyhow!(
"key_id {} is trusted but no public_key_path provided in policy",
sig.key_id
)
})?
} else if args.allow_embedded_key {
if !args.quiet {
eprintln!("WARNING: Using embedded public key from signature.");
eprintln!(" This provides NO security guarantee - anyone can sign and embed their own key!");
eprintln!(" Use --pubkey or --trust-policy for production verification.");
eprintln!();
}
let sig = extract_signature(&tool).ok_or(VerifyError::NoSignature)?;
let pubkey_b64 = sig.public_key.ok_or_else(|| {
anyhow::anyhow!("--allow-embedded-key specified but no public_key in signature")
})?;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use ed25519_dalek::pkcs8::DecodePublicKey;
let pubkey_bytes = BASE64
.decode(&pubkey_b64)
.context("failed to decode embedded public key base64")?;
ed25519_dalek::VerifyingKey::from_public_key_der(&pubkey_bytes)
.context("failed to parse embedded public key")?
} else {
anyhow::bail!("must specify --pubkey, --trust-policy, or --allow-embedded-key");
};
let result = verify_tool(&tool, &verifying_key)?;
if !args.quiet {
println!("Verification successful!");
println!();
println!(" key_id: {}", result.key_id);
println!(" signed_at: {}", result.signed_at);
}
Ok(())
}