inherence-verifier 0.1.0

Reference verifier for Inherence receipts (verification protocol v1).
Documentation
//! Replay the 22 verification test vectors from
//! `inherence-python/spec/receipts/v1/FIXTURES.json` and assert
//! the verdict + failure code matches `expected_verdict`.
//!
//! This is the conformance suite. A change to it requires a
//! corresponding change in SPEC.md and FIXTURES.json.

use base64::Engine;
use inherence_verifier::{verify_receipt, VerifyConfig};
use serde_json::Value;

const FIXTURES_PATH: &str =
    concat!(env!("CARGO_MANIFEST_DIR"), "/../inherence-python/spec/receipts/v1/FIXTURES.json");

const TEST_IAT: u64 = 1779177600;

fn load_fixtures() -> Value {
    let s = std::fs::read_to_string(FIXTURES_PATH)
        .expect("FIXTURES.json must exist alongside inherence-python");
    serde_json::from_str(&s).expect("FIXTURES.json is JSON")
}

fn build_cfg(fixtures: &Value) -> VerifyConfig {
    let jwk = &fixtures["test_authority"]["jwk"];
    let mut cfg = VerifyConfig::new()
        .at_time(TEST_IAT + 60)  // 60s into the receipt validity window
        .pin_authority_jwk(&serde_json::to_string(jwk).unwrap())
        .expect("pin authority jwk");
    // Pin the vk used by V05 / V22.
    if let Some(vk_b64) = fixtures["proof_artifacts"]["vk_b64"].as_str() {
        let vk_bytes = base64::engine::general_purpose::STANDARD
            .decode(vk_b64).expect("vk_b64 decodes");
        let vk_hash = fixtures["proof_artifacts"]["vk_sha256"].as_str().unwrap();
        cfg = cfg.pin_vk(vk_hash, vk_bytes);
    }
    // Also pin the placeholder vk (0x55…55) used by the non-proof V01–V04 fixtures.
    cfg = cfg.pin_vk(&"55".repeat(32), vec![0u8; 0]);
    cfg
}

#[test]
fn all_vectors_match_expected_verdict() {
    let fixtures = load_fixtures();
    let cfg = build_cfg(&fixtures);
    let vectors = fixtures["vectors"].as_array().expect("vectors array");
    let mut report: Vec<(String, String, String, bool)> = vec![];
    let mut all_ok = true;
    for v in vectors {
        let slug = v["slug"].as_str().unwrap_or("?").to_string();
        let expected = v["expected_verdict"].as_str().unwrap_or("?").to_string();
        let jwt = v["receipt_jwt"].as_str().unwrap_or("");
        if jwt == "<skipped>" {
            report.push((slug, expected, "<skipped>".into(), true));
            continue;
        }
        let actual = match verify_receipt(jwt, &cfg) {
            Ok(()) => "VALID".to_string(),
            Err(e) => format!("INVALID: {}", e.code()),
        };
        let ok = matches_expected(&expected, &actual);
        if !ok { all_ok = false; }
        report.push((slug, expected, actual, ok));
    }
    println!("\n=== Verifier conformance — {} vectors ===", report.len());
    println!("{:<48} {:<48} {}", "slug", "expected", "actual");
    for (slug, exp, act, ok) in &report {
        println!("{}{:<48} {:<48} {}", if *ok { "  " } else { "" }, slug, exp, act);
    }
    assert!(all_ok, "some vectors disagree with expected verdict");
}

fn matches_expected(expected: &str, actual: &str) -> bool {
    if expected == actual { return true; }
    // V19 has two acceptable verdicts (strict vs base implementation —
    // we ship as strict). Accept either as long as our actual matches
    // one of the listed.
    if expected.contains("state_invalid") && actual == "INVALID: state_invalid" { return true; }
    if expected.contains("VALID") && actual == "VALID" { return true; }
    false
}