montana/Montana-Protocol/Code/crates/mt-crypto/src/lib.rs

663 lines
26 KiB
Rust
Raw Normal View History

// 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<Self> {
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<Self> {
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<Self> {
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<Signature, CryptoError> {
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<Self> {
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<Self> {
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<SuiteId> {
match v {
0x0001 => Some(SuiteId::Mldsa65),
_ => None,
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CryptoSelfTestError {
KeyGenSizeMismatch,
KeyGenDeterminismFailure,
SignVerifyFailure,
KatPubkeyMismatch,
KatSignatureMismatch,
PrimitiveError(CryptoError),
}
impl From<CryptoError> 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(()));
}
}