Skip to main content

inherence_verifier/
lib.rs

1//! Inherence receipt verifier — reference implementation of
2//! verification protocol v1 (see `spec/receipts/v1/SPEC.md`).
3//!
4//! This crate is **a verifier only**. It contains no policy-
5//! compilation, no proving, no circuit-construction logic. Those
6//! live behind the hosted gate and are not needed (and not
7//! available) to verify a receipt.
8//!
9//! Usage:
10//!
11//! ```ignore
12//! use inherence_verifier::{verify_receipt, VerifyConfig};
13//!
14//! let cfg = VerifyConfig::new()
15//!     .pin_authority_jwk(authority_jwk_json)
16//!     .pin_vk(vk_hash_hex, vk_bytes);
17//!
18//! match verify_receipt(jwt_str, &cfg) {
19//!     Ok(()) => println!("VALID"),
20//!     Err(e) => println!("INVALID: {}", e),
21//! }
22//! ```
23
24pub 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/// Pinned trust roots the verifier consults. Constructed by the
40/// integrator; the verifier itself never fetches keys from the
41/// network.
42#[derive(Default, Debug, Clone)]
43pub struct VerifyConfig {
44    /// JWKs (Ed25519) acceptable as authority signers, keyed by
45    /// the `kid` derived from the public bytes (per SDK convention,
46    /// first 16 hex chars of sha256(pubkey)).
47    authority_jwks: HashMap<String, Vec<u8>>,
48    /// VK bytes (arkworks `VerifyingKey<Bn254>::serialize_compressed`)
49    /// keyed by `vk_hash` (sha256 of the bytes, lowercase hex w/o 0x).
50    pinned_vks: HashMap<String, Vec<u8>>,
51    /// Wall clock override for tests.
52    now_unix: Option<u64>,
53    /// Tolerance (seconds) around iat/exp for clock skew.
54    clock_skew_secs: u64,
55}
56
57impl VerifyConfig {
58    pub fn new() -> Self {
59        Self { clock_skew_secs: 30, ..Default::default() }
60    }
61
62    /// Add an authority JWK (Ed25519, RFC 8037 form). The verifier
63    /// computes `kid = sha256(pubkey)[..16].hex()` and indexes by it.
64    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    /// Pin a verifying key by its content hash. `vk_hash_hex` MAY
87    /// have an optional `0x` prefix; it is normalized to lowercase
88    /// hex without prefix.
89    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    /// Override the wall clock for tests + fixture replay.
96    pub fn at_time(mut self, unix_secs: u64) -> Self {
97        self.now_unix = Some(unix_secs);
98        self
99    }
100
101    /// Allow more clock skew than the default 30s (e.g. for batch
102    /// replay of historical receipts).
103    pub fn with_clock_skew(mut self, secs: u64) -> Self {
104        self.clock_skew_secs = secs;
105        self
106    }
107
108    /// True iff this vk_hash is pinned. `vk_hash_hex` may include `0x`.
109    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
136/// Verify a receipt. Returns `Ok(())` for VALID, `Err(VerifyError)`
137/// for INVALID with the failure code attached.
138pub fn verify_receipt(jwt: &str, cfg: &VerifyConfig) -> Result<(), VerifyError> {
139    // §3 step 1+2: parse + verify EdDSA signature
140    let parsed = jwt::parse_and_verify(jwt, cfg)?;
141    // §3 step 3: shape validation
142    payload::validate_shape(&parsed.payload)?;
143    // §3 step 4: temporal claims
144    payload::check_temporal(&parsed.payload, cfg)?;
145    // §3 step 5: iss / aud
146    payload::check_issuer_audience(&parsed.payload)?;
147
148    // §6 — v2-only checks
149    if payload::is_v2(&parsed.payload) {
150        payload::check_cross_block_contract_hash(&parsed.payload)?;     // §6.5
151        payload::check_decision_bit_consistency(&parsed.payload)?;      // §6.3
152        payload::check_state_machine(&parsed.payload)?;                 // §6.2
153        payload::check_vk_pin(&parsed.payload, cfg)?;                   // §6.4
154        #[cfg(feature = "eip712")]
155        payload::check_principal_signatures(&parsed.payload)?;          // §6.1 (optional; needs envelope)
156        proof::verify_if_present(&parsed.payload, cfg)?;                // §6.6
157    }
158
159    Ok(())
160}
161
162// ─── Helpers shared across modules ─────────────────────────────────────
163
164pub(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}