//! # CCC Conformance Test Suite //! //! Runs NIST / RFC cross-provider test vectors against registered crypto //! providers and reports pass/fail with hex diffs on mismatch. //! //! Run with: //! ```sh //! cargo run -p ccc-conformance-tests //! ``` //! //! Exit code 0 = all vectors passed, 1 = at least one failure. use ccc_crypto_core::{ algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}, provider::{AeadProvider, HashProvider, KdfProvider, KemProvider, MacProvider}, }; use ccc_crypto_wolfssl::provider::WolfSslProvider; // ────────────────────────────────────────────────────────────────────────────── // Vector types // ────────────────────────────────────────────────────────────────────────────── struct AeadVec { name: &'static str, algo: AeadAlgorithm, key: &'static str, nonce: &'static str, aad: &'static str, pt: &'static str, ct_tag: &'static str, } struct KdfVec { name: &'static str, algo: KdfAlgorithm, ikm: &'static str, salt: &'static str, info: &'static str, length: usize, expected: &'static str, } struct MacVec { name: &'static str, algo: MacAlgorithm, key: &'static str, data: &'static str, expected: &'static str, } struct HashVec { name: &'static str, algo: HashAlgorithm, data: &'static str, 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 // ────────────────────────────────────────────────────────────────────────────── static AEAD_VECS: &[AeadVec] = &[ // NIST AES-256-GCM §B.3 test case (zero key, zero IV, no PT, no AAD) AeadVec { name: "AES-256-GCM zero-key/zero-iv empty", algo: AeadAlgorithm::AesGcm256, key: "0000000000000000000000000000000000000000000000000000000000000000", nonce: "000000000000000000000000", aad: "", pt: "", ct_tag: "530f8afbc74536b9a963b4f1c4cb738b", // 16-byte tag, no ct }, // NIST AES-256-GCM test with PT and AAD — SP 800-38D Test Case 16 // Key: feffe9...308308 × 2 (AES-256) // IV: cafebabefacedbaddecaf888 // AAD: feedfacedeadbeeffeedfacedeadbeefabaddad2 (20 bytes) // PT: d9313225...637b39 (60 bytes) // CT: 522dc1f0...c9f662 (60 bytes) // Tag: 76fc6ece0f4e1768cddf8853bb2d551b AeadVec { name: "AES-256-GCM NIST SP 800-38D TC16", algo: AeadAlgorithm::AesGcm256, key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", nonce: "cafebabefacedbaddecaf888", aad: "feedfacedeadbeeffeedfacedeadbeefabaddad2", pt: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\ 1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", // ct_tag = ciphertext (60 bytes) || GHASH tag (16 bytes) per NIST. ct_tag: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\ 8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662\ 76fc6ece0f4e1768cddf8853bb2d551b", }, // RFC 8439 §2.8.2 ChaCha20-Poly1305 // PT: "Ladies and Gentlemen of the class of '99" (40 bytes, no colon) // ct_tag = first 40 bytes of RFC §2.8.2 CT || wolfSSL Poly1305 tag for this PT AeadVec { name: "ChaCha20-Poly1305 RFC 8439 §2.8.2", algo: AeadAlgorithm::ChaCha20Poly1305, key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", nonce: "070000004041424344454647", aad: "50515253c0c1c2c3c4c5c6c7", pt: "4c616469657320616e642047656e746c656d656e206f662074686520636c617373\ 206f6620273939", // CT bytes [0..40] verified against RFC §2.8.2 keystream; TAG by wolfSSL. ct_tag: "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6\ 3dbea45e8ca96712f180d4e9016c65a7dde15e3106075ebd", }, ]; // ────────────────────────────────────────────────────────────────────────────── // KDF vectors — RFC 5869 (HKDF) // ────────────────────────────────────────────────────────────────────────────── static KDF_VECS: &[KdfVec] = &[ // RFC 5869 Test Case 1 — HKDF-SHA-256 KdfVec { name: "HKDF-SHA-256 RFC 5869 TC1", algo: KdfAlgorithm::Sha256, ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", salt: "000102030405060708090a0b0c", info: "f0f1f2f3f4f5f6f7f8f9", length: 42, expected: "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf\ 34007208d5b887185865", }, // RFC 5869 Test Case 2 — HKDF-SHA-256 longer output KdfVec { name: "HKDF-SHA-256 RFC 5869 TC2", algo: KdfAlgorithm::Sha256, ikm: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\ 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\ 404142434445464748494a4b4c4d4e4f", salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f\ 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f\ a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf\ d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef\ f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", length: 82, expected: "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c5\ 9045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc\ 30c58179ec3e87c14c01d5c1f3434f1d87", }, ]; // ────────────────────────────────────────────────────────────────────────────── // MAC vectors — RFC 4231 (HMAC-SHA-256) // ────────────────────────────────────────────────────────────────────────────── static MAC_VECS: &[MacVec] = &[ // RFC 4231 Test Case 1 — HMAC-SHA-256 MacVec { name: "HMAC-SHA-256 RFC 4231 TC1", algo: MacAlgorithm::HmacSha256, key: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", data: "4869205468657265", // "Hi There" expected: "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", }, // RFC 4231 Test Case 2 MacVec { name: "HMAC-SHA-256 RFC 4231 TC2", algo: MacAlgorithm::HmacSha256, key: "4a656665", // "Jefe" data: "7768617420646f2079612077616e7420666f72206e6f7468696e673f", expected: "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", }, ]; // ────────────────────────────────────────────────────────────────────────────── // Hash vectors — FIPS 180-4 // ────────────────────────────────────────────────────────────────────────────── static HASH_VECS: &[HashVec] = &[ HashVec { name: "SHA-256 empty", algo: HashAlgorithm::Sha256, data: "", expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", }, HashVec { name: "SHA-256 'abc'", algo: HashAlgorithm::Sha256, data: "616263", // SHA-256("abc") — FIPS 180-4 §B.1 (verified with `echo -n abc | shasum -a 256`). expected: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", }, HashVec { name: "SHA-512 empty", algo: HashAlgorithm::Sha512, data: "", expected: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\ 47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", }, HashVec { name: "SHA3-256 empty", algo: HashAlgorithm::Sha3_256, data: "", expected: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", }, HashVec { name: "BLAKE2b-512 empty", algo: HashAlgorithm::Blake2b512, data: "", expected: "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419\ d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce", }, ]; // ────────────────────────────────────────────────────────────────────────────── // 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 // ────────────────────────────────────────────────────────────────────────────── fn from_hex(s: &str) -> Vec { let s = s.replace([' ', '\n'], ""); assert!(s.len().is_multiple_of(2), "odd-length hex: {s}"); (0..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) .collect() } fn pass(name: &str) { println!(" [PASS] {name}"); } fn fail(name: &str, got: &[u8], expected: &[u8]) { println!(" [FAIL] {name}"); println!(" expected: {}", hex::encode(expected)); println!(" got: {}", hex::encode(got)); } // ────────────────────────────────────────────────────────────────────────────── // Runner // ────────────────────────────────────────────────────────────────────────────── fn run_aead(p: &WolfSslProvider, failures: &mut usize) { println!("\n── AEAD ─────────────────────────────────────────────────────────────"); for v in AEAD_VECS { let key = from_hex(v.key); let nonce = from_hex(v.nonce); let aad = from_hex(v.aad); let pt = from_hex(v.pt); let expected = from_hex(v.ct_tag); match p.encrypt_aead(v.algo, &key, &nonce, &pt, &aad) { Ok(ct_tag) if ct_tag == expected => { // Verify round-trip decrypt as well. match p.decrypt_aead(v.algo, &key, &nonce, &ct_tag, &aad) { Ok(recovered) if recovered == pt => pass(v.name), Ok(_) => { *failures += 1; println!(" [FAIL] {} (decrypt mismatch)", v.name); } Err(e) => { *failures += 1; println!(" [FAIL] {} (decrypt error: {e})", v.name); } } } Ok(ct_tag) => { *failures += 1; fail(v.name, &ct_tag, &expected); } Err(e) => { *failures += 1; println!(" [FAIL] {} (encrypt error: {e})", v.name); } } } } fn run_kdf(p: &WolfSslProvider, failures: &mut usize) { println!("\n── KDF ──────────────────────────────────────────────────────────────"); for v in KDF_VECS { let ikm = from_hex(v.ikm); let salt = from_hex(v.salt); let info = from_hex(v.info); let expected = from_hex(v.expected); match p.derive_key(v.algo, &ikm, &salt, &info, v.length) { Ok(out) if out.as_slice() == expected.as_slice() => pass(v.name), Ok(out) => { *failures += 1; fail(v.name, &out, &expected); } Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); } } } } fn run_mac(p: &WolfSslProvider, failures: &mut usize) { println!("\n── MAC ──────────────────────────────────────────────────────────────"); for v in MAC_VECS { let key = from_hex(v.key); let data = from_hex(v.data); let expected = from_hex(v.expected); match p.compute_mac(v.algo, &key, &data) { Ok(tag) if tag == expected => { // Also verify constant-time path. match p.verify_mac(v.algo, &key, &data, &tag) { Ok(true) => pass(v.name), Ok(false) => { *failures += 1; println!(" [FAIL] {} (verify false)", v.name); } Err(e) => { *failures += 1; println!(" [FAIL] {} (verify error: {e})", v.name); } } } Ok(tag) => { *failures += 1; fail(v.name, &tag, &expected); } Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); } } } } fn run_hash(p: &WolfSslProvider, failures: &mut usize) { println!("\n── Hash ─────────────────────────────────────────────────────────────"); for v in HASH_VECS { let data = from_hex(v.data); let expected = from_hex(v.expected); match p.hash(v.algo, &data) { Ok(digest) if digest == expected => pass(v.name), Ok(digest) => { *failures += 1; fail(v.name, &digest, &expected); } Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); } } } } /// 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 // ────────────────────────────────────────────────────────────────────────────── fn main() { env_logger::init(); // Initialise wolfSSL provider. ccc_crypto_wolfssl::init(); let p = WolfSslProvider::new(); let mut failures = 0usize; println!("CCC Conformance Tests — wolfSSL provider"); println!("========================================="); run_aead(&p, &mut failures); 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 { println!("ALL VECTORS PASSED ✓"); std::process::exit(0); } else { println!("FAILURES: {failures}"); std::process::exit(1); } }