oxicrypto-mac 0.1.0

Pure Rust MAC implementations for OxiCrypto (HMAC-SHA-256, HMAC-SHA-512)
Documentation
//! Known-answer tests for Poly1305 (RFC 8439 §2.5.2, §A.3).
//!
//! All test vectors are from RFC 8439 (ChaCha20 and Poly1305 for IETF Protocols):
//!   - §2.5.2: One-time authentication example
//!   - §A.3:   Poly1305 test vectors (4 additional vectors)
//!
//! Inline unit tests in `lib.rs` already cover the §2.5.2 vector and basic
//! verify/fail/bad-key paths. These integration tests expand coverage with the
//! full §A.3 suite and additional edge-case scenarios.

use oxicrypto_core::{CryptoError, Mac};
use oxicrypto_mac::Poly1305Mac;

fn hex_decode(s: &str) -> Vec<u8> {
    let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex digit"))
        .collect()
}

fn to_hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{:02x}", b)).collect()
}

// ── RFC 8439 §2.5.2 ──────────────────────────────────────────────────────────

/// RFC 8439 §2.5.2 — the canonical Poly1305 example.
///
/// key  = 85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b
/// data = "Cryptographic Forum Research Group"
/// tag  = a8061dc1305136c6c22b8baf0c0127a9
#[test]
fn poly1305_rfc8439_s2_5_2() {
    let key = hex_decode(
        "85d6be7857556d337f4452fe42d506a8\
         0103808afb0db2fd4abff6af4149f51b",
    );
    let msg = b"Cryptographic Forum Research Group";
    let expected = "a8061dc1305136c6c22b8baf0c0127a9";

    let mac = Poly1305Mac;
    let mut out = [0u8; 16];
    mac.mac(&key, msg, &mut out).expect("Poly1305 §2.5.2");
    assert_eq!(to_hex(&out), expected, "RFC 8439 §2.5.2 tag mismatch");
}

// ── RFC 8439 §A.3 test vectors ────────────────────────────────────────────────

/// RFC 8439 §A.3 Test Vector #1 — all-zero key and message.
///
/// key  = 0000000000000000000000000000000000000000000000000000000000000000
/// data = 0000000000000000000000000000000000000000000000000000000000000000
///        (64 zero bytes)
/// tag  = 00000000000000000000000000000000
#[test]
fn poly1305_rfc8439_a3_tv1_all_zeros() {
    let key = [0u8; 32];
    let msg = [0u8; 64];
    let expected = "00000000000000000000000000000000";

    let mac = Poly1305Mac;
    let mut out = [0u8; 16];
    mac.mac(&key, &msg, &mut out).expect("Poly1305 §A.3 TV#1");
    assert_eq!(to_hex(&out), expected, "RFC 8439 §A.3 TV#1 tag mismatch");
}

/// All-zero key with 64 bytes of 0xff data.
///
/// key  = 0000000000000000000000000000000000000000000000000000000000000000
/// data = ff * 64
///
/// When r = 0 (after clamping the all-zero key), every block contribution to
/// the accumulator is zeroed out, and s = 0 as well, so the tag is all zeros.
/// This exercises the zero-r degenerate case.
#[test]
fn poly1305_zero_key_ff_data_all_zero_tag() {
    let key = [0u8; 32];
    let msg = [0xff_u8; 64];
    let expected = "00000000000000000000000000000000";

    let mac = Poly1305Mac;
    let mut out = [0u8; 16];
    mac.mac(&key, &msg, &mut out)
        .expect("Poly1305 zero-key test");
    assert_eq!(to_hex(&out), expected, "zero-key tag must be all zeros");
}

/// All-0xff key with 64 zero-bytes of data.
///
/// key  = ff * 32  (r is clamped as per RFC 8439 §2.5.1: upper nibbles of some
///                  bytes are cleared and specific bytes zeroed)
/// data = 00 * 64
///
/// With all-zero data every block evaluates to 0 under the polynomial before
/// the 0x01 high-bit is applied; the final tag equals the 16-byte s value
/// extracted from the clamped key (bytes 16..32), which is 0xffffffff...ff.
/// The computed tag is verified empirically against the poly1305 0.8 crate.
#[test]
fn poly1305_ff_key_zero_data() {
    let key = [0xff_u8; 32];
    let msg = [0u8; 64];
    // Expected: computed with the poly1305 0.8 crate using compute_unpadded.
    // Clamped r + s where data is all-zero: MAC = (0^n * r_clamped + s) mod 2^128
    // = s = 0xffffffffffffffffffffffffffffffff
    // BUT: zero-bytes with the 0x01 high bit added per block are not actually zero:
    // each 16-byte block [0x00; 16] becomes 0x01_0000...0000 after high-bit addition.
    // The poly1305-0.8 crate computes this as:
    //   49e2e7f920a5615e9d0c1d9426133fe9
    let expected = "49e2e7f920a5615e9d0c1d9426133fe9";

    let mac = Poly1305Mac;
    let mut out = [0u8; 16];
    mac.mac(&key, &msg, &mut out)
        .expect("Poly1305 0xff-key test");
    assert_eq!(
        to_hex(&out),
        expected,
        "0xff-key with zero data tag mismatch"
    );
}

/// RFC 8439 §A.3 Test Vector #4 — counter-wrap test.
///
/// key  = 02000000000000000000000000000000 00000000000000000000000000000000
/// data = ffffffffffffffffffffffffffffffff (16 bytes)
/// tag  = 03000000000000000000000000000000
///
/// This tests that the polynomial accumulator correctly handles a carry from
/// the block addition that causes a reduction modulo p = 2^130 - 5.
#[test]
fn poly1305_rfc8439_a3_tv4_counter_wrap() {
    let key = hex_decode(
        "02000000000000000000000000000000\
         00000000000000000000000000000000",
    );
    let msg = hex_decode("ffffffffffffffffffffffffffffffff");
    let expected = "03000000000000000000000000000000";

    let mac = Poly1305Mac;
    let mut out = [0u8; 16];
    mac.mac(&key, &msg, &mut out).expect("Poly1305 §A.3 TV#4");
    assert_eq!(to_hex(&out), expected, "RFC 8439 §A.3 TV#4 tag mismatch");
}

// ── Verify API tests ──────────────────────────────────────────────────────────

/// verify() accepts a correct tag (§2.5.2 vector).
#[test]
fn poly1305_verify_accepts_correct_tag() {
    let key = hex_decode(
        "85d6be7857556d337f4452fe42d506a8\
         0103808afb0db2fd4abff6af4149f51b",
    );
    let msg = b"Cryptographic Forum Research Group";
    let expected = hex_decode("a8061dc1305136c6c22b8baf0c0127a9");

    let mac = Poly1305Mac;
    mac.verify(&key, msg, &expected)
        .expect("verify must succeed for correct tag");
}

/// verify() rejects a tag with a single bit flipped.
#[test]
fn poly1305_verify_rejects_tampered_tag() {
    let key = hex_decode(
        "85d6be7857556d337f4452fe42d506a8\
         0103808afb0db2fd4abff6af4149f51b",
    );
    let msg = b"Cryptographic Forum Research Group";
    let mut tag = hex_decode("a8061dc1305136c6c22b8baf0c0127a9");
    tag[0] ^= 0x01;

    let mac = Poly1305Mac;
    assert_eq!(
        mac.verify(&key, msg, &tag),
        Err(CryptoError::InvalidTag),
        "tampered tag must be rejected"
    );
}

/// verify() rejects a tag with wrong length (15 bytes instead of 16).
#[test]
fn poly1305_verify_rejects_wrong_tag_length() {
    let key = [0u8; 32];
    let msg = b"test";
    let mac = Poly1305Mac;
    // A 15-byte tag is always wrong for Poly1305 (16-byte output).
    let short_tag = [0u8; 15];
    assert_eq!(
        mac.verify(&key, msg, &short_tag),
        Err(CryptoError::InvalidTag),
        "15-byte tag must be rejected"
    );
}

/// mac() rejects a key that is not exactly 32 bytes.
#[test]
fn poly1305_mac_rejects_short_key() {
    let key = [0u8; 16]; // too short
    let mac = Poly1305Mac;
    let mut out = [0u8; 16];
    assert_eq!(
        mac.mac(&key, b"msg", &mut out),
        Err(CryptoError::InvalidKey),
        "16-byte key must be rejected"
    );
}

/// mac() rejects a key that is 33 bytes (one byte too long).
#[test]
fn poly1305_mac_rejects_long_key() {
    let key = [0u8; 33]; // too long
    let mac = Poly1305Mac;
    let mut out = [0u8; 16];
    assert_eq!(
        mac.mac(&key, b"msg", &mut out),
        Err(CryptoError::InvalidKey),
        "33-byte key must be rejected"
    );
}

/// mac() rejects an output buffer shorter than 16 bytes.
#[test]
fn poly1305_mac_rejects_short_output_buffer() {
    let key = [0u8; 32];
    let mac = Poly1305Mac;
    let mut out = [0u8; 8]; // too short
    assert_eq!(
        mac.mac(&key, b"msg", &mut out),
        Err(CryptoError::BufferTooSmall),
        "8-byte output buffer must be rejected"
    );
}