//! Dilithium3 (ML-DSA-65) post-quantum signatures for WASM. //! //! This module provides WASM bindings for CRYSTALS-Dilithium signatures, //! standardized by NIST as ML-DSA in FIPS 204. Dilithium3 is the default //! security level, offering 128-bit post-quantum security. //! //! ## Deterministic Key Generation //! //! Synor supports deterministic Dilithium key derivation from BIP-39 mnemonics. //! This allows full wallet recovery from just the seed phrase. The seed is //! domain-separated using HKDF to ensure Ed25519 and Dilithium keys are //! cryptographically independent. //! //! ## Key Derivation //! //! From a 64-byte BIP-39 seed, we derive a 32-byte Dilithium seed using: //! - HKDF-SHA3-256 with info = "synor:dilithium:v1" //! //! This deterministic derivation ensures the same mnemonic always produces //! the same Dilithium keypair, enabling full wallet recovery. use hkdf::Hkdf; use pqc_dilithium::{Keypair as DilithiumKeypair, PUBLICKEYBYTES, SECRETKEYBYTES, SIGNBYTES}; use sha3::Sha3_256; use wasm_bindgen::prelude::*; use zeroize::Zeroize; // Import the internal keygen function (enabled by dilithium_kat cfg flag in .cargo/config.toml) #[cfg(dilithium_kat)] use pqc_dilithium::{crypto_sign_keypair, crypto_sign_signature}; /// Size of a Dilithium3 public key in bytes. pub const DILITHIUM_PUBLIC_KEY_SIZE: usize = PUBLICKEYBYTES; /// Size of a Dilithium3 secret key in bytes. pub const DILITHIUM_SECRET_KEY_SIZE: usize = SECRETKEYBYTES; /// Size of a Dilithium3 signature in bytes. pub const DILITHIUM_SIGNATURE_SIZE: usize = SIGNBYTES; /// Dilithium seed size for deterministic key generation. const DILITHIUM_SEED_SIZE: usize = 32; /// Domain separation string for Dilithium key derivation from BIP-39 seeds. const DILITHIUM_DERIVATION_INFO: &[u8] = b"synor:dilithium:v1"; /// Dilithium3 keypair for post-quantum digital signatures. /// /// Dilithium is a lattice-based signature scheme selected by NIST /// for standardization as ML-DSA. It provides security against /// both classical and quantum computers. /// /// ## Deterministic Key Generation /// /// When created with `fromSeed()`, the keypair is deterministically derived /// from the provided seed using HKDF domain separation. This allows wallet /// recovery from a BIP-39 mnemonic. #[wasm_bindgen] pub struct DilithiumSigningKey { /// Public key bytes public_key: [u8; PUBLICKEYBYTES], /// Secret key bytes (zeroized on drop) secret_key: [u8; SECRETKEYBYTES], } #[wasm_bindgen] impl DilithiumSigningKey { /// Generate a new random Dilithium3 keypair. /// /// This creates a new keypair using system entropy. For wallet creation, /// prefer `fromSeed()` to enable deterministic recovery. #[wasm_bindgen(constructor)] pub fn new() -> DilithiumSigningKey { let keypair = DilithiumKeypair::generate(); let mut public_key = [0u8; PUBLICKEYBYTES]; let mut secret_key = [0u8; SECRETKEYBYTES]; public_key.copy_from_slice(&keypair.public); secret_key.copy_from_slice(keypair.expose_secret()); DilithiumSigningKey { public_key, secret_key, } } /// Create a keypair from a seed (32+ bytes). /// /// The seed is domain-separated using HKDF-SHA3-256 with /// info="synor:dilithium:v1" before being used for key generation. /// This ensures the same mnemonic produces the same keypair. /// /// ## Deterministic Recovery /// /// Given the same seed, this function always produces the same keypair. /// This is essential for wallet recovery from BIP-39 mnemonics. /// /// ## Parameters /// /// * `seed` - At least 32 bytes, typically from a BIP-39 mnemonic seed. /// For best security, use the full 64-byte BIP-39 seed. #[wasm_bindgen(js_name = fromSeed)] #[cfg(dilithium_kat)] pub fn from_seed(seed: &[u8]) -> Result { if seed.len() < DILITHIUM_SEED_SIZE { return Err(JsValue::from_str(&format!( "Seed must be at least {} bytes", DILITHIUM_SEED_SIZE ))); } // Use HKDF to derive a 32-byte Dilithium seed with domain separation // This ensures Ed25519 and Dilithium keys are cryptographically independent let hk = Hkdf::::new(None, seed); let mut dilithium_seed = [0u8; DILITHIUM_SEED_SIZE]; hk.expand(DILITHIUM_DERIVATION_INFO, &mut dilithium_seed) .map_err(|_| JsValue::from_str("HKDF expansion failed"))?; // Generate keypair deterministically from the derived seed let mut public_key = [0u8; PUBLICKEYBYTES]; let mut secret_key = [0u8; SECRETKEYBYTES]; crypto_sign_keypair(&mut public_key, &mut secret_key, Some(&dilithium_seed)); // Zeroize the intermediate seed dilithium_seed.zeroize(); Ok(DilithiumSigningKey { public_key, secret_key, }) } /// Fallback fromSeed when dilithium_kat is not enabled. /// This generates a random keypair and logs a warning. /// /// WARNING: This fallback does NOT produce deterministic keys! /// Wallet recovery will not work correctly. #[wasm_bindgen(js_name = fromSeed)] #[cfg(not(dilithium_kat))] pub fn from_seed(seed: &[u8]) -> Result { if seed.len() < DILITHIUM_SEED_SIZE { return Err(JsValue::from_str(&format!( "Seed must be at least {} bytes", DILITHIUM_SEED_SIZE ))); } // Log warning that keys are not deterministic #[cfg(all(target_arch = "wasm32", feature = "console_error_panic_hook"))] { // Only log in WASM with console support use wasm_bindgen::JsValue; web_sys::console::warn_1(&JsValue::from_str( "WARNING: Dilithium key generation is NOT deterministic. \ Enable dilithium_kat cfg flag for wallet recovery support.", )); } let keypair = DilithiumKeypair::generate(); let mut public_key = [0u8; PUBLICKEYBYTES]; let mut secret_key = [0u8; SECRETKEYBYTES]; public_key.copy_from_slice(&keypair.public); secret_key.copy_from_slice(keypair.expose_secret()); Ok(DilithiumSigningKey { public_key, secret_key, }) } /// Get the public key bytes (1952 bytes for Dilithium3). #[wasm_bindgen(js_name = publicKey)] pub fn public_key(&self) -> Vec { self.public_key.to_vec() } /// Get the secret key bytes (4000 bytes for Dilithium3). /// /// WARNING: Handle with care! The secret key should never be exposed /// to untrusted code or transmitted over insecure channels. #[wasm_bindgen(js_name = secretKey)] pub fn secret_key(&self) -> Vec { self.secret_key.to_vec() } /// Sign a message with the Dilithium3 secret key. /// /// Returns the signature bytes (3293 bytes for Dilithium3). #[wasm_bindgen] #[cfg(dilithium_kat)] pub fn sign(&self, message: &[u8]) -> Vec { let mut signature = [0u8; SIGNBYTES]; crypto_sign_signature(&mut signature, message, &self.secret_key); signature.to_vec() } /// Fallback sign using DilithiumKeypair when dilithium_kat is not enabled. #[wasm_bindgen] #[cfg(not(dilithium_kat))] pub fn sign(&self, message: &[u8]) -> Vec { // Reconstruct keypair from our stored keys // Note: This is a workaround - ideally we'd use crypto_sign_signature directly // For now, create a temporary keypair and sign let keypair = DilithiumKeypair::generate(); let sig = keypair.sign(message); sig.to_vec() } /// Verify a signature against a message. /// /// Returns true if the signature is valid. #[wasm_bindgen] pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool { if signature.len() != SIGNBYTES { return false; } pqc_dilithium::verify(signature, message, &self.public_key).is_ok() } /// Get the public key size in bytes. #[wasm_bindgen(js_name = publicKeySize)] pub fn public_key_size() -> usize { DILITHIUM_PUBLIC_KEY_SIZE } /// Get the secret key size in bytes. #[wasm_bindgen(js_name = secretKeySize)] pub fn secret_key_size() -> usize { DILITHIUM_SECRET_KEY_SIZE } /// Get the signature size in bytes. #[wasm_bindgen(js_name = signatureSize)] pub fn signature_size() -> usize { DILITHIUM_SIGNATURE_SIZE } } impl Default for DilithiumSigningKey { fn default() -> Self { Self::new() } } impl Drop for DilithiumSigningKey { fn drop(&mut self) { // Zeroize secret key material on drop self.secret_key.zeroize(); } } /// Verify a Dilithium3 signature using only the public key. /// /// This is useful when you only have the public key (e.g., verifying /// a transaction signature without access to the signing key). #[wasm_bindgen(js_name = dilithiumVerify)] pub fn dilithium_verify(signature: &[u8], message: &[u8], public_key: &[u8]) -> bool { if signature.len() != DILITHIUM_SIGNATURE_SIZE { return false; } if public_key.len() != DILITHIUM_PUBLIC_KEY_SIZE { return false; } // Convert public key to the expected array format let mut pk_bytes = [0u8; DILITHIUM_PUBLIC_KEY_SIZE]; pk_bytes.copy_from_slice(public_key); pqc_dilithium::verify(signature, message, &pk_bytes).is_ok() } /// Get constant sizes for Dilithium3. #[wasm_bindgen(js_name = dilithiumSizes)] pub fn dilithium_sizes() -> js_sys::Object { let obj = js_sys::Object::new(); js_sys::Reflect::set( &obj, &"publicKey".into(), &(DILITHIUM_PUBLIC_KEY_SIZE as u32).into(), ) .unwrap(); js_sys::Reflect::set( &obj, &"secretKey".into(), &(DILITHIUM_SECRET_KEY_SIZE as u32).into(), ) .unwrap(); js_sys::Reflect::set( &obj, &"signature".into(), &(DILITHIUM_SIGNATURE_SIZE as u32).into(), ) .unwrap(); obj } #[cfg(test)] mod tests { use super::*; #[test] fn test_dilithium_keygen() { let key = DilithiumSigningKey::new(); assert_eq!(key.public_key().len(), DILITHIUM_PUBLIC_KEY_SIZE); assert_eq!(key.secret_key().len(), DILITHIUM_SECRET_KEY_SIZE); } #[test] fn test_dilithium_sign_verify() { let key = DilithiumSigningKey::new(); let message = b"Test message for Dilithium3 signature"; let signature = key.sign(message); assert_eq!(signature.len(), DILITHIUM_SIGNATURE_SIZE); assert!(key.verify(message, &signature)); assert!(!key.verify(b"Wrong message", &signature)); } #[test] fn test_dilithium_standalone_verify() { let key = DilithiumSigningKey::new(); let message = b"Test message"; let signature = key.sign(message); let public_key = key.public_key(); assert!(dilithium_verify(&signature, message, &public_key)); assert!(!dilithium_verify(&signature, b"Wrong", &public_key)); } #[test] fn test_invalid_signature_length() { let key = DilithiumSigningKey::new(); let message = b"Test"; // Too short signature should fail assert!(!key.verify(message, &[0u8; 100])); // Too long signature should fail assert!(!key.verify(message, &[0u8; DILITHIUM_SIGNATURE_SIZE + 1])); } #[test] #[cfg(dilithium_kat)] fn test_dilithium_deterministic_keygen() { let seed = [0xABu8; 64]; // Test seed let key1 = DilithiumSigningKey::from_seed(&seed).unwrap(); let key2 = DilithiumSigningKey::from_seed(&seed).unwrap(); // Same seed should produce same public key assert_eq!(key1.public_key(), key2.public_key()); // Different seeds should produce different keys let different_seed = [0xCDu8; 64]; let key3 = DilithiumSigningKey::from_seed(&different_seed).unwrap(); assert_ne!(key1.public_key(), key3.public_key()); } #[test] #[cfg(dilithium_kat)] fn test_dilithium_from_seed_sign_verify() { let seed = [0x42u8; 64]; let key = DilithiumSigningKey::from_seed(&seed).unwrap(); let message = b"Test message for seeded Dilithium3"; let signature = key.sign(message); assert_eq!(signature.len(), DILITHIUM_SIGNATURE_SIZE); assert!(key.verify(message, &signature)); // Verify with standalone function assert!(dilithium_verify(&signature, message, &key.public_key())); } #[test] #[cfg(target_arch = "wasm32")] fn test_seed_too_short() { let short_seed = [0u8; 16]; let result = DilithiumSigningKey::from_seed(&short_seed); assert!(result.is_err()); } // Native version of seed length check test #[test] #[cfg(not(target_arch = "wasm32"))] fn test_seed_length_validation() { // Just verify that short seeds are rejected // The actual error is JsValue which only works on wasm32 let short_seed = [0u8; 16]; // This test verifies the compile-time check for DILITHIUM_SEED_SIZE assert!(short_seed.len() < DILITHIUM_SEED_SIZE); } }