inherence-verifier 0.1.0

Reference verifier for Inherence receipts (verification protocol v1).
Documentation
//! Inherence receipt verifier — reference implementation of
//! verification protocol v1 (see `spec/receipts/v1/SPEC.md`).
//!
//! This crate is **a verifier only**. It contains no policy-
//! compilation, no proving, no circuit-construction logic. Those
//! live behind the hosted gate and are not needed (and not
//! available) to verify a receipt.
//!
//! Usage:
//!
//! ```ignore
//! use inherence_verifier::{verify_receipt, VerifyConfig};
//!
//! let cfg = VerifyConfig::new()
//!     .pin_authority_jwk(authority_jwk_json)
//!     .pin_vk(vk_hash_hex, vk_bytes);
//!
//! match verify_receipt(jwt_str, &cfg) {
//!     Ok(()) => println!("VALID"),
//!     Err(e) => println!("INVALID: {}", e),
//! }
//! ```

pub mod binding;
pub mod error;
pub mod jwt;
pub mod payload;
pub mod proof;
#[cfg(feature = "eip712")]
pub mod eip712;

pub use error::VerifyError;

use std::collections::{HashMap, HashSet};
use std::time::{SystemTime, UNIX_EPOCH};

use serde_json::Value;

/// Pinned trust roots the verifier consults. Constructed by the
/// integrator; the verifier itself never fetches keys from the
/// network.
#[derive(Default, Debug, Clone)]
pub struct VerifyConfig {
    /// JWKs (Ed25519) acceptable as authority signers, keyed by
    /// the `kid` derived from the public bytes (per SDK convention,
    /// first 16 hex chars of sha256(pubkey)).
    authority_jwks: HashMap<String, Vec<u8>>,
    /// VK bytes (arkworks `VerifyingKey<Bn254>::serialize_compressed`)
    /// keyed by `vk_hash` (sha256 of the bytes, lowercase hex w/o 0x).
    pinned_vks: HashMap<String, Vec<u8>>,
    /// Wall clock override for tests.
    now_unix: Option<u64>,
    /// Tolerance (seconds) around iat/exp for clock skew.
    clock_skew_secs: u64,
}

impl VerifyConfig {
    pub fn new() -> Self {
        Self { clock_skew_secs: 30, ..Default::default() }
    }

    /// Add an authority JWK (Ed25519, RFC 8037 form). The verifier
    /// computes `kid = sha256(pubkey)[..16].hex()` and indexes by it.
    pub fn pin_authority_jwk(mut self, jwk_json: &str) -> Result<Self, VerifyError> {
        let v: Value = serde_json::from_str(jwk_json)
            .map_err(|_| VerifyError::SchemaViolation("authority jwk is not valid JSON".into()))?;
        let kty = v.get("kty").and_then(|v| v.as_str()).unwrap_or("");
        let crv = v.get("crv").and_then(|v| v.as_str()).unwrap_or("");
        if kty != "OKP" || crv != "Ed25519" {
            return Err(VerifyError::SchemaViolation(
                format!("authority jwk: only OKP/Ed25519 accepted, got {}/{}", kty, crv)));
        }
        let x = v.get("x").and_then(|v| v.as_str()).ok_or_else(||
            VerifyError::SchemaViolation("authority jwk missing 'x'".into()))?;
        let pubkey = base64_url_decode(x)
            .map_err(|_| VerifyError::SchemaViolation("authority jwk 'x' not base64url".into()))?;
        if pubkey.len() != 32 {
            return Err(VerifyError::SchemaViolation(
                format!("authority jwk public key must be 32 bytes, got {}", pubkey.len())));
        }
        let kid = jwt::kid_for_pubkey(&pubkey);
        self.authority_jwks.insert(kid, pubkey);
        Ok(self)
    }

    /// Pin a verifying key by its content hash. `vk_hash_hex` MAY
    /// have an optional `0x` prefix; it is normalized to lowercase
    /// hex without prefix.
    pub fn pin_vk(mut self, vk_hash_hex: &str, vk_bytes: Vec<u8>) -> Self {
        let key = vk_hash_hex.trim_start_matches("0x").to_ascii_lowercase();
        self.pinned_vks.insert(key, vk_bytes);
        self
    }

    /// Override the wall clock for tests + fixture replay.
    pub fn at_time(mut self, unix_secs: u64) -> Self {
        self.now_unix = Some(unix_secs);
        self
    }

    /// Allow more clock skew than the default 30s (e.g. for batch
    /// replay of historical receipts).
    pub fn with_clock_skew(mut self, secs: u64) -> Self {
        self.clock_skew_secs = secs;
        self
    }

    /// True iff this vk_hash is pinned. `vk_hash_hex` may include `0x`.
    pub fn knows_vk(&self, vk_hash_hex: &str) -> bool {
        let key = vk_hash_hex.trim_start_matches("0x").to_ascii_lowercase();
        self.pinned_vks.contains_key(&key)
    }

    pub(crate) fn lookup_vk(&self, vk_hash_hex: &str) -> Option<&[u8]> {
        let key = vk_hash_hex.trim_start_matches("0x").to_ascii_lowercase();
        self.pinned_vks.get(&key).map(|v| v.as_slice())
    }

    pub(crate) fn lookup_authority(&self, kid: &str) -> Option<&[u8]> {
        self.authority_jwks.get(kid).map(|v| v.as_slice())
    }

    pub(crate) fn known_kids(&self) -> HashSet<String> {
        self.authority_jwks.keys().cloned().collect()
    }

    pub(crate) fn now(&self) -> u64 {
        self.now_unix.unwrap_or_else(|| {
            SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_secs()
        })
    }

    pub(crate) fn skew(&self) -> u64 { self.clock_skew_secs }
}

/// Verify a receipt. Returns `Ok(())` for VALID, `Err(VerifyError)`
/// for INVALID with the failure code attached.
pub fn verify_receipt(jwt: &str, cfg: &VerifyConfig) -> Result<(), VerifyError> {
    // §3 step 1+2: parse + verify EdDSA signature
    let parsed = jwt::parse_and_verify(jwt, cfg)?;
    // §3 step 3: shape validation
    payload::validate_shape(&parsed.payload)?;
    // §3 step 4: temporal claims
    payload::check_temporal(&parsed.payload, cfg)?;
    // §3 step 5: iss / aud
    payload::check_issuer_audience(&parsed.payload)?;

    // §6 — v2-only checks
    if payload::is_v2(&parsed.payload) {
        payload::check_cross_block_contract_hash(&parsed.payload)?;     // §6.5
        payload::check_decision_bit_consistency(&parsed.payload)?;      // §6.3
        payload::check_state_machine(&parsed.payload)?;                 // §6.2
        payload::check_vk_pin(&parsed.payload, cfg)?;                   // §6.4
        #[cfg(feature = "eip712")]
        payload::check_principal_signatures(&parsed.payload)?;          // §6.1 (optional; needs envelope)
        proof::verify_if_present(&parsed.payload, cfg)?;                // §6.6
    }

    Ok(())
}

// ─── Helpers shared across modules ─────────────────────────────────────

pub(crate) fn base64_url_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
    use base64::Engine;
    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s)
}

pub(crate) fn base64_std_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
    use base64::Engine;
    base64::engine::general_purpose::STANDARD.decode(s)
}