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;
#[derive(Default, Debug, Clone)]
pub struct VerifyConfig {
authority_jwks: HashMap<String, Vec<u8>>,
pinned_vks: HashMap<String, Vec<u8>>,
now_unix: Option<u64>,
clock_skew_secs: u64,
}
impl VerifyConfig {
pub fn new() -> Self {
Self { clock_skew_secs: 30, ..Default::default() }
}
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)
}
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
}
pub fn at_time(mut self, unix_secs: u64) -> Self {
self.now_unix = Some(unix_secs);
self
}
pub fn with_clock_skew(mut self, secs: u64) -> Self {
self.clock_skew_secs = secs;
self
}
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 }
}
pub fn verify_receipt(jwt: &str, cfg: &VerifyConfig) -> Result<(), VerifyError> {
let parsed = jwt::parse_and_verify(jwt, cfg)?;
payload::validate_shape(&parsed.payload)?;
payload::check_temporal(&parsed.payload, cfg)?;
payload::check_issuer_audience(&parsed.payload)?;
if payload::is_v2(&parsed.payload) {
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")]
payload::check_principal_signatures(&parsed.payload)?; proof::verify_if_present(&parsed.payload, cfg)?; }
Ok(())
}
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)
}