inherence_verifier/
payload.rs1use serde_json::Value;
4
5use crate::error::VerifyError;
6use crate::VerifyConfig;
7
8const ACCEPTED_SCHEMA_VERSIONS: &[&str] = &["inherence/1.6", "inherence/2.0"];
9const REQUIRED_TOP: &[&str] = &[
10 "iss", "sub", "aud", "iat", "exp", "jti", "schema_version", "verification", "claims",
11];
12const REQUIRED_VERIFICATION: &[&str] = &[
13 "trust_framework", "verification_process", "policy", "evidence", "attestor",
14];
15const REQUIRED_CLAIMS: &[&str] = &[
16 "service_id", "operation_type", "input_hash", "output_hash",
17];
18const CONTRACT_STATES: &[&str] = &[
19 "Quoted", "Accepted", "InProgress", "Delivered", "Verified", "Disputed", "Terminated",
20];
21
22pub fn validate_shape(p: &Value) -> Result<(), VerifyError> {
23 let obj = p.as_object().ok_or_else(|| sv("payload is not an object"))?;
24 for k in REQUIRED_TOP {
25 if !obj.contains_key(*k) {
26 return Err(sv(&format!("missing top-level {k:?}")));
27 }
28 }
29 let sv_str = obj["schema_version"].as_str().ok_or_else(|| sv("schema_version not a string"))?;
30 if !ACCEPTED_SCHEMA_VERSIONS.contains(&sv_str) {
31 return Err(sv(&format!("schema_version {sv_str:?} not in accepted set")));
32 }
33 let verif = obj["verification"].as_object().ok_or_else(|| sv("verification not an object"))?;
34 for k in REQUIRED_VERIFICATION {
35 if !verif.contains_key(*k) {
36 return Err(sv(&format!("verification missing {k:?}")));
37 }
38 }
39 let evidence = verif["evidence"].as_array().ok_or_else(|| sv("evidence not an array"))?;
40 if evidence.len() < 2 {
41 return Err(sv("evidence must have at least 2 entries"));
42 }
43 let has_pe = evidence.iter().any(|e| e.get("type").and_then(|v| v.as_str()) == Some("policy_evaluation"));
44 let has_cp = evidence.iter().any(|e| e.get("type").and_then(|v| v.as_str()) == Some("cryptographic_proof"));
45 if !has_pe { return Err(sv("evidence missing policy_evaluation")); }
46 if !has_cp { return Err(sv("evidence missing cryptographic_proof")); }
47
48 let claims = obj["claims"].as_object().ok_or_else(|| sv("claims not an object"))?;
49 for k in REQUIRED_CLAIMS {
50 if !claims.contains_key(*k) {
51 return Err(sv(&format!("claims missing {k:?}")));
52 }
53 }
54
55 if is_v2(p) {
56 let contract = verif.get("contract").and_then(|v| v.as_object())
57 .ok_or_else(|| sv("v2 receipt missing verification.contract"))?;
58 for k in &["contract_id", "contract_hash", "state", "principal_signatures", "predicate_witness"] {
59 if !contract.contains_key(*k) {
60 return Err(sv(&format!("verification.contract missing {k:?}")));
61 }
62 }
63 let state = contract["state"].as_str().unwrap_or("");
64 if !CONTRACT_STATES.contains(&state) {
65 return Err(sv(&format!("contract.state {state:?} not canonical")));
66 }
67 let witness = contract["predicate_witness"].as_object()
68 .ok_or_else(|| sv("predicate_witness not an object"))?;
69 for (k, v) in witness {
70 if !v.is_boolean() {
71 return Err(sv(&format!("predicate_witness[{k:?}] not boolean")));
72 }
73 }
74
75 let pi = verif.get("public_inputs").and_then(|v| v.as_object())
76 .ok_or_else(|| sv("v2 receipt missing verification.public_inputs"))?;
77 for k in &["action_hash", "contract_hash", "decision_bit", "vk_hash"] {
78 if !pi.contains_key(*k) {
79 return Err(sv(&format!("public_inputs missing {k:?}")));
80 }
81 }
82 let db = pi["decision_bit"].as_i64().unwrap_or(-1);
83 if db != 0 && db != 1 {
84 return Err(sv(&format!("decision_bit must be 0 or 1, got {db}")));
85 }
86 }
87
88 Ok(())
89}
90
91pub fn check_temporal(p: &Value, cfg: &VerifyConfig) -> Result<(), VerifyError> {
92 let iat = p["iat"].as_u64().ok_or_else(|| sv("iat not unsigned int"))?;
93 let exp = p["exp"].as_u64().ok_or_else(|| sv("exp not unsigned int"))?;
94 let now = cfg.now();
95 let skew = cfg.skew();
96 if now + skew < iat {
97 return Err(VerifyError::NotYetValid);
98 }
99 if exp + skew < now {
100 return Err(VerifyError::Expired);
101 }
102 Ok(())
103}
104
105pub fn check_issuer_audience(p: &Value) -> Result<(), VerifyError> {
106 let iss = p["iss"].as_str().unwrap_or("");
107 if iss != "https://inherence.dev/v1" {
108 return Err(VerifyError::WrongIssuer(iss.to_string()));
109 }
110 let aud = p["aud"].as_str().unwrap_or("");
111 if aud != "audit" {
112 return Err(VerifyError::WrongAudience(aud.to_string()));
113 }
114 Ok(())
115}
116
117pub fn is_v2(p: &Value) -> bool {
118 p.get("schema_version").and_then(|v| v.as_str())
119 .map(|s| s.starts_with("inherence/2."))
120 .unwrap_or(false)
121}
122
123pub fn check_cross_block_contract_hash(p: &Value) -> Result<(), VerifyError> {
124 let cb = &p["verification"]["contract"]["contract_hash"];
125 let pi = &p["verification"]["public_inputs"]["contract_hash"];
126 if cb != pi {
127 return Err(VerifyError::ContractHashMismatch);
128 }
129 Ok(())
130}
131
132pub fn check_decision_bit_consistency(p: &Value) -> Result<(), VerifyError> {
133 let decision = p["claims"]["agent_decision"].as_str().unwrap_or("");
134 let bit = p["verification"]["public_inputs"]["decision_bit"].as_i64().unwrap_or(-1);
135 let expected = match decision { "allow" => 1, "deny" => 0, _ => -1 };
136 if bit != expected {
137 return Err(VerifyError::DecisionBitMismatch);
138 }
139 Ok(())
140}
141
142pub fn check_state_machine(p: &Value) -> Result<(), VerifyError> {
143 let state = p["verification"]["contract"]["state"].as_str().unwrap_or("");
144 let decision = p["claims"]["agent_decision"].as_str().unwrap_or("");
145 if (state == "Terminated" || state == "Disputed") && decision == "allow" {
148 return Err(VerifyError::StateInvalid(
149 format!("decision=allow not permitted in state {state:?}")));
150 }
151 Ok(())
152}
153
154pub fn check_vk_pin(p: &Value, cfg: &VerifyConfig) -> Result<(), VerifyError> {
155 let vk = p["verification"]["public_inputs"]["vk_hash"].as_str().unwrap_or("");
156 if !cfg.knows_vk(vk) {
157 return Err(VerifyError::UnknownVk(vk.to_string()));
158 }
159 Ok(())
160}
161
162#[cfg(feature = "eip712")]
163pub fn check_principal_signatures(p: &Value) -> Result<(), VerifyError> {
164 use crate::eip712;
165 let contract = &p["verification"]["contract"];
166 let contract_hash_hex = contract["contract_hash"].as_str().unwrap_or("");
167 let sigs = &contract["principal_signatures"];
168 for &role in &["principal", "agent"] {
169 let did = sigs[role]["did"].as_str().unwrap_or("");
170 let sig_hex = sigs[role]["signature"].as_str().unwrap_or("");
171 match eip712::recover_address(contract_hash_hex, sig_hex) {
172 Ok(recovered) => {
173 let expected = eip712::address_from_did_ethr(did);
174 if expected.eq_ignore_ascii_case(&recovered) {
175 continue;
176 }
177 return Err(VerifyError::PrincipalSignatureInvalid(role_str(role)));
178 }
179 Err(_) => return Err(VerifyError::PrincipalSignatureInvalid(role_str(role))),
180 }
181 }
182 Ok(())
183}
184
185#[cfg(feature = "eip712")]
186fn role_str(role: &str) -> &'static str {
187 match role {
188 "principal" => "principal",
189 "agent" => "agent",
190 _ => "principal",
191 }
192}
193
194fn sv(msg: &str) -> VerifyError {
195 VerifyError::SchemaViolation(msg.to_string())
196}