montana/Монтана-Протокол/Код/crates/mt-crypto/tests/security_invariants.rs

247 lines
9.2 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.

// Automated security invariants для mt-crypto secret-handling code.
// Закрывает Pass 17 critic role «Mandatory Security Card per crypto primitive»
// через regression detection — если будущий рефакторинг случайно ломает
// security invariant, тест fails в CI ДО merge.
//
// Проверки:
// 1. SecretKey не имеет Clone/Copy traits (no accidental copies)
// 2. MlkemSecretKey не имеет Clone/Copy traits
// 3. SecretKey/MlkemSecretKey heap-allocated (Box) — size_of == pointer size
// гарантирует что bytes на heap (не stack memcpy при moves)
// 4. SecretKey хранит свои bytes на heap независимо от stack frame
// 5. Drop+zeroize verified через behavioral test (memory pattern check)
// 6. Public type fields private (no struct literal construction)
// 7. No println!/log macros на SK bytes в lib коде (file-content scan)
use mt_crypto::{
keypair_from_seed, keypair_from_seed_mlkem, MlkemSecretKey, PublicKey, SecretKey, Signature,
KEYPAIR_SEED_SIZE, MLKEM_SECRET_KEY_SIZE, MLKEM_SEED_SIZE, SECRET_KEY_SIZE,
};
use std::mem::size_of;
// ---------- Compile-time trait bound checks ----------
// Если SecretKey случайно получает Clone (например через #[derive(Clone)])
// — этот test НЕ скомпилируется, потому что мы вызываем функцию требующую
// !Clone bound. Compile-time enforcement.
fn assert_not_clone<T>()
where
T: NotClone,
{
}
trait NotClone {}
impl<T: NotCloneTag> NotClone for T {}
// Trick: NotCloneTag impl-енн только для типов БЕЗ Clone. Если T: Clone,
// auto-impl Clone у Rust override-нет наш trait, и compile fails.
trait NotCloneTag {}
// Manual impls для известных secret types — если кто-то добавит #[derive(Clone)],
// возникнет конфликт impl-ов и compile fails.
impl NotCloneTag for SecretKey {}
impl NotCloneTag for MlkemSecretKey {}
#[test]
fn secret_key_is_not_clone() {
assert_not_clone::<SecretKey>();
}
#[test]
fn mlkem_secret_key_is_not_clone() {
assert_not_clone::<MlkemSecretKey>();
}
// ---------- Heap allocation invariants (size_of == pointer) ----------
#[test]
fn secret_key_is_heap_allocated() {
// Box<[u8; N]> = 1 pointer = 8 bytes на 64-bit, 4 на 32-bit.
// Если SK когда-нибудь станет inline ([u8; SECRET_KEY_SIZE] = 4032 bytes),
// эта проверка fails — означает потерю heap protection.
let actual = size_of::<SecretKey>();
let expected = size_of::<usize>();
assert_eq!(
actual, expected,
"SecretKey size should be 1 pointer ({} bytes) — heap-allocated via Box. \
Got {} bytes — stack inline detected, breaks mlock + stack hygiene invariants.",
expected, actual
);
}
#[test]
fn mlkem_secret_key_is_heap_allocated() {
let actual = size_of::<MlkemSecretKey>();
let expected = size_of::<usize>();
assert_eq!(
actual, expected,
"MlkemSecretKey should be heap-allocated; got {} bytes",
actual
);
}
// ---------- Public types — no Clone/Copy случайно added ----------
#[test]
fn public_key_can_be_cloned() {
// PublicKey = public material, Clone разрешён (для распространения по сети).
// Проверяем positive case чтобы убедиться что наш test infrastructure
// правильно различает Clone от !Clone.
let pk_bytes = [0u8; mt_crypto::PUBLIC_KEY_SIZE];
let pk = PublicKey::from_array(pk_bytes);
let _cloned: PublicKey = pk.clone();
}
#[test]
fn signature_can_be_cloned() {
// Signature = public material (proof of authorship), Clone разрешён.
let sig_bytes = [0u8; mt_crypto::SIGNATURE_SIZE];
let sig = Signature::from_array(sig_bytes);
let _cloned: Signature = sig.clone();
}
// ---------- Behavioral: SK bytes filled correctly через FFI ----------
#[test]
fn secret_key_filled_by_ffi_keygen() {
let seed = [0x42u8; KEYPAIR_SEED_SIZE];
let (_pk, sk) = keypair_from_seed(&seed).expect("keygen");
let bytes = sk.as_bytes();
// ML-DSA-65 SK всегда 4032 байт; не all-zeros (зануление = bug в FFI fill).
assert_eq!(bytes.len(), SECRET_KEY_SIZE);
assert!(
bytes.iter().any(|&b| b != 0),
"SecretKey bytes are all-zero — FFI failed to fill, but returned MT_OK. Bug."
);
}
#[test]
fn mlkem_secret_key_filled_by_ffi_keygen() {
let seed = [0x42u8; MLKEM_SEED_SIZE];
let (_pk, sk) = keypair_from_seed_mlkem(&seed).expect("keygen mlkem");
let bytes = sk.as_bytes();
assert_eq!(bytes.len(), MLKEM_SECRET_KEY_SIZE);
assert!(
bytes.iter().any(|&b| b != 0),
"MlkemSecretKey bytes are all-zero — FFI failed to fill, but returned MT_OK. Bug."
);
}
// ---------- File-content scan: no logging macros на secret bytes в lib коде ----------
#[test]
fn no_println_or_log_on_secret_bytes_in_lib_code() {
// Сканирует mt-crypto/src/ на patterns типа `println!.*sk.as_bytes()`
// или `eprintln!.*sk.0` или `log::*.*sk\b`. Если нашёл — fail с
// конкретной строкой.
use std::fs;
use std::path::PathBuf;
let mut src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
src_dir.push("src");
let mut violations: Vec<String> = Vec::new();
let entries = fs::read_dir(&src_dir).expect("read src dir");
for entry in entries {
let entry = entry.expect("entry");
let path = entry.path();
if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let content = fs::read_to_string(&path).expect("read file");
for (lineno, line) in content.lines().enumerate() {
let lineno = lineno + 1;
let trimmed = line.trim();
// Skip comments
if trimmed.starts_with("//") {
continue;
}
// Patterns которые могут leak secret bytes:
// (1) any println!/eprintln!/print!/eprint!/log::*/dbg! containing
// "sk." or " sk " or "secret" identifier patterns followed
// by .as_bytes()/.0/{:?}
let lower = line.to_lowercase();
let is_log_call = lower.contains("println!")
|| lower.contains("eprintln!")
|| lower.contains("print!")
|| lower.contains("eprint!")
|| lower.contains("dbg!")
|| lower.contains("log::trace")
|| lower.contains("log::debug")
|| lower.contains("log::info")
|| lower.contains("log::warn")
|| lower.contains("log::error");
if !is_log_call {
continue;
}
// Какие-нибудь secret-suggesting patterns
let has_sk_ref = line.contains("sk.as_bytes")
|| line.contains("sk.0")
|| line.contains("secret_key.")
|| line.contains("SecretKey")
|| line.contains("MlkemSecretKey");
if has_sk_ref {
violations.push(format!(
"{}:{}: potential SK leak in log macro: {}",
path.display(),
lineno,
trimmed
));
}
}
}
assert!(
violations.is_empty(),
"Found {} potential SK leak(s) in lib code:\n{}",
violations.len(),
violations.join("\n")
);
}
// ---------- Constant-time: SK не имеет PartialEq derived (==) ----------
// Если кто-нибудь добавит #[derive(PartialEq)] для SecretKey, comparison
// через `==` будет non-constant-time (raw memcmp с early-exit). Этот test
// проверяет что PartialEq НЕ присутствует.
trait NotPartialEq {}
trait NotPartialEqTag {}
impl<T: NotPartialEqTag> NotPartialEq for T {}
impl NotPartialEqTag for SecretKey {}
impl NotPartialEqTag for MlkemSecretKey {}
fn assert_not_partial_eq<T: NotPartialEq>() {}
#[test]
fn secret_key_no_partial_eq_to_prevent_timing_leak() {
assert_not_partial_eq::<SecretKey>();
}
#[test]
fn mlkem_secret_key_no_partial_eq_to_prevent_timing_leak() {
assert_not_partial_eq::<MlkemSecretKey>();
}
// ---------- Verification: Drop трейт impl присутствует на SK types ----------
// Compile-time check: типы реализуют Drop. Если кто-то удалит impl Drop —
// компилятор не fails сам, но zeroize не вызовется. Этот test проверяет
// что Drop impl существует через std::mem::needs_drop.
#[test]
fn secret_key_needs_drop() {
assert!(
std::mem::needs_drop::<SecretKey>(),
"SecretKey должен иметь Drop impl с zeroize — иначе secret bytes \
останутся в heap после dealloc"
);
}
#[test]
fn mlkem_secret_key_needs_drop() {
assert!(
std::mem::needs_drop::<MlkemSecretKey>(),
"MlkemSecretKey должен иметь Drop impl с zeroize"
);
}