mod common;
use common::*;
use merlin::Transcript;
use vauban_claim::claim::ClaimRefAlg;
use vauban_claim::transcript::bind_claim;
const DOMAIN_TAG: &[u8] = b"vauban-claim-v1-transcript-t1";
static SENTINEL: &[u8] = &[];
#[test]
fn p01_canonical_transcript_t1_accepts() {
let claim = fixture_claim();
let mut transcript = bind_claim(&claim, b"verifier-id-32-bytes-long-0000", &[0x42; 32]).unwrap();
let mut challenge = [0u8; 32];
transcript.challenge_bytes(b"vauban-challenge", &mut challenge);
let mut t2 = bind_claim(&claim, b"verifier-id-32-bytes-long-0000", &[0x42; 32]).unwrap();
let mut c2 = [0u8; 32];
t2.challenge_bytes(b"vauban-challenge", &mut c2);
assert_eq!(challenge, c2);
}
#[test]
fn p02_claim_ref_sha256_deterministic() {
let c = fixture_claim();
let r1 = c.claim_ref().unwrap();
let r2 = c.claim_ref().unwrap();
assert_eq!(r1.digest, r2.digest);
assert_eq!(r1.digest_alg, Some(ClaimRefAlg::Sha256));
}
#[test]
fn p03_cddl_validator_accepts_conformant() {
let report = vauban_claim::validate(&fixture_claim());
assert!(report.is_conformant(), "conformant claim must pass: {:?}", report.violations);
}
#[test]
fn v01_missing_subject_commit() {
let claim = fixture_claim();
let mut t = Transcript::new(DOMAIN_TAG);
t.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let p = &claim.predicate;
t.append_message(b"predicate.type", p.predicate_type_str().as_bytes());
t.append_message(b"predicate.domain", p.domain().as_bytes());
t.append_message(b"predicate.body", p.body());
let e = &claim.evidence;
t.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t.append_message(b"evidence.proof", e.proof());
t.append_message(b"evidence.public-inputs", SENTINEL);
t.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t.append_message(b"temporal.not-after", SENTINEL);
t.append_message(b"revelation-mask", SENTINEL);
let a = &claim.anchor;
t.append_message(b"anchor.uri", a.uri().as_bytes());
t.append_message(b"anchor.hash", a.hash());
t.append_message(b"anchor.epoch", &a.epoch().to_be_bytes());
t.append_message(b"verifier-ctx", b"ctx");
t.append_message(b"presentation-nonce", b"nonce");
let canonical = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut c_canonical = [0u8; 32];
let mut c_attack = [0u8; 32];
{
let mut t_canonical = canonical;
t_canonical.challenge_bytes(b"vauban-challenge", &mut c_canonical);
}
t.challenge_bytes(b"vauban-challenge", &mut c_attack);
assert_ne!(c_canonical, c_attack,
"V1: omitting subject MUST produce different challenge — subject-swap forgery foreclosed");
}
#[test]
fn v02_missing_predicate_commit() {
let claim = fixture_claim();
let mut t = Transcript::new(DOMAIN_TAG);
t.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let s = &claim.subject;
t.append_message(b"subject.type", s.subject_type_str().as_bytes());
let id_bytes = s.id_bytes().unwrap_or_else(|| s.id_utf8().unwrap().as_bytes());
t.append_message(b"subject.id", id_bytes);
t.append_message(b"subject.binding", SENTINEL);
let e = &claim.evidence;
t.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t.append_message(b"evidence.proof", e.proof());
t.append_message(b"evidence.public-inputs", SENTINEL);
t.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t.append_message(b"temporal.not-after", SENTINEL);
t.append_message(b"revelation-mask", SENTINEL);
let a = &claim.anchor;
t.append_message(b"anchor.uri", a.uri().as_bytes());
t.append_message(b"anchor.hash", a.hash());
t.append_message(b"anchor.epoch", &a.epoch().to_be_bytes());
t.append_message(b"verifier-ctx", b"ctx");
t.append_message(b"presentation-nonce", b"nonce");
let canonical = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut c_canonical = [0u8; 32];
let mut c_attack = [0u8; 32];
{
let mut tc = canonical;
tc.challenge_bytes(b"vauban-challenge", &mut c_canonical);
}
t.challenge_bytes(b"vauban-challenge", &mut c_attack);
assert_ne!(c_canonical, c_attack,
"V2: omitting predicate MUST produce different challenge — predicate-swap foreclosed");
}
#[test]
fn v03_concatenation_ambiguity_detected() {
let claim = fixture_claim();
let mut t_raw = Transcript::new(DOMAIN_TAG);
t_raw.append_message(b"merged", b"");
t_raw.append_message(
b"raw",
&[
claim.subject.subject_type_str().as_bytes(),
claim.predicate.domain().as_bytes(),
]
.concat(),
);
let canonical = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut c_canonical = [0u8; 32];
let mut c_attack = [0u8; 32];
{
let mut tc = canonical;
tc.challenge_bytes(b"vauban-challenge", &mut c_canonical);
}
t_raw.challenge_bytes(b"vauban-challenge", &mut c_attack);
assert_ne!(c_canonical, c_attack,
"V3: concatenation without length prefix MUST produce different challenge");
}
#[test]
fn v04_predicate_body_bypass_detected() {
let claim = fixture_claim();
let canonical = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut t_partial = Transcript::new(DOMAIN_TAG);
t_partial.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let s = &claim.subject;
t_partial.append_message(b"subject.type", s.subject_type_str().as_bytes());
let id_bytes = s.id_bytes().unwrap_or_else(|| s.id_utf8().unwrap().as_bytes());
t_partial.append_message(b"subject.id", id_bytes);
t_partial.append_message(b"subject.binding", SENTINEL);
t_partial.append_message(b"predicate.type", claim.predicate.predicate_type_str().as_bytes());
t_partial.append_message(b"predicate.domain", claim.predicate.domain().as_bytes());
t_partial.append_message(b"predicate.body", &claim.predicate.body()[..2.min(claim.predicate.body().len())]);
let e = &claim.evidence;
t_partial.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t_partial.append_message(b"evidence.proof", e.proof());
t_partial.append_message(b"evidence.public-inputs", SENTINEL);
t_partial.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t_partial.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t_partial.append_message(b"temporal.not-after", SENTINEL);
t_partial.append_message(b"revelation-mask", SENTINEL);
let a = &claim.anchor;
t_partial.append_message(b"anchor.uri", a.uri().as_bytes());
t_partial.append_message(b"anchor.hash", a.hash());
t_partial.append_message(b"anchor.epoch", &a.epoch().to_be_bytes());
t_partial.append_message(b"verifier-ctx", b"ctx");
t_partial.append_message(b"presentation-nonce", b"nonce");
let mut c_canonical = [0u8; 32];
let mut c_attack = [0u8; 32];
{
let mut tc = canonical;
tc.challenge_bytes(b"vauban-challenge", &mut c_canonical);
}
t_partial.challenge_bytes(b"vauban-challenge", &mut c_attack);
assert_ne!(c_canonical, c_attack,
"V4: partial predicate.body absorption MUST produce different challenge");
}
#[test]
fn v05_missing_committed_fields() {
let claim = fixture_claim();
let canonical = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut t = Transcript::new(DOMAIN_TAG);
t.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let s = &claim.subject;
t.append_message(b"subject.type", s.subject_type_str().as_bytes());
let id_bytes = s.id_bytes().unwrap_or_else(|| s.id_utf8().unwrap().as_bytes());
t.append_message(b"subject.id", id_bytes);
t.append_message(b"subject.binding", SENTINEL);
let p = &claim.predicate;
t.append_message(b"predicate.type", p.predicate_type_str().as_bytes());
t.append_message(b"predicate.domain", p.domain().as_bytes());
t.append_message(b"predicate.body", p.body());
let e = &claim.evidence;
t.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t.append_message(b"evidence.proof", e.proof());
t.append_message(b"evidence.public-inputs", SENTINEL);
t.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t.append_message(b"temporal.not-after", SENTINEL);
t.append_message(b"revelation-mask", SENTINEL);
let a = &claim.anchor;
t.append_message(b"anchor.uri", a.uri().as_bytes());
t.append_message(b"anchor.hash", a.hash());
t.append_message(b"anchor.epoch", &a.epoch().to_be_bytes());
t.append_message(b"verifier-ctx", b"ctx");
t.append_message(b"presentation-nonce", b"nonce");
let mut c_canonical = [0u8; 32];
let mut c_attack = [0u8; 32];
{
let mut tc = canonical;
tc.challenge_bytes(b"vauban-challenge", &mut c_canonical);
}
t.challenge_bytes(b"vauban-challenge", &mut c_attack);
assert_ne!(c_canonical, c_attack,
"V5: skipping committed fields MUST produce different challenge — path-rebinding foreclosed");
}
#[test]
fn v06_anchor_omission() {
let claim = fixture_claim();
let canonical = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut t = Transcript::new(DOMAIN_TAG);
t.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let s = &claim.subject;
t.append_message(b"subject.type", s.subject_type_str().as_bytes());
let id_bytes = s.id_bytes().unwrap_or_else(|| s.id_utf8().unwrap().as_bytes());
t.append_message(b"subject.id", id_bytes);
t.append_message(b"subject.binding", SENTINEL);
let p = &claim.predicate;
t.append_message(b"predicate.type", p.predicate_type_str().as_bytes());
t.append_message(b"predicate.domain", p.domain().as_bytes());
t.append_message(b"predicate.body", p.body());
let e = &claim.evidence;
t.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t.append_message(b"evidence.proof", e.proof());
t.append_message(b"evidence.public-inputs", SENTINEL);
t.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t.append_message(b"temporal.not-after", SENTINEL);
t.append_message(b"revelation-mask", SENTINEL);
t.append_message(b"verifier-ctx", b"ctx");
t.append_message(b"presentation-nonce", b"nonce");
let mut c_canonical = [0u8; 32];
let mut c_attack = [0u8; 32];
{
let mut tc = canonical;
tc.challenge_bytes(b"vauban-challenge", &mut c_canonical);
}
t.challenge_bytes(b"vauban-challenge", &mut c_attack);
assert_ne!(c_canonical, c_attack,
"V6: omitting anchor MUST produce different challenge — anchor-substitution foreclosed");
}
#[test]
fn v07_verifier_id_omission() {
let claim = fixture_claim();
let t_full = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut t_no_vid = Transcript::new(DOMAIN_TAG);
t_no_vid.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let s = &claim.subject;
t_no_vid.append_message(b"subject.type", s.subject_type_str().as_bytes());
let id_bytes = s.id_bytes().unwrap_or_else(|| s.id_utf8().unwrap().as_bytes());
t_no_vid.append_message(b"subject.id", id_bytes);
t_no_vid.append_message(b"subject.binding", SENTINEL);
let p = &claim.predicate;
t_no_vid.append_message(b"predicate.type", p.predicate_type_str().as_bytes());
t_no_vid.append_message(b"predicate.domain", p.domain().as_bytes());
t_no_vid.append_message(b"predicate.body", p.body());
let e = &claim.evidence;
t_no_vid.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t_no_vid.append_message(b"evidence.proof", e.proof());
t_no_vid.append_message(b"evidence.public-inputs", SENTINEL);
t_no_vid.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t_no_vid.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t_no_vid.append_message(b"temporal.not-after", SENTINEL);
t_no_vid.append_message(b"revelation-mask", SENTINEL);
let a = &claim.anchor;
t_no_vid.append_message(b"anchor.uri", a.uri().as_bytes());
t_no_vid.append_message(b"anchor.hash", a.hash());
t_no_vid.append_message(b"anchor.epoch", &a.epoch().to_be_bytes());
t_no_vid.append_message(b"presentation-nonce", b"nonce");
let mut c_full = [0u8; 32];
let mut c_attack = [0u8; 32];
{
let mut tf = t_full;
tf.challenge_bytes(b"vauban-challenge", &mut c_full);
}
t_no_vid.challenge_bytes(b"vauban-challenge", &mut c_attack);
assert_ne!(c_full, c_attack,
"V7: omitting verifier-id MUST produce different challenge — cross-verifier replay foreclosed");
}
#[test]
fn v08_silent_omission_sentinel() {
let claim = fixture_claim();
let t_with_sentinel = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut t_skip = Transcript::new(DOMAIN_TAG);
t_skip.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let s = &claim.subject;
t_skip.append_message(b"subject.type", s.subject_type_str().as_bytes());
let id_bytes = s.id_bytes().unwrap_or_else(|| s.id_utf8().unwrap().as_bytes());
t_skip.append_message(b"subject.id", id_bytes);
t_skip.append_message(b"subject.binding", SENTINEL);
let p = &claim.predicate;
t_skip.append_message(b"predicate.type", p.predicate_type_str().as_bytes());
t_skip.append_message(b"predicate.domain", p.domain().as_bytes());
t_skip.append_message(b"predicate.body", p.body());
let e = &claim.evidence;
t_skip.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t_skip.append_message(b"evidence.proof", e.proof());
t_skip.append_message(b"evidence.public-inputs", SENTINEL);
t_skip.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t_skip.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t_skip.append_message(b"revelation-mask", SENTINEL);
let a = &claim.anchor;
t_skip.append_message(b"anchor.uri", a.uri().as_bytes());
t_skip.append_message(b"anchor.hash", a.hash());
t_skip.append_message(b"anchor.epoch", &a.epoch().to_be_bytes());
t_skip.append_message(b"verifier-ctx", b"ctx");
t_skip.append_message(b"presentation-nonce", b"nonce");
let mut c_sentinel = [0u8; 32];
let mut c_skip = [0u8; 32];
{
let mut ts = t_with_sentinel;
ts.challenge_bytes(b"vauban-challenge", &mut c_sentinel);
}
t_skip.challenge_bytes(b"vauban-challenge", &mut c_skip);
assert_ne!(c_sentinel, c_skip,
"V8: silent omission vs sentinel MUST produce different challenge — shape ambiguity foreclosed");
}
#[test]
fn v09_hash_algorithm_substitution() {
let c1 = fixture_claim();
let mut c2 = c1.clone();
c2.revelation_mask.hash_alg = Some(vauban_claim::HashAlgTag::Sha256);
let t1 = bind_claim(&c1, b"ctx", b"nonce").unwrap();
let t2 = bind_claim(&c2, b"ctx", b"nonce").unwrap();
let mut c1_hash = [0u8; 32];
let mut c2_hash = [0u8; 32];
{
let mut t1c = t1;
let mut t2c = t2;
t1c.challenge_bytes(b"vauban-challenge", &mut c1_hash);
t2c.challenge_bytes(b"vauban-challenge", &mut c2_hash);
}
assert_ne!(c1_hash, c2_hash,
"V9: hash-alg substitution MUST produce different challenge — algorithm-substitution foreclosed");
}
#[test]
fn v10_late_verifier_context_binding() {
let claim = fixture_claim();
let _t_correct = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut t_late = Transcript::new(DOMAIN_TAG);
t_late.append_message(b"vauban-claim-v1", DOMAIN_TAG);
let s = &claim.subject;
t_late.append_message(b"subject.type", s.subject_type_str().as_bytes());
let id_bytes = s.id_bytes().unwrap_or_else(|| s.id_utf8().unwrap().as_bytes());
t_late.append_message(b"subject.id", id_bytes);
t_late.append_message(b"subject.binding", SENTINEL);
let p = &claim.predicate;
t_late.append_message(b"predicate.type", p.predicate_type_str().as_bytes());
t_late.append_message(b"predicate.domain", p.domain().as_bytes());
t_late.append_message(b"predicate.body", p.body());
let e = &claim.evidence;
t_late.append_message(b"evidence.scheme", e.scheme_tag().as_bytes());
t_late.append_message(b"evidence.proof", e.proof());
t_late.append_message(b"evidence.public-inputs", SENTINEL);
t_late.append_message(b"evidence.verifier-key", SENTINEL);
let tmp = &claim.temporal_frame;
t_late.append_message(b"temporal.not-before", &tmp.not_before().to_be_bytes());
t_late.append_message(b"temporal.not-after", SENTINEL);
t_late.append_message(b"revelation-mask", SENTINEL);
let a = &claim.anchor;
t_late.append_message(b"anchor.uri", a.uri().as_bytes());
t_late.append_message(b"anchor.hash", a.hash());
t_late.append_message(b"anchor.epoch", &a.epoch().to_be_bytes());
let mut c_late = [0u8; 32];
t_late.challenge_bytes(b"vauban-challenge", &mut c_late);
let t_correct_2 = bind_claim(&claim, b"ctx", b"nonce").unwrap();
let mut c_correct = [0u8; 32];
{
let mut tc = t_correct_2;
tc.challenge_bytes(b"vauban-challenge", &mut c_correct);
}
let late_binding_differs = c_late != c_correct;
assert!(late_binding_differs,
"V10: challenge derived before verifier-context absorption MUST differ from correct challenge");
}
#[test]
fn v11_label_collision_detected() {
let mut t = Transcript::new(DOMAIN_TAG);
t.append_message(b"data", b"round-1-input");
let mut c1 = [0u8; 32];
t.challenge_bytes(b"challenge", &mut c1);
let mut t2 = Transcript::new(DOMAIN_TAG);
t2.append_message(b"data", b"round-1-input");
let mut c2 = [0u8; 32];
t2.challenge_bytes(b"challenge", &mut c2);
assert_eq!(c1, c2,
"V11: same label at same transcript state produces SAME challenge — confirms label-collision risk exists");
}
#[test]
fn v12_domain_tag_spoofing() {
let canonical_tag = b"vauban-claim-v1-transcript-t1";
let spoofed_tag = b"Vauban-Claim-V1-Transcript-T1";
let mut t_canonical = Transcript::new(canonical_tag);
t_canonical.append_message(b"data", b"test");
let mut t_spoof = Transcript::new(spoofed_tag);
t_spoof.append_message(b"data", b"test");
let mut c_canonical = [0u8; 32];
let mut c_spoof = [0u8; 32];
t_canonical.challenge_bytes(b"vauban-challenge", &mut c_canonical);
t_spoof.challenge_bytes(b"vauban-challenge", &mut c_spoof);
assert_ne!(c_canonical, c_spoof,
"V12: case-spoofed domain tag MUST produce different challenge");
}
#[test]
fn v13_transcript_tag_hash_mismatch() {
let t1 = Transcript::new(b"vauban-claim-v1-transcript-t1");
let t2 = Transcript::new(b"vauban-claim-v1-transcript-t1-poseidon");
let mut t1c = t1;
let mut t2c = t2;
t1c.append_message(b"data", b"identical-payload");
t2c.append_message(b"data", b"identical-payload");
let mut c1 = [0u8; 32];
let mut c2 = [0u8; 32];
t1c.challenge_bytes(b"vauban-challenge", &mut c1);
t2c.challenge_bytes(b"vauban-challenge", &mut c2);
assert_ne!(c1, c2,
"V13: different transcript tags MUST produce different challenges — hash-function confusion foreclosed");
}