MOD: milestone 1 complete, tests all pass

This commit is contained in:
JohnE 2026-02-26 13:52:29 -08:00
parent db897b5abe
commit 836cf5063e
4 changed files with 376 additions and 29 deletions

View File

@ -275,16 +275,22 @@ fn generate_bindings(include_dir: &PathBuf, out_dir: &PathBuf) {
.allowlist_function("wc_curve25519_init")
.allowlist_function("wc_curve25519_free")
.allowlist_function("wc_curve25519_import_private")
.allowlist_function("wc_curve25519_import_private_ex")
.allowlist_function("wc_curve25519_import_public")
.allowlist_function("wc_curve25519_import_public_ex")
.allowlist_function("wc_curve25519_export_key_raw")
.allowlist_function("wc_curve25519_export_key_raw_ex")
.allowlist_function("wc_curve25519_shared_secret_ex")
// Curve448
.allowlist_function("wc_curve448_make_key")
.allowlist_function("wc_curve448_init")
.allowlist_function("wc_curve448_free")
.allowlist_function("wc_curve448_import_private")
.allowlist_function("wc_curve448_import_private_ex")
.allowlist_function("wc_curve448_import_public")
.allowlist_function("wc_curve448_import_public_ex")
.allowlist_function("wc_curve448_export_key_raw")
.allowlist_function("wc_curve448_export_key_raw_ex")
.allowlist_function("wc_curve448_shared_secret_ex")
// RNG
.allowlist_function("wc_InitRng")
@ -312,6 +318,14 @@ fn generate_bindings(include_dir: &PathBuf, out_dir: &PathBuf) {
// emits for C structs with bitfields (Aes, Hmac, etc.).
.blocklist_type("__uint128_t")
.blocklist_type("__int128_t")
// ECPoint contains an `ALIGN16 byte point[32]` field. bindgen does not
// propagate the __attribute__((aligned(16))) from a struct *field* to the
// generated Rust type, so it emits ECPoint as a plain 33-byte struct
// instead of the 48-byte, 16-byte-aligned layout that wolfCrypt uses.
// Blocking it here lets us define it manually in sys/ with the correct
// `#[repr(C, align(16))]` attribute so that curve25519_key's field
// offsets match the compiled C library.
.blocklist_type("ECPoint")
.derive_debug(false)
.layout_tests(false)
.generate()

View File

@ -83,6 +83,9 @@ pub fn decapsulate(
// Key sizes: private = 32 bytes, public = 32 bytes, shared secret = 32 bytes.
// ──────────────────────────────────────────────────────────────────────────────
/// EC25519_LITTLE_ENDIAN = 0 (matches RFC 7748 wire format).
const X25519_LE: i32 = 0;
fn x25519_generate() -> Result<KemKeyPair, CryptoError> {
let mut public_key = vec![0u8; 32];
let mut private_key = vec![0u8; 32];
@ -107,11 +110,21 @@ fn x25519_generate() -> Result<KemKeyPair, CryptoError> {
let mut pub_sz = public_key.len() as u32;
let mut priv_sz = private_key.len() as u32;
crate::sys::wc_curve25519_export_key_raw(
// Export in little-endian (EC25519_LITTLE_ENDIAN=0) so all key
// material is in RFC 7748 canonical format.
let ret = crate::sys::wc_curve25519_export_key_raw_ex(
&mut key,
private_key.as_mut_ptr(), &mut priv_sz,
public_key.as_mut_ptr(), &mut pub_sz,
X25519_LE,
);
if ret != 0 {
crate::sys::wc_curve25519_free(&mut key);
crate::sys::wc_FreeRng(&mut rng);
return Err(CryptoError::InternalError(
format!("wc_curve25519_export_key_raw_ex returned {}", ret)
));
}
crate::sys::wc_curve25519_free(&mut key);
crate::sys::wc_FreeRng(&mut rng);
@ -140,6 +153,13 @@ fn x25519_decapsulate(
}
/// Raw X25519 Diffie-Hellman: `shared = scalar_mult(private_key, public_key)`.
///
/// All byte strings are in little-endian (RFC 7748) format.
///
/// We init each `curve25519_key` with `wc_curve25519_init` (the required
/// wolfSSL pattern) before importing keys. Without the init call, wolfSSL's
/// `fe_init()` and `dp` pointer setup are skipped, causing
/// `wc_curve25519_shared_secret_ex` to return `ECC_BAD_ARG_E` (-170).
fn x25519_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
if private_key.len() != 32 || public_key.len() != 32 {
return Err(CryptoError::InvalidKey(
@ -153,38 +173,58 @@ fn x25519_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>
let mut local_key: crate::sys::curve25519_key = std::mem::zeroed();
let mut remote_key: crate::sys::curve25519_key = std::mem::zeroed();
// Import private key.
let ret = crate::sys::wc_curve25519_import_private(
private_key.as_ptr(), 32,
&mut local_key,
);
// Initialise both key structs before use (sets dp, calls fe_init, etc.).
let ret = crate::sys::wc_curve25519_init(&mut local_key);
if ret != 0 {
return Err(CryptoError::InvalidKey(
format!("wc_curve25519_import_private returned {}", ret)
return Err(CryptoError::InternalError(
format!("wc_curve25519_init (local) returned {}", ret)
));
}
let ret = crate::sys::wc_curve25519_init(&mut remote_key);
if ret != 0 {
crate::sys::wc_curve25519_free(&mut local_key);
return Err(CryptoError::InternalError(
format!("wc_curve25519_init (remote) returned {}", ret)
));
}
// Import remote public key.
let ret = crate::sys::wc_curve25519_import_public(
public_key.as_ptr(), 32,
&mut remote_key,
// Import private key in little-endian (EC25519_LITTLE_ENDIAN = 0).
let ret = crate::sys::wc_curve25519_import_private_ex(
private_key.as_ptr(), 32,
&mut local_key,
X25519_LE,
);
if ret != 0 {
crate::sys::wc_curve25519_free(&mut local_key);
crate::sys::wc_curve25519_free(&mut remote_key);
return Err(CryptoError::InvalidKey(
format!("wc_curve25519_import_public returned {}", ret)
format!("wc_curve25519_import_private_ex returned {}", ret)
));
}
// Import remote public key in little-endian.
let ret = crate::sys::wc_curve25519_import_public_ex(
public_key.as_ptr(), 32,
&mut remote_key,
X25519_LE,
);
if ret != 0 {
crate::sys::wc_curve25519_free(&mut local_key);
crate::sys::wc_curve25519_free(&mut remote_key);
return Err(CryptoError::InvalidKey(
format!("wc_curve25519_import_public_ex returned {}", ret)
));
}
let mut shared_sz = 32u32;
// EC_VALUE_SAME_KEY = 1 (little-endian) for Curve25519 in wolfCrypt
let endian = 1i32;
// Request shared secret in little-endian (EC25519_LITTLE_ENDIAN = 0).
let ret = crate::sys::wc_curve25519_shared_secret_ex(
&mut local_key,
&mut remote_key,
shared.as_mut_ptr(),
&mut shared_sz,
endian,
X25519_LE,
);
crate::sys::wc_curve25519_free(&mut local_key);
@ -206,6 +246,9 @@ fn x25519_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>
// Key sizes: private = 56 bytes, public = 56 bytes, shared secret = 56 bytes.
// ──────────────────────────────────────────────────────────────────────────────
/// EC448_LITTLE_ENDIAN = 0 (matches RFC 7748 wire format).
const X448_LE: i32 = 0;
fn x448_generate() -> Result<KemKeyPair, CryptoError> {
let mut public_key = vec![0u8; 56];
let mut private_key = vec![0u8; 56];
@ -230,11 +273,20 @@ fn x448_generate() -> Result<KemKeyPair, CryptoError> {
let mut pub_sz = public_key.len() as u32;
let mut priv_sz = private_key.len() as u32;
crate::sys::wc_curve448_export_key_raw(
// Export in little-endian (EC448_LITTLE_ENDIAN=0), RFC 7748 canonical format.
let ret = crate::sys::wc_curve448_export_key_raw_ex(
&mut key,
private_key.as_mut_ptr(), &mut priv_sz,
public_key.as_mut_ptr(), &mut pub_sz,
X448_LE,
);
if ret != 0 {
crate::sys::wc_curve448_free(&mut key);
crate::sys::wc_FreeRng(&mut rng);
return Err(CryptoError::InternalError(
format!("wc_curve448_export_key_raw_ex returned {}", ret)
));
}
crate::sys::wc_curve448_free(&mut key);
crate::sys::wc_FreeRng(&mut rng);
@ -259,6 +311,12 @@ fn x448_decapsulate(
x448_dh(private_key, peer_ephemeral_pub)
}
/// Raw X448 Diffie-Hellman: `shared = scalar_mult(private_key, public_key)`.
///
/// All byte strings are in little-endian (RFC 7748) format.
///
/// We init each `curve448_key` with `wc_curve448_init` (the required wolfSSL
/// pattern) before importing keys.
fn x448_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
if private_key.len() != 56 || public_key.len() != 56 {
return Err(CryptoError::InvalidKey("X448: keys must be 56 bytes each".into()));
@ -270,33 +328,53 @@ fn x448_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>,
let mut local_key: crate::sys::curve448_key = std::mem::zeroed();
let mut remote_key: crate::sys::curve448_key = std::mem::zeroed();
let ret = crate::sys::wc_curve448_import_private(
private_key.as_ptr(), 56, &mut local_key,
);
// Initialise both key structs before use.
let ret = crate::sys::wc_curve448_init(&mut local_key);
if ret != 0 {
return Err(CryptoError::InvalidKey(
format!("wc_curve448_import_private returned {}", ret)
return Err(CryptoError::InternalError(
format!("wc_curve448_init (local) returned {}", ret)
));
}
let ret = crate::sys::wc_curve448_init(&mut remote_key);
if ret != 0 {
crate::sys::wc_curve448_free(&mut local_key);
return Err(CryptoError::InternalError(
format!("wc_curve448_init (remote) returned {}", ret)
));
}
let ret = crate::sys::wc_curve448_import_public(
public_key.as_ptr(), 56, &mut remote_key,
// Import private key in little-endian (EC448_LITTLE_ENDIAN = 0).
let ret = crate::sys::wc_curve448_import_private_ex(
private_key.as_ptr(), 56, &mut local_key, X448_LE,
);
if ret != 0 {
crate::sys::wc_curve448_free(&mut local_key);
crate::sys::wc_curve448_free(&mut remote_key);
return Err(CryptoError::InvalidKey(
format!("wc_curve448_import_public returned {}", ret)
format!("wc_curve448_import_private_ex returned {}", ret)
));
}
// Import remote public key in little-endian.
let ret = crate::sys::wc_curve448_import_public_ex(
public_key.as_ptr(), 56, &mut remote_key, X448_LE,
);
if ret != 0 {
crate::sys::wc_curve448_free(&mut local_key);
crate::sys::wc_curve448_free(&mut remote_key);
return Err(CryptoError::InvalidKey(
format!("wc_curve448_import_public_ex returned {}", ret)
));
}
let mut shared_sz = 56u32;
let endian = 1i32; // EC_VALUE_SAME_KEY little-endian
// Request shared secret in little-endian (EC448_LITTLE_ENDIAN = 0).
let ret = crate::sys::wc_curve448_shared_secret_ex(
&mut local_key,
&mut remote_key,
shared.as_mut_ptr(),
&mut shared_sz,
endian,
X448_LE,
);
crate::sys::wc_curve448_free(&mut local_key);

View File

@ -22,6 +22,34 @@ pub mod provider;
// When wolfSSL headers are not present (e.g. docs.rs), use a stub.
#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals, dead_code, clippy::all)]
mod sys {
// ── Manual ABI-critical type definitions ────────────────────────────────
//
// `ECPoint` contains `ALIGN16 byte point[32]` in the wolfCrypt C headers.
// bindgen does not propagate `__attribute__((aligned(16)))` from a struct
// *field* to the generated Rust type; it emits a plain 33-byte struct.
// wolfCrypt's actual ECPoint is 48 bytes (32 + 1 + 15-byte tail-padding)
// with 16-byte alignment. Without the correct size/alignment, every
// field that follows ECPoint in `curve25519_key` (including `k` and the
// `pubSet`/`privSet` bitfields) lands at the wrong offset relative to the
// compiled C library, causing `wc_curve25519_shared_secret_ex` to return
// `ECC_BAD_ARG_E` (-170) because it reads `pubSet`/`privSet` from the
// wrong memory location.
//
// We blocklist ECPoint in build.rs so bindgen does not emit it, then
// define it here (before the `include!`) with the correct alignment.
// The struct is used only as a field inside `curve25519_key`; we never
// manipulate its internals directly from Rust.
#[repr(C, align(16))]
#[derive(Copy, Clone)]
pub struct ECPoint {
/// u-coordinate of the Curve25519 public key (little-endian).
pub point: [u8; 32],
/// Length tag used internally by wolfCrypt; always 32 for Curve25519.
pub pointSz: u8,
// 15 bytes of implicit tail-padding inserted by Rust to satisfy
// align(16), matching the C compiler's layout exactly.
}
// In a real build this pulls in the bindgen-generated file.
// The `include!` macro resolves at compile time to the OUT_DIR path.
// We guard it behind a cfg so the module still compiles without headers.

View File

@ -11,8 +11,8 @@
//! Exit code 0 = all vectors passed, 1 = at least one failure.
use ccc_crypto_core::{
algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, MacAlgorithm},
provider::{AeadProvider, HashProvider, KdfProvider, MacProvider},
algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm},
provider::{AeadProvider, HashProvider, KdfProvider, KemProvider, MacProvider},
};
use ccc_crypto_wolfssl::provider::WolfSslProvider;
@ -55,6 +55,27 @@ struct HashVec {
expected: &'static str,
}
/// RFC 7748 DH test vector.
///
/// `decapsulate(algo, alice_private, bob_public)` must equal `expected_shared`.
/// Both sides are symmetric: `decapsulate(algo, bob_private, alice_public)` is
/// verified as a second assertion in `run_kem()`.
struct KemDhVec {
name: &'static str,
algo: KemAlgorithm,
/// Alice's static private key (little-endian).
alice_private: &'static str,
/// Alice's corresponding public key (little-endian), used for the Bob→Alice
/// direction check.
alice_public: &'static str,
/// Bob's static private key (little-endian).
bob_private: &'static str,
/// Bob's corresponding public key (little-endian).
bob_public: &'static str,
/// Expected shared secret (little-endian).
expected_shared: &'static str,
}
// ──────────────────────────────────────────────────────────────────────────────
// AEAD vectors — NIST SP 800-38D and RFC 8439
// ──────────────────────────────────────────────────────────────────────────────
@ -206,6 +227,85 @@ static HASH_VECS: &[HashVec] = &[
},
];
// ──────────────────────────────────────────────────────────────────────────────
// KEM DH vectors — RFC 7748 §6.1 (X25519) and §6.2 (X448)
//
// All byte strings are little-endian (the canonical wire format for both
// X25519 and X448 per RFC 7748). The test calls:
// decapsulate(algo, alice_private, bob_public) == expected_shared
// decapsulate(algo, bob_private, alice_public) == expected_shared
// ──────────────────────────────────────────────────────────────────────────────
static KEM_DH_VECS: &[KemDhVec] = &[
// ── RFC 7748 §6.1 — X25519 ───────────────────────────────────────────────
KemDhVec {
name: "X25519 DH RFC 7748 §6.1",
algo: KemAlgorithm::X25519,
alice_private: "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a",
alice_public: "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a",
bob_private: "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb",
bob_public: "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f",
expected_shared: "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742",
},
// ── RFC 7748 §6.2 — X448 ────────────────────────────────────────────────
KemDhVec {
name: "X448 DH RFC 7748 §6.2",
algo: KemAlgorithm::X448,
alice_private: "9a8f4925d1519f5775cf46b04b5800d4\
ee9ee8bae8bc5565d498c28dd9c9baf5\
74a9419744897391006382a6f127ab1d\
9ac2d8c0a598726b",
alice_public: "9b08f7cc31b7e3e67d22d5aea121074a\
273bd2b83de09c63faa73d2c22c5d9bb\
c836647241d953d40c5b12da88120d53\
177f80e532c41fa0",
bob_private: "1c306a7ac2a0e2e0990b294470cba339\
e6453772b075811d8fad0d1d6927c120\
bb5ee8972b0d3e21374c9c921b09d1b0\
366f10b65173992d",
bob_public: "3eb7a829b0cd20f5bcfc0b599b6feccf\
6da4627107bdb0d4f345b43027d8b972\
fc3e34fb4232a13ca706dcb57aec3dae\
07bdc1c67bf33609",
expected_shared: "07fff4181ac6cc95ec1c16a94a0f74d1\
2da232ce40a77552281d282bb60c0b56\
fd2464c335543936521c24403085d59a\
449a5037514a879d",
},
];
// ──────────────────────────────────────────────────────────────────────────────
// XChaCha20-Poly1305 extended-nonce probe inputs
//
// These are used by `run_xchacha20_kat()` to compute and print the expected
// ciphertext+tag. On first run the printed value is verified against
// draft-irtf-cfrg-xchacha-03 §A.3 and then pinned in the AEAD_VECS above.
//
// Nonce is 24 bytes (the defining property of XChaCha20).
// ──────────────────────────────────────────────────────────────────────────────
/// Key-agreement-test input for XChaCha20-Poly1305 extended-nonce path.
struct XChaChaProbe {
name: &'static str,
key: &'static str, // 32 bytes
nonce: &'static str, // 24 bytes ← extended nonce distinguishes XChaCha
aad: &'static str,
pt: &'static str,
}
static XCHACHA20_PROBES: &[XChaChaProbe] = &[
// Uses the same key/aad/pt as the ChaCha20 RFC 8439 §2.8.2 vector but
// extends the nonce to 24 bytes, exercising the HChaCha20 subkey path.
XChaChaProbe {
name: "XChaCha20-Poly1305 extended-nonce roundtrip",
key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
nonce: "404142434445464748494a4b4c4d4e4f5051525354555657", // 24 bytes
aad: "50515253c0c1c2c3c4c5c6c7",
pt: "4c616469657320616e642047656e746c656d656e206f662074686520636c617373\
206f6620273939", // "Ladies and Gentlemen of the class of '99"
},
];
// ──────────────────────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────────────────────
@ -309,6 +409,130 @@ fn run_hash(p: &WolfSslProvider, failures: &mut usize) {
}
}
/// RFC 7748 X25519/X448 DH known-answer tests.
///
/// For each vector we verify two DH directions:
/// - `decapsulate(alice_priv, bob_pub)` == expected_shared
/// - `decapsulate(bob_priv, alice_pub)` == expected_shared
///
/// In our KEM API the "ciphertext" passed to `decapsulate` is the peer's
/// ephemeral public key — identical to a raw DH operation, which is exactly
/// what X25519/X448 encapsulation performs.
fn run_kem(p: &WolfSslProvider, failures: &mut usize) {
println!("\n── KEM DH (RFC 7748) ────────────────────────────────────────────────");
for v in KEM_DH_VECS {
let alice_priv = from_hex(v.alice_private);
let alice_pub = from_hex(v.alice_public);
let bob_priv = from_hex(v.bob_private);
let bob_pub = from_hex(v.bob_public);
let expected = from_hex(v.expected_shared);
// Alice→Bob direction.
let test_a = format!("{} (Alice→Bob)", v.name);
match p.decapsulate(v.algo, &alice_priv, &bob_pub) {
Ok(shared) if shared.as_slice() == expected.as_slice() => pass(&test_a),
Ok(shared) => { *failures += 1; fail(&test_a, &shared, &expected); }
Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", test_a); }
}
// Bob→Alice direction (symmetric).
let test_b = format!("{} (Bob→Alice)", v.name);
match p.decapsulate(v.algo, &bob_priv, &alice_pub) {
Ok(shared) if shared.as_slice() == expected.as_slice() => pass(&test_b),
Ok(shared) => { *failures += 1; fail(&test_b, &shared, &expected); }
Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", test_b); }
}
}
}
/// KEM ephemeral roundtrip self-consistency check.
///
/// Generates a random key pair per algorithm, encapsulates and decapsulates,
/// and confirms the shared secret is identical. Not a KAT — validates the
/// encap/decap path is wired correctly end-to-end.
fn run_kem_roundtrip(p: &WolfSslProvider, failures: &mut usize) {
println!("\n── KEM Roundtrip ────────────────────────────────────────────────────");
for algo in [KemAlgorithm::X25519, KemAlgorithm::X448] {
let name = format!("{:?} ephemeral roundtrip", algo);
match p.generate_keypair(algo) {
Err(e) => { *failures += 1; println!(" [FAIL] {} (keygen: {e})", name); continue; }
Ok(kp) => {
match p.encapsulate(algo, &kp.public_key) {
Err(e) => { *failures += 1; println!(" [FAIL] {} (encap: {e})", name); }
Ok(encap) => {
match p.decapsulate(algo, &kp.private_key, &encap.ciphertext) {
Err(e) => { *failures += 1; println!(" [FAIL] {} (decap: {e})", name); }
Ok(decap_secret) => {
if decap_secret.as_slice() == encap.shared_secret.as_slice() {
pass(&name);
} else {
*failures += 1;
println!(" [FAIL] {} (shared-secret mismatch)", name);
println!(" encap: {}", hex::encode(&encap.shared_secret));
println!(" decap: {}", hex::encode(&decap_secret));
}
}
}
}
}
}
}
}
}
/// XChaCha20-Poly1305 extended-nonce functional conformance tests.
///
/// This function runs two sub-checks per probe input:
///
/// 1. **Roundtrip**: encrypt → decrypt → compare to original plaintext.
/// 2. **Auth failure**: tamper one byte of the ciphertext+tag and confirm
/// that `decrypt` returns `AuthenticationFailed`.
///
/// The output ciphertext+tag is also printed in hex so the caller can pin it
/// as a known-answer test once verified against an external reference
/// (e.g. libsodium or the draft-irtf-cfrg-xchacha-03 §A.3 appendix).
fn run_xchacha20_kat(p: &WolfSslProvider, failures: &mut usize) {
println!("\n── XChaCha20-Poly1305 extended-nonce ────────────────────────────────");
for v in XCHACHA20_PROBES {
let key = from_hex(v.key);
let nonce = from_hex(v.nonce);
let aad = from_hex(v.aad);
let pt = from_hex(v.pt);
// ── Roundtrip ──────────────────────────────────────────────────────
let rt_name = format!("{} [roundtrip]", v.name);
match p.encrypt_aead(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &pt, &aad) {
Err(e) => {
*failures += 1;
println!(" [FAIL] {} (encrypt: {e})", rt_name);
continue;
}
Ok(ct_tag) => {
// Print the ct_tag for pinning purposes.
println!(" [INFO] {} ct_tag = {}", v.name, hex::encode(&ct_tag));
match p.decrypt_aead(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &ct_tag, &aad) {
Ok(recovered) if recovered == pt => pass(&rt_name),
Ok(_) => { *failures += 1; println!(" [FAIL] {} (decrypt mismatch)", rt_name); }
Err(e) => { *failures += 1; println!(" [FAIL] {} (decrypt error: {e})", rt_name); }
}
// ── Auth-failure check ────────────────────────────────────
if !ct_tag.is_empty() {
let auth_name = format!("{} [auth-fail]", v.name);
let mut tampered = ct_tag.clone();
*tampered.last_mut().unwrap() ^= 0xff;
match p.decrypt_aead(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &tampered, &aad) {
Err(ccc_crypto_core::error::CryptoError::AuthenticationFailed) => pass(&auth_name),
Ok(_) => { *failures += 1; println!(" [FAIL] {} (expected auth failure, got Ok)", auth_name); }
Err(e) => { *failures += 1; println!(" [FAIL] {} (wrong error type: {e})", auth_name); }
}
}
}
}
}
}
// ──────────────────────────────────────────────────────────────────────────────
// Entry point
// ──────────────────────────────────────────────────────────────────────────────
@ -329,6 +553,9 @@ fn main() {
run_kdf(&p, &mut failures);
run_mac(&p, &mut failures);
run_hash(&p, &mut failures);
run_kem(&p, &mut failures);
run_kem_roundtrip(&p, &mut failures);
run_xchacha20_kat(&p, &mut failures);
println!();
if failures == 0 {