57 KiB
Внешний аудит кода Montana — отчёт
Аудитор: Claude Opus 4.7 (1M context), модель claude-opus-4-7[1m]
Дата проведения: 2026-04-26, T20:18:05 — T20:55:00 (московское время плюс)
Локация: /Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код/
Длительность: ~37 минут активного аудита
Режим выполнения: одно ядро / один процесс (соблюдён .cargo/config.toml: jobs = 1, RUST_TEST_THREADS = 1)
1. Аудиторская методология и доверие
Что я доверял (источники истины)
- Только исходный код:
*.rs,*.c,*.h,Cargo.toml,Cargo.lock,build.rs,rust-toolchain.toml,clippy.toml,.cargo/config.toml - Тестовые fixtures при условии независимой верификации против внешнего источника
- Внешние публичные стандарты как основу истины:
- NIST FIPS 204 (ML-DSA), FIPS 203 (ML-KEM), FIPS 180-4 (SHA-256)
- RFC 5869 (HKDF), RFC 4231 (HMAC-SHA-256), RFC 7914 (PBKDF2 test vectors), RFC 8018 (PBKDF2)
- BIP-39 (контрольная сумма мнемоники)
- Публичный репозиторий NIST CAVP —
https://github.com/usnistgov/ACVP-Serverкак независимый источник KAT-векторов - RustSec Advisory DB через
cargo audit(1058 advisories, обновлено 2026-04-25)
Что я НЕ доверял (исключённые источники)
- Все markdown-файлы в
/Users/kh./Python/Ничто/Монтана/Русский/Протокол/:AUDIT.md— рассматривался как текст с заявлениями требующими проверкиCLAUDE.md,CRITIC.md(роли архитектора и критика)ROADMAP.md,VERSION.md,README.md- Спецификация
Montana v33.0.0.mdи приложенияMontana App v3.8.0.md - Все исторические артефакты в
Архив/
- Комментарии в коде (
// SAFETY:,// spec, раздел "...") — рассматривались как claims-to-verify, не как авторитетные утверждения - Хардкоженные тестовые expected hex значения в
tests/— где они self-derived (regression baseline), а не cross-checked против NIST/RFC, оценивались как regression-tests, не как conformance-proofs
Источники ground truth, использованные в аудите
| Стандарт / источник | URL / ссылка | Применение |
|---|---|---|
| NIST FIPS 204 (ML-DSA) | csrc.nist.gov/pubs/fips/204/final | Размеры ключей, deterministic Sign Algorithm 2 |
| NIST FIPS 203 (ML-KEM) | csrc.nist.gov/pubs/fips/203/final | Размеры ключей, KeyGen_internal(d, z) |
| NIST FIPS 180-4 (SHA-256) | csrc.nist.gov/pubs/fips/180/4/final | Hash test vector "abc" |
| NIST CAVP ACVP-Server | github.com/usnistgov/ACVP-Server | 51 KAT для ML-DSA-65, ML-KEM-768, ML-DSA SigGen |
| RFC 5869 (HKDF) | rfc-editor.org/rfc/rfc5869 | Test vectors A.1, A.2, A.3 |
| RFC 4231 (HMAC-SHA-256) | rfc-editor.org/rfc/rfc4231 | Test cases 1, 2, 4, 6 |
| RFC 7914 (PBKDF2) §11 | rfc-editor.org/rfc/rfc7914 | Test vectors 1, 2 |
| BIP-39 | github.com/bitcoin/bips/blob/master/bip-0039.mediawiki | Структура мнемоники, контрольная сумма |
| RustSec Advisory DB | github.com/RustSec/advisory-db | 1058 advisories на 2026-04-25 |
2. Объём аудита и фактическое состояние кода
2.1. Workspace inventory
Workspace содержит 14 crates на момент начала аудита (изменилось до 14 в той же конфигурации к моменту окончания: один crate mt-timechain был замен на mt-timechain во время аудита — отмечено как finding F-12).
Crates присутствующие в workspace:
mt-account mt-codec mt-consensus mt-crypto mt-crypto-native
mt-entry mt-examples mt-genesis mt-lottery mt-merkle
mt-mnemonic mt-timechain mt-state mt-store
2.2. Audit scope (M1 — фундаментальный криптографический слой)
Согласно AUDIT.md (которому я не доверяю, но используется как scope-marker), audit-ready scope ограничен M1:
| Crate | Файл | Строк (фактически) | Назначение |
|---|---|---|---|
| mt-crypto | crates/mt-crypto/src/lib.rs | 643 | Public Rust API (ML-DSA-65 + ML-KEM-768 + SHA-256) |
| mt-crypto-native (Rust) | crates/mt-crypto-native/src/lib.rs | 40 | FFI декларации к Layer 2 |
| mt-crypto-native (C) | crates/mt-crypto-native/csrc/mt_crypto.c | 375 | C-обёртка над OpenSSL EVP API |
| mt-crypto-native (header) | crates/mt-crypto-native/csrc/mt_crypto.h | 56 | C декларации + 13 кодов ошибок |
| mt-crypto-native build | crates/mt-crypto-native/build.rs | 45 | Сборка vendored OpenSSL + С |
| mt-mnemonic core | crates/mt-mnemonic/src/lib.rs | 17 | Re-exports |
| mt-mnemonic mnemonic | crates/mt-mnemonic/src/mnemonic.rs | 194 | Mnemonic ↔ master_seed ↔ per-role seeds |
| mt-mnemonic pbkdf2 | crates/mt-mnemonic/src/pbkdf2.rs | 136 | PBKDF2-HMAC-SHA-256 |
| mt-mnemonic hkdf | crates/mt-mnemonic/src/hkdf.rs | 180 | HKDF-Expand RFC 5869 |
| mt-mnemonic hmac | crates/mt-mnemonic/src/hmac.rs | 143 | HMAC-SHA-256 RFC 2104 |
| mt-mnemonic bit_packing | crates/mt-mnemonic/src/bit_packing.rs | 135 | 24×11 бит ↔ 33 байта |
| mt-mnemonic wordlist | crates/mt-mnemonic/src/wordlist.rs | 132 | 2048-слов wordlist + fingerprint check |
| mt-codec | crates/mt-codec/src/lib.rs | 351 | CanonicalEncode + Domain separators (32) |
| mt-merkle | crates/mt-merkle/src/lib.rs | 474 | Sparse Merkle Tree depth=256 |
Total M1 audit surface (без тестов): ~2 921 строка.
2.3. Тестовая инфраструктура M1
| Test файл | Строк | Тестов | Назначение |
|---|---|---|---|
| crates/mt-crypto/tests/security_invariants.rs | 246 | 13 | Security invariants (no Clone, heap, no log) |
| crates/mt-crypto-native/tests/kat_independent.rs | 228 | 6 | Internal regression baselines |
| crates/mt-crypto-native/tests/nist_acvp_kat.rs | 279 | 3 | NIST CAVP cross-check (51/51 cases) |
| crates/mt-mnemonic/tests/keygen_vectors.rs | 170 | 7 | 5 KAT-векторов KeyGen + determinism |
| crates/mt-mnemonic/tests/test_vectors.rs | 121 | 6 | M-1 binding векторы (mnemonic → master_seed) |
| crates/mt-mnemonic/tests/e2e_recovery.rs | 161 | 3 | End-to-end recovery определённый идемпотентным |
NIST CAVP fixtures (verified против externally downloaded NIST source):
| Fixture | Тестов | Источник | Размер |
|---|---|---|---|
| ml_dsa_65_keygen.json | 25 | NIST ACVP-Server gen-val/json-files/ML-DSA-keyGen-FIPS204 | 302 940 байт |
| ml_kem_768_keygen.json | 25 | NIST ACVP-Server gen-val/json-files/ML-KEM-keyGen-FIPS203 | 184 841 байт |
| ml_dsa_65_siggen_det_external_pure_empty_ctx.json | 1 | NIST ACVP-Server gen-val/json-files/ML-DSA-sigGen-FIPS204 (tgId=3, deterministic, external, pure preHash) | 19 503 байта |
2.4. Unsafe blocks (фактический подсчёт)
В crates/mt-crypto/src/lib.rs найдено 7 unsafe-блоков на строках:
| Строка | Контекст | Содержит // SAFETY: |
|---|---|---|
| 168 | impl Drop for SecretKey — libc::munlock |
НЕТ |
| 187 | fn alloc_locked_secret_box — libc::mlock |
НЕТ |
| 224 | fn keypair_from_seed — FFI в mt_keypair_from_seed_mldsa |
ДА (lines 225-229) |
| 267 | fn sign — FFI в mt_sign_mldsa |
ДА (lines 268-272) |
| 282 | fn verify — FFI в mt_verify_mldsa |
ДА (lines 283-286) |
| 351 | impl Drop for MlkemSecretKey — libc::munlock |
НЕТ |
| 365 | fn keypair_from_seed_mlkem — FFI в mt_keypair_from_seed_mlkem |
ДА (lines 366-372) |
Итог: 7 unsafe-блоков, из которых только 4 имеют формальный // SAFETY: комментарий.
2.5. Зависимости (Cargo.lock)
Полное дерево production-зависимостей mt-crypto:
mt-crypto
├── libc =0.2.169
├── sha2 =0.10.9
│ ├── cfg-if =1.0.4
│ ├── cpufeatures =0.2.17
│ └── digest =0.10.7
│ ├── block-buffer =0.10.4
│ └── crypto-common =0.1.7
│ └── generic-array =0.14.7
│ ├── typenum =1.19.0
│ └── version_check =0.9.5
├── zeroize =1.8.1
└── mt-crypto-native (path)
├── libc =0.2.169
└── (build) openssl-src =300.5.5+3.5.5
└── (build) cc =1.2.16
├── jobserver =0.1.32
└── shlex =1.3.0
Все версии закреплены exact (=X.Y.Z). Это правильно для воспроизводимости. Production-зависимостей в реальном release-билде — 8 на верхнем уровне (libc, sha2, zeroize, mt-crypto-native, и transitive cfg-if, cpufeatures, digest, block-buffer, crypto-common, generic-array, typenum, version_check).
OpenSSL версия: 3.5.5 LTS (vendored через openssl-src). Это production-grade библиотека с FIPS 140-3 валидацией, многолетней эксплуатацией в TLS-стеке, поддержкой до апреля 2030 года.
3. Сильные стороны
3.1. NIST FIPS conformance подтверждена независимо
Самый значимый positive finding аудита. Я скачал источники NIST CAVP test vectors напрямую с публичного репозитория https://github.com/usnistgov/ACVP-Server и сравнил байт-в-байт с локальными fixtures:
| Тест | Байт-в-байт совпадение |
|---|---|
| ML-DSA-65 KeyGen (25 cases, tcId 26-50) | 25/25 ✅ |
| ML-KEM-768 KeyGen (25 cases) | 25/25 ✅ |
| ML-DSA-65 SigGen (1 case, tgId=3 deterministic external pure empty ctx) | 1/1 ✅ |
Canonical SHA-256 локального ML-DSA-65 fixture: 2cbfd5571eabd93255bfee654f97b5a29d61351e11d17024cabf726b4f864b67
Canonical SHA-256 NIST source ML-DSA-65 группы 2: 2cbfd5571eabd93255bfee654f97b5a29d61351e11d17024cabf726b4f864b67
Это значит: код Montana (через OpenSSL 3.5.5 LTS backend) byte-exact производит pubkey/secretkey/signature такие же, как заявлено NIST как official correct output для этих PQ алгоритмов.
3.2. Постквантовая криптография через production-grade backend
Архитектурное решение использовать OpenSSL 3.5.5 LTS вместо pre-1.0 RustCrypto pure-Rust крейтов корректное для production audit readiness:
- OpenSSL 3.5 имеет встроенную поддержку ML-DSA и ML-KEM начиная с этой версии
- FIPS 140-3 валидированный криптографический модуль
- Десятилетия эксплуатации в TLS-стеке (Apache, nginx, OpenSSH, Linux ядро, AWS, Cloudflare)
- Audit history: множественные публичные аудиты OpenSSL Foundation и партнёров
Layer 2 (own thin C wrapper) корректно реализован — 375 строк фокусированной обвязки EVP API, читаемых и аудируемых.
3.3. Hygiena секретного материала
Реализация хранения секретов реализована с двумя слоями защиты:
- Heap-allocation через
Box<[u8; SECRET_KEY_SIZE]>— секретные байты живут в одной heap-локации от создания до уничтожения, никаких stack memcpy при move-операциях. libc::mlockна heap-странице — best-effort защита от swap-out. На macOS использует kern.maxlockedmem, на Linux требует CAP_IPC_LOCK либо адекватного RLIMIT_MEMLOCK. При неудаче — fallback на non-locked Box (полагается на encrypted swap: FileVault / LUKS).
Drop реализация для SecretKey и MlkemSecretKey правильная:
- Сначала
self.0.zeroize()— перезапись байтов нулями - Затем
libc::munlock— освобождение mlock'а перед dealloc
Compile-time проверки в tests/security_invariants.rs:
secret_key_is_not_clone— гарантия чтоSecretKeyне может быть случайно склонирован через#[derive(Clone)]mlkem_secret_key_is_not_clone— то же для ML-KEMsecret_key_no_partial_eq_to_prevent_timing_leak— нетPartialEq(защита от timing-leak через memcmp)secret_key_is_heap_allocated—size_of::<SecretKey>() == size_of::<usize>()(1 указатель)secret_key_needs_drop—std::mem::needs_drop::<SecretKey>()true
Также file-content scan в тесте no_println_or_log_on_secret_bytes_in_lib_code — runtime проверка что в mt-crypto/src/ нет логирующих макросов с sk.as_bytes()/sk.0/SecretKey references.
3.4. Memory safety FFI границы
Все 4 unsafe блока, реально пересекающие FFI границу к C-коду (mt_keypair_from_seed_mldsa, mt_sign_mldsa, mt_verify_mldsa, mt_keypair_from_seed_mlkem), имеют:
// SAFETY:комментарий с объяснением валидности указателей- Указатели на стек или heap-buffer известного размера
- Размеры буферов соответствуют объявленным C-константам
C-код в mt_crypto.c следует правильному pattern:
goto cleanupдля error handling- NULL-checks для всех входных указателей
- NULL-check для
msgтолько когдаmsg_len != 0— корректная edge case - Memory cleanup на ВСЕХ путях (включая ошибки)
- Размеры проверяются
actual_len != expected_lenпосле OpenSSL вызовов - Deterministic Sign явно установлен через
OSSL_SIGNATURE_PARAM_DETERMINISTIC=1
3.5. Самостоятельные реализации криптопримитивов с RFC test vectors
Recovery flow (mnemonic → master_seed → per-role keys) реализован собственным кодом, не зависит от внешних crypto-крейтов:
pbkdf2_hmac_sha256(136 строк) — реализация по RFC 8018 §5.2, с проверкой против:- RFC 7914 §11 vector 1 (passwd, salt, c=1, dkLen=64)
- RFC 7914 §11 vector 2 (Password, NaCl, c=80000, dkLen=64)
- Public CryptoJS vector (password, salt, c=4096, dkLen=32)
hkdf_expand(180 строк) — реализация по RFC 5869 §2.3, с проверкой против:- RFC 5869 §A.1 (basic case with SHA-256)
- RFC 5869 §A.2 (long inputs)
- RFC 5869 §A.3 (empty info)
hmac_sha256(143 строки) — реализация по RFC 2104, с проверкой против:- RFC 4231 §4.2 case 1 (key 0x0b×20, "Hi There")
- RFC 4231 §4.3 case 2 ("Jefe", "what do ya want for nothing?")
- RFC 4231 §4.5 case 4 (long key, repeated 0xCD)
- RFC 4231 §4.7 case 6 (key longer than block size — triggers SHA-256 reduction)
Каждая реализация прошла RFC test vectors на момент аудита (через cargo test).
3.6. Anti-brute-force защита мнемоники
PBKDF2 итераций: KDF_ITER = 1_048_576 = 2²⁰ — на 9 порядков сильнее BIP-39 стандарта (2²¹¹ = 2048). Это сознательное усиление защиты от brute-force.
Wordlist binding: SHA-256 fingerprint встроенного Montana wordlist.txt файла проверяется при инициализации (init_wordlist). Mismatch = panic при первом обращении к wordlist (paranoid integrity check).
3.7. Build infrastructure
rust-toolchain.toml— pinned channel = stable, components = rustfmt + clippyclippy.toml—msrv = "1.70".cargo/config.toml— single-thread/single-process для предотвращения перегрева (jobs=1, RUST_TEST_THREADS=1)Cargo.toml—[profile.release]сlto = "fat",codegen-units = 1,panic = "abort",overflow-checks = truebuild.rsкорректно используетCARGO_CFG_TARGET_OS(а неcfg!(target_os)) для cross-compile
3.8. Cargo audit clean
cargo audit показал:
- 0 уязвимостей (
vulnerabilities.found = false,count = 0) - 0 информационных warnings (
warnings = {}) - 39 транзитивных зависимостей просканированы
- Advisory DB: 1058 advisories, последнее обновление 2026-04-25
3.9. Cargo clippy clean
cargo clippy --all-targets -- -D warnings прошёл успешно (exit 0). Все 14 crates checked, ни одного warning.
3.10. Все тесты M1 проходят
Прогон тестов M1 в одно ядро / один процесс:
| Crate | Тестов passed | Тестов failed | Время |
|---|---|---|---|
| mt-crypto unit | 23 | 0 | 0.06s |
| mt-crypto security_invariants | 13 | 0 | 0.01s |
| mt-crypto-native kat_independent | 6 | 0 | 0.14s |
| mt-crypto-native nist_acvp_kat | 3 | 0 | 0.02s |
| mt-mnemonic unit | 57 | 0 | 136s |
| mt-mnemonic e2e_recovery | 3 | 0 | 175s |
| mt-mnemonic keygen_vectors | 7 | 0 | 167s |
| mt-mnemonic test_vectors | 6 | 0 | 215s |
Итог: 118 тестов passed, 0 failed.
Длительность mt-mnemonic тестов объясняется PBKDF2 итерациями 2²⁰ (необходимо для anti-brute-force защиты).
3.11. Detached-keys design
Архитектура recovery flow не хранит долгосрочно privkey:
- Источник истины — мнемоника (24 слова на устройстве пользователя)
- master_seed выводится из мнемоники по требованию через PBKDF2-HMAC-SHA-256
- Per-role keys (account_key, node_key, app_encryption_key) выводятся из master_seed через HKDF-Expand
- Privkey материализуется в памяти только в момент подписи
Это правильная структура для recovery flow.
3.12. Domain separation
mt-codec определяет 32 различных domain separators (все начинающиеся с mt-). Используются для разделения contexts хеширования и derivation:
- Hashing:
mt-op,mt-proposal,mt-bundle,mt-merkle-leaf, ... - Identity derivation:
mt-account-key,mt-node-key,mt-app-encryption-key - PBKDF2 salt:
mt-seed
Domain separation предотвращает cross-protocol confusion атаки на хеши.
4. Слабые стороны и Findings
Все findings нумерованы для трассировки. Severity:
- CRITICAL — может привести к компрометации secret material или consensus break
- HIGH — существенный риск безопасности или discipline
- MEDIUM — требует устранения для production audit
- LOW — minor / cosmetic / документация
F-1 [LOW] — AUDIT.md устарел: расхождение line counts
Описание. AUDIT.md (line 16) заявляет что mt-crypto/src/lib.rs содержит 568 строк. Фактически 643 строки (расхождение 75 строк, 13.2%).
AUDIT.md также суммирует «Total own audit surface (Layer 1 + Layer 2): 1084 lines». Фактически: 643 + 40 + 375 + 56 + 45 = 1 159 строк.
Воспроизведение.
wc -l crates/mt-crypto/src/lib.rs crates/mt-crypto-native/src/lib.rs \
crates/mt-crypto-native/csrc/mt_crypto.c crates/mt-crypto-native/csrc/mt_crypto.h \
crates/mt-crypto-native/build.rs
Воздействие. Аудитор может пропустить новый код добавленный после написания AUDIT.md. Внешний аудитор, читающий AUDIT.md как ground truth, получит неверную картину объёма работы.
Рекомендация. Установить CI gate: при любом PR верифицировать что line counts в AUDIT.md соответствуют реальным. Либо удалить hardcoded counts из AUDIT.md и оставить только команду для проверки.
F-2 [HIGH] — cargo fmt --check FAILS, AUDIT.md заявляет «clean»
Описание. AUDIT.md (раздел 7) явно заявляет: [x] cargo fmt --all -- --check clean. Фактический прогон возвращает exit code 1 с 48 строк diff в mt-crypto/src/lib.rs.
Воспроизведение.
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && cargo fmt --all -- --check; echo "EXIT=$?"
Результат: EXIT=1, diff в строках 137, 149, 166, 235.
Воздействие.
- Нарушение discipline формализованной в роли архитектора (CLAUDE.md): «Все четыре — зелёные. Иначе не коммитить.»
- Несоответствие задекларированному pre-audit self-attestation
- Демонстрирует что AUDIT.md заявления не верифицированы автоматически перед публикацией
Рекомендация. Запустить cargo fmt --all, закоммитить, обновить AUDIT.md self-attestation только после реального прохождения проверки. Установить pre-commit hook, отвергающий коммиты при не-чистом fmt.
F-3 [MEDIUM] — Stale references к pre-migration RustCrypto в example-коде
Описание. В crates/mt-examples/examples/m1_crypto.rs:
- Line 127:
print_kv("library", "ml-dsa 0.1.0-rc.8 (RustCrypto pure-Rust)"); - Line 299:
print_kv("internal", "ml_dsa::ExpandedSigningKey::sign_deterministic (FIPS 204 Algorithm 2, deterministic variant)");
Реальная library: OpenSSL 3.5.5 LTS через own thin C wrapper. RustCrypto ml-dsa крейт не присутствует в Cargo.lock.
Воздействие. Пользователь, запустивший пример, видит ложную информацию о backend. При external audit это вызывает confusion: «они мигрировали с RustCrypto на OpenSSL или нет?». Свидетельствует о неполной cleanup при миграции (M1-E phase migration).
Рекомендация. Обновить строки 127, 299 в m1_crypto.rs на актуальную информацию: "OpenSSL 3.5.5 LTS via own C FFI wrapper" и "EVP_DigestSign with OSSL_SIGNATURE_PARAM_DETERMINISTIC=1".
F-4 [MEDIUM] — 3 unsafe блока без // SAFETY: комментария
Описание. В crates/mt-crypto/src/lib.rs следующие unsafe-блоки не имеют формального // SAFETY: префикса:
| Строка | Контекст | Что делает |
|---|---|---|
| 168 | impl Drop for SecretKey |
libc::munlock heap-страницы |
| 187 | fn alloc_locked_secret_box |
libc::mlock heap-страницы |
| 351 | impl Drop for MlkemSecretKey |
libc::munlock heap-страницы |
Объяснения в обычных комментариях есть рядом, но не следуют требуемому формату // SAFETY: который установлен ролью архитектора (CLAUDE.md, Code Style: «unsafe блоки без архитектурного обоснования (комментарий формата // SAFETY: ...)`»).
Воздействие. AUDIT.md (line 16) явно указывает «Все unsafe blocks с // SAFETY: комментариями (4 блока: ...)». Это и неточно (фактически 7 блоков), и неверно по содержанию (3 из 7 без формального SAFETY).
Сами unsafe-операции (mlock/munlock) — простые системные вызовы с известной семантикой. Реального security-риска от отсутствия SAFETY-комментария нет, но это нарушение discipline и AUDIT.md utterance.
Рекомендация. Добавить // SAFETY: префикс к каждому из 3 блоков с явным обоснованием (например: «pointer valid for the lifetime of the Box; size matches allocated size»).
F-5 [MEDIUM] — Best-effort mlock без runtime warning при failure
Описание. Функция alloc_locked_secret_box (строки 185-193 в mt-crypto/src/lib.rs):
fn alloc_locked_secret_box(size: usize) -> Box<[u8]> {
let boxed = vec![0u8; size].into_boxed_slice();
unsafe {
let _ = libc::mlock(boxed.as_ptr() as *const libc::c_void, size);
}
boxed
}
Return code mlock игнорируется через let _ =. При failure (например RLIMIT_MEMLOCK exceeded на Linux без CAP_IPC_LOCK, или kern.maxlockedmem exceeded на macOS) mlock возвращает -1, но код продолжает работу с non-locked Box.
Это означает: secret bytes могут быть выгружены в swap при memory pressure ОС. Защитой остаётся только encrypted swap (FileVault / LUKS) — оборона второй линии, которая зависит от настройки системы (не гарантирована).
Комментарий в коде (lines 177-184) корректно описывает это как «best-effort», но никакой runtime сигнал не идёт пользователю/администратору о fallback.
Воздействие.
- На systems без CAP_IPC_LOCK (типичный Docker container, default user account на Linux)
mlockбудет fail silently - Администратор не узнает что secret material выгружается на диск
- При unencrypted swap — реальная утечка privkey
Рекомендация. Один из двух вариантов:
- Логировать через telemetry/stderr при первом failure
mlock(«WARNING: secret memory не залочена в RAM, fallback на encrypted swap») - Делать
mlockобязательным (panic при failure), force-ить администратора настроить ulimit/CAP_IPC_LOCK
CLAUDE.md упоминает «Failure сигнал документируется через future telemetry, не блокирует операцию» — finding закрывается этим в roadmap.
F-6 [HIGH] — Test-only keypair() использует слабую энтропию
Описание. В crates/mt-crypto/src/lib.rs строки 244-263:
#[cfg(any(test, feature = "testing"))]
pub fn keypair() -> (PublicKey, SecretKey) {
let mut seed = [0u8; KEYPAIR_SEED_SIZE];
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)...;
hasher.update(now.to_le_bytes());
hasher.update(std::process::id().to_le_bytes());
let addr = &seed as *const _ as usize;
hasher.update(addr.to_le_bytes());
...
}
Energy: SystemTime::now() (наносекунды UNIX epoch) + PID + stack address → SHA-256(64 байт). Это не CSPRNG.
Воздействие.
- Привязка через
#[cfg(any(test, feature = "testing"))]означает функция доступна только в test-сборке либо при явном включении feature flag. - Если разработчик ошибочно включит
--features testingв production-бинарь — production identity будет генерироваться с низкоэнтропийным seed. - Atttacker наблюдающий время запуска + знающий PID + heuristics на typical stack address может narrow-down keyspace до brute-forceable
- Используется внутри тестов только как sanity-check для primitive (real identity всегда через
keypair_from_seedиз HKDF)
Рекомендация. Заменить на CSPRNG через getrandom крейт (или OsRng) даже в test-сборке. Альтернативно: разместить keypair() в отдельном mt-test-utils крейте с dev-dependencies-only, чтобы исключить любой риск активации в production.
F-7 [LOW] — Промежуточные buffers PBKDF2/HKDF/HMAC не zeroized
Описание. В mt-mnemonic/src/pbkdf2.rs:
t_i: Hash32,u_prev: Hash32,u_k: Hash32— массивы 32 байта на стекеsalt_with_counter: Vec<u8>— heap-allocateddk: Vec<u8>— выходной buffer
Эти буферы содержат производные значения от password (entropy ≡ secret после M-1 шага). После функции return:
- Stack-resident
t_i/u_prev/u_kмогут быть в неинициализированной stack-памяти до next stack-frame overwrite salt_with_counter/dkVec drop'аются БЕЗ zeroize (Rust default Drop для Vec не zeroize)dkвозвращается caller'у — caller отвечает за zeroize
В mt-mnemonic/src/hkdf.rs: то же для hmac_input, t_prev, t_i.
В mt-mnemonic/src/hmac.rs:
key_block,key_ipad,key_opad— массивы 64 байта на стеке (содержат key-derived material)combined: Vec<u8>вsha256_concat— heap-allocated (содержит inner padding ⊕ key plus message)
Воздействие.
- При side-channel атаке через memory inspection (например ядро crash-дамп, debugger attach) промежуточные buffer'ы могут быть найдены ещё некоторое время
- Production risk низкий: kernel core dump unusable без kernel exploit; debugger требует root
- Это hygiene-issue, не immediate vulnerability
Рекомендация. Импортировать zeroize::Zeroizing<T> для wrappе intermediate buffers, или explicit .zeroize() перед drop в коде. Не критично для current risk model, но повышает defense-in-depth.
F-8 [MEDIUM] — Ограниченное покрытие SigGen NIST KAT
Описание. В nist_acvp_kat.rs функция nist_acvp_ml_dsa_65_siggen_deterministic_external_pure_empty_context тестирует только 1 case из NIST CAVP коллекции. Это test от группы tgId=3 ML-DSA-65 (deterministic, external interface, pure preHash, empty context), tcId=40, sk начинается с EE564C44....
NIST CAVP содержит 8 групп для ML-DSA-65 SigGen:
- tgId=3: deterministic + external + pure (15 tests) — тестируется 1/15
- tgId=4: deterministic + external + preHash (15 tests) — не тестируется
- tgId=9, 10: deterministic + internal interface (15 + 15 tests) — не тестируется
- tgId=15, 16, 21, 22: nondeterministic variants — не тестируется (Montana только deterministic)
Воздействие.
- ML-DSA SigGen byte-exact conformance подтверждена только для 1 узкой комбинации параметров
- Если OpenSSL имеет bug в обработке non-empty context — Montana code это не поймает
- AUDIT.md acknowledges это в "Known limitations 1" — известный gap
Рекомендация. Расширить fixture до всех 15 cases tgId=3 (тот же group, разные seeds). Это даёт стабильную coverage для current Montana usage pattern (deterministic + external + pure + empty context). Расширение на не-empty context — отдельная phase когда понадобится FIPS context support.
F-9 [MEDIUM] — KAT-baselines в kat_independent.rs self-derived, не cross-checked
Описание. В crates/mt-crypto-native/tests/kat_independent.rs все hardcoded SHA-256 fingerprints — own baseline, derived при первом прогоне теста, а не из NIST или другой external source.
Имя файла «kat_independent» вводит в заблуждение: «independent» здесь означает «independent of HKDF-derivation-tested-elsewhere», а не «independent reference implementation». Это regression-tests, не conformance-tests.
Real conformance-tests находятся в nist_acvp_kat.rs (51 case verified).
Воздействие. Минорное. Для самой conformance-проверки это OK — nist_acvp_kat.rs покрывает основной path (KeyGen). Но naming kat_independent может ввести в заблуждение auditor-а, читающего этот файл первым.
Рекомендация. Переименовать kat_independent.rs → regression_baselines.rs либо internal_baselines.rs для ясности. Удалить из его docstring любые claims о cross-implementation conformance.
F-10 [LOW] — Self-test не проверяет против NIST output
Описание. Функция mt_crypto::self_test() (lines 430-460) проверяет:
- Sizes match
- Determinism (двойной KeyGen с тем же seed)
- Sign/verify roundtrip
- KAT 1 byte-exact:
keypair_from_seed([0x00; 32])→ SHA-256(pk) == hardcodedEXPECTED_KAT_1_PK_SHA256(own baseline)
KAT 1 hardcoded — own baseline, не NIST-derived. Если OpenSSL когда-то поменяет implementation (например baseline changes между OpenSSL 3.5.5 → 3.5.6), self_test fails — но это regression detection, не conformance proof.
Воздействие. Self-test покрывает определённый corner case (zero seed) и ловит drift, но не проверяет NIST conformance. Hardcoded values могли быть скопированы из failing implementation и потом self-test passing — без cross-check невозможно отличить.
Рекомендация. В self_test() добавить минимум 1-2 NIST CAVP test cases byte-exact (не self-derived hashes, а реальные NIST expected pk/sk). Это превратит self_test из pure-regression в mini-conformance check.
F-11 [MEDIUM] — Cargo.lock divergence от Cargo.toml в момент аудита
Описание. В начале аудита (T20:18) Cargo.lock содержал запись mt-timechain (version 0.0.0). В конце аудита (T20:48) Cargo.toml workspace.members содержит mt-timechain вместо mt-timechain. Папка crates/mt-timechain/ появилась во время аудита.
Это означает что состояние репозитория изменилось во время аудита — кто-то (вероятно агент архитектора в фоне или manual edit) переименовал/добавил crate.
Воздействие.
- Аудит провёл на снимке кода в T20:18
- Cargo build в момент T20:48 (cached) уже работает с новой структурой
- Cargo.lock на момент чтения был out-of-sync с Cargo.toml —
cargo buildобновит его при следующем запуске
Не security finding, но методологическая нота для аудита.
Рекомендация. Внешний аудит должен проводиться на frozen branch (release tag), не на active development branch. Подписать audit branch git tag перед началом, audit на этот tag.
F-12 [LOW] — mnemonic.split(' ') строгий single-space parsing
Описание. В mt-mnemonic/src/mnemonic.rs line 41:
let words: Vec<&str> = mnemonic.split(' ').collect();
Если пользователь введёт мнемонику с двойным пробелом, табом, или newline — split(' ') даст пустые элементы или wrong tokens, что приведёт к MnemonicError::WordCount либо MnemonicError::UnknownWord. Многие BIP-39 wallets используют split_whitespace() для большей user-friendly.
Воздействие.
- UX issue: пользователь может думать что мнемоника не работает, хотя просто скопировал её с лишним whitespace
- Не security risk: malformed input correctly rejected
- Может быть intentional strict mode
Рекомендация. Проверить design intent. Если строгость намеренна (anti-tampering) — задокументировать в API docs. Если нет — заменить на split_whitespace().
F-13 [LOW] — 13 vs 12 error codes — semantic ambiguity
Описание. AUDIT.md (line 24) заявляет «13 error codes» в mt_crypto.h. Фактически:
MT_OK = 0(success, не error)MT_ERR_INVALID_INPUT = 1...MT_ERR_SIGN_LENGTH_MISMATCH = 12(12 error codes)
Итого 13 кодов total, 12 errors. Семантическая неоднозначность в documentation: «13 error codes» vs «12 errors + 1 ok = 13 total status codes».
Также: from_code() функция в mt-crypto/src/lib.rs обрабатывает 10 error variants явно + Other(c) catch-all. Не обрабатывает явно MT_ERR_VERIFY_FAILED (5) и MT_ERR_KAT_MISMATCH (6) — попадают в Other(c). Не проблема (Verify возвращает bool, KAT-mismatch только из self_test), но также не отражено в error display.
Рекомендация. Уточнить AUDIT.md: «13 status codes (1 success + 12 errors)». Опционально: добавить явные variants для VerifyFailed и KatMismatch в CryptoError enum для полноты.
F-14 [INFO] — Side-channel свойства не verified конструкцией
Описание. Constant-time свойства cryptographic operations (защита от timing-based extraction privkey) не доказаны для Montana code:
- ML-DSA/ML-KEM internal — ответственность OpenSSL (документировано как constant-time для production-grade builds)
- Montana FFI wrapper — простой проброс, без data-dependent branches
- HMAC/PBKDF2/HKDF собственные реализации в
mt-mnemonic— XOR/SHA-256 операции, в принципе constant-time для текущей реализации, но без формального verification memcmpнигде не используется на user-controlled secret material (хорошо)- Compile-time
!PartialEqна SK types — защита от случайного==use
Воздействие. Без formal verification (через subtle crate, dudect testing, F* / hax) constant-time property — assumption based on code reading, не proof. На VPS (cloud neighbour shared cache) timing-based extraction теоретически возможна для не constant-time operations.
Рекомендация.
- Документировать threat model явно: Montana не предполагает физический доступ или cloud-neighbour atтак (single-tenant deployment)
- Опционально: добавить
subtle::ConstantTimeEqдля всех critical comparisons - Опционально: dudect testing harness для hot path операций
F-15 [INFO] — Нет fuzzing infrastructure
Описание. В репозитории нет fuzz/ директории, нет cargo fuzz setup, нет AFL/libFuzzer harness'ов. AUDIT.md упоминает fuzzing как possibly-applicable в pre-prerequisite checklist, но ни один harness не присутствует.
Воздействие. FFI entry points (mt_keypair_from_seed_mldsa, mt_sign_mldsa, mt_verify_mldsa, mt_keypair_from_seed_mlkem) не fuzzed. Malformed input через FFI может вызвать crash в C-коде или OpenSSL. C-wrapper делает NULL checks, но boundary conditions (например msg_len = SIZE_MAX) не explicitly tested.
Также: mnemonic_to_master_seed принимает &str от пользователя — fuzzing на это не настроен.
Рекомендация. Установить cargo-fuzz harness:
fuzz/fuzz_targets/fuzz_sign.rs— fuzzmt_crypto::sign(sk, msg)с various sk corruption + various msgfuzz/fuzz_targets/fuzz_verify.rs— fuzzmt_crypto::verify(pk, msg, sig)fuzz/fuzz_targets/fuzz_mnemonic.rs— fuzzmnemonic_to_master_seed
С учётом доступа к серверам Moscow/Frankfurt — запустить 24-48 часов fuzzing на каждом.
F-16 [INFO] — Отсутствует signature aggregation / threshold infrastructure
Описание. Заметка по архитектуре: M1 покрывает только individual key generation + sign + verify. Threshold signatures, multi-signature, signature aggregation — отсутствуют в текущем коде (что соответствует scope M1 по AUDIT.md).
Воздействие. Это не bug и не finding в M1 scope. Просто scope acknowledgment.
Рекомендация. Если будущие phases требуют threshold ML-DSA signatures (или ML-KEM-based encapsulation) — учесть что OpenSSL EVP API не предоставляет эти примитивы напрямую. Потребуется отдельный crate либо C extension.
F-17 [LOW] — serde_json зависимость только для test fixtures parse
Описание. mt-crypto-native/Cargo.toml имеет dev-dependencies:
serde = { version = "=1.0.219", features = ["derive"] }
serde_json = "=1.0.140"
Используется только в tests/nist_acvp_kat.rs для парсинга NIST JSON fixtures. Это dev-dependencies — не попадает в production builds.
serde_json имеет довольно большое transitive deps (proc-macro2, quote, syn, serde_derive — auxiliary build crates). Не угроза но adds dependency surface.
Воздействие. Минимальное (dev-only). Acknowledgment.
Рекомендация. Опционально: заменить на ручной JSON parser через std::str::Lines для NIST fixtures (avoid serde dependency). Не приоритетно.
F-18 [LOW] — parallel feature cc крейта противоречит single-thread политике
Описание. В mt-crypto-native/Cargo.toml:
cc = { version = "=1.2.16", features = ["parallel"] }
.cargo/config.toml устанавливает jobs = 1 глобально. Feature parallel для cc крейта позволяет parallel компиляцию C файлов. У нас единственный C файл (mt_crypto.c), так что фича не активирует параллелизм. Но противоречит заявленной политике «single-process / single-thread».
Воздействие. Поведенчески — никакого (один C файл). Документально — рассогласование с .cargo/config.toml comment.
Рекомендация. Удалить features = ["parallel"] из cc dependency для consistency.
F-19 [LOW] — OSSL_PARAM_construct_octet_string имплицитный const-cast
Описание. В mt_crypto.c line 62:
params[0] = OSSL_PARAM_construct_octet_string(
seed_param_name, (void*)seed, seed_len
);
seed — declared as const uint8_t*. Cast (void*)seed теоретически удаляет const. Это convention OpenSSL API: OSSL_PARAM_construct_octet_string принимает void*, но не модифицирует данные — но тип API не выражает immutability. Не bug, но C compiler без -Wcast-qual это не ловит.
Воздействие. Никакого. OSSL_PARAM_construct_octet_string documented как read-only on input data. Это OpenSSL API limitation, не Montana bug.
Рекомендация. Acknowledgment в SAFETY-комментарии или документация. Альтернативно: добавить C-flag -Wno-cast-qual в build.rs если warning ловится.
5. Известные ограничения этого аудита
Что я не мог проверить даже с серверным доступом:
- Side-channel attacks через физическое оборудование — нет осциллографа, измерителя мощности, EM-зонда
- Cryptanalysis самих ML-DSA / ML-KEM — академическая работа NIST PQC competition, out of scope
- Formal verification через F* / hax / EasyCrypt / Coq — toolchain недоступен в среде, требует переписывания кода под proof framework
- Корректность OpenSSL внутри (Layer 3) — миллионы строк C-кода, отдельный аудит OpenSSL Foundation
- Bugs в компиляторе rustc/cc — атака «Trusting Trust» (Ken Thompson 1984), требует второго независимого compilers
- Документ-уровневая legal certification — нет печати NCC Group / Trail of Bits / Quarkslab / Cure53 / Kudelski
Что я мог бы сделать с серверами но не делал в этой сессии (ограничение 1 ядро / 1 процесс + рамки времени):
- Двойная независимая Docker сборка для верификации reproducible builds (Mac + Moscow + Frankfurt)
- Long-running fuzzing 24-48 часов через
cargo fuzzна сервере - Cross-platform smoke testing на Linux x86_64 vs macOS ARM64
- Supply chain audit OpenSSL bytes из openssl-src vs openssl.org official tarball SHA-256
- Statistical timing measurement на серверах для weak constant-time signal
Эти 5 пунктов не были выполнены в текущем аудите но доступны для расширения.
6. Рекомендации по приоритетам
Приоритет «закрыть до production audit» (HIGH severity)
- F-2 —
cargo fmt --all, обновить AUDIT.md self-attestation после real-prog проверки - F-6 — заменить
keypair()test helper на CSPRNG-based, либо вынести вmt-test-utilsкрейт - F-5 — runtime warning при
mlockfailure, либо makemandatory с graceful error
Приоритет «закрыть до v1.0 release» (MEDIUM severity)
- F-3 — обновить stale comments в
m1_crypto.rs(line 127, 299) - F-4 — добавить
// SAFETY:комментарии к 3 unused-marker блокам (lines 168, 187, 351) - F-8 — расширить SigGen NIST KAT до 15 cases tgId=3
- F-9 — переименовать
kat_independent.rs→regression_baselines.rs
Приоритет «закрыть для документационного качества» (LOW severity)
- F-1 — sync line counts в AUDIT.md
- F-10 — добавить NIST CAVP byte-exact в
self_test()функцию - F-13 — уточнить «13 status codes (1 success + 12 errors)»
- F-12 — design intent strict whitespace mode mnemonic
- F-7 —
Zeroizing<T>для PBKDF2/HKDF/HMAC intermediate state - F-17 — опционально удалить
serde_jsondev-dependency - F-18 — удалить
ccparallel feature - F-19 — комментарий OpenSSL API const-cast convention
Приоритет «infrastructure improvement» (INFO)
- F-14 — формализовать constant-time свойства (subtle crate / dudect)
- F-15 — установить
cargo-fuzzharness - F-11 — audit на frozen git tag
Приоритет «требуется external auditor с физическим/легальным доступом»
- Side-channel hardware testing (осциллограф, power meter)
- Formal verification ML-DSA/ML-KEM internal — ответственность OpenSSL Foundation / NIST
- Audit firm signature (NCC Group / Trail of Bits / Quarkslab / Cure53 / Kudelski)
7. Итоговая оценка уровня безопасности
Шкала 1-10:
- 10 — formally verified, audit firm signed, side-channel proven, multi-vendor reviewed, deployed at scale years
- 9 — audited by recognized firm, side-channel constant-time documented, NIST FIPS validated implementation
- 8 — strong cryptographic foundation, NIST conformance independently verified, minimal attack surface, comprehensive testing, documented threat model, with minor discipline/documentation findings
- 7 — strong foundation but multiple medium-severity findings outstanding
- 6 — code reads well but missing critical infrastructure (fuzzing, formal verification, external audit)
- ≤5 — security-critical issues found
Оценка Montana M1: 8 / 10
Обоснование оценки 8:
За что ставлю 8 (положительное):
- ✅ NIST FIPS 204/203 byte-exact conformance независимо подтверждена (51/51 KAT)
- ✅ Production-grade backend — OpenSSL 3.5.5 LTS (не pre-1.0 RustCrypto)
- ✅ Heap-allocated SK с mlock + Drop+zeroize — правильная hygiene
- ✅ Compile-time security invariants (No Clone/Copy/PartialEq на SK)
- ✅ 0 уязвимостей в
cargo audit(39 deps scanned, 1058 advisories) - ✅ Все RFC test vectors PBKDF2/HKDF/HMAC проходят
- ✅ 118 тестов passed, 0 failed
- ✅ Clippy clean (
-D warnings) - ✅ Strict version pinning (все exact
=X.Y.Z) - ✅ Cross-platform build correctness через
CARGO_CFG_TARGET_OS - ✅ Reproducible build infrastructure prepared (Cargo.lock + rust-toolchain.toml)
- ✅ Thoughtful threat model (heap+mlock, deterministic Sign, no logging SK)
За что снимаю 2 (отрицательное):
- ❌
cargo fmt --checkFAILS (F-2) — нарушение discipline которая декларируется - ❌ AUDIT.md заявления не сверены с фактом (F-1, F-4, F-13, F-18) — документация устарела
- ❌ Stale references к pre-migration RustCrypto (F-3) — confusion для auditor
- ❌ Test-only
keypair()использует weak entropy (F-6) — теоретический risk при misuse - ❌ Best-effort
mlockбез runtime warning (F-5) — silent fallback на encrypted swap - ❌ Coverage SigGen NIST KAT — только 1/15 cases в supported group (F-8)
- ❌ Нет fuzzing infrastructure (F-15) — FFI boundary не tested на malformed input
- ❌ Constant-time свойства не verified конструкцией (F-14)
- ❌ Нет внешней audit firm signature (требование regulator/insurer)
- ❌ Нет formal verification (F* / hax / Coq)
Чтобы поднять до 9: закрыть F-2, F-3, F-4, F-5, F-6, F-8 + sync AUDIT.md (F-1) + установить fuzzing harness (F-15).
Чтобы поднять до 10: + audit firm signature + formal verification критических путей + side-channel hardware testing + multi-tenant deployment hardening.
Заключение
Кодовая база Montana M1 (foundational crypto + identity recovery) демонстрирует сильную инженерную дисциплину и независимо подтверждённую NIST FIPS 204/203 conformance. Архитектурный выбор использовать OpenSSL 3.5.5 LTS вместо pre-1.0 RustCrypto — правильный для production audit readiness.
Найденные findings — преимущественно документационная drift (AUDIT.md устарел) и minor security hygiene issues. Критические уязвимости отсутствуют в audited scope.
Код готов к external audit firm review после закрытия HIGH-severity findings (F-2, F-5, F-6). До закрытия этих — рекомендую дополнительный pass самокритики со стороны команды.
Я не подменяю аудит recognized firm, не предоставляю legal certification, не закрываю side-channel и formal verification gaps. Этот отчёт — подготовительный аудит уровня внутренней проверки качества, который сэкономит платный audit firm часы на очевидное и предоставит им более чистую базу для критического обзора.
8. Метаданные воспроизведения
Команды для проверки findings (одной строкой каждая):
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && wc -l crates/mt-crypto/src/lib.rs
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && cargo fmt --all -- --check
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && cargo clippy --all-targets -- -D warnings
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && cargo audit --json
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && cargo test -p mt-crypto-native --test nist_acvp_kat -- --nocapture
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && cargo test -p mt-crypto -p mt-crypto-native -p mt-mnemonic
cd /tmp && curl -sL "https://raw.githubusercontent.com/usnistgov/ACVP-Server/master/gen-val/json-files/ML-DSA-keyGen-FIPS204/internalProjection.json" -o nist_mldsa_keygen.json
Среда выполнения:
- Платформа: Darwin 24.6.0 (macOS)
- Архитектура: ARM64 (Apple Silicon)
- rustc: 1.92.0 (Homebrew, ded5c06cf 2025-12-08)
- cargo: 1.92.0 (Homebrew)
- cargo-audit: установлен в
/Users/kh./.cargo/bin/cargo-audit
Доступные но не использованные ресурсы (для будущего расширения):
- montana-moscow (176.124.208.93, Linux x86_64)
- montana-frankfurt (89.19.208.158, Linux x86_64)
Аудитор: Claude Opus 4.7 (1M context)
Подпись модели: claude-opus-4-7[1m]
Дата создания отчёта: 2026-04-26
Идентификатор аудита: claude-opus-4-7_2026-04-26_T201805