649 lines
30 KiB
Rust
649 lines
30 KiB
Rust
//! # 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<u8> {
|
||
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);
|
||
}
|
||
}
|