From cbad181ae625cc7cd21afcbe40afdaf865d9b19a Mon Sep 17 00:00:00 2001 From: JohnE Date: Thu, 16 Apr 2026 16:31:05 -0700 Subject: [PATCH] FIX: wc_XChaCha20Poly1305_Init issues with null AAD when length 0, aad.as_prt() always passed now; also added mroe tests --- crates/ccc-crypto-wolfssl/src/aead.rs | 297 +++++++++++++++++++++++++- 1 file changed, 287 insertions(+), 10 deletions(-) diff --git a/crates/ccc-crypto-wolfssl/src/aead.rs b/crates/ccc-crypto-wolfssl/src/aead.rs index c4aefd3..c4c2c25 100644 --- a/crates/ccc-crypto-wolfssl/src/aead.rs +++ b/crates/ccc-crypto-wolfssl/src/aead.rs @@ -343,16 +343,18 @@ fn xchacha20_poly1305_encrypt( unsafe { // wc_XChaCha20Poly1305_Encrypt(dst, dst_space, src, src_len, // ad, ad_len, nonce, nonce_len, key, key_len) + // + // NOTE: wolfSSL's wc_XChaCha20Poly1305_Init unconditionally rejects + // a NULL `ad` pointer (BAD_FUNC_ARG / -173), unlike AES-GCM and + // ChaCha20-Poly1305 which allow NULL when ad_len == 0. Always pass + // aad.as_ptr() — Rust guarantees a non-null dangling pointer for + // empty slices. let ret = crate::sys::wc_XChaCha20Poly1305_Encrypt( dst.as_mut_ptr(), dst.len(), plaintext.as_ptr(), plaintext.len(), - if aad.is_empty() { - std::ptr::null() - } else { - aad.as_ptr() - }, + aad.as_ptr(), aad.len(), nonce.as_ptr(), nonce.len(), @@ -387,16 +389,14 @@ fn xchacha20_poly1305_decrypt( unsafe { // wc_XChaCha20Poly1305_Decrypt(dst, dst_space, src, src_len, // ad, ad_len, nonce, nonce_len, key, key_len) + // + // NOTE: See encrypt above — wolfSSL rejects NULL ad for XChaCha20. let ret = crate::sys::wc_XChaCha20Poly1305_Decrypt( dst.as_mut_ptr(), dst.len(), ciphertext_and_tag.as_ptr(), ciphertext_and_tag.len(), - if aad.is_empty() { - std::ptr::null() - } else { - aad.as_ptr() - }, + aad.as_ptr(), aad.len(), nonce.as_ptr(), nonce.len(), @@ -410,3 +410,280 @@ fn xchacha20_poly1305_decrypt( Ok(dst) } + +// ────────────────────────────────────────────────────────────────────────────── +// Tests +// ────────────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use ccc_crypto_core::algorithms::AeadAlgorithm; + + // ── Helpers ────────────────────────────────────────────────────────── + + /// Encrypt then decrypt and assert the plaintext round-trips. + fn roundtrip(algo: AeadAlgorithm, key: &[u8], nonce: &[u8], pt: &[u8], aad: &[u8]) { + let ct = encrypt(algo, key, nonce, pt, aad) + .unwrap_or_else(|e| panic!("{} encrypt failed: {e}", algo.name())); + + // ciphertext must be plaintext + 16-byte tag + assert_eq!( + ct.len(), + pt.len() + TAG_LEN, + "{}: unexpected ciphertext length", + algo.name() + ); + + let recovered = decrypt(algo, key, nonce, &ct, aad) + .unwrap_or_else(|e| panic!("{} decrypt failed: {e}", algo.name())); + assert_eq!(recovered, pt, "{}: plaintext mismatch after roundtrip", algo.name()); + } + + // ── AES-256-GCM ───────────────────────────────────────────────────── + + #[test] + fn aes_gcm_roundtrip_with_aad() { + roundtrip( + AeadAlgorithm::AesGcm256, + &[0x01; 32], + &[0x02; 12], + b"aes-gcm payload with associated data", + b"some associated data", + ); + } + + #[test] + fn aes_gcm_roundtrip_empty_aad() { + roundtrip( + AeadAlgorithm::AesGcm256, + &[0x11; 32], + &[0x22; 12], + b"aes-gcm payload no aad", + &[], + ); + } + + #[test] + fn aes_gcm_roundtrip_empty_plaintext() { + roundtrip( + AeadAlgorithm::AesGcm256, + &[0x31; 32], + &[0x32; 12], + &[], + b"aad only, no plaintext", + ); + } + + #[test] + fn aes_gcm_roundtrip_empty_both() { + roundtrip( + AeadAlgorithm::AesGcm256, + &[0x41; 32], + &[0x42; 12], + &[], + &[], + ); + } + + #[test] + fn aes_gcm_tampered_tag_fails() { + let key = [0x51u8; 32]; + let nonce = [0x52u8; 12]; + let pt = b"integrity check"; + let mut ct = encrypt(AeadAlgorithm::AesGcm256, &key, &nonce, pt, &[]).unwrap(); + // Flip last byte of the tag + let last = ct.len() - 1; + ct[last] ^= 0xFF; + assert!(decrypt(AeadAlgorithm::AesGcm256, &key, &nonce, &ct, &[]).is_err()); + } + + // ── ChaCha20-Poly1305 ─────────────────────────────────────────────── + + #[test] + fn chacha20_roundtrip_with_aad() { + roundtrip( + AeadAlgorithm::ChaCha20Poly1305, + &[0x61; 32], + &[0x62; 12], + b"chacha20 payload with associated data", + b"chacha20 aad", + ); + } + + #[test] + fn chacha20_roundtrip_empty_aad() { + roundtrip( + AeadAlgorithm::ChaCha20Poly1305, + &[0x71; 32], + &[0x72; 12], + b"chacha20 payload no aad", + &[], + ); + } + + #[test] + fn chacha20_roundtrip_empty_plaintext() { + roundtrip( + AeadAlgorithm::ChaCha20Poly1305, + &[0x81; 32], + &[0x82; 12], + &[], + b"chacha20 aad only", + ); + } + + #[test] + fn chacha20_roundtrip_empty_both() { + roundtrip( + AeadAlgorithm::ChaCha20Poly1305, + &[0x91; 32], + &[0x92; 12], + &[], + &[], + ); + } + + #[test] + fn chacha20_tampered_tag_fails() { + let key = [0xA1u8; 32]; + let nonce = [0xA2u8; 12]; + let pt = b"integrity check chacha"; + let mut ct = encrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce, pt, &[]).unwrap(); + let last = ct.len() - 1; + ct[last] ^= 0xFF; + assert!(decrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce, &ct, &[]).is_err()); + } + + // ── XChaCha20-Poly1305 ────────────────────────────────────────────── + + #[test] + fn xchacha20_roundtrip_with_aad() { + roundtrip( + AeadAlgorithm::XChaCha20Poly1305, + &[0xB1; 32], + &[0xB2; 24], + b"xchacha20 payload with associated data", + b"xchacha20 aad", + ); + } + + /// Regression: wolfSSL's `wc_XChaCha20Poly1305_Init` unconditionally + /// rejects a NULL `ad` pointer (BAD_FUNC_ARG / -173), even when + /// `ad_len == 0`. Must pass a non-null dangling pointer for empty AAD. + #[test] + fn xchacha20_roundtrip_empty_aad() { + roundtrip( + AeadAlgorithm::XChaCha20Poly1305, + &[0xC1; 32], + &[0xC2; 24], + b"xchacha20 payload no aad", + &[], + ); + } + + #[test] + fn xchacha20_roundtrip_empty_plaintext() { + roundtrip( + AeadAlgorithm::XChaCha20Poly1305, + &[0xD1; 32], + &[0xD2; 24], + &[], + b"xchacha20 aad only", + ); + } + + /// Regression: both AAD and plaintext empty — exercises the NULL-pointer + /// code paths simultaneously. + #[test] + fn xchacha20_roundtrip_empty_both() { + roundtrip( + AeadAlgorithm::XChaCha20Poly1305, + &[0xE1; 32], + &[0xE2; 24], + &[], + &[], + ); + } + + #[test] + fn xchacha20_tampered_tag_fails() { + let key = [0xF1u8; 32]; + let nonce = [0xF2u8; 24]; + let pt = b"integrity check xchacha"; + let mut ct = encrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, pt, &[]).unwrap(); + let last = ct.len() - 1; + ct[last] ^= 0xFF; + assert!(decrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &ct, &[]).is_err()); + } + + #[test] + fn xchacha20_wrong_aad_fails() { + let key = [0xF3u8; 32]; + let nonce = [0xF4u8; 24]; + let pt = b"aad mismatch test"; + let ct = encrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, pt, b"correct").unwrap(); + assert!(decrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &ct, b"wrong").is_err()); + } + + // ── Key / nonce validation ────────────────────────────────────────── + + #[test] + fn aes_gcm_wrong_key_len_rejected() { + assert!(encrypt(AeadAlgorithm::AesGcm256, &[0; 16], &[0; 12], b"x", &[]).is_err()); + } + + #[test] + fn aes_gcm_wrong_nonce_len_rejected() { + assert!(encrypt(AeadAlgorithm::AesGcm256, &[0; 32], &[0; 8], b"x", &[]).is_err()); + } + + #[test] + fn chacha20_wrong_key_len_rejected() { + assert!(encrypt(AeadAlgorithm::ChaCha20Poly1305, &[0; 16], &[0; 12], b"x", &[]).is_err()); + } + + #[test] + fn chacha20_wrong_nonce_len_rejected() { + assert!(encrypt(AeadAlgorithm::ChaCha20Poly1305, &[0; 32], &[0; 8], b"x", &[]).is_err()); + } + + #[test] + fn xchacha20_wrong_key_len_rejected() { + assert!(encrypt(AeadAlgorithm::XChaCha20Poly1305, &[0; 16], &[0; 24], b"x", &[]).is_err()); + } + + #[test] + fn xchacha20_wrong_nonce_len_rejected() { + assert!(encrypt(AeadAlgorithm::XChaCha20Poly1305, &[0; 32], &[0; 12], b"x", &[]).is_err()); + } + + // ── Large payload ─────────────────────────────────────────────────── + + #[test] + fn xchacha20_large_payload_roundtrip() { + let key = [0xAAu8; 32]; + let nonce = [0xBBu8; 24]; + let pt = vec![0x42u8; 64 * 1024]; // 64 KiB — exercises the 16k chunked loop + roundtrip(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &pt, b"large"); + } + + // ── Cross-algorithm isolation ─────────────────────────────────────── + + #[test] + fn ciphertext_not_interchangeable_between_algorithms() { + let key = [0xDD; 32]; + let nonce_12 = [0xEE; 12]; + let nonce_24 = [0xEE; 24]; + let pt = b"cross-algo test"; + + let ct_aes = encrypt(AeadAlgorithm::AesGcm256, &key, &nonce_12, pt, &[]).unwrap(); + let ct_chacha = encrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce_12, pt, &[]).unwrap(); + let ct_xchacha = encrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce_24, pt, &[]).unwrap(); + + // Each ciphertext must fail to decrypt under a different algorithm + assert!(decrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce_12, &ct_aes, &[]).is_err()); + assert!(decrypt(AeadAlgorithm::AesGcm256, &key, &nonce_12, &ct_chacha, &[]).is_err()); + assert!(decrypt(AeadAlgorithm::AesGcm256, &key, &nonce_24, &ct_xchacha, &[]).is_err()); + } +}