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

663 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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(()));
}
}