use serde_json::Value;
use crate::error::VerifyError;
use crate::VerifyConfig;
const ACCEPTED_SCHEMA_VERSIONS: &[&str] = &["inherence/1.6", "inherence/2.0"];
const REQUIRED_TOP: &[&str] = &[
"iss", "sub", "aud", "iat", "exp", "jti", "schema_version", "verification", "claims",
];
const REQUIRED_VERIFICATION: &[&str] = &[
"trust_framework", "verification_process", "policy", "evidence", "attestor",
];
const REQUIRED_CLAIMS: &[&str] = &[
"service_id", "operation_type", "input_hash", "output_hash",
];
const CONTRACT_STATES: &[&str] = &[
"Quoted", "Accepted", "InProgress", "Delivered", "Verified", "Disputed", "Terminated",
];
pub fn validate_shape(p: &Value) -> Result<(), VerifyError> {
let obj = p.as_object().ok_or_else(|| sv("payload is not an object"))?;
for k in REQUIRED_TOP {
if !obj.contains_key(*k) {
return Err(sv(&format!("missing top-level {k:?}")));
}
}
let sv_str = obj["schema_version"].as_str().ok_or_else(|| sv("schema_version not a string"))?;
if !ACCEPTED_SCHEMA_VERSIONS.contains(&sv_str) {
return Err(sv(&format!("schema_version {sv_str:?} not in accepted set")));
}
let verif = obj["verification"].as_object().ok_or_else(|| sv("verification not an object"))?;
for k in REQUIRED_VERIFICATION {
if !verif.contains_key(*k) {
return Err(sv(&format!("verification missing {k:?}")));
}
}
let evidence = verif["evidence"].as_array().ok_or_else(|| sv("evidence not an array"))?;
if evidence.len() < 2 {
return Err(sv("evidence must have at least 2 entries"));
}
let has_pe = evidence.iter().any(|e| e.get("type").and_then(|v| v.as_str()) == Some("policy_evaluation"));
let has_cp = evidence.iter().any(|e| e.get("type").and_then(|v| v.as_str()) == Some("cryptographic_proof"));
if !has_pe { return Err(sv("evidence missing policy_evaluation")); }
if !has_cp { return Err(sv("evidence missing cryptographic_proof")); }
let claims = obj["claims"].as_object().ok_or_else(|| sv("claims not an object"))?;
for k in REQUIRED_CLAIMS {
if !claims.contains_key(*k) {
return Err(sv(&format!("claims missing {k:?}")));
}
}
if is_v2(p) {
let contract = verif.get("contract").and_then(|v| v.as_object())
.ok_or_else(|| sv("v2 receipt missing verification.contract"))?;
for k in &["contract_id", "contract_hash", "state", "principal_signatures", "predicate_witness"] {
if !contract.contains_key(*k) {
return Err(sv(&format!("verification.contract missing {k:?}")));
}
}
let state = contract["state"].as_str().unwrap_or("");
if !CONTRACT_STATES.contains(&state) {
return Err(sv(&format!("contract.state {state:?} not canonical")));
}
let witness = contract["predicate_witness"].as_object()
.ok_or_else(|| sv("predicate_witness not an object"))?;
for (k, v) in witness {
if !v.is_boolean() {
return Err(sv(&format!("predicate_witness[{k:?}] not boolean")));
}
}
let pi = verif.get("public_inputs").and_then(|v| v.as_object())
.ok_or_else(|| sv("v2 receipt missing verification.public_inputs"))?;
for k in &["action_hash", "contract_hash", "decision_bit", "vk_hash"] {
if !pi.contains_key(*k) {
return Err(sv(&format!("public_inputs missing {k:?}")));
}
}
let db = pi["decision_bit"].as_i64().unwrap_or(-1);
if db != 0 && db != 1 {
return Err(sv(&format!("decision_bit must be 0 or 1, got {db}")));
}
}
Ok(())
}
pub fn check_temporal(p: &Value, cfg: &VerifyConfig) -> Result<(), VerifyError> {
let iat = p["iat"].as_u64().ok_or_else(|| sv("iat not unsigned int"))?;
let exp = p["exp"].as_u64().ok_or_else(|| sv("exp not unsigned int"))?;
let now = cfg.now();
let skew = cfg.skew();
if now + skew < iat {
return Err(VerifyError::NotYetValid);
}
if exp + skew < now {
return Err(VerifyError::Expired);
}
Ok(())
}
pub fn check_issuer_audience(p: &Value) -> Result<(), VerifyError> {
let iss = p["iss"].as_str().unwrap_or("");
if iss != "https://inherence.dev/v1" {
return Err(VerifyError::WrongIssuer(iss.to_string()));
}
let aud = p["aud"].as_str().unwrap_or("");
if aud != "audit" {
return Err(VerifyError::WrongAudience(aud.to_string()));
}
Ok(())
}
pub fn is_v2(p: &Value) -> bool {
p.get("schema_version").and_then(|v| v.as_str())
.map(|s| s.starts_with("inherence/2."))
.unwrap_or(false)
}
pub fn check_cross_block_contract_hash(p: &Value) -> Result<(), VerifyError> {
let cb = &p["verification"]["contract"]["contract_hash"];
let pi = &p["verification"]["public_inputs"]["contract_hash"];
if cb != pi {
return Err(VerifyError::ContractHashMismatch);
}
Ok(())
}
pub fn check_decision_bit_consistency(p: &Value) -> Result<(), VerifyError> {
let decision = p["claims"]["agent_decision"].as_str().unwrap_or("");
let bit = p["verification"]["public_inputs"]["decision_bit"].as_i64().unwrap_or(-1);
let expected = match decision { "allow" => 1, "deny" => 0, _ => -1 };
if bit != expected {
return Err(VerifyError::DecisionBitMismatch);
}
Ok(())
}
pub fn check_state_machine(p: &Value) -> Result<(), VerifyError> {
let state = p["verification"]["contract"]["state"].as_str().unwrap_or("");
let decision = p["claims"]["agent_decision"].as_str().unwrap_or("");
if (state == "Terminated" || state == "Disputed") && decision == "allow" {
return Err(VerifyError::StateInvalid(
format!("decision=allow not permitted in state {state:?}")));
}
Ok(())
}
pub fn check_vk_pin(p: &Value, cfg: &VerifyConfig) -> Result<(), VerifyError> {
let vk = p["verification"]["public_inputs"]["vk_hash"].as_str().unwrap_or("");
if !cfg.knows_vk(vk) {
return Err(VerifyError::UnknownVk(vk.to_string()));
}
Ok(())
}
#[cfg(feature = "eip712")]
pub fn check_principal_signatures(p: &Value) -> Result<(), VerifyError> {
use crate::eip712;
let contract = &p["verification"]["contract"];
let contract_hash_hex = contract["contract_hash"].as_str().unwrap_or("");
let sigs = &contract["principal_signatures"];
for &role in &["principal", "agent"] {
let did = sigs[role]["did"].as_str().unwrap_or("");
let sig_hex = sigs[role]["signature"].as_str().unwrap_or("");
match eip712::recover_address(contract_hash_hex, sig_hex) {
Ok(recovered) => {
let expected = eip712::address_from_did_ethr(did);
if expected.eq_ignore_ascii_case(&recovered) {
continue;
}
return Err(VerifyError::PrincipalSignatureInvalid(role_str(role)));
}
Err(_) => return Err(VerifyError::PrincipalSignatureInvalid(role_str(role))),
}
}
Ok(())
}
#[cfg(feature = "eip712")]
fn role_str(role: &str) -> &'static str {
match role {
"principal" => "principal",
"agent" => "agent",
_ => "principal",
}
}
fn sv(msg: &str) -> VerifyError {
VerifyError::SchemaViolation(msg.to_string())
}