//! Byte-exact conformance against the frozen TCE vectors produced by the //! reference implementation. The vectors file lives in the relay repo at //! ~/niko_trust/testdata/vectors/tce_vectors.json and is the normative ground //! truth. If the file is absent the tests are skipped (keeps this crate //! self-contained). use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use niko_trust_gui::address::Address; use niko_trust_gui::protocol::{self, Decision, MAX_CLOCK_SKEW}; use niko_trust_gui::signer::{self, Signer}; use niko_trust_gui::tce::number::canonical_number; use niko_trust_gui::tce::value::Value; use serde_json::Value as Json; fn vectors_path() -> PathBuf { if let Ok(p) = std::env::var("NIKO_VECTORS") { return PathBuf::from(p); } let d = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()); Path::new(&d).join("../niko_trust/testdata/vectors/tce_vectors.json") } fn load_vectors() -> Option { let p = vectors_path(); if !p.exists() { eprintln!("skipping: vectors file not found at {}", p.display()); return None; } let raw = std::fs::read_to_string(&p).expect("read vectors"); serde_json::from_str(&raw).map(Some).expect("parse vectors json") } fn pubkey_of(addr: &str) -> [u8; 32] { let a = Address::parse(addr).expect("valid address"); *a.pubkey() } fn seed_for_pubkey(parties: &Json, pubkey_hex: &str) -> [u8; 32] { for (_name, p) in parties.as_object().unwrap() { if p["pubkey_hex"].as_str() == Some(pubkey_hex) { let s = hex::decode(p["seed_hex"].as_str().unwrap()).unwrap(); return s.try_into().unwrap(); } } panic!("party not found for pubkey {pubkey_hex}") } fn signer_for(parties: &Json, pubkey_hex: &str) -> Signer { Signer::from_seed(seed_for_pubkey(parties, pubkey_hex)).expect("signer") } fn hex_bytes(s: &str) -> Vec { hex::decode(s).expect("hex") } fn value_from_json(v: &Json) -> Value { match v { Json::Null => Value::Null, Json::Bool(b) => Value::Bool(*b), Json::String(s) => Value::Str(s.clone()), Json::Number(n) => Value::Number(n.to_string()), other => panic!("unsupported value in vector json: {other}"), } } fn map_from_json(v: &Json) -> BTreeMap { let mut m = BTreeMap::new(); for (k, val) in v.as_object().unwrap() { m.insert(k.clone(), value_from_json(val)); } m } fn nonce_from_json(v: &Json) -> [u8; 16] { hex_bytes(v.as_str().unwrap()).try_into().unwrap() } fn hash_from_json(v: &Json) -> [u8; 32] { hex_bytes(v.as_str().unwrap()).try_into().unwrap() } fn check_vector(parties: &Json, v: &Json) { let tce_expected = hex_bytes(v["tce_hex"].as_str().unwrap()); let object_id = v["object_id_hex"].as_str().unwrap(); let signer_key = v["signer_pubkey_hex"].as_str().unwrap(); let _ = v["signature_hex"]; let j = &v["json"]; let typ = j["type"].as_str().unwrap(); let signer = signer_for(parties, signer_key); // Build the typed object from the json view. let encoded: Vec = match typ { "identity" => protocol::encode_identity(&protocol::Identity { pubkey: signer.pubkey(), alias: j["alias"].as_str().unwrap().to_string(), created_at: j["created_at"].as_u64().unwrap(), }) .unwrap(), "claim" => protocol::encode_claim(&protocol::Claim { issuer: signer.pubkey(), subject: pubkey_of(j["subject"].as_str().unwrap()), claims: map_from_json(&j["claims"]), created_at: j["created_at"].as_u64().unwrap(), expires_at: j["expires_at"].as_u64().unwrap(), serial: j["serial"].as_u64().unwrap(), nonce: nonce_from_json(&j["nonce"]), }) .unwrap(), "revocation" => protocol::encode_revocation(&protocol::Revocation { issuer: signer.pubkey(), claim_id: hash_from_json(&j["claim_id"]), reason: j["reason"].as_str().unwrap().to_string(), created_at: j["created_at"].as_u64().unwrap(), nonce: nonce_from_json(&j["nonce"]), }) .unwrap(), "approval_request" => protocol::encode_approval_request(&protocol::ApprovalRequest { sender: signer.pubkey(), recipient: pubkey_of(j["recipient"].as_str().unwrap()), action: j["action"].as_str().unwrap().to_string(), payload: map_from_json(&j["payload"]), message: j["message"].as_str().unwrap().to_string(), created_at: j["created_at"].as_u64().unwrap(), expires_at: j["expires_at"].as_u64().unwrap(), nonce: nonce_from_json(&j["nonce"]), }) .unwrap(), "approval_response" => { let decision = match j["decision"].as_str().unwrap() { "allow" => Decision::Allow, "deny" => Decision::Deny, other => panic!("bad decision {other}"), }; protocol::encode_approval_response(&protocol::ApprovalResponse { request_hash: hash_from_json(&j["request_hash"]), responder: signer.pubkey(), decision, created_at: j["created_at"].as_u64().unwrap(), nonce: nonce_from_json(&j["nonce"]), }) .unwrap() } "auth_assertion" => protocol::encode_auth_assertion(&protocol::AuthAssertion { pubkey: signer.pubkey(), challenge: hash_from_json(&j["challenge"]), scope: j["scope"].as_str().unwrap().to_string(), audience: j["audience"].as_str().unwrap().to_string(), created_at: j["created_at"].as_u64().unwrap(), }) .unwrap(), other => panic!("unknown vector type {other}"), }; assert_eq!(encoded, tce_expected, "vector {}: encoded bytes differ", v["name"]); assert_eq!( hex::encode(niko_trust_gui::crypto::sha256(&encoded)), object_id, "vector {}: object id differs", v["name"] ); // Deterministic Ed25519 must reproduce the reference signature. let sig = signer.sign(&encoded); assert_eq!(hex::encode(sig), v["signature_hex"], "vector {}: signature differs", v["name"]); // Rule checks on the decoded form. let _ = MAX_CLOCK_SKEW; // decode -> re-encode round trip must be byte-identical. let reencoded: Vec = match typ { "identity" => { let d = protocol::decode_identity(&tce_expected).unwrap(); protocol::encode_identity(&d).unwrap() } "claim" => { let d = protocol::decode_claim(&tce_expected).unwrap(); protocol::encode_claim(&d).unwrap() } "revocation" => { let d = protocol::decode_revocation(&tce_expected).unwrap(); protocol::encode_revocation(&d).unwrap() } "approval_request" => { let d = protocol::decode_approval_request(&tce_expected).unwrap(); protocol::encode_approval_request(&d).unwrap() } "approval_response" => { let d = protocol::decode_approval_response(&tce_expected).unwrap(); protocol::encode_approval_response(&d).unwrap() } "auth_assertion" => { let d = protocol::decode_auth_assertion(&tce_expected).unwrap(); protocol::encode_auth_assertion(&d).unwrap() } _ => unreachable!(), }; assert_eq!(reencoded, tce_expected, "vector {}: decode/re-encode differs", v["name"]); // Signature verification over the exact bytes with the embedded key. let verify_pub = match typ { "claim" => pubkey_of(j["issuer"].as_str().unwrap()), "approval_response" => signer.pubkey(), "revocation" => signer.pubkey(), "approval_request" => signer.pubkey(), "auth_assertion" => signer.pubkey(), "identity" => signer.pubkey(), _ => unreachable!(), }; let sig_vec = hex_bytes(v["signature_hex"].as_str().unwrap()); let sig_arr: [u8; 64] = sig_vec.try_into().unwrap(); signer::verify(&verify_pub, &tce_expected, &sig_arr).expect("signature must verify"); } #[test] fn frozen_tce_vectors_byte_exact() { let Some(doc) = load_vectors() else { return }; let parties = &doc["parties"]; for v in doc["vectors"].as_array().unwrap() { check_vector(parties, v); } assert!(doc["vectors"].as_array().unwrap().len() >= 9); } #[test] fn number_canonicalization_vectors() { let Some(doc) = load_vectors() else { return }; let nc = &doc["number_canonicalization"]; for (input, want) in nc["accept"].as_object().unwrap() { assert_eq!( canonical_number(input).ok(), Some(want.as_str().unwrap().to_string()), "accept {input}" ); } for (input, _reason) in nc["reject"].as_object().unwrap() { assert!(canonical_number(input).is_err(), "reject {input}"); } } #[test] fn strict_decoder_rejects() { let Some(doc) = load_vectors() else { return }; // Every reject vector is hex bytes that a strict decoder must refuse as a // Claim (the reference uses a claim context for these). Failing to decode // is the assertion. for r in doc["rejects"].as_array().unwrap() { let name = r["name"].as_str().unwrap(); // Some reject rows are prose descriptions without bytes; only rows // carrying tce_hex are byte-checkable here. let Some(b) = r["tce_hex"].as_str().map(hex_bytes) else { continue; }; assert!( niko_trust_gui::protocol::decode_claim(&b).is_err(), "reject vector {name} must fail to decode" ); } }