From 3853f7552216bda4a45f50e45646d82a2479d005 Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:15:19 +0200 Subject: [PATCH 1/5] Implement tests that generate shared secrets encrypt/decrypt values + Update some versions at cargo --- Cargo.lock | 84 +++-- Cargo.toml | 1 + nucypher-core-wasm/Cargo.toml | 2 +- nucypher-core/Cargo.toml | 3 + .../tests/fixtures/shared-secret-vectors.json | 235 ++++++++++++++ .../tests/test_encryption_vectors.rs | 305 ++++++++++++++++++ 6 files changed, 596 insertions(+), 34 deletions(-) create mode 100644 nucypher-core/tests/fixtures/shared-secret-vectors.json create mode 100644 nucypher-core/tests/test_encryption_vectors.rs diff --git a/Cargo.lock b/Cargo.lock index 2f345798..88966a3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aead" @@ -361,7 +361,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -409,7 +409,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -431,7 +431,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core 0.20.1", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -852,6 +852,12 @@ dependencies = [ "log", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "memoffset" version = "0.8.0" @@ -881,6 +887,7 @@ dependencies = [ "rand_core 0.6.4", "rmp-serde", "serde", + "serde_json", "serde_with 1.14.0", "sha2", "sha3", @@ -1014,9 +1021,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -1084,9 +1091,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -1212,6 +1219,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.14" @@ -1251,9 +1264,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -1269,22 +1282,23 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -1336,7 +1350,7 @@ dependencies = [ "darling 0.20.1", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -1414,9 +1428,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.25" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -1446,7 +1460,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -1550,26 +1564,27 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -1609,9 +1624,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1619,22 +1634,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-bindgen-test" @@ -1787,5 +1805,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] diff --git a/Cargo.toml b/Cargo.toml index d96b77dd..65f00b5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "nucypher-core", "nucypher-core-python", diff --git a/nucypher-core-wasm/Cargo.toml b/nucypher-core-wasm/Cargo.toml index 3434ed6f..6322865e 100644 --- a/nucypher-core-wasm/Cargo.toml +++ b/nucypher-core-wasm/Cargo.toml @@ -22,7 +22,7 @@ default = ["console_error_panic_hook"] umbral-pre = { version = "0.11.0", features = ["bindings-wasm"] } ferveo = { package = "ferveo-pre-release", version = "0.3.0", features = ["bindings-wasm"] } nucypher-core = { path = "../nucypher-core" } -wasm-bindgen = "0.2.86" +wasm-bindgen = "0.2.88" js-sys = "0.3.63" console_error_panic_hook = { version = "0.1", optional = true } derive_more = { version = "0.99", default-features = false, features = ["from", "as_ref"] } diff --git a/nucypher-core/Cargo.toml b/nucypher-core/Cargo.toml index 2043430b..1d5ebc22 100644 --- a/nucypher-core/Cargo.toml +++ b/nucypher-core/Cargo.toml @@ -26,3 +26,6 @@ zeroize = { version = "1.6.0", features = ["derive"] } rand_core = "0.6.4" rand_chacha = "0.3.1" rand = "0.8.5" + +[dev-dependencies] +serde_json = "1.0.219" diff --git a/nucypher-core/tests/fixtures/shared-secret-vectors.json b/nucypher-core/tests/fixtures/shared-secret-vectors.json new file mode 100644 index 00000000..8909878b --- /dev/null +++ b/nucypher-core/tests/fixtures/shared-secret-vectors.json @@ -0,0 +1,235 @@ +{ + "testVectors": [ + { + "description": "Basic encryption/decryption compatibility", + "expected_ciphertext": "0102030405060708090a0b0c30e02d19293bd79329c9232eacc4e82eca99e0ea66e4da3808bccbacb3fd2ccbce94fe0c26cbbb81be779817", + "fixed_nonce": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "id": "vector1", + "plaintext": "This is a fixed test message", + "shared_secret": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31 + ] + }, + { + "description": "Empty plaintext compatibility", + "expected_ciphertext": "102030405060708090a0b0c0d9abc79645718dc328d5c3faa129fced", + "fixed_nonce": [ + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192 + ], + "id": "vector2", + "plaintext": "", + "shared_secret": [ + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63 + ] + }, + { + "description": "Rust-generated ciphertext for TypeScript compatibility check", + "expected_plaintext": "This is a message encrypted by the Rust implementation", + "id": "vector3", + "rust_generated_ciphertext": [ + 244, + 240, + 54, + 24, + 5, + 65, + 204, + 10, + 149, + 10, + 78, + 107, + 203, + 196, + 102, + 251, + 108, + 158, + 36, + 7, + 14, + 87, + 153, + 251, + 176, + 242, + 94, + 153, + 244, + 118, + 124, + 216, + 154, + 5, + 88, + 98, + 171, + 221, + 222, + 14, + 247, + 21, + 74, + 25, + 205, + 80, + 35, + 47, + 195, + 218, + 221, + 23, + 85, + 23, + 82, + 145, + 234, + 171, + 136, + 141, + 215, + 95, + 178, + 103, + 141, + 70, + 236, + 104, + 80, + 106, + 249, + 218, + 236, + 96, + 76, + 77, + 239, + 197, + 216, + 31, + 187, + 146 + ], + "shared_secret": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31 + ] + } + ] +} \ No newline at end of file diff --git a/nucypher-core/tests/test_encryption_vectors.rs b/nucypher-core/tests/test_encryption_vectors.rs new file mode 100644 index 00000000..c8cd7569 --- /dev/null +++ b/nucypher-core/tests/test_encryption_vectors.rs @@ -0,0 +1,305 @@ +#[cfg(test)] +mod tests { + use chacha20poly1305::aead::Aead; + use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce}; + use rand::rngs::OsRng; + use rand::RngCore; // Add this import for the fill_bytes method + use serde::{Deserialize, Serialize}; + use serde_json::{json, Value}; + use std::fs; + use std::path::Path; + + // Since the dkg module is private, we reimplement the encryption/decryption functions here + // based on the implementation in src/dkg.rs + + // Structure that matches the JSON test vector format + #[derive(Serialize, Deserialize)] + struct TestVector { + id: String, + description: String, + shared_secret: Vec, + plaintext: Option, + fixed_nonce: Option>, + expected_ciphertext: Option, + rust_generated_ciphertext: Option>, + expected_plaintext: Option, + } + + #[derive(Serialize, Deserialize)] + struct TestVectors { + test_vectors: Vec, + } + + // Implementation of encrypt_with_shared_secret as found in dkg.rs + fn encrypt_with_shared_secret( + shared_secret: &[u8], + plaintext: &[u8], + ) -> Result, Box> { + let key = Key::from_slice(shared_secret); + let cipher = ChaCha20Poly1305::new(key); + + // Generate random nonce + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + // Create result with nonce + let mut result = nonce_bytes.to_vec(); + + // Encrypt plaintext + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|_| "Encryption failed: plaintext too large")?; + + // Append ciphertext to nonce + result.extend(ciphertext); + + Ok(result) + } + + // Implementation of decrypt_with_shared_secret as found in dkg.rs + fn decrypt_with_shared_secret( + shared_secret: &[u8], + ciphertext: &[u8], + ) -> Result, Box> { + if ciphertext.len() <= 12 { + return Err("The ciphertext must include the nonce".into()); + } + + let key = Key::from_slice(shared_secret); + let cipher = ChaCha20Poly1305::new(key); + + let nonce = Nonce::from_slice(&ciphertext[..12]); + let decrypt_result = cipher + .decrypt(nonce, &ciphertext[12..]) + .map_err(|_| "Decryption of ciphertext failed")?; + + Ok(decrypt_result) + } + + // Function to encrypt with fixed nonce for test vector generation + fn encrypt_with_fixed_nonce( + shared_secret: &[u8], + plaintext: &[u8], + fixed_nonce: &[u8], + ) -> Result, Box> { + let key = Key::from_slice(shared_secret); + let cipher = ChaCha20Poly1305::new(key); + + // Use the provided fixed nonce + let nonce = Nonce::from_slice(fixed_nonce); + + // Create the result starting with the nonce + let mut result = fixed_nonce.to_vec(); + + // Encrypt the plaintext with the fixed nonce + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|_| "Encryption failed: plaintext too large")?; + + // Append the ciphertext to the nonce + result.extend(ciphertext); + + Ok(result) + } + + #[test] + fn generate_test_vectors() { + println!("Generating encryption test vectors for TypeScript compatibility..."); + + // Define test vectors + let vectors = vec![ + // Vector 1: Basic encryption/decryption with fixed nonce + json!({ + "id": "vector1", + "description": "Basic encryption/decryption compatibility", + "shared_secret": (0..32).collect::>(), + "plaintext": "This is a fixed test message", + "fixed_nonce": vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + }), + // Vector 2: Empty plaintext with fixed nonce + json!({ + "id": "vector2", + "description": "Empty plaintext compatibility", + "shared_secret": (32..64).collect::>(), + "plaintext": "", + "fixed_nonce": vec![16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192], + }), + // Vector 3: For Rust-generated ciphertext compatibility using normal encryption + json!({ + "id": "vector3", + "description": "Rust-generated ciphertext for TypeScript compatibility check", + "shared_secret": (0..32).collect::>(), + "expected_plaintext": "This is a message encrypted by the Rust implementation", + }), + ]; + + // Process each vector to add encryption outputs + let mut processed_vectors = Vec::new(); + + for vector in vectors { + let mut processed = vector.clone(); + + // Extract shared secret + let shared_secret = vector["shared_secret"].as_array().unwrap(); + let shared_secret_bytes: Vec = shared_secret + .iter() + .map(|v| v.as_u64().unwrap() as u8) + .collect(); + + // Handle vector3 - generate ciphertext with standard Rust implementation + if vector["id"].as_str().unwrap() == "vector3" { + let plaintext = vector["expected_plaintext"].as_str().unwrap().as_bytes(); + + println!("Creating vector3 with Rust-generated ciphertext"); + // Standard encryption with random nonce + let ciphertext = + encrypt_with_shared_secret(&shared_secret_bytes, plaintext).unwrap(); + let ciphertext_vec = ciphertext.to_vec(); + + // Add the Rust-generated ciphertext to the vector + processed["rust_generated_ciphertext"] = json!(ciphertext_vec); + processed_vectors.push(processed); + continue; + } + + // For vectors 1 & 2, use fixed nonces + if let (Some(plaintext_str), Some(fixed_nonce)) = ( + vector["plaintext"].as_str(), + vector["fixed_nonce"].as_array(), + ) { + let plaintext = plaintext_str.as_bytes(); + let fixed_nonce_bytes: Vec = fixed_nonce + .iter() + .map(|v| v.as_u64().unwrap() as u8) + .collect(); + + println!("Processing vector {} with fixed nonce", vector["id"]); + + // Generate ciphertext with fixed nonce + match encrypt_with_fixed_nonce(&shared_secret_bytes, plaintext, &fixed_nonce_bytes) + { + Ok(ciphertext) => { + // Convert ciphertext to hex string for expected_ciphertext + let ciphertext_hex = hex::encode(&ciphertext); + processed["expected_ciphertext"] = json!(ciphertext_hex); + println!(" ✓ Successfully generated ciphertext with fixed nonce"); + } + Err(e) => { + eprintln!("Error encrypting vector {}: {}", vector["id"], e); + } + } + } + + processed_vectors.push(processed); + } + + // Create the final JSON structure with camelCase keys for TypeScript + let final_json = json!({ + "testVectors": processed_vectors + }); + + // Format the JSON with pretty-printing + let formatted_json = serde_json::to_string_pretty(&final_json).unwrap(); + + // Path for the output file + let output_dir = Path::new("tests/fixtures"); + let output_file = output_dir.join("shared-secret-vectors.json"); + + // Create directory if it doesn't exist + fs::create_dir_all(output_dir).expect("Failed to create output directory"); + + // Save to file in the Rust project first + fs::write(&output_file, &formatted_json).expect("Unable to write test vectors file"); + println!("Test vectors saved to {:?}", output_file); + + // Also save to TypeScript project if path exists + let ts_path = + Path::new("../taco-web/packages/shared/test/fixtures/shared-secret-vectors.json"); + if let Ok(()) = fs::write(ts_path, &formatted_json) { + println!( + "Test vectors also copied to TypeScript project: {:?}", + ts_path + ); + } else { + println!( + "Note: Couldn't copy to TypeScript project. You'll need to manually copy the file." + ); + } + + // Verify vectors by decrypting + verify_test_vectors(&final_json); + + println!("\nInstructions for manually copying test vectors:"); + println!("1. The file has been saved to: {:?}", output_file); + println!( + "2. Copy it to: ../taco-web/packages/shared/test/fixtures/shared-secret-vectors.json" + ); + println!("3. Run the TypeScript tests to verify compatibility"); + } + + fn verify_test_vectors(test_vectors_json: &Value) { + println!("\nVerifying test vectors..."); + let vectors = test_vectors_json["testVectors"].as_array().unwrap(); + + for vector in vectors { + let id = vector["id"].as_str().unwrap(); + let shared_secret = vector["shared_secret"].as_array().unwrap(); + let shared_secret_bytes: Vec = shared_secret + .iter() + .map(|v| v.as_u64().unwrap() as u8) + .collect(); + + println!("Verifying vector: {}", id); + + // Verify vector3 with rust-generated ciphertext + if id == "vector3" && vector["rust_generated_ciphertext"].is_array() { + let ciphertext_json = vector["rust_generated_ciphertext"].as_array().unwrap(); + let ciphertext: Vec = ciphertext_json + .iter() + .map(|v| v.as_u64().unwrap() as u8) + .collect(); + + let expected_plaintext = vector["expected_plaintext"].as_str().unwrap(); + + match decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { + Ok(decrypted) => { + let decrypted_str = String::from_utf8_lossy(&decrypted); + assert_eq!( + decrypted_str, expected_plaintext, + "Decryption mismatch for vector {}", + id + ); + println!(" ✓ Successfully verified rust-generated ciphertext"); + } + Err(e) => { + panic!("Failed to decrypt rust-generated ciphertext: {:?}", e); + } + } + continue; + } + + // Verify vectors with expected_ciphertext + if let Some(ciphertext_hex) = vector["expected_ciphertext"].as_str() { + let ciphertext = hex::decode(ciphertext_hex).unwrap(); + let plaintext = vector["plaintext"].as_str().unwrap().as_bytes(); + + match decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { + Ok(decrypted) => { + assert_eq!( + &decrypted, plaintext, + "Decryption mismatch for vector {}", + id + ); + println!(" ✓ Successfully verified expected_ciphertext"); + } + Err(e) => { + panic!("Failed to decrypt expected_ciphertext: {:?}", e); + } + } + } + } + + println!("All test vectors verified successfully!"); + } +} From 1c3ceb8dcf5b658d86cecb292eca611bf7a4a5e6 Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:53:21 +0200 Subject: [PATCH 2/5] fix serde_json mistaken version --- nucypher-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher-core/Cargo.toml b/nucypher-core/Cargo.toml index 1d5ebc22..5f0cc378 100644 --- a/nucypher-core/Cargo.toml +++ b/nucypher-core/Cargo.toml @@ -28,4 +28,4 @@ rand_chacha = "0.3.1" rand = "0.8.5" [dev-dependencies] -serde_json = "1.0.219" +serde_json = "1.0.140" From be53022ce501bf95dadac0d7eee2b04bd08fc09a Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:57:02 +0200 Subject: [PATCH 3/5] modify tests to use code from source instead of copying --- nucypher-core/src/dkg.rs | 35 +++++++- nucypher-core/src/lib.rs | 1 + .../tests/test_encryption_vectors.rs | 81 ++++++++----------- 3 files changed, 66 insertions(+), 51 deletions(-) diff --git a/nucypher-core/src/dkg.rs b/nucypher-core/src/dkg.rs index 43fd127b..a3e6243d 100644 --- a/nucypher-core/src/dkg.rs +++ b/nucypher-core/src/dkg.rs @@ -64,7 +64,10 @@ impl fmt::Display for DecryptionError { type NonceSize = ::NonceSize; -fn encrypt_with_shared_secret( +/// Encrypt data using the provided shared secret. +/// +/// The ciphertext consists of a randomly generated nonce followed by the encrypted data. +pub fn encrypt_with_shared_secret( shared_secret: &SessionSharedSecret, plaintext: &[u8], ) -> Result, EncryptionError> { @@ -79,7 +82,10 @@ fn encrypt_with_shared_secret( Ok(result.into_boxed_slice()) } -fn decrypt_with_shared_secret( +/// Decrypt data using the provided shared secret. +/// +/// The ciphertext is expected to start with a nonce, followed by the encrypted data. +pub fn decrypt_with_shared_secret( shared_secret: &SessionSharedSecret, ciphertext: &[u8], ) -> Result, DecryptionError> { @@ -138,6 +144,31 @@ pub mod session { Self { derived_bytes } } + /// Create a shared secret directly from raw bytes for testing purposes. + /// + /// This bypasses the normal key derivation process and should only be used for + /// testing with known byte vectors. + #[cfg(test)] + pub fn from_bytes(bytes: &[u8]) -> Self { + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes[0..32]); + Self { + derived_bytes: array, + } + } + + /// Create a shared secret directly from raw bytes for test vectors. + /// + /// This is a public API only intended for use in test vectors. It bypasses + /// the normal key derivation process to allow for deterministic tests. + pub fn from_test_vector(bytes: &[u8]) -> Self { + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes[0..32]); + Self { + derived_bytes: array, + } + } + /// View this shared secret as a byte array. pub fn as_bytes(&self) -> &[u8; 32] { &self.derived_bytes diff --git a/nucypher-core/src/lib.rs b/nucypher-core/src/lib.rs index da0fb1a6..081ba5f8 100644 --- a/nucypher-core/src/lib.rs +++ b/nucypher-core/src/lib.rs @@ -32,6 +32,7 @@ pub use access_control::{encrypt_for_dkg, AccessControlPolicy, AuthenticatedData pub use address::Address; pub use conditions::{Conditions, Context}; pub use dkg::{ + decrypt_with_shared_secret, encrypt_with_shared_secret, session::{SessionSecretFactory, SessionSharedSecret, SessionStaticKey, SessionStaticSecret}, DecryptionError, EncryptedThresholdDecryptionRequest, EncryptedThresholdDecryptionResponse, EncryptionError, ThresholdDecryptionRequest, ThresholdDecryptionResponse, diff --git a/nucypher-core/tests/test_encryption_vectors.rs b/nucypher-core/tests/test_encryption_vectors.rs index c8cd7569..8e9f7096 100644 --- a/nucypher-core/tests/test_encryption_vectors.rs +++ b/nucypher-core/tests/test_encryption_vectors.rs @@ -1,17 +1,14 @@ #[cfg(test)] mod tests { - use chacha20poly1305::aead::Aead; - use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce}; - use rand::rngs::OsRng; - use rand::RngCore; // Add this import for the fill_bytes method + use chacha20poly1305::{Key, KeyInit, Nonce}; + use nucypher_core::{ + decrypt_with_shared_secret, encrypt_with_shared_secret, SessionSharedSecret, + }; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::fs; use std::path::Path; - // Since the dkg module is private, we reimplement the encryption/decryption functions here - // based on the implementation in src/dkg.rs - // Structure that matches the JSON test vector format #[derive(Serialize, Deserialize)] struct TestVector { @@ -30,51 +27,36 @@ mod tests { test_vectors: Vec, } - // Implementation of encrypt_with_shared_secret as found in dkg.rs - fn encrypt_with_shared_secret( + // Wrapper for encrypt_with_shared_secret that takes raw bytes for compatibility with test vectors + fn test_encrypt_with_shared_secret( shared_secret: &[u8], plaintext: &[u8], ) -> Result, Box> { - let key = Key::from_slice(shared_secret); - let cipher = ChaCha20Poly1305::new(key); - - // Generate random nonce - let mut nonce_bytes = [0u8; 12]; - OsRng.fill_bytes(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - - // Create result with nonce - let mut result = nonce_bytes.to_vec(); + // Create SessionSharedSecret from raw bytes + let shared_secret_obj = SessionSharedSecret::from_test_vector(shared_secret); - // Encrypt plaintext - let ciphertext = cipher - .encrypt(nonce, plaintext) - .map_err(|_| "Encryption failed: plaintext too large")?; - - // Append ciphertext to nonce - result.extend(ciphertext); + // Use the actual library function + let result = encrypt_with_shared_secret(&shared_secret_obj, plaintext).map_err(|e| { + Box::::from(format!("Encryption error: {:?}", e)) + })?; - Ok(result) + Ok(result.to_vec()) } - // Implementation of decrypt_with_shared_secret as found in dkg.rs - fn decrypt_with_shared_secret( + // Wrapper for decrypt_with_shared_secret that takes raw bytes for compatibility with test vectors + fn test_decrypt_with_shared_secret( shared_secret: &[u8], ciphertext: &[u8], ) -> Result, Box> { - if ciphertext.len() <= 12 { - return Err("The ciphertext must include the nonce".into()); - } - - let key = Key::from_slice(shared_secret); - let cipher = ChaCha20Poly1305::new(key); + // Create SessionSharedSecret from raw bytes + let shared_secret_obj = SessionSharedSecret::from_test_vector(shared_secret); - let nonce = Nonce::from_slice(&ciphertext[..12]); - let decrypt_result = cipher - .decrypt(nonce, &ciphertext[12..]) - .map_err(|_| "Decryption of ciphertext failed")?; + // Use the actual library function + let result = decrypt_with_shared_secret(&shared_secret_obj, ciphertext).map_err(|e| { + Box::::from(format!("Decryption error: {:?}", e)) + })?; - Ok(decrypt_result) + Ok(result.to_vec()) } // Function to encrypt with fixed nonce for test vector generation @@ -83,21 +65,22 @@ mod tests { plaintext: &[u8], fixed_nonce: &[u8], ) -> Result, Box> { + use chacha20poly1305::aead::Aead; + + // Create key from shared secret bytes let key = Key::from_slice(shared_secret); - let cipher = ChaCha20Poly1305::new(key); + let cipher = chacha20poly1305::ChaCha20Poly1305::new(key); // Use the provided fixed nonce let nonce = Nonce::from_slice(fixed_nonce); - // Create the result starting with the nonce - let mut result = fixed_nonce.to_vec(); - // Encrypt the plaintext with the fixed nonce let ciphertext = cipher - .encrypt(nonce, plaintext) + .encrypt(nonce, plaintext.as_ref()) .map_err(|_| "Encryption failed: plaintext too large")?; - // Append the ciphertext to the nonce + // Format the result as nonce + ciphertext, matching the library format + let mut result = fixed_nonce.to_vec(); result.extend(ciphertext); Ok(result) @@ -154,7 +137,7 @@ mod tests { println!("Creating vector3 with Rust-generated ciphertext"); // Standard encryption with random nonce let ciphertext = - encrypt_with_shared_secret(&shared_secret_bytes, plaintext).unwrap(); + test_encrypt_with_shared_secret(&shared_secret_bytes, plaintext).unwrap(); let ciphertext_vec = ciphertext.to_vec(); // Add the Rust-generated ciphertext to the vector @@ -262,7 +245,7 @@ mod tests { let expected_plaintext = vector["expected_plaintext"].as_str().unwrap(); - match decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { + match test_decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { Ok(decrypted) => { let decrypted_str = String::from_utf8_lossy(&decrypted); assert_eq!( @@ -284,7 +267,7 @@ mod tests { let ciphertext = hex::decode(ciphertext_hex).unwrap(); let plaintext = vector["plaintext"].as_str().unwrap().as_bytes(); - match decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { + match test_decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { Ok(decrypted) => { assert_eq!( &decrypted, plaintext, From 9771e1859c7fbbd25fe0152d26f168d49f8e57ae Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:20:52 +0200 Subject: [PATCH 4/5] use env variable for the path of the typescript project test vectors + in addtion to some refactorying + update README file --- nucypher-core/README.md | 13 + .../tests/fixtures/shared-secret-vectors.json | 308 +++++++++--------- ...s.rs => generate_shared_secret_vectors.rs} | 226 +++++++------ .../test_utils/cross_impl_test_vectors.rs | 64 ++++ 4 files changed, 352 insertions(+), 259 deletions(-) rename nucypher-core/tests/{test_encryption_vectors.rs => generate_shared_secret_vectors.rs} (55%) create mode 100644 nucypher-core/tests/test_utils/cross_impl_test_vectors.rs diff --git a/nucypher-core/README.md b/nucypher-core/README.md index 99b0c01d..34756fc7 100644 --- a/nucypher-core/README.md +++ b/nucypher-core/README.md @@ -17,6 +17,19 @@ Bindings for several languages are available: * [JavaScript](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-wasm) (WASM-based) * [Python](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-python) +## Cross-Implementation Testing + +This library tests generate test vectors for ensuring compatibility between different implementations. The test vector generators automatically produce JSON files in both the Rust project and the TypeScript project. + +### Setting Custom Path for TypeScript Test Vectors + +By default, the test vector generators will look for the TypeScript project at a relative path. If your project structure is different, you can customize the TypeScript project path using an environment variable: + +```bash +# Generate shared secret test vectors with custom TypeScript project path +TS_PROJECT_TEST_VECTORS_PATH=/path/to/taco-web/packages/shared/test/fixtures/ cargo test -p nucypher-core --test generate_shared_secret_vectors +``` + [crate-image]: https://img.shields.io/crates/v/nucypher-core.svg [crate-link]: https://crates.io/crates/nucypher-core [docs-image]: https://docs.rs/nucypher-core/badge.svg diff --git a/nucypher-core/tests/fixtures/shared-secret-vectors.json b/nucypher-core/tests/fixtures/shared-secret-vectors.json index 8909878b..58a15dc3 100644 --- a/nucypher-core/tests/fixtures/shared-secret-vectors.json +++ b/nucypher-core/tests/fixtures/shared-secret-vectors.json @@ -1,24 +1,8 @@ { - "testVectors": [ + "test_vectors": [ { - "description": "Basic encryption/decryption compatibility", - "expected_ciphertext": "0102030405060708090a0b0c30e02d19293bd79329c9232eacc4e82eca99e0ea66e4da3808bccbacb3fd2ccbce94fe0c26cbbb81be779817", - "fixed_nonce": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12 - ], "id": "vector1", - "plaintext": "This is a fixed test message", + "description": "Fixed nonce encryption with known plaintext", "shared_secret": [ 0, 1, @@ -52,150 +36,81 @@ 29, 30, 31 - ] - }, - { - "description": "Empty plaintext compatibility", - "expected_ciphertext": "102030405060708090a0b0c0d9abc79645718dc328d5c3faa129fced", + ], + "plaintext": "This is a test message", "fixed_nonce": [ - 16, - 32, - 48, - 64, - 80, - 96, - 112, - 128, - 144, - 160, - 176, - 192 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 ], - "id": "vector2", - "plaintext": "", - "shared_secret": [ - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63 - ] + "expected_ciphertext": "0000000000000000000000004cd02b428d8fd5f172412804dc376e4a9dc2809486c8b548d407dad8f75ee84d6d9e92a42c4a" }, { - "description": "Rust-generated ciphertext for TypeScript compatibility check", - "expected_plaintext": "This is a message encrypted by the Rust implementation", - "id": "vector3", - "rust_generated_ciphertext": [ - 244, - 240, - 54, + "id": "vector2", + "description": "Fixed nonce encryption with alternative values", + "shared_secret": [ + 31, + 30, + 29, + 28, + 27, + 26, + 25, 24, - 5, - 65, - 204, - 10, - 149, + 23, + 22, + 21, + 20, + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, 10, - 78, - 107, - 203, - 196, - 102, - 251, - 108, - 158, - 36, + 9, + 8, 7, - 14, - 87, - 153, - 251, - 176, - 242, - 94, - 153, - 244, - 118, - 124, - 216, - 154, + 6, 5, - 88, - 98, - 171, - 221, - 222, - 14, - 247, - 21, - 74, - 25, - 205, - 80, - 35, - 47, - 195, - 218, - 221, - 23, - 85, - 23, - 82, - 145, - 234, - 171, - 136, - 141, - 215, - 95, - 178, - 103, - 141, - 70, - 236, - 104, - 80, - 106, - 249, - 218, - 236, - 96, - 76, - 77, - 239, - 197, - 216, - 31, - 187, - 146 + 4, + 3, + 2, + 1, + 0 ], + "plaintext": "", + "fixed_nonce": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "expected_ciphertext": "0101010101010101010101018726c5f2d5b81872a86493ef5232aaa1" + }, + { + "id": "vector3", + "description": "Rust-generated ciphertext for TypeScript compatibility check", "shared_secret": [ 0, 1, @@ -229,7 +144,92 @@ 29, 30, 31 - ] + ], + "rust_generated_ciphertext": [ + 87, + 119, + 14, + 255, + 68, + 116, + 249, + 152, + 58, + 121, + 245, + 185, + 157, + 62, + 23, + 3, + 50, + 163, + 150, + 212, + 167, + 157, + 244, + 227, + 159, + 130, + 110, + 26, + 30, + 161, + 173, + 70, + 232, + 207, + 110, + 161, + 252, + 228, + 149, + 42, + 173, + 185, + 61, + 157, + 144, + 163, + 128, + 200, + 87, + 80, + 172, + 63, + 235, + 241, + 169, + 124, + 248, + 158, + 213, + 62, + 241, + 209, + 117, + 216, + 202, + 230, + 106, + 248, + 170, + 150, + 126, + 40, + 203, + 34, + 14, + 44, + 71, + 186, + 234, + 92, + 13, + 248 + ], + "expected_plaintext": "This is a message encrypted by the Rust implementation" } ] } \ No newline at end of file diff --git a/nucypher-core/tests/test_encryption_vectors.rs b/nucypher-core/tests/generate_shared_secret_vectors.rs similarity index 55% rename from nucypher-core/tests/test_encryption_vectors.rs rename to nucypher-core/tests/generate_shared_secret_vectors.rs index 8e9f7096..e5377c3b 100644 --- a/nucypher-core/tests/test_encryption_vectors.rs +++ b/nucypher-core/tests/generate_shared_secret_vectors.rs @@ -1,11 +1,22 @@ +// Include the test_utils module at the crate root level +mod test_utils { + pub mod cross_impl_test_vectors; +} + #[cfg(test)] mod tests { + // Import the test utilities for TypeScript project paths + use crate::test_utils::cross_impl_test_vectors; + + // json file name + const JSON_FILE_NAME: &str = "shared-secret-vectors.json"; + use chacha20poly1305::{Key, KeyInit, Nonce}; use nucypher_core::{ decrypt_with_shared_secret, encrypt_with_shared_secret, SessionSharedSecret, }; use serde::{Deserialize, Serialize}; - use serde_json::{json, Value}; + use serde_json::Value; use std::fs; use std::path::Path; @@ -15,10 +26,15 @@ mod tests { id: String, description: String, shared_secret: Vec, + #[serde(skip_serializing_if = "Option::is_none")] plaintext: Option, + #[serde(skip_serializing_if = "Option::is_none")] fixed_nonce: Option>, + #[serde(skip_serializing_if = "Option::is_none")] expected_ciphertext: Option, + #[serde(skip_serializing_if = "Option::is_none")] rust_generated_ciphertext: Option>, + #[serde(skip_serializing_if = "Option::is_none")] expected_plaintext: Option, } @@ -90,100 +106,104 @@ mod tests { fn generate_test_vectors() { println!("Generating encryption test vectors for TypeScript compatibility..."); - // Define test vectors - let vectors = vec![ - // Vector 1: Basic encryption/decryption with fixed nonce - json!({ - "id": "vector1", - "description": "Basic encryption/decryption compatibility", - "shared_secret": (0..32).collect::>(), - "plaintext": "This is a fixed test message", - "fixed_nonce": vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - }), - // Vector 2: Empty plaintext with fixed nonce - json!({ - "id": "vector2", - "description": "Empty plaintext compatibility", - "shared_secret": (32..64).collect::>(), - "plaintext": "", - "fixed_nonce": vec![16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192], - }), - // Vector 3: For Rust-generated ciphertext compatibility using normal encryption - json!({ - "id": "vector3", - "description": "Rust-generated ciphertext for TypeScript compatibility check", - "shared_secret": (0..32).collect::>(), - "expected_plaintext": "This is a message encrypted by the Rust implementation", - }), - ]; - - // Process each vector to add encryption outputs - let mut processed_vectors = Vec::new(); - - for vector in vectors { - let mut processed = vector.clone(); - - // Extract shared secret - let shared_secret = vector["shared_secret"].as_array().unwrap(); - let shared_secret_bytes: Vec = shared_secret - .iter() - .map(|v| v.as_u64().unwrap() as u8) - .collect(); - - // Handle vector3 - generate ciphertext with standard Rust implementation - if vector["id"].as_str().unwrap() == "vector3" { - let plaintext = vector["expected_plaintext"].as_str().unwrap().as_bytes(); - - println!("Creating vector3 with Rust-generated ciphertext"); - // Standard encryption with random nonce - let ciphertext = - test_encrypt_with_shared_secret(&shared_secret_bytes, plaintext).unwrap(); - let ciphertext_vec = ciphertext.to_vec(); - - // Add the Rust-generated ciphertext to the vector - processed["rust_generated_ciphertext"] = json!(ciphertext_vec); - processed_vectors.push(processed); - continue; + // Define test vectors directly as TestVector structs + let mut test_vectors = Vec::new(); + + // Vector 1: Known plaintext + fixed nonce -> expected ciphertext + let shared_secret1: Vec = (0..32).collect(); + let plaintext1 = "This is a test message"; + let fixed_nonce1: Vec = vec![0; 12]; // 12 zeros + + println!("Processing vector1 with fixed nonce"); + let mut vector1 = TestVector { + id: "vector1".to_string(), + description: "Fixed nonce encryption with known plaintext".to_string(), + shared_secret: shared_secret1.clone(), + plaintext: Some(plaintext1.to_string()), + fixed_nonce: Some(fixed_nonce1.clone()), + expected_ciphertext: None, + rust_generated_ciphertext: None, + expected_plaintext: None, + }; + + // Generate ciphertext with fixed nonce for vector1 + match encrypt_with_fixed_nonce(&shared_secret1, plaintext1.as_bytes(), &fixed_nonce1) { + Ok(ciphertext) => { + // Convert ciphertext to hex string + let ciphertext_hex = hex::encode(&ciphertext); + vector1.expected_ciphertext = Some(ciphertext_hex); + println!(" ✓ Successfully generated ciphertext with fixed nonce"); } - - // For vectors 1 & 2, use fixed nonces - if let (Some(plaintext_str), Some(fixed_nonce)) = ( - vector["plaintext"].as_str(), - vector["fixed_nonce"].as_array(), - ) { - let plaintext = plaintext_str.as_bytes(); - let fixed_nonce_bytes: Vec = fixed_nonce - .iter() - .map(|v| v.as_u64().unwrap() as u8) - .collect(); - - println!("Processing vector {} with fixed nonce", vector["id"]); - - // Generate ciphertext with fixed nonce - match encrypt_with_fixed_nonce(&shared_secret_bytes, plaintext, &fixed_nonce_bytes) - { - Ok(ciphertext) => { - // Convert ciphertext to hex string for expected_ciphertext - let ciphertext_hex = hex::encode(&ciphertext); - processed["expected_ciphertext"] = json!(ciphertext_hex); - println!(" ✓ Successfully generated ciphertext with fixed nonce"); - } - Err(e) => { - eprintln!("Error encrypting vector {}: {}", vector["id"], e); - } - } + Err(e) => { + eprintln!("Error encrypting vector1: {}", e); } - - processed_vectors.push(processed); } + test_vectors.push(vector1); + + // Vector 2: Known plaintext + fixed nonce -> expected ciphertext (different values) + let shared_secret2: Vec = (0..32).rev().collect(); // Reversed range + let plaintext2 = ""; // Empty plaintext for testing empty message encryption + let fixed_nonce2: Vec = vec![1; 12]; // 12 ones + + println!("Processing vector2 with fixed nonce"); + let mut vector2 = TestVector { + id: "vector2".to_string(), + description: "Fixed nonce encryption with alternative values".to_string(), + shared_secret: shared_secret2.clone(), + plaintext: Some(plaintext2.to_string()), + fixed_nonce: Some(fixed_nonce2.clone()), + expected_ciphertext: None, + rust_generated_ciphertext: None, + expected_plaintext: None, + }; + + // Generate ciphertext with fixed nonce for vector2 + match encrypt_with_fixed_nonce(&shared_secret2, plaintext2.as_bytes(), &fixed_nonce2) { + Ok(ciphertext) => { + // Convert ciphertext to hex string + let ciphertext_hex = hex::encode(&ciphertext); + vector2.expected_ciphertext = Some(ciphertext_hex); + println!(" ✓ Successfully generated ciphertext with fixed nonce"); + } + Err(e) => { + eprintln!("Error encrypting vector2: {}", e); + } + } + test_vectors.push(vector2); + + // Vector 3: For Rust-generated ciphertext compatibility using normal encryption + let shared_secret3: Vec = (0..32).collect(); + let plaintext3 = "This is a message encrypted by the Rust implementation"; + + println!("Creating vector3 with Rust-generated ciphertext"); + let mut vector3 = TestVector { + id: "vector3".to_string(), + description: "Rust-generated ciphertext for TypeScript compatibility check".to_string(), + shared_secret: shared_secret3.clone(), + plaintext: None, + fixed_nonce: None, + expected_ciphertext: None, + rust_generated_ciphertext: None, + expected_plaintext: Some(plaintext3.to_string()), + }; + + // Standard encryption with random nonce + match test_encrypt_with_shared_secret(&shared_secret3, plaintext3.as_bytes()) { + Ok(ciphertext) => { + vector3.rust_generated_ciphertext = Some(ciphertext); + println!(" ✓ Successfully generated ciphertext for vector3"); + } + Err(e) => { + eprintln!("Error encrypting vector3: {}", e); + } + } + test_vectors.push(vector3); - // Create the final JSON structure with camelCase keys for TypeScript - let final_json = json!({ - "testVectors": processed_vectors - }); + // Create the complete test vectors structure + let test_vectors_output = TestVectors { test_vectors }; // Format the JSON with pretty-printing - let formatted_json = serde_json::to_string_pretty(&final_json).unwrap(); + let formatted_json = serde_json::to_string_pretty(&test_vectors_output).unwrap(); // Path for the output file let output_dir = Path::new("tests/fixtures"); @@ -196,34 +216,30 @@ mod tests { fs::write(&output_file, &formatted_json).expect("Unable to write test vectors file"); println!("Test vectors saved to {:?}", output_file); - // Also save to TypeScript project if path exists - let ts_path = - Path::new("../taco-web/packages/shared/test/fixtures/shared-secret-vectors.json"); - if let Ok(()) = fs::write(ts_path, &formatted_json) { - println!( - "Test vectors also copied to TypeScript project: {:?}", - ts_path - ); - } else { - println!( - "Note: Couldn't copy to TypeScript project. You'll need to manually copy the file." - ); - } + // Write test vectors to TypeScript project + cross_impl_test_vectors::write_to_ts_project_path( + JSON_FILE_NAME, + &formatted_json, + &output_file, + ); // Verify vectors by decrypting - verify_test_vectors(&final_json); + // Parse the JSON string back to a Value before passing to verify_test_vectors + let test_vectors_value: Value = serde_json::from_str(&formatted_json).unwrap(); + verify_test_vectors(&test_vectors_value); println!("\nInstructions for manually copying test vectors:"); println!("1. The file has been saved to: {:?}", output_file); println!( - "2. Copy it to: ../taco-web/packages/shared/test/fixtures/shared-secret-vectors.json" + "2. Copy it to: {}", + cross_impl_test_vectors::get_ts_project_path(JSON_FILE_NAME) ); println!("3. Run the TypeScript tests to verify compatibility"); } fn verify_test_vectors(test_vectors_json: &Value) { println!("\nVerifying test vectors..."); - let vectors = test_vectors_json["testVectors"].as_array().unwrap(); + let vectors = test_vectors_json["test_vectors"].as_array().unwrap(); for vector in vectors { let id = vector["id"].as_str().unwrap(); diff --git a/nucypher-core/tests/test_utils/cross_impl_test_vectors.rs b/nucypher-core/tests/test_utils/cross_impl_test_vectors.rs new file mode 100644 index 00000000..df323289 --- /dev/null +++ b/nucypher-core/tests/test_utils/cross_impl_test_vectors.rs @@ -0,0 +1,64 @@ +// Utility functions for handling TypeScript project paths in tests +use std::path::Path; + +/// Default base path to TypeScript project test fixtures +pub const DEFAULT_TS_PROJECT_TEST_VECTORS_PATH: &str = + "../../taco-web/packages/shared/test/fixtures/"; + +/// Environment variable name for TypeScript project path +pub const TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR: &str = "TS_PROJECT_TEST_VECTORS_PATH"; + +/// Get the TypeScript project path combining the base directory with the specified file name +/// +/// Checks for the environment variable `TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR` first, +/// and falls back to the default path if not set. +pub fn get_ts_project_path(file_name: &str) -> String { + // Check for environment variable + match std::env::var(TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR) { + Ok(path) if !path.is_empty() => { + println!( + "Using custom path from {} environment variable", + TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR + ); + format!("{}{}", path, file_name) + } + _ => { + println!("Using default TypeScript project path"); + format!("{}{}", DEFAULT_TS_PROJECT_TEST_VECTORS_PATH, file_name) + } + } +} + +/// Write test vectors to TypeScript project path +/// +/// Returns true if successful, false otherwise +/// +/// If writing fails, it will print manual copy instructions +/// using the provided source file path +pub fn write_to_ts_project_path(file_name: &str, content: &str, source_file_path: &Path) -> bool { + let ts_project_path = get_ts_project_path(file_name); + println!("TypeScript project path: {}", ts_project_path); + + let ts_path = Path::new(&ts_project_path); + match std::fs::write(ts_path, content) { + Ok(()) => { + println!( + "✓ Test vectors successfully copied to TypeScript project: {:?}", + ts_path + ); + true + } + Err(e) => { + println!( + "Note: Couldn't copy to TypeScript project ({:?}): {}", + ts_path, e + ); + // Add manual copy instructions + println!( + "You'll need to manually copy the file from {:?} to the TypeScript project.", + source_file_path + ); + false + } + } +} From 7d3eddad4f8a62b29f0ab622273d4466dab7db1b Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:54:37 +0200 Subject: [PATCH 5/5] Implement tests that generate session key test vectors for TS ported code + Update related section at README --- nucypher-core/README.md | 10 +- .../tests/fixtures/session-key-vectors.json | 906 ++++++++++++++++++ .../tests/generate_session_key_vectors.rs | 280 ++++++ 3 files changed, 1194 insertions(+), 2 deletions(-) create mode 100644 nucypher-core/tests/fixtures/session-key-vectors.json create mode 100644 nucypher-core/tests/generate_session_key_vectors.rs diff --git a/nucypher-core/README.md b/nucypher-core/README.md index 34756fc7..bdb616a6 100644 --- a/nucypher-core/README.md +++ b/nucypher-core/README.md @@ -14,8 +14,8 @@ Bindings for several languages are available: -* [JavaScript](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-wasm) (WASM-based) -* [Python](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-python) +- [JavaScript](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-wasm) (WASM-based) +- [Python](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-python) ## Cross-Implementation Testing @@ -26,8 +26,14 @@ This library tests generate test vectors for ensuring compatibility between diff By default, the test vector generators will look for the TypeScript project at a relative path. If your project structure is different, you can customize the TypeScript project path using an environment variable: ```bash +# Generate both session key and shared secret test vectors with a single command +TS_PROJECT_TEST_VECTORS_PATH=/path/to/taco-web/packages/shared/test/fixtures/ cargo test -p nucypher-core --test generate_session_key_vectors --test generate_shared_secret_vectors + +# Or run individual generators if needed # Generate shared secret test vectors with custom TypeScript project path TS_PROJECT_TEST_VECTORS_PATH=/path/to/taco-web/packages/shared/test/fixtures/ cargo test -p nucypher-core --test generate_shared_secret_vectors +# Generate session key test vectors with custom TypeScript project path +TS_PROJECT_TEST_VECTORS_PATH=/path/to/taco-web/packages/shared/test/fixtures/ cargo test -p nucypher-core --test generate_session_key_vectors ``` [crate-image]: https://img.shields.io/crates/v/nucypher-core.svg diff --git a/nucypher-core/tests/fixtures/session-key-vectors.json b/nucypher-core/tests/fixtures/session-key-vectors.json new file mode 100644 index 00000000..a31166bb --- /dev/null +++ b/nucypher-core/tests/fixtures/session-key-vectors.json @@ -0,0 +1,906 @@ +{ + "schema_version": "1.0", + "timestamp": "1744692747", + "curve": "X25519", + "algorithm": "Diffie-Hellman Key Exchange", + "test_vectors": [ + { + "id": "vector1", + "description": "Random key pair generation examples", + "vector_type": "random_generation", + "random_key_pairs": [ + { + "public_key": [ + 195, + 201, + 214, + 35, + 47, + 212, + 93, + 101, + 255, + 55, + 100, + 205, + 173, + 165, + 89, + 246, + 7, + 37, + 241, + 168, + 48, + 192, + 60, + 39, + 177, + 74, + 69, + 179, + 209, + 37, + 170, + 92 + ] + }, + { + "public_key": [ + 169, + 69, + 146, + 179, + 19, + 192, + 73, + 201, + 101, + 211, + 155, + 255, + 102, + 236, + 146, + 168, + 61, + 163, + 178, + 92, + 45, + 203, + 223, + 89, + 0, + 85, + 155, + 198, + 82, + 25, + 175, + 50 + ] + }, + { + "public_key": [ + 179, + 73, + 100, + 224, + 126, + 192, + 146, + 103, + 218, + 62, + 177, + 212, + 193, + 84, + 71, + 58, + 193, + 121, + 27, + 177, + 117, + 205, + 33, + 243, + 165, + 204, + 149, + 7, + 205, + 46, + 92, + 95 + ] + }, + { + "public_key": [ + 12, + 172, + 160, + 15, + 236, + 231, + 162, + 214, + 250, + 36, + 132, + 177, + 48, + 239, + 159, + 240, + 113, + 46, + 68, + 11, + 35, + 27, + 74, + 174, + 17, + 61, + 254, + 132, + 155, + 107, + 59, + 51 + ] + }, + { + "public_key": [ + 212, + 72, + 82, + 34, + 13, + 29, + 140, + 97, + 219, + 216, + 35, + 134, + 49, + 16, + 51, + 119, + 31, + 161, + 56, + 207, + 177, + 139, + 149, + 86, + 212, + 92, + 9, + 35, + 170, + 253, + 146, + 95 + ] + }, + { + "public_key": [ + 32, + 216, + 50, + 111, + 171, + 56, + 139, + 188, + 252, + 43, + 66, + 172, + 248, + 200, + 42, + 197, + 213, + 134, + 209, + 28, + 66, + 121, + 255, + 25, + 100, + 222, + 170, + 37, + 203, + 31, + 216, + 37 + ] + }, + { + "public_key": [ + 219, + 93, + 26, + 90, + 122, + 223, + 159, + 137, + 233, + 173, + 117, + 189, + 38, + 130, + 224, + 39, + 208, + 230, + 41, + 84, + 26, + 27, + 180, + 240, + 59, + 140, + 20, + 92, + 217, + 204, + 135, + 6 + ] + }, + { + "public_key": [ + 69, + 231, + 114, + 33, + 150, + 46, + 222, + 194, + 196, + 48, + 114, + 37, + 127, + 117, + 235, + 68, + 81, + 91, + 20, + 94, + 174, + 99, + 206, + 95, + 18, + 18, + 115, + 56, + 227, + 120, + 170, + 55 + ] + }, + { + "public_key": [ + 255, + 210, + 5, + 206, + 231, + 243, + 68, + 26, + 131, + 244, + 144, + 195, + 233, + 117, + 164, + 178, + 79, + 159, + 193, + 173, + 246, + 251, + 168, + 240, + 18, + 130, + 16, + 144, + 217, + 19, + 110, + 67 + ] + }, + { + "public_key": [ + 216, + 130, + 227, + 219, + 141, + 132, + 106, + 154, + 169, + 198, + 42, + 187, + 207, + 105, + 79, + 200, + 3, + 39, + 211, + 220, + 97, + 193, + 62, + 130, + 227, + 23, + 203, + 130, + 26, + 64, + 252, + 83 + ] + } + ], + "key_exchange_scenarios": null, + "interoperability_check": true + }, + { + "id": "vector2", + "description": "Key exchange scenarios (Diffie-Hellman)", + "vector_type": "key_exchange", + "random_key_pairs": null, + "key_exchange_scenarios": [ + { + "initiator_public_key": [ + 179, + 120, + 254, + 179, + 253, + 8, + 92, + 134, + 26, + 141, + 214, + 239, + 245, + 240, + 45, + 86, + 122, + 154, + 73, + 89, + 59, + 240, + 61, + 246, + 31, + 29, + 244, + 107, + 141, + 74, + 173, + 11 + ], + "responder_public_key": [ + 249, + 142, + 127, + 2, + 168, + 220, + 242, + 194, + 208, + 200, + 14, + 87, + 132, + 45, + 61, + 142, + 17, + 73, + 221, + 98, + 48, + 93, + 54, + 161, + 37, + 143, + 10, + 0, + 7, + 17, + 165, + 14 + ], + "shared_secret": [ + 68, + 225, + 168, + 100, + 58, + 6, + 254, + 142, + 126, + 20, + 186, + 150, + 102, + 33, + 131, + 126, + 148, + 242, + 154, + 80, + 48, + 29, + 111, + 178, + 190, + 131, + 163, + 231, + 12, + 236, + 2, + 79 + ] + }, + { + "initiator_public_key": [ + 181, + 191, + 43, + 236, + 155, + 230, + 251, + 163, + 93, + 253, + 176, + 25, + 246, + 73, + 88, + 227, + 25, + 204, + 255, + 47, + 95, + 37, + 113, + 218, + 249, + 145, + 236, + 118, + 154, + 158, + 109, + 53 + ], + "responder_public_key": [ + 234, + 151, + 84, + 133, + 143, + 178, + 91, + 150, + 38, + 67, + 193, + 137, + 104, + 155, + 48, + 137, + 207, + 215, + 213, + 37, + 6, + 54, + 123, + 148, + 208, + 63, + 160, + 234, + 190, + 239, + 24, + 72 + ], + "shared_secret": [ + 229, + 15, + 142, + 62, + 235, + 123, + 75, + 253, + 101, + 141, + 64, + 54, + 31, + 57, + 49, + 117, + 18, + 200, + 159, + 2, + 195, + 95, + 159, + 3, + 102, + 57, + 71, + 59, + 52, + 81, + 121, + 105 + ] + }, + { + "initiator_public_key": [ + 147, + 80, + 204, + 196, + 164, + 58, + 49, + 220, + 159, + 245, + 188, + 243, + 181, + 149, + 247, + 133, + 155, + 198, + 70, + 190, + 50, + 254, + 249, + 31, + 199, + 98, + 220, + 133, + 61, + 172, + 117, + 27 + ], + "responder_public_key": [ + 196, + 172, + 125, + 164, + 115, + 185, + 171, + 25, + 113, + 60, + 252, + 166, + 240, + 68, + 17, + 58, + 226, + 175, + 160, + 83, + 154, + 5, + 95, + 227, + 77, + 218, + 59, + 74, + 56, + 156, + 37, + 79 + ], + "shared_secret": [ + 241, + 253, + 155, + 142, + 6, + 36, + 134, + 247, + 169, + 211, + 66, + 252, + 183, + 142, + 168, + 225, + 220, + 240, + 128, + 8, + 155, + 49, + 228, + 48, + 42, + 80, + 99, + 188, + 194, + 138, + 40, + 148 + ] + }, + { + "initiator_public_key": [ + 121, + 46, + 61, + 185, + 45, + 192, + 59, + 95, + 96, + 120, + 1, + 211, + 156, + 47, + 24, + 75, + 235, + 183, + 186, + 229, + 117, + 126, + 66, + 36, + 8, + 163, + 234, + 249, + 6, + 20, + 143, + 57 + ], + "responder_public_key": [ + 26, + 47, + 30, + 26, + 44, + 32, + 154, + 25, + 156, + 95, + 34, + 67, + 122, + 20, + 171, + 219, + 131, + 71, + 7, + 44, + 101, + 248, + 128, + 1, + 12, + 156, + 133, + 252, + 246, + 123, + 130, + 51 + ], + "shared_secret": [ + 167, + 106, + 7, + 10, + 56, + 146, + 248, + 73, + 125, + 136, + 185, + 163, + 220, + 123, + 153, + 247, + 29, + 86, + 112, + 250, + 69, + 172, + 250, + 151, + 117, + 23, + 126, + 106, + 67, + 144, + 189, + 133 + ] + }, + { + "initiator_public_key": [ + 160, + 211, + 228, + 115, + 171, + 40, + 234, + 227, + 177, + 25, + 75, + 204, + 108, + 53, + 186, + 174, + 61, + 251, + 41, + 109, + 27, + 103, + 196, + 127, + 11, + 193, + 79, + 25, + 51, + 174, + 249, + 10 + ], + "responder_public_key": [ + 74, + 255, + 130, + 72, + 230, + 112, + 13, + 245, + 213, + 151, + 181, + 109, + 159, + 77, + 131, + 97, + 166, + 31, + 133, + 245, + 81, + 125, + 24, + 1, + 10, + 44, + 163, + 198, + 93, + 167, + 248, + 18 + ], + "shared_secret": [ + 86, + 66, + 248, + 243, + 116, + 10, + 203, + 129, + 201, + 136, + 94, + 123, + 31, + 124, + 56, + 107, + 182, + 95, + 255, + 231, + 244, + 56, + 194, + 175, + 217, + 19, + 192, + 81, + 101, + 240, + 175, + 111 + ] + } + ], + "interoperability_check": true + } + ] +} \ No newline at end of file diff --git a/nucypher-core/tests/generate_session_key_vectors.rs b/nucypher-core/tests/generate_session_key_vectors.rs new file mode 100644 index 00000000..89eb4482 --- /dev/null +++ b/nucypher-core/tests/generate_session_key_vectors.rs @@ -0,0 +1,280 @@ +// Include the test_utils module at the crate root level +mod test_utils { + pub mod cross_impl_test_vectors; +} + +#[cfg(test)] +mod tests { + // Import the test utilities for TypeScript project paths + use crate::test_utils::cross_impl_test_vectors; + + // json file name + const JSON_FILE_NAME: &str = "session-key-vectors.json"; + use nucypher_core::{SessionSharedSecret, SessionStaticKey, SessionStaticSecret}; + use serde::{Deserialize, Serialize}; + use std::fs; + use std::path::Path; + use std::time::{SystemTime, UNIX_EPOCH}; + use umbral_pre::serde_bytes; + + // Structures to represent the test vectors + #[derive(Serialize, Deserialize, Debug)] + struct KeyPair { + public_key: Vec, // Public key bytes + } + + #[derive(Serialize, Deserialize, Debug)] + struct SharedSecretResult { + initiator_public_key: Vec, // First party's public key + responder_public_key: Vec, // Second party's public key + shared_secret: Vec, // Resulting shared secret + } + + #[derive(Serialize, Deserialize, Debug)] + struct TestVector { + id: String, + description: String, + vector_type: String, + + // For random key pair generation tests + random_key_pairs: Option>, + + // For key exchange tests + key_exchange_scenarios: Option>, + + // For compatibility verification + interoperability_check: Option, + } + + #[derive(Serialize, Deserialize, Debug)] + struct TestVectors { + schema_version: String, + timestamp: String, + curve: String, + algorithm: String, + test_vectors: Vec, + } + + // Helper function to generate a random key pair + fn generate_random_key_pair() -> (SessionStaticSecret, SessionStaticKey) { + // Use the built-in random generator + let secret = SessionStaticSecret::random(); + let public = secret.public_key(); + + (secret, public) + } + + // Helper to create a key exchange scenario between two parties + fn create_key_exchange( + initiator_secret: &SessionStaticSecret, + responder_secret: &SessionStaticSecret, + ) -> SharedSecretResult { + // Generate public keys + let initiator_public = initiator_secret.public_key(); + let responder_public = responder_secret.public_key(); + + // Exchange keys + let shared_secret_initiator = initiator_secret.derive_shared_secret(&responder_public); + let shared_secret_responder = responder_secret.derive_shared_secret(&initiator_public); + + // Verify they match (DH property) + assert_eq!( + shared_secret_initiator.as_bytes(), + shared_secret_responder.as_bytes() + ); + + SharedSecretResult { + initiator_public_key: initiator_public.to_bytes().to_vec(), + responder_public_key: responder_public.to_bytes().to_vec(), + shared_secret: shared_secret_initiator.as_bytes().to_vec(), + } + } + + #[test] + fn generate_test_vectors() { + println!( + "Generating SessionStaticSecret test vectors for cross-implementation compatibility..." + ); + + // Create timestamp for the test vectors + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + // Create test vectors container + let mut test_vectors = Vec::new(); + + // === Vector 1: Random Key Generation Test === + { + println!("Generating Vector 1: Random Key Pair Generation"); + let mut random_pairs = Vec::new(); + + // Generate multiple random key pairs + for i in 0..10 { + let (_, public) = generate_random_key_pair(); + + println!(" - Generated random key pair {}", i + 1); + + // Store the public key bytes + random_pairs.push(KeyPair { + public_key: public.to_bytes().to_vec(), + }); + } + + test_vectors.push(TestVector { + id: "vector1".to_string(), + description: "Random key pair generation examples".to_string(), + vector_type: "random_generation".to_string(), + random_key_pairs: Some(random_pairs), + key_exchange_scenarios: None, + interoperability_check: Some(true), + }); + } + + // === Vector 2: Key Exchange Test === + { + println!("Generating Vector 2: Key Exchange Scenarios"); + let mut exchange_scenarios = Vec::new(); + + // Generate multiple random key exchange scenarios + for i in 0..5 { + let (initiator_secret, _) = generate_random_key_pair(); + let (responder_secret, _) = generate_random_key_pair(); + + println!(" - Generated key exchange scenario {}", i + 1); + let scenario = create_key_exchange(&initiator_secret, &responder_secret); + exchange_scenarios.push(scenario); + } + + test_vectors.push(TestVector { + id: "vector2".to_string(), + description: "Key exchange scenarios (Diffie-Hellman)".to_string(), + vector_type: "key_exchange".to_string(), + random_key_pairs: None, + key_exchange_scenarios: Some(exchange_scenarios), + interoperability_check: Some(true), + }); + } + + // Create the complete test vectors structure + let test_vectors_output = TestVectors { + schema_version: "1.0".to_string(), + timestamp: timestamp.to_string(), + curve: "X25519".to_string(), + algorithm: "Diffie-Hellman Key Exchange".to_string(), + test_vectors, + }; + + // Format the JSON with pretty-printing + let formatted_json = serde_json::to_string_pretty(&test_vectors_output) + .expect("Failed to serialize test vectors to JSON"); + + // Ensure the fixtures directory exists + let fixtures_dir = Path::new("tests/fixtures"); + if !fixtures_dir.exists() { + fs::create_dir_all(fixtures_dir).expect("Failed to create fixtures directory"); + } + + // Write the test vectors to the fixtures file in the Rust project + let fixture_path = fixtures_dir.join("session-key-vectors.json"); + fs::write(&fixture_path, &formatted_json).expect("Failed to write test vectors to file"); + println!("✓ Test vectors saved to {:?}", fixture_path); + + // Write test vectors to TypeScript project using the shared utility + cross_impl_test_vectors::write_to_ts_project_path( + JSON_FILE_NAME, + &formatted_json, + &fixture_path, + ); + + // Verify the test vectors are valid + verify_test_vectors(&test_vectors_output, &fixture_path); + + println!("\nInstructions for manual copying test vectors (if needed):"); + println!( + " cp {} {}", + fixture_path.display(), + cross_impl_test_vectors::DEFAULT_TS_PROJECT_TEST_VECTORS_PATH + ); + } + + // Verify the test vectors to ensure they're properly formatted and usable + fn verify_test_vectors(vectors: &TestVectors, _path: &Path) { + println!("\nVerifying generated test vectors..."); + + // Verify each test vector category + for vector in &vectors.test_vectors { + match vector.vector_type.as_str() { + "random_generation" => { + if let Some(key_pairs) = &vector.random_key_pairs { + for (i, key_pair) in key_pairs.iter().enumerate() { + // Check the public key has correct X25519 length (32 bytes) + assert_eq!( + key_pair.public_key.len(), + 32, + "Key pair {} has invalid public key length", + i + ); + + // Create SessionStaticKey to verify format using serde_bytes::TryFromBytes trait + let _pub_key = + ::try_from_bytes( + &key_pair.public_key, + ) + .expect("Failed to convert bytes to SessionStaticKey"); + } + println!(" ✓ Validated {} random key pairs", key_pairs.len()); + } + } + "key_exchange" => { + if let Some(scenarios) = &vector.key_exchange_scenarios { + for (i, scenario) in scenarios.iter().enumerate() { + // Check key lengths + assert_eq!( + scenario.initiator_public_key.len(), + 32, + "Scenario {} has invalid initiator public key length", + i + ); + assert_eq!( + scenario.responder_public_key.len(), + 32, + "Scenario {} has invalid responder public key length", + i + ); + assert_eq!( + scenario.shared_secret.len(), + 32, + "Scenario {} has invalid shared secret length", + i + ); + + // Verify we can reconstruct the public keys using serde_bytes::TryFromBytes trait + let _initiator_key = + ::try_from_bytes( + &scenario.initiator_public_key, + ) + .expect("Failed to convert bytes to initiator SessionStaticKey"); + let _responder_key = + ::try_from_bytes( + &scenario.responder_public_key, + ) + .expect("Failed to convert bytes to responder SessionStaticKey"); + + // Create a shared secret for validation + let _shared_secret = + SessionSharedSecret::from_test_vector(&scenario.shared_secret); + } + println!(" ✓ Validated {} key exchange scenarios", scenarios.len()); + } + } + _ => panic!("Unknown vector type: {}", vector.vector_type), + } + } + + println!( + "\n✓ Test vectors successfully validated! Ready for cross-implementation testing." + ); + } +}