mirror of
https://github.com/zap-proto/spec.git
synced 2026-10-30 15:04:03 +00:00
135 lines
5.0 KiB
Rust
135 lines
5.0 KiB
Rust
//! Cross-language Known-Answer Test for the ZAP capability runtime.
|
|
//!
|
|
//! The fixture `tests/testdata/cap_go_kat.hex` was produced by the canonical
|
|
//! Go reference runtime (`github.com/zap-proto/go/cap`) with a FIXED ed25519
|
|
//! seed (32 x 0x42) and FIXED cap inputs. This test proves that:
|
|
//!
|
|
//! 1. The Rust port decodes Go's wire bytes.
|
|
//! 2. Rust's `canonical_bytes()` is byte-identical to Go's `CanonicalBytes`.
|
|
//! 3. Rust's `cap_id()` (SHA-256(canonical || sig)) matches Go's CapID.
|
|
//! 4. The Ed25519 signature Go produced VERIFIES in Rust under the known
|
|
//! public key — i.e. a Go-signed capability is accepted by the Rust
|
|
//! verifier.
|
|
//! 5. A single-bit tamper of the signed header makes verification FAIL.
|
|
//!
|
|
//! This is the interop contract: caps cross the Go<->Rust boundary unchanged.
|
|
|
|
#![cfg(feature = "zwing")] // cap requires ed25519-dalek + pqcrypto-mldsa (zwing deps)
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use zap::cap::{hash32, CapError, Capability, Verifier};
|
|
|
|
const FIXTURE: &str = include_str!("testdata/cap_go_kat.hex");
|
|
|
|
/// Parse the `label.field=hexvalue` fixture into a map keyed by `label.field`.
|
|
fn parse_fixture() -> HashMap<String, Vec<u8>> {
|
|
let mut map = HashMap::new();
|
|
for line in FIXTURE.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let (key, val) = line
|
|
.split_once('=')
|
|
.expect("fixture line must be key=value");
|
|
let bytes = hex::decode(val.trim()).expect("fixture value must be hex");
|
|
map.insert(key.to_string(), bytes);
|
|
}
|
|
map
|
|
}
|
|
|
|
fn get<'a>(m: &'a HashMap<String, Vec<u8>>, k: &str) -> &'a [u8] {
|
|
m.get(k)
|
|
.unwrap_or_else(|| panic!("missing fixture key {k}"))
|
|
}
|
|
|
|
/// Run the full KAT for one labelled case in the fixture.
|
|
fn check_case(m: &HashMap<String, Vec<u8>>, label: &str) {
|
|
let wire = get(m, &format!("{label}.wire"));
|
|
let go_canonical = get(m, &format!("{label}.canonical"));
|
|
let go_cap_id = get(m, &format!("{label}.cap_id"));
|
|
let go_sig = get(m, &format!("{label}.sig"));
|
|
let go_pubkey = get(m, &format!("{label}.ed25519_pubkey")); // raw 32-byte ed25519 pk
|
|
let go_issuer_hash = get(m, &format!("{label}.issuer_hash")); // SHA-256(pubkey)
|
|
|
|
// (1) Rust decodes Go's bytes.
|
|
let cap = Capability::parse(wire).expect("rust must parse go-produced wire bytes");
|
|
|
|
// (2) Canonical bytes match Go byte-for-byte.
|
|
assert_eq!(
|
|
cap.canonical_bytes(),
|
|
go_canonical,
|
|
"[{label}] canonical_bytes diverged from Go"
|
|
);
|
|
|
|
// (3) cap_id matches Go.
|
|
assert_eq!(
|
|
cap.cap_id().as_slice(),
|
|
go_cap_id,
|
|
"[{label}] cap_id diverged from Go"
|
|
);
|
|
|
|
// Sanity: the signature footer Rust reads off the wire equals Go's.
|
|
assert_eq!(
|
|
cap.signature().as_slice(),
|
|
go_sig,
|
|
"[{label}] on-wire signature footer mismatch"
|
|
);
|
|
|
|
// Sanity: issuer hash in the cap equals SHA-256(go_pubkey) and Go's value.
|
|
let pk_arr: [u8; 32] = go_pubkey.try_into().expect("pubkey is 32 bytes");
|
|
assert_eq!(
|
|
hash32(&pk_arr).as_slice(),
|
|
go_issuer_hash,
|
|
"[{label}] issuer hash != SHA-256(pubkey)"
|
|
);
|
|
assert_eq!(
|
|
cap.issuer().as_slice(),
|
|
go_issuer_hash,
|
|
"[{label}] cap.issuer() != Go issuer hash"
|
|
);
|
|
|
|
// (4) The Go-produced Ed25519 signature VERIFIES in Rust.
|
|
let pk_for_verify = go_pubkey.to_vec();
|
|
let verifier = Verifier::new().with_issuer_key(move |_issuer| Ok(pk_for_verify.clone()));
|
|
verifier
|
|
.verify(&cap, 1_700_000_000)
|
|
.unwrap_or_else(|e| panic!("[{label}] Go-signed cap failed Rust verify: {e}"));
|
|
|
|
// (5) Tamper one byte of the signed header (Kind, at root+0) -> verify FAILS.
|
|
let mut tampered = wire.to_vec();
|
|
let root = u32::from_le_bytes([tampered[8], tampered[9], tampered[10], tampered[11]]) as usize;
|
|
tampered[root] ^= 0x01; // flip a bit in Kind (inside the signed [0..164) header)
|
|
let tcap = Capability::parse(&tampered).expect("tampered buffer still parses structurally");
|
|
let pk2 = go_pubkey.to_vec();
|
|
let verifier2 = Verifier::new().with_issuer_key(move |_| Ok(pk2.clone()));
|
|
assert_eq!(
|
|
verifier2.verify(&tcap, 1_700_000_000).unwrap_err(),
|
|
CapError::SigMismatch,
|
|
"[{label}] tampered header must fail signature verification"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn go_signed_zero_caveat_cap_verifies_in_rust() {
|
|
let m = parse_fixture();
|
|
check_case(&m, "zero_caveat");
|
|
}
|
|
|
|
#[test]
|
|
fn go_signed_two_caveat_cap_verifies_in_rust() {
|
|
// Exercises the caveat canonical codec AND the in-header list-pointer bytes
|
|
// (relOffset 3420, length 2) — the subtle part of canonical-bytes parity.
|
|
let m = parse_fixture();
|
|
check_case(&m, "two_caveat");
|
|
}
|
|
|
|
#[test]
|
|
fn fixture_seed_is_the_documented_constant() {
|
|
// Guards against the fixture being regenerated with a different seed.
|
|
let m = parse_fixture();
|
|
assert_eq!(get(&m, "zero_caveat.ed25519_seed"), [0x42u8; 32]);
|
|
assert_eq!(get(&m, "two_caveat.ed25519_seed"), [0x42u8; 32]);
|
|
}
|