evidence 0.1.0

Type-level tags for cryptographic primitives
Documentation
//! Property-based tests for Mac.

#![cfg(feature = "hmac")]

use bolero::check;
use evidence::{
    codec::Identity,
    mac::{Mac, MacUnchecked, hmac::HmacSha256},
};
use hmac::Hmac;

/// Helper to create a key from seed bytes (64 bytes for HMAC-SHA256 block size).
fn key_from_seed(seed: [u8; 64]) -> hmac::digest::Key<Hmac<sha2::Sha256>> {
    *hmac::digest::Key::<Hmac<sha2::Sha256>>::from_slice(&seed)
}

#[test]
#[allow(clippy::expect_used)] // test should panic if verification fails
fn mac_verify_roundtrip() {
    check!()
        .with_type::<([u8; 64], Vec<u8>)>()
        .for_each(|(seed, payload)| {
            let key = key_from_seed(*seed);

            let mac: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&key, payload);

            let verified = mac
                .try_verify(&key)
                .expect("verification should succeed for just-tagged payload");

            assert_eq!(&verified, payload, "verified payload must match original");
        });
}

#[test]
fn mac_deterministic() {
    check!()
        .with_type::<([u8; 64], Vec<u8>)>()
        .for_each(|(seed, payload)| {
            let key = key_from_seed(*seed);

            let mac1: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&key, payload);
            let mac2: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&key, payload);

            assert_eq!(mac1, mac2, "same input must produce same MAC");
        });
}

#[test]
fn different_payloads_different_macs() {
    check!()
        .with_type::<([u8; 64], Vec<u8>, Vec<u8>)>()
        .filter(|(_, a, b)| a != b)
        .for_each(|(seed, payload_a, payload_b)| {
            let key = key_from_seed(*seed);

            let mac_a: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&key, payload_a);
            let mac_b: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&key, payload_b);

            assert_ne!(
                mac_a.mac_tag(),
                mac_b.mac_tag(),
                "different payloads should produce different MACs"
            );
        });
}

#[test]
fn wrong_key_fails_verification() {
    check!()
        .with_type::<([u8; 64], [u8; 64], Vec<u8>)>()
        .filter(|(a, b, _)| a != b)
        .for_each(|(seed_correct, seed_wrong, payload)| {
            let correct_key = key_from_seed(*seed_correct);
            let wrong_key = key_from_seed(*seed_wrong);

            let mac: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&correct_key, payload);

            assert!(
                mac.try_verify(&wrong_key).is_err(),
                "verification must fail with wrong key"
            );
        });
}

#[test]
#[allow(clippy::indexing_slicing)] // payload is non-empty (filter guarantees)
fn tampered_payload_fails_verification() {
    check!()
        .with_type::<([u8; 64], Vec<u8>)>()
        .cloned()
        .filter(|(_, payload)| !payload.is_empty())
        .for_each(|(seed, payload)| {
            let key = key_from_seed(seed);

            let mac: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&key, &payload);

            let mut tampered_bytes = mac.encoded_payload().to_vec();
            tampered_bytes[0] = tampered_bytes[0].wrapping_add(1);

            let tampered: Mac<Vec<u8>, HmacSha256, Identity> =
                Mac::from_unchecked_parts(*mac.mac_tag(), tampered_bytes);

            assert!(
                tampered.try_verify(&key).is_err(),
                "verification must fail with tampered payload"
            );
        });
}

#[test]
fn tampered_tag_fails_verification() {
    check!()
        .with_type::<([u8; 64], Vec<u8>)>()
        .cloned()
        .for_each(|(seed, payload)| {
            let key = key_from_seed(seed);

            let mac: Mac<Vec<u8>, HmacSha256, Identity> = Mac::tag(&key, &payload);

            let mut tampered_tag = *mac.mac_tag();
            tampered_tag[0] = tampered_tag[0].wrapping_add(1);

            let tampered: Mac<Vec<u8>, HmacSha256, Identity> =
                Mac::from_unchecked_parts(tampered_tag, mac.encoded_payload().to_vec());

            assert!(
                tampered.try_verify(&key).is_err(),
                "verification must fail with tampered tag"
            );
        });
}