diff --git a/crates/ccc-crypto-wolfssl/build.rs b/crates/ccc-crypto-wolfssl/build.rs index f39426b..f0f5371 100644 --- a/crates/ccc-crypto-wolfssl/build.rs +++ b/crates/ccc-crypto-wolfssl/build.rs @@ -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() diff --git a/crates/ccc-crypto-wolfssl/src/kem.rs b/crates/ccc-crypto-wolfssl/src/kem.rs index bf332d5..2b6e777 100644 --- a/crates/ccc-crypto-wolfssl/src/kem.rs +++ b/crates/ccc-crypto-wolfssl/src/kem.rs @@ -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 { let mut public_key = vec![0u8; 32]; let mut private_key = vec![0u8; 32]; @@ -107,11 +110,21 @@ fn x25519_generate() -> Result { 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>, 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> 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> // 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 { let mut public_key = vec![0u8; 56]; let mut private_key = vec![0u8; 56]; @@ -230,11 +273,20 @@ fn x448_generate() -> Result { 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>, 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>, 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); diff --git a/crates/ccc-crypto-wolfssl/src/lib.rs b/crates/ccc-crypto-wolfssl/src/lib.rs index ba51ca1..c268ff3 100644 --- a/crates/ccc-crypto-wolfssl/src/lib.rs +++ b/crates/ccc-crypto-wolfssl/src/lib.rs @@ -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. diff --git a/tests/conformance/src/main.rs b/tests/conformance/src/main.rs index 1ca05eb..d20858e 100644 --- a/tests/conformance/src/main.rs +++ b/tests/conformance/src/main.rs @@ -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 {