inherence_verifier/
lib.rs1pub mod binding;
25pub mod error;
26pub mod jwt;
27pub mod payload;
28pub mod proof;
29#[cfg(feature = "eip712")]
30pub mod eip712;
31
32pub use error::VerifyError;
33
34use std::collections::{HashMap, HashSet};
35use std::time::{SystemTime, UNIX_EPOCH};
36
37use serde_json::Value;
38
39#[derive(Default, Debug, Clone)]
43pub struct VerifyConfig {
44 authority_jwks: HashMap<String, Vec<u8>>,
48 pinned_vks: HashMap<String, Vec<u8>>,
51 now_unix: Option<u64>,
53 clock_skew_secs: u64,
55}
56
57impl VerifyConfig {
58 pub fn new() -> Self {
59 Self { clock_skew_secs: 30, ..Default::default() }
60 }
61
62 pub fn pin_authority_jwk(mut self, jwk_json: &str) -> Result<Self, VerifyError> {
65 let v: Value = serde_json::from_str(jwk_json)
66 .map_err(|_| VerifyError::SchemaViolation("authority jwk is not valid JSON".into()))?;
67 let kty = v.get("kty").and_then(|v| v.as_str()).unwrap_or("");
68 let crv = v.get("crv").and_then(|v| v.as_str()).unwrap_or("");
69 if kty != "OKP" || crv != "Ed25519" {
70 return Err(VerifyError::SchemaViolation(
71 format!("authority jwk: only OKP/Ed25519 accepted, got {}/{}", kty, crv)));
72 }
73 let x = v.get("x").and_then(|v| v.as_str()).ok_or_else(||
74 VerifyError::SchemaViolation("authority jwk missing 'x'".into()))?;
75 let pubkey = base64_url_decode(x)
76 .map_err(|_| VerifyError::SchemaViolation("authority jwk 'x' not base64url".into()))?;
77 if pubkey.len() != 32 {
78 return Err(VerifyError::SchemaViolation(
79 format!("authority jwk public key must be 32 bytes, got {}", pubkey.len())));
80 }
81 let kid = jwt::kid_for_pubkey(&pubkey);
82 self.authority_jwks.insert(kid, pubkey);
83 Ok(self)
84 }
85
86 pub fn pin_vk(mut self, vk_hash_hex: &str, vk_bytes: Vec<u8>) -> Self {
90 let key = vk_hash_hex.trim_start_matches("0x").to_ascii_lowercase();
91 self.pinned_vks.insert(key, vk_bytes);
92 self
93 }
94
95 pub fn at_time(mut self, unix_secs: u64) -> Self {
97 self.now_unix = Some(unix_secs);
98 self
99 }
100
101 pub fn with_clock_skew(mut self, secs: u64) -> Self {
104 self.clock_skew_secs = secs;
105 self
106 }
107
108 pub fn knows_vk(&self, vk_hash_hex: &str) -> bool {
110 let key = vk_hash_hex.trim_start_matches("0x").to_ascii_lowercase();
111 self.pinned_vks.contains_key(&key)
112 }
113
114 pub(crate) fn lookup_vk(&self, vk_hash_hex: &str) -> Option<&[u8]> {
115 let key = vk_hash_hex.trim_start_matches("0x").to_ascii_lowercase();
116 self.pinned_vks.get(&key).map(|v| v.as_slice())
117 }
118
119 pub(crate) fn lookup_authority(&self, kid: &str) -> Option<&[u8]> {
120 self.authority_jwks.get(kid).map(|v| v.as_slice())
121 }
122
123 pub(crate) fn known_kids(&self) -> HashSet<String> {
124 self.authority_jwks.keys().cloned().collect()
125 }
126
127 pub(crate) fn now(&self) -> u64 {
128 self.now_unix.unwrap_or_else(|| {
129 SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_secs()
130 })
131 }
132
133 pub(crate) fn skew(&self) -> u64 { self.clock_skew_secs }
134}
135
136pub fn verify_receipt(jwt: &str, cfg: &VerifyConfig) -> Result<(), VerifyError> {
139 let parsed = jwt::parse_and_verify(jwt, cfg)?;
141 payload::validate_shape(&parsed.payload)?;
143 payload::check_temporal(&parsed.payload, cfg)?;
145 payload::check_issuer_audience(&parsed.payload)?;
147
148 if payload::is_v2(&parsed.payload) {
150 payload::check_cross_block_contract_hash(&parsed.payload)?; payload::check_decision_bit_consistency(&parsed.payload)?; payload::check_state_machine(&parsed.payload)?; payload::check_vk_pin(&parsed.payload, cfg)?; #[cfg(feature = "eip712")]
155 payload::check_principal_signatures(&parsed.payload)?; proof::verify_if_present(&parsed.payload, cfg)?; }
158
159 Ok(())
160}
161
162pub(crate) fn base64_url_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
165 use base64::Engine;
166 base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s)
167}
168
169pub(crate) fn base64_std_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
170 use base64::Engine;
171 base64::engine::general_purpose::STANDARD.decode(s)
172}