diff --git a/crates/auths-verifier/tests/cases/cross_surface_parity.rs b/crates/auths-verifier/tests/cases/cross_surface_parity.rs index cb9aaf6d..4f116626 100644 --- a/crates/auths-verifier/tests/cases/cross_surface_parity.rs +++ b/crates/auths-verifier/tests/cases/cross_surface_parity.rs @@ -10,6 +10,10 @@ //! curve *code* is rejected at the FFI boundary. The WASM leg of the same fixtures runs on //! the wasm32 target in `tests/wasm_bindings.rs`. +use super::parity_cases::{ + CREDENTIAL_VALID, FIXTURE_ATTESTATION_JSON, FIXTURE_ISSUER_PK_HEX, PRESENTATION_VALID, + credential_cases, kind, presentation_cases, +}; use auths_crypto::CurveType; use auths_verifier::core::{Attestation, DevicePublicKey}; use auths_verifier::ffi::{ @@ -19,15 +23,6 @@ use auths_verifier::ffi::{ use auths_verifier::{Verifier, verify_credential_json, verify_presentation_json}; use core::ffi::c_int; -const PRESENTATION_VALID: &str = include_str!("../fixtures/presentation_valid.json"); -const CREDENTIAL_VALID: &str = include_str!("../fixtures/credential_valid.json"); -const CREDENTIAL_REVOKED: &str = include_str!("../fixtures/credential_revoked.json"); - -// Deterministic Ed25519 attestation fixture (shared with `ffi_smoke` / `wasm_bindings`). -const FIXTURE_ISSUER_PK_HEX: &str = - "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c"; -const FIXTURE_ATTESTATION_JSON: &str = r#"{"version":1,"rid":"test-rid","issuer":"did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX","subject":"did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH","device_public_key":"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394","identity_signature":"1690dee2371b2bd586e696c6f891c509140ff808b82cda8c83ecfa0ea396cb3e295006ad2e6498389b5e3b1ff9d089a9ab654c30adb68d55bde04a64d7e80208","device_signature":"df199539fd0367b3684fef8b484f829c679c1d02373acf9787150032a573a3e79c878e3c4c403dfeffc25f5d4695aecb64ea67a286068ed7ca4a51f042adfc08","timestamp":null}"#; - type JsonVerifyFn = unsafe extern "C" fn(*const u8, usize, *mut u8, *mut usize) -> c_int; /// Drive a JSON-verdict FFI entrypoint and return the verdict string it writes. The verdict @@ -45,73 +40,31 @@ fn ffi_verdict(request: &str, f: JsonVerifyFn) -> String { String::from_utf8(buf[..len].to_vec()).expect("verdict bytes are UTF-8") } -/// Re-serialize a fixture after mutating one field — the forgery builder. -fn tampered(fixture: &str, mutate: impl FnOnce(&mut serde_json::Value)) -> String { - let mut value: serde_json::Value = serde_json::from_str(fixture).expect("fixture is JSON"); - mutate(&mut value); - serde_json::to_string(&value).expect("re-serialize") -} - -fn zero_sig_b64() -> serde_json::Value { - use base64::Engine as _; - serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode([0u8; 64])) -} - -fn kind(verdict_json: &str) -> String { - let v: serde_json::Value = serde_json::from_str(verdict_json).expect("verdict JSON"); - v["kind"].as_str().expect("kind discriminant").to_string() -} - #[test] fn presentation_verdicts_are_byte_identical_native_and_ffi() { - let truncated = &PRESENTATION_VALID[..PRESENTATION_VALID.len() / 2]; - let zeroed = tampered(PRESENTATION_VALID, |v| { - v["envelope"]["signatureB64"] = zero_sig_b64(); - }); - let wrong_audience = tampered(PRESENTATION_VALID, |v| { - v["audience"] = serde_json::Value::String("evil.example".to_string()); - }); - - let cases: [(&str, &str); 4] = [ - (PRESENTATION_VALID, "valid"), - (&zeroed, "holderNotCurrentKey"), - (&wrong_audience, "wrongAudience"), - (truncated, "malformedRequest"), - ]; - - for (request, expected_kind) in cases { - let native = verify_presentation_json(request); - let ffi = ffi_verdict(request, auths_verify_presentation_json); + for case in presentation_cases() { + let native = verify_presentation_json(&case.request); + let ffi = ffi_verdict(&case.request, auths_verify_presentation_json); assert_eq!( native, ffi, - "native and FFI must return the identical presentation verdict for {expected_kind}" + "native and FFI must return the identical presentation verdict for {}", + case.label ); - assert_eq!(kind(&native), expected_kind); + assert_eq!(kind(&native), case.expected_kind, "{}", case.label); } } #[test] fn credential_verdicts_are_byte_identical_native_and_ffi() { - let zeroed = tampered(CREDENTIAL_VALID, |v| { - v["credential"]["signatureB64"] = zero_sig_b64(); - }); - let truncated = &CREDENTIAL_VALID[..CREDENTIAL_VALID.len() / 2]; - - let cases: [(&str, &str); 4] = [ - (CREDENTIAL_VALID, "valid"), - (CREDENTIAL_REVOKED, "credentialRevoked"), - (&zeroed, "issuerSignatureInvalid"), - (truncated, "malformedRequest"), - ]; - - for (request, expected_kind) in cases { - let native = verify_credential_json(request); - let ffi = ffi_verdict(request, auths_verify_credential_json); + for case in credential_cases() { + let native = verify_credential_json(&case.request); + let ffi = ffi_verdict(&case.request, auths_verify_credential_json); assert_eq!( native, ffi, - "native and FFI must return the identical credential verdict for {expected_kind}" + "native and FFI must return the identical credential verdict for {}", + case.label ); - assert_eq!(kind(&native), expected_kind); + assert_eq!(kind(&native), case.expected_kind, "{}", case.label); } } diff --git a/crates/auths-verifier/tests/cases/ffi_smoke.rs b/crates/auths-verifier/tests/cases/ffi_smoke.rs index 2a657971..a4196705 100644 --- a/crates/auths-verifier/tests/cases/ffi_smoke.rs +++ b/crates/auths-verifier/tests/cases/ffi_smoke.rs @@ -1,12 +1,7 @@ +use super::parity_cases::{FIXTURE_ATTESTATION_JSON, FIXTURE_ISSUER_PK_HEX}; use auths_verifier::ffi::*; use std::ptr; -// Same fixture as wasm_bindings.rs — generated with deterministic ring keypairs. -const FIXTURE_ISSUER_PK_HEX: &str = - "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c"; - -const FIXTURE_ATTESTATION_JSON: &str = r#"{"version":1,"rid":"test-rid","issuer":"did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX","subject":"did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH","device_public_key":"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394","identity_signature":"1690dee2371b2bd586e696c6f891c509140ff808b82cda8c83ecfa0ea396cb3e295006ad2e6498389b5e3b1ff9d089a9ab654c30adb68d55bde04a64d7e80208","device_signature":"df199539fd0367b3684fef8b484f829c679c1d02373acf9787150032a573a3e79c878e3c4c403dfeffc25f5d4695aecb64ea67a286068ed7ca4a51f042adfc08","timestamp":null}"#; - const FIXTURE_ISSUER_DID: &str = "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX"; const FIXTURE_SUBJECT_DID: &str = "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH"; diff --git a/crates/auths-verifier/tests/cases/mod.rs b/crates/auths-verifier/tests/cases/mod.rs index f894d2d9..285c0376 100644 --- a/crates/auths-verifier/tests/cases/mod.rs +++ b/crates/auths-verifier/tests/cases/mod.rs @@ -13,6 +13,8 @@ mod freshness_honesty; mod issuer_signature_required; mod kel_verification; mod newtypes; +#[cfg(feature = "ffi")] +mod parity_cases; mod presentation; mod proptest_core; mod revocation_adversarial; diff --git a/crates/auths-verifier/tests/cases/parity_cases.rs b/crates/auths-verifier/tests/cases/parity_cases.rs new file mode 100644 index 00000000..2c2faeb7 --- /dev/null +++ b/crates/auths-verifier/tests/cases/parity_cases.rs @@ -0,0 +1,143 @@ +//! Shared cross-surface parity battery: one forgery set, fed identically through the +//! native, FFI, and WASM verdict surfaces (`cross_surface_parity.rs` drives native+FFI; +//! `tests/wasm_bindings.rs` `#[path]`-includes this and drives the wasm32 surface). Holding +//! the fixtures, the forgery builder, and the case list here is the single source of truth — +//! the surface-specific drivers (the C-ABI shim, the wasm-bindgen shim) live with their tests. + +#![allow(dead_code)] + +pub const PRESENTATION_VALID: &str = include_str!("../fixtures/presentation_valid.json"); +pub const CREDENTIAL_VALID: &str = include_str!("../fixtures/credential_valid.json"); +pub const CREDENTIAL_REVOKED: &str = include_str!("../fixtures/credential_revoked.json"); + +/// Deterministic Ed25519 attestation fixture (raw-key surface; shared with `ffi_smoke`). +pub const FIXTURE_ISSUER_PK_HEX: &str = + "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c"; +pub const FIXTURE_ATTESTATION_JSON: &str = r#"{"version":1,"rid":"test-rid","issuer":"did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX","subject":"did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH","device_public_key":"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394","identity_signature":"1690dee2371b2bd586e696c6f891c509140ff808b82cda8c83ecfa0ea396cb3e295006ad2e6498389b5e3b1ff9d089a9ab654c30adb68d55bde04a64d7e80208","device_signature":"df199539fd0367b3684fef8b484f829c679c1d02373acf9787150032a573a3e79c878e3c4c403dfeffc25f5d4695aecb64ea67a286068ed7ca4a51f042adfc08","timestamp":null}"#; + +/// Re-serialize a fixture after mutating one field — the forgery builder. +pub fn tampered(fixture: &str, mutate: impl FnOnce(&mut serde_json::Value)) -> String { + let mut value: serde_json::Value = serde_json::from_str(fixture).expect("fixture is JSON"); + mutate(&mut value); + serde_json::to_string(&value).expect("re-serialize") +} + +/// A base64 all-zero 64-byte signature — a possessor who cannot produce the real signature. +pub fn zero_sig_b64() -> serde_json::Value { + use base64::Engine as _; + serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode([0u8; 64])) +} + +/// The `kind` discriminant of a verdict JSON string. +pub fn kind(verdict_json: &str) -> String { + let v: serde_json::Value = serde_json::from_str(verdict_json).expect("verdict JSON"); + v["kind"].as_str().expect("kind discriminant").to_string() +} + +/// One parity case: a label, the request JSON, and the verdict `kind` every surface must +/// return for it. +pub struct Case { + pub label: &'static str, + pub request: String, + pub expected_kind: &'static str, +} + +/// The presentation forgery battery — each forged field maps to a distinct verdict kind. +pub fn presentation_cases() -> Vec { + let c = |label, request, expected_kind| Case { + label, + request, + expected_kind, + }; + vec![ + c("valid", PRESENTATION_VALID.to_string(), "valid"), + c( + "zeroed-binding-sig", + tampered(PRESENTATION_VALID, |v| { + v["envelope"]["signatureB64"] = zero_sig_b64(); + }), + "holderNotCurrentKey", + ), + c( + "wrong-audience", + tampered(PRESENTATION_VALID, |v| { + v["envelope"]["audience"] = serde_json::Value::String("evil.example".into()); + }), + "wrongAudience", + ), + c( + "tampered-nonce", + tampered(PRESENTATION_VALID, |v| { + v["envelope"]["binding"]["nonceB64"] = zero_sig_b64(); + }), + "nonceMismatchOrConsumed", + ), + c( + "unsupported-schema-version", + tampered(PRESENTATION_VALID, |v| { + v["schemaVersion"] = serde_json::Value::from(9999); + }), + "unsupportedSchemaVersion", + ), + c( + "tampered-credential-said", + tampered(PRESENTATION_VALID, |v| { + v["credential"]["acdc"]["d"] = serde_json::Value::String( + "EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into(), + ); + }), + "credentialNotValid", + ), + c( + "truncated", + PRESENTATION_VALID[..PRESENTATION_VALID.len() / 2].to_string(), + "malformedRequest", + ), + ] +} + +/// The credential forgery battery. +pub fn credential_cases() -> Vec { + let c = |label, request, expected_kind| Case { + label, + request, + expected_kind, + }; + vec![ + c("valid", CREDENTIAL_VALID.to_string(), "valid"), + c( + "revoked", + CREDENTIAL_REVOKED.to_string(), + "credentialRevoked", + ), + c( + "zeroed-issuer-sig", + tampered(CREDENTIAL_VALID, |v| { + v["credential"]["signatureB64"] = zero_sig_b64(); + }), + "issuerSignatureInvalid", + ), + c( + "tampered-said", + tampered(CREDENTIAL_VALID, |v| { + v["credential"]["acdc"]["d"] = serde_json::Value::String( + "EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into(), + ); + }), + "saidMismatch", + ), + c( + "privilege-escalated-capability", + tampered(CREDENTIAL_VALID, |v| { + v["credential"]["acdc"]["a"]["capability"] = + serde_json::Value::String("admin".into()); + }), + "saidMismatch", + ), + c( + "truncated", + CREDENTIAL_VALID[..CREDENTIAL_VALID.len() / 2].to_string(), + "malformedRequest", + ), + ] +} diff --git a/crates/auths-verifier/tests/wasm_bindings.rs b/crates/auths-verifier/tests/wasm_bindings.rs index 14793891..73b883dc 100644 --- a/crates/auths-verifier/tests/wasm_bindings.rs +++ b/crates/auths-verifier/tests/wasm_bindings.rs @@ -17,19 +17,16 @@ use auths_verifier::wasm::{ }; use wasm_bindgen_test::*; -// Committed cross-language fixtures, generated from the native verifier -// (`AUTHS_EMIT_FIXTURES=1`); the native contract tests assert these produce the same -// typed verdicts, so the WASM verdict here is checked against an independent surface. -const PRESENTATION_VALID: &str = include_str!("fixtures/presentation_valid.json"); -const CREDENTIAL_VALID: &str = include_str!("fixtures/credential_valid.json"); -const CREDENTIAL_REVOKED: &str = include_str!("fixtures/credential_revoked.json"); - -// Pre-computed fixture: valid attestation JSON signed with deterministic test keypairs. -// Generated via `cargo run --example gen_wasm_fixture -p auths-verifier`. -const FIXTURE_ISSUER_PK_HEX: &str = - "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c"; - -const FIXTURE_ATTESTATION_JSON: &str = r#"{"version":1,"rid":"test-rid","issuer":"did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX","subject":"did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH","device_public_key":"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394","identity_signature":"1690dee2371b2bd586e696c6f891c509140ff808b82cda8c83ecfa0ea396cb3e295006ad2e6498389b5e3b1ff9d089a9ab654c30adb68d55bde04a64d7e80208","device_signature":"df199539fd0367b3684fef8b484f829c679c1d02373acf9787150032a573a3e79c878e3c4c403dfeffc25f5d4695aecb64ea67a286068ed7ca4a51f042adfc08","timestamp":null}"#; +// The cross-surface forgery battery + fixtures, shared verbatim with the native/FFI parity +// suite (`cases/cross_surface_parity.rs`). Driving the *same* cases here is the third leg of +// the §3.8 lockstep: a forgery the native and FFI surfaces refuse must be refused — with the +// identical verdict kind — by the compiled WASM verdict too. +#[path = "cases/parity_cases.rs"] +mod parity_cases; +use parity_cases::{ + CREDENTIAL_REVOKED, CREDENTIAL_VALID, FIXTURE_ATTESTATION_JSON, FIXTURE_ISSUER_PK_HEX, + PRESENTATION_VALID, credential_cases, kind, presentation_cases, +}; // RFC 8032 Section 7.1, Test Vector 2 — used for artifact signature tests. const RFC8032_PUBKEY_HEX: &str = "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"; @@ -53,6 +50,34 @@ fn tampered(fixture_json: &str, mutate: impl FnOnce(&mut serde_json::Value)) -> serde_json::to_string(&value).expect("re-serialize") } +// ---- cross-surface parity battery (same cases as native/FFI) ---- + +#[wasm_bindgen_test] +fn presentation_battery_kinds_match_native_and_ffi() { + for case in presentation_cases() { + let v = wasm_verify_presentation_json(&case.request); + assert_eq!( + kind(&v), + case.expected_kind, + "WASM presentation verdict must match the native/FFI kind for {}", + case.label + ); + } +} + +#[wasm_bindgen_test] +fn credential_battery_kinds_match_native_and_ffi() { + for case in credential_cases() { + let v = wasm_verify_credential_json(&case.request); + assert_eq!( + kind(&v), + case.expected_kind, + "WASM credential verdict must match the native/FFI kind for {}", + case.label + ); + } +} + // ---- verifyPresentationJson (synchronous, pure-Rust verdict path) ---- #[wasm_bindgen_test]