Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 16 additions & 63 deletions crates/auths-verifier/tests/cases/cross_surface_parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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
Expand All @@ -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);
}
}

Expand Down
7 changes: 1 addition & 6 deletions crates/auths-verifier/tests/cases/ffi_smoke.rs
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
2 changes: 2 additions & 0 deletions crates/auths-verifier/tests/cases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
143 changes: 143 additions & 0 deletions crates/auths-verifier/tests/cases/parity_cases.rs
Original file line number Diff line number Diff line change
@@ -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<Case> {
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<Case> {
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",
),
]
}
51 changes: 38 additions & 13 deletions crates/auths-verifier/tests/wasm_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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]
Expand Down
Loading