// spec, раздел "Криптография" + "Криптографическая реализация → Primitive layer" // // Internals delegируются к mt-crypto-native (тонкий FFI shim над OpenSSL 3.5.5 LTS // vendored через openssl-src). Public API mt-crypto byte-stable per [C-6] hard // requirement #8 (pluggability через mt-crypto API): swap implementation library // без re-architecture protocol. use mt_crypto_native::{ mt_keypair_from_seed_mldsa, mt_keypair_from_seed_mlkem, mt_sign_mldsa, mt_verify_mldsa, MT_ERR_INVALID_INPUT, MT_ERR_INVALID_PUBLIC_KEY, MT_ERR_INVALID_SECRET_KEY, MT_ERR_KEYGEN_FAILED, MT_ERR_OPENSSL_INIT, MT_ERR_PARAM_FETCH_FAILED, MT_ERR_PARAM_QUERY_FAILED, MT_ERR_PARAM_SIZE_MISMATCH, MT_ERR_SIGN_FAILED, MT_ERR_SIGN_LENGTH_MISMATCH, MT_OK, }; use sha2::{Digest, Sha256}; use zeroize::Zeroize; #[derive(Debug, Clone, Eq, PartialEq)] pub enum CryptoError { InvalidInput, OpensslInit, KeygenFailed, SignFailed, SignLengthMismatch, ParamQueryFailed, ParamSizeMismatch, ParamFetchFailed, InvalidSecretKey, InvalidPublicKey, Other(i32), } impl CryptoError { fn from_code(c: i32) -> Self { match c { MT_ERR_INVALID_INPUT => Self::InvalidInput, MT_ERR_OPENSSL_INIT => Self::OpensslInit, MT_ERR_KEYGEN_FAILED => Self::KeygenFailed, MT_ERR_SIGN_FAILED => Self::SignFailed, MT_ERR_SIGN_LENGTH_MISMATCH => Self::SignLengthMismatch, MT_ERR_PARAM_QUERY_FAILED => Self::ParamQueryFailed, MT_ERR_PARAM_SIZE_MISMATCH => Self::ParamSizeMismatch, MT_ERR_PARAM_FETCH_FAILED => Self::ParamFetchFailed, MT_ERR_INVALID_SECRET_KEY => Self::InvalidSecretKey, MT_ERR_INVALID_PUBLIC_KEY => Self::InvalidPublicKey, other => Self::Other(other), } } } impl std::fmt::Display for CryptoError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InvalidInput => write!(f, "invalid input"), Self::OpensslInit => write!(f, "OpenSSL init failed"), Self::KeygenFailed => write!(f, "keygen failed"), Self::SignFailed => write!(f, "sign failed"), Self::SignLengthMismatch => write!(f, "signature length mismatch"), Self::ParamQueryFailed => write!(f, "OSSL param query failed"), Self::ParamSizeMismatch => write!(f, "OSSL param size mismatch"), Self::ParamFetchFailed => write!(f, "OSSL param fetch failed"), Self::InvalidSecretKey => write!(f, "invalid ML-DSA secret key bytes"), Self::InvalidPublicKey => write!(f, "invalid ML-DSA public key bytes"), Self::Other(c) => write!(f, "crypto error code {}", c), } } } impl std::error::Error for CryptoError {} pub const HASH_SIZE: usize = 32; // spec: ML-DSA-65 (FIPS 204 level 3) — pubkey/secret/signature sizes pub const PUBLIC_KEY_SIZE: usize = 1952; pub const SECRET_KEY_SIZE: usize = 4032; pub const SIGNATURE_SIZE: usize = 3309; // spec: ML-DSA seed (FIPS 204 §3.1, ξ ∈ B32) для deterministic KeyGen_internal pub const KEYPAIR_SEED_SIZE: usize = 32; // spec: ML-KEM-768 (FIPS 203 security level 3) — pubkey/secret/seed sizes pub const MLKEM_PUBLIC_KEY_SIZE: usize = 1184; pub const MLKEM_SECRET_KEY_SIZE: usize = 2400; pub const MLKEM_SEED_SIZE: usize = 64; pub type Hash32 = [u8; HASH_SIZE]; pub fn hash(domain: &[u8], parts: &[&[u8]]) -> Hash32 { let mut hasher = Sha256::new(); hasher.update(domain); hasher.update([0u8]); for part in parts { hasher.update(part); } hasher.finalize().into() } pub fn sha256_raw(bytes: &[u8]) -> Hash32 { let mut hasher = Sha256::new(); hasher.update(bytes); hasher.finalize().into() } #[derive(Clone, Eq, PartialEq, Debug)] pub struct PublicKey([u8; PUBLIC_KEY_SIZE]); impl PublicKey { pub fn from_array(bytes: [u8; PUBLIC_KEY_SIZE]) -> Self { Self(bytes) } pub fn from_slice(bytes: &[u8]) -> Option { if bytes.len() != PUBLIC_KEY_SIZE { return None; } let mut arr = [0u8; PUBLIC_KEY_SIZE]; arr.copy_from_slice(bytes); Some(Self(arr)) } pub fn as_bytes(&self) -> &[u8; PUBLIC_KEY_SIZE] { &self.0 } } // SecretKey хранит SK на heap (Box), не stack — два преимущества: // (a) bytes живут в одной heap-локации от construction до drop, никаких // stack memcpy при moves (только pointer copy) // (b) mlock применяется к heap странице один раз при construction; // вся жизнь SK страница защищена от swap-out // Public API через as_bytes() возвращает &[u8; N] (auto-deref), эквивалентно // предыдущему stack-варианту. pub struct SecretKey(Box<[u8; SECRET_KEY_SIZE]>); impl SecretKey { // Создание из stack-array: bytes принимается by-value (Rust move). После // копирования в heap явно zeroize stack copy чтобы bytes не остались в // stack frame после возврата. pub fn from_array(mut bytes: [u8; SECRET_KEY_SIZE]) -> Self { let mut boxed = alloc_locked_secret_box(SECRET_KEY_SIZE); boxed.copy_from_slice(&bytes); bytes.zeroize(); let arr_box: Box<[u8; SECRET_KEY_SIZE]> = boxed.try_into().expect("box size matches SECRET_KEY_SIZE"); Self(arr_box) } pub fn from_slice(bytes: &[u8]) -> Option { if bytes.len() != SECRET_KEY_SIZE { return None; } let mut boxed = alloc_locked_secret_box(SECRET_KEY_SIZE); boxed.copy_from_slice(bytes); let arr_box: Box<[u8; SECRET_KEY_SIZE]> = boxed.try_into().expect("box size matches SECRET_KEY_SIZE"); Some(Self(arr_box)) } pub fn as_bytes(&self) -> &[u8; SECRET_KEY_SIZE] { &self.0 } } impl Drop for SecretKey { fn drop(&mut self) { self.0.zeroize(); unsafe { // SAFETY: self.0 — owned heap-allocated Box<[u8; SECRET_KEY_SIZE]>, // pointer valid for SECRET_KEY_SIZE bytes на момент Drop. munlock // принимает const void*, не мутирует данные. errno EINVAL // (если mlock не применялся в fallback path) игнорируется // как no-op. libc::munlock(self.0.as_ptr() as *const libc::c_void, SECRET_KEY_SIZE); } } } // Best-effort heap allocation для secret bytes с mlock защитой от swap. // При memory pressure ОС не выгружает локированные страницы в swap файл. // Если mlock fails (RLIMIT_MEMLOCK exceeded на Linux, kern.maxlockedmem // на macOS, либо процесс без CAP_IPC_LOCK на Linux) — возвращаем // non-locked Box. Это best-effort: secret bytes не утекают в swap при // успешном mlock; при failure полагаемся на encrypted swap (FileVault // macOS / LUKS Linux). // // TODO (F-5 closure pending mt-telemetry crate): runtime warning hook при // mlock failure. Текущая реализация silently fallback-ит на non-locked Box // без логирования. После появления mt-telemetry crate — emit структурный // event `crypto.mlock.failure` с errno → operator видит что secret bytes // могут быть в plaintext swap и принимает решение (увеличить // RLIMIT_MEMLOCK / включить FileVault). Без telemetry hook log-в-stderr // никто не читает в production — поэтому отложено до telemetry framework. fn alloc_locked_secret_box(size: usize) -> Box<[u8]> { let boxed = vec![0u8; size].into_boxed_slice(); unsafe { // SAFETY: boxed — freshly allocated heap-buffer of `size` bytes // (vec![0u8; size].into_boxed_slice() гарантирует valid pointer + // exact size). mlock принимает const void*, не мутирует данные; // operates на page-aligned region containing the buffer. Return // code 0 = success, -1 = failure (errno set: ENOMEM при превышении // RLIMIT_MEMLOCK, EPERM без CAP_IPC_LOCK на Linux, // kern.maxlockedmem на macOS). Best-effort: при failure возвращаем // non-locked Box, polагаемся на encrypted swap (FileVault/LUKS). let _ = libc::mlock(boxed.as_ptr() as *const libc::c_void, size); } boxed } #[derive(Clone, Eq, PartialEq, Debug)] pub struct Signature([u8; SIGNATURE_SIZE]); impl Signature { pub fn from_array(bytes: [u8; SIGNATURE_SIZE]) -> Self { Self(bytes) } pub fn from_slice(bytes: &[u8]) -> Option { if bytes.len() != SIGNATURE_SIZE { return None; } let mut arr = [0u8; SIGNATURE_SIZE]; arr.copy_from_slice(bytes); Some(Self(arr)) } pub fn as_bytes(&self) -> &[u8; SIGNATURE_SIZE] { &self.0 } } pub fn keypair_from_seed( seed: &[u8; KEYPAIR_SEED_SIZE], ) -> Result<(PublicKey, SecretKey), CryptoError> { let mut pk = [0u8; PUBLIC_KEY_SIZE]; // SK alloc на heap с mlock — FFI пишет напрямую в locked heap, // никаких stack temporary buffers с secret bytes. let mut sk_box = alloc_locked_secret_box(SECRET_KEY_SIZE); let r = unsafe { // SAFETY: seed/pk — valid pointers (stack); sk_box — heap-allocated // buffer of SECRET_KEY_SIZE bytes (mlock-protected). Размеры // соответствуют FFI контракту mt-crypto-native (seed = // MLDSA65_SEED_SIZE = 32, pk_out = MLDSA65_PUBKEY_SIZE = 1952, // sk_out = MLDSA65_SECRETKEY_SIZE = 4032). C wrapper применяет // (void*)seed cast для OpenSSL EVP API convention (см. mt_crypto.c // keypair_from_seed_generic) — read-only, не мутирует seed bytes. mt_keypair_from_seed_mldsa(seed.as_ptr(), pk.as_mut_ptr(), sk_box.as_mut_ptr()) }; if r != MT_OK { // sk_box drops here, bytes are zeroized via Vec drop (no zeroize // on raw Box<[u8]>) — explicitly zeroize before drop: sk_box.zeroize(); return Err(CryptoError::from_code(r)); } let arr_box: Box<[u8; SECRET_KEY_SIZE]> = sk_box.try_into().expect("box size matches SECRET_KEY_SIZE"); Ok((PublicKey(pk), SecretKey(arr_box))) } #[cfg(any(test, feature = "testing"))] pub fn keypair() -> (PublicKey, SecretKey) { // Test-only helper. Энтропия через `getrandom` — OS CSPRNG (на Linux: // getrandom(2) syscall, fallback на /dev/urandom; на macOS: // SecRandomCopyBytes; на Windows: BCryptGenRandom). Это // production-grade источник энтропии, в отличие от старой реализации // через `SystemTime::now() + PID + stack address` (~50 бит entropy, // theoretically brute-forceable). // // Gate-нут `#[cfg(any(test, feature = "testing"))]` — даже с // CSPRNG-источником функция не должна быть в production binary, потому // что real identity всегда через `keypair_from_seed` от HKDF-derived // master_seed (deterministic recovery flow). let mut seed = [0u8; KEYPAIR_SEED_SIZE]; getrandom::getrandom(&mut seed).expect("OS CSPRNG (getrandom) недоступен"); keypair_from_seed(&seed).expect("keypair: random seed cannot fail ML-DSA KeyGen") } pub fn sign(sk: &SecretKey, msg: &[u8]) -> Result { let mut sig = [0u8; SIGNATURE_SIZE]; let r = unsafe { // SAFETY: sk.0 — valid pointer на массив SECRET_KEY_SIZE байт; msg — // valid slice, msg.len() корректен; sig — valid pointer на буфер // SIGNATURE_SIZE байт. mt-crypto-native эмитит ровно SIGNATURE_SIZE // байт deterministic ML-DSA подписи (FIPS 204 Algorithm 2 deterministic // вариант — обязателен per [I-3] consensus determinism). C wrapper // применяет (void*)sk cast в mldsa_pkey_from_secret для OpenSSL EVP // convention — read-only, не мутирует SK bytes. mt_sign_mldsa(sk.0.as_ptr(), msg.as_ptr(), msg.len(), sig.as_mut_ptr()) }; if r != MT_OK { return Err(CryptoError::from_code(r)); } Ok(Signature(sig)) } pub fn verify(pk: &PublicKey, msg: &[u8], sig: &Signature) -> bool { let r = unsafe { // SAFETY: pk.0 / sig.0 — valid pointers на массивы фиксированного // размера (PUBLIC_KEY_SIZE / SIGNATURE_SIZE); msg — valid slice // длины msg.len(). mt-crypto-native возвращает MT_OK при successful // verify, любой другой код — невалидная подпись. C wrapper применяет // (void*)pk cast в mldsa_pkey_from_public для OpenSSL EVP convention // — read-only, не мутирует PK bytes. mt_verify_mldsa(pk.0.as_ptr(), msg.as_ptr(), msg.len(), sig.0.as_ptr()) }; r == MT_OK } // spec: ML-KEM-768 (FIPS 203 level 3) — encapsulation/decapsulation keys // для шифрования сообщений (Application Layer). Используется через // deterministic from_seed(64B) для recovery flow per HKDF-Expand // per-role derivation ("mt-app-encryption-key"). #[derive(Clone, Eq, PartialEq, Debug)] pub struct MlkemPublicKey([u8; MLKEM_PUBLIC_KEY_SIZE]); impl MlkemPublicKey { pub fn from_array(bytes: [u8; MLKEM_PUBLIC_KEY_SIZE]) -> Self { Self(bytes) } pub fn from_slice(bytes: &[u8]) -> Option { if bytes.len() != MLKEM_PUBLIC_KEY_SIZE { return None; } let mut arr = [0u8; MLKEM_PUBLIC_KEY_SIZE]; arr.copy_from_slice(bytes); Some(Self(arr)) } pub fn as_bytes(&self) -> &[u8; MLKEM_PUBLIC_KEY_SIZE] { &self.0 } } pub struct MlkemSecretKey(Box<[u8; MLKEM_SECRET_KEY_SIZE]>); impl MlkemSecretKey { pub fn from_array(mut bytes: [u8; MLKEM_SECRET_KEY_SIZE]) -> Self { let mut boxed = alloc_locked_secret_box(MLKEM_SECRET_KEY_SIZE); boxed.copy_from_slice(&bytes); bytes.zeroize(); let arr_box: Box<[u8; MLKEM_SECRET_KEY_SIZE]> = boxed .try_into() .expect("box size matches MLKEM_SECRET_KEY_SIZE"); Self(arr_box) } pub fn from_slice(bytes: &[u8]) -> Option { if bytes.len() != MLKEM_SECRET_KEY_SIZE { return None; } let mut boxed = alloc_locked_secret_box(MLKEM_SECRET_KEY_SIZE); boxed.copy_from_slice(bytes); let arr_box: Box<[u8; MLKEM_SECRET_KEY_SIZE]> = boxed .try_into() .expect("box size matches MLKEM_SECRET_KEY_SIZE"); Some(Self(arr_box)) } pub fn as_bytes(&self) -> &[u8; MLKEM_SECRET_KEY_SIZE] { &self.0 } } impl Drop for MlkemSecretKey { fn drop(&mut self) { self.0.zeroize(); unsafe { // SAFETY: self.0 — owned heap-allocated Box<[u8; // MLKEM_SECRET_KEY_SIZE]>, pointer valid for // MLKEM_SECRET_KEY_SIZE bytes на момент Drop. munlock не мутирует // данные. errno EINVAL при no-prior-mlock игнорируется (no-op // semantics в fallback path). libc::munlock( self.0.as_ptr() as *const libc::c_void, MLKEM_SECRET_KEY_SIZE, ); } } } pub fn keypair_from_seed_mlkem( seed: &[u8; MLKEM_SEED_SIZE], ) -> Result<(MlkemPublicKey, MlkemSecretKey), CryptoError> { let mut pk = [0u8; MLKEM_PUBLIC_KEY_SIZE]; let mut sk_box = alloc_locked_secret_box(MLKEM_SECRET_KEY_SIZE); let r = unsafe { // SAFETY: seed/pk — valid pointers (stack); sk_box — heap-allocated // buffer of MLKEM_SECRET_KEY_SIZE bytes (mlock-protected). Размеры // соответствуют FFI контракту mt-crypto-native (seed = // MLKEM768_SEED_SIZE = 64, pk_out = MLKEM768_PUBKEY_SIZE = 1184, // sk_out = MLKEM768_SECRETKEY_SIZE = 2400). FIPS 203 // ML-KEM.KeyGen_internal(d, z) deterministic с d=seed[0..32], // z=seed[32..64]. C wrapper применяет (void*)seed cast для OpenSSL // EVP API convention — read-only, не мутирует seed bytes. mt_keypair_from_seed_mlkem(seed.as_ptr(), pk.as_mut_ptr(), sk_box.as_mut_ptr()) }; if r != MT_OK { sk_box.zeroize(); return Err(CryptoError::from_code(r)); } let arr_box: Box<[u8; MLKEM_SECRET_KEY_SIZE]> = sk_box .try_into() .expect("box size matches MLKEM_SECRET_KEY_SIZE"); Ok((MlkemPublicKey(pk), MlkemSecretKey(arr_box))) } #[repr(u16)] #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum SuiteId { // spec: "Активная схема на момент запуска: ML-DSA-65", suite_id = 0x0001 Mldsa65 = 0x0001, } pub fn suite_id_from_u16(v: u16) -> Option { match v { 0x0001 => Some(SuiteId::Mldsa65), _ => None, } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum CryptoSelfTestError { KeyGenSizeMismatch, KeyGenDeterminismFailure, SignVerifyFailure, KatPubkeyMismatch, KatSignatureMismatch, PrimitiveError(CryptoError), } impl From for CryptoSelfTestError { fn from(e: CryptoError) -> Self { Self::PrimitiveError(e) } } // KAT 1 binding fingerprints — ML-DSA-65.KeyGen(seed=[0x00; 32]) per spec // «KeyGen output binding vectors». Full pk/sk проверяются через // `crates/mt-mnemonic/tests/keygen_vectors.rs::kat_1_mldsa_seed_zero` — // здесь сверяется SHA-256 fingerprint (collision-resistance гарантирует // byte-equivalence). Cross-implementation conformance: одинаковый pk/sk // ⇔ одинаковый SHA-256 → достаточно для determinism check. pub const EXPECTED_KAT_1_PK_SHA256: [u8; HASH_SIZE] = [ 0x08, 0x5b, 0xa3, 0x80, 0xff, 0x38, 0x6d, 0xd5, 0x2e, 0x42, 0x34, 0x9c, 0x6e, 0xb8, 0x84, 0x89, 0xd6, 0x05, 0x8e, 0xa5, 0x41, 0xa4, 0xe3, 0xfb, 0x0d, 0xce, 0x9a, 0x3f, 0xd1, 0xf7, 0xa9, 0x11, ]; pub const EXPECTED_KAT_1_SK_SHA256: [u8; HASH_SIZE] = [ 0xcf, 0xcb, 0x5e, 0x7e, 0xdf, 0x43, 0x48, 0xf7, 0x12, 0xb7, 0x00, 0x2b, 0x05, 0x53, 0xd2, 0x89, 0x29, 0x85, 0x69, 0x36, 0xc9, 0x8e, 0x4a, 0xdf, 0x17, 0x2e, 0x51, 0xd5, 0xc9, 0x93, 0x42, 0x62, ]; pub fn self_test() -> Result<(), CryptoSelfTestError> { // Structural invariants + determinism + sign/verify roundtrip let seed = [0x42u8; KEYPAIR_SEED_SIZE]; let (pk1, sk1) = keypair_from_seed(&seed)?; if pk1.as_bytes().len() != PUBLIC_KEY_SIZE || sk1.as_bytes().len() != SECRET_KEY_SIZE { return Err(CryptoSelfTestError::KeyGenSizeMismatch); } let (pk2, sk2) = keypair_from_seed(&seed)?; if pk1.as_bytes() != pk2.as_bytes() || sk1.as_bytes() != sk2.as_bytes() { return Err(CryptoSelfTestError::KeyGenDeterminismFailure); } let msg = b"mt-crypto self-test message"; let sig = sign(&sk1, msg)?; if !verify(&pk1, msg, &sig) { return Err(CryptoSelfTestError::SignVerifyFailure); } // KAT 1 byte-exact conformance: ML-DSA-65.KeyGen([0x00; 32]) let kat_seed = [0x00u8; KEYPAIR_SEED_SIZE]; let (kat_pk, kat_sk) = keypair_from_seed(&kat_seed)?; let pk_hash = sha256_raw(kat_pk.as_bytes()); if pk_hash != EXPECTED_KAT_1_PK_SHA256 { return Err(CryptoSelfTestError::KatPubkeyMismatch); } let sk_hash = sha256_raw(kat_sk.as_bytes()); if sk_hash != EXPECTED_KAT_1_SK_SHA256 { return Err(CryptoSelfTestError::KatSignatureMismatch); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn sizes_match_spec() { assert_eq!(HASH_SIZE, 32); assert_eq!(PUBLIC_KEY_SIZE, 1952); assert_eq!(SECRET_KEY_SIZE, 4032); assert_eq!(SIGNATURE_SIZE, 3309); assert_eq!(KEYPAIR_SEED_SIZE, 32); } #[test] fn hash_determinism() { let a = hash(b"mt-account", &[b"hello"]); let b = hash(b"mt-account", &[b"hello"]); assert_eq!(a, b); } #[test] fn hash_different_domain_different_output() { let a = hash(b"mt-account", &[b"hello"]); let b = hash(b"mt-node", &[b"hello"]); assert_ne!(a, b); } #[test] fn hash_different_input_different_output() { let a = hash(b"mt-account", &[b"hello"]); let b = hash(b"mt-account", &[b"world"]); assert_ne!(a, b); } #[test] fn hash_nist_vector_abc() { let expected = [ 0xBA, 0x78, 0x16, 0xBF, 0x8F, 0x01, 0xCF, 0xEA, 0x41, 0x41, 0x40, 0xDE, 0x5D, 0xAE, 0x22, 0x23, 0xB0, 0x03, 0x61, 0xA3, 0x96, 0x17, 0x7A, 0x9C, 0xB4, 0x10, 0xFF, 0x61, 0xF2, 0x00, 0x15, 0xAD, ]; assert_eq!(sha256_raw(b"abc"), expected); } #[test] fn hash_parts_concatenate() { let a = hash(b"mt-op", &[b"aa", b"bb", b"cc"]); let b = hash(b"mt-op", &[b"aabbcc"]); assert_eq!(a, b); } #[test] fn keypair_correct_sizes() { let (pk, sk) = keypair(); assert_eq!(pk.as_bytes().len(), PUBLIC_KEY_SIZE); assert_eq!(sk.as_bytes().len(), SECRET_KEY_SIZE); } #[test] fn keypair_from_seed_deterministic() { let seed = [0x37u8; KEYPAIR_SEED_SIZE]; let (pk1, sk1) = keypair_from_seed(&seed).expect("keygen"); let (pk2, sk2) = keypair_from_seed(&seed).expect("keygen"); assert_eq!(pk1.as_bytes(), pk2.as_bytes()); assert_eq!(sk1.as_bytes(), sk2.as_bytes()); } #[test] fn keypair_from_seed_different_seeds_different_keys() { let s1 = [0x11u8; KEYPAIR_SEED_SIZE]; let s2 = [0x22u8; KEYPAIR_SEED_SIZE]; let (pk1, _) = keypair_from_seed(&s1).expect("keygen s1"); let (pk2, _) = keypair_from_seed(&s2).expect("keygen s2"); assert_ne!(pk1.as_bytes(), pk2.as_bytes()); } #[test] fn keypair_returns_different_keys_on_consecutive_calls() { let (pk1, _) = keypair(); let (pk2, _) = keypair(); // Random seed-based — два последовательных вызова возвращают разные ключи assert_ne!(pk1.as_bytes(), pk2.as_bytes()); } #[test] fn sign_verify_roundtrip() { let (pk, sk) = keypair(); let msg = b"Montana protocol test message"; let sig = sign(&sk, msg).expect("sign"); assert_eq!(sig.as_bytes().len(), SIGNATURE_SIZE); assert!(verify(&pk, msg, &sig)); } #[test] fn sign_deterministic() { let seed = [0x55u8; KEYPAIR_SEED_SIZE]; let (_, sk) = keypair_from_seed(&seed).expect("keygen"); let msg = b"determinism check"; let s1 = sign(&sk, msg).expect("sign s1"); let s2 = sign(&sk, msg).expect("sign s2"); assert_eq!(s1.as_bytes(), s2.as_bytes()); } #[test] fn verify_rejects_mutated_message() { let (pk, sk) = keypair(); let msg = b"original"; let sig = sign(&sk, msg).expect("sign"); assert!(!verify(&pk, b"mutated", &sig)); } #[test] fn verify_rejects_mutated_signature() { let (pk, sk) = keypair(); let msg = b"payload"; let sig = sign(&sk, msg).expect("sign"); let mut bad = *sig.as_bytes(); bad[0] ^= 0xFF; bad[100] ^= 0xAA; let bad_sig = Signature::from_array(bad); assert!(!verify(&pk, msg, &bad_sig)); } #[test] fn verify_rejects_wrong_public_key() { let (_, sk) = keypair(); let (other_pk, _) = keypair(); let msg = b"cross-key test"; let sig = sign(&sk, msg).expect("sign"); assert!(!verify(&other_pk, msg, &sig)); } #[test] fn public_key_from_slice_rejects_wrong_size() { assert!(PublicKey::from_slice(&[0u8; PUBLIC_KEY_SIZE - 1]).is_none()); assert!(PublicKey::from_slice(&[0u8; PUBLIC_KEY_SIZE + 1]).is_none()); assert!(PublicKey::from_slice(&[0u8; PUBLIC_KEY_SIZE]).is_some()); } #[test] fn secret_key_from_slice_rejects_wrong_size() { assert!(SecretKey::from_slice(&[0u8; SECRET_KEY_SIZE - 1]).is_none()); assert!(SecretKey::from_slice(&[0u8; SECRET_KEY_SIZE]).is_some()); } #[test] fn signature_from_slice_rejects_wrong_size() { assert!(Signature::from_slice(&[0u8; SIGNATURE_SIZE - 1]).is_none()); assert!(Signature::from_slice(&[0u8; SIGNATURE_SIZE + 1]).is_none()); assert!(Signature::from_slice(&[0u8; SIGNATURE_SIZE]).is_some()); } #[test] fn suite_id_mldsa65_value() { assert_eq!(SuiteId::Mldsa65 as u16, 0x0001); } #[test] fn suite_id_from_u16_valid() { assert_eq!(suite_id_from_u16(0x0001), Some(SuiteId::Mldsa65)); } #[test] fn suite_id_from_u16_invalid() { assert_eq!(suite_id_from_u16(0x0000), None); assert_eq!(suite_id_from_u16(0x0002), None); assert_eq!(suite_id_from_u16(0xFFFF), None); } #[test] fn public_key_roundtrip_from_array_to_bytes() { let (pk, _) = keypair(); let bytes = *pk.as_bytes(); let reconstructed = PublicKey::from_array(bytes); assert_eq!(pk, reconstructed); } #[test] fn self_test_passes() { assert_eq!(self_test(), Ok(())); } }