montana/Монтана-Протокол/Внешний аудит/claude-opus-4-7_2026-04-26_T201805.md

57 KiB
Raw Permalink Blame History

Внешний аудит кода 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 CAVPhttps://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 SecretKeylibc::munlock НЕТ
187 fn alloc_locked_secret_boxlibc::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 MlkemSecretKeylibc::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 секретного материала

Реализация хранения секретов реализована с двумя слоями защиты:

  1. Heap-allocation через Box<[u8; SECRET_KEY_SIZE]> — секретные байты живут в одной heap-локации от создания до уничтожения, никаких stack memcpy при move-операциях.
  2. 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-KEM
  • secret_key_no_partial_eq_to_prevent_timing_leak — нет PartialEq (защита от timing-leak через memcmp)
  • secret_key_is_heap_allocatedsize_of::<SecretKey>() == size_of::<usize>() (1 указатель)
  • secret_key_needs_dropstd::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 + clippy
  • clippy.tomlmsrv = "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 = true
  • build.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

Рекомендация. Один из двух вариантов:

  1. Логировать через telemetry/stderr при первом failure mlock («WARNING: secret memory не залочена в RAM, fallback на encrypted swap»)
  2. Делать 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-allocated
  • dk: 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/dk Vec 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.rsregression_baselines.rs либо internal_baselines.rs для ясности. Удалить из его docstring любые claims о cross-implementation conformance.

F-10 [LOW] — Self-test не проверяет против NIST output

Описание. Функция mt_crypto::self_test() (lines 430-460) проверяет:

  1. Sizes match
  2. Determinism (двойной KeyGen с тем же seed)
  3. Sign/verify roundtrip
  4. KAT 1 byte-exact: keypair_from_seed([0x00; 32]) → SHA-256(pk) == hardcoded EXPECTED_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.

Рекомендация.

  1. Документировать threat model явно: Montana не предполагает физический доступ или cloud-neighbour atтак (single-tenant deployment)
  2. Опционально: добавить subtle::ConstantTimeEq для всех critical comparisons
  3. Опционально: 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 — fuzz mt_crypto::sign(sk, msg) с various sk corruption + various msg
  • fuzz/fuzz_targets/fuzz_verify.rs — fuzz mt_crypto::verify(pk, msg, sig)
  • fuzz/fuzz_targets/fuzz_mnemonic.rs — fuzz mnemonic_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. Известные ограничения этого аудита

Что я не мог проверить даже с серверным доступом:

  1. Side-channel attacks через физическое оборудование — нет осциллографа, измерителя мощности, EM-зонда
  2. Cryptanalysis самих ML-DSA / ML-KEM — академическая работа NIST PQC competition, out of scope
  3. Formal verification через F* / hax / EasyCrypt / Coq — toolchain недоступен в среде, требует переписывания кода под proof framework
  4. Корректность OpenSSL внутри (Layer 3) — миллионы строк C-кода, отдельный аудит OpenSSL Foundation
  5. Bugs в компиляторе rustc/cc — атака «Trusting Trust» (Ken Thompson 1984), требует второго независимого compilers
  6. Документ-уровневая legal certification — нет печати NCC Group / Trail of Bits / Quarkslab / Cure53 / Kudelski

Что я мог бы сделать с серверами но не делал в этой сессии (ограничение 1 ядро / 1 процесс + рамки времени):

  1. Двойная независимая Docker сборка для верификации reproducible builds (Mac + Moscow + Frankfurt)
  2. Long-running fuzzing 24-48 часов через cargo fuzz на сервере
  3. Cross-platform smoke testing на Linux x86_64 vs macOS ARM64
  4. Supply chain audit OpenSSL bytes из openssl-src vs openssl.org official tarball SHA-256
  5. Statistical timing measurement на серверах для weak constant-time signal

Эти 5 пунктов не были выполнены в текущем аудите но доступны для расширения.


6. Рекомендации по приоритетам

Приоритет «закрыть до production audit» (HIGH severity)

  1. F-2cargo fmt --all, обновить AUDIT.md self-attestation после real-prog проверки
  2. F-6 — заменить keypair() test helper на CSPRNG-based, либо вынести в mt-test-utils крейт
  3. F-5 — runtime warning при mlock failure, либо makemandatory с graceful error

Приоритет «закрыть до v1.0 release» (MEDIUM severity)

  1. F-3 — обновить stale comments в m1_crypto.rs (line 127, 299)
  2. F-4 — добавить // SAFETY: комментарии к 3 unused-marker блокам (lines 168, 187, 351)
  3. F-8 — расширить SigGen NIST KAT до 15 cases tgId=3
  4. F-9 — переименовать kat_independent.rsregression_baselines.rs

Приоритет «закрыть для документационного качества» (LOW severity)

  1. F-1 — sync line counts в AUDIT.md
  2. F-10 — добавить NIST CAVP byte-exact в self_test() функцию
  3. F-13 — уточнить «13 status codes (1 success + 12 errors)»
  4. F-12 — design intent strict whitespace mode mnemonic
  5. F-7Zeroizing<T> для PBKDF2/HKDF/HMAC intermediate state
  6. F-17 — опционально удалить serde_json dev-dependency
  7. F-18 — удалить cc parallel feature
  8. F-19 — комментарий OpenSSL API const-cast convention

Приоритет «infrastructure improvement» (INFO)

  1. F-14 — формализовать constant-time свойства (subtle crate / dudect)
  2. F-15 — установить cargo-fuzz harness
  3. 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 (положительное):

  1. NIST FIPS 204/203 byte-exact conformance независимо подтверждена (51/51 KAT)
  2. Production-grade backend — OpenSSL 3.5.5 LTS (не pre-1.0 RustCrypto)
  3. Heap-allocated SK с mlock + Drop+zeroize — правильная hygiene
  4. Compile-time security invariants (No Clone/Copy/PartialEq на SK)
  5. 0 уязвимостей в cargo audit (39 deps scanned, 1058 advisories)
  6. Все RFC test vectors PBKDF2/HKDF/HMAC проходят
  7. 118 тестов passed, 0 failed
  8. Clippy clean (-D warnings)
  9. Strict version pinning (все exact =X.Y.Z)
  10. Cross-platform build correctness через CARGO_CFG_TARGET_OS
  11. Reproducible build infrastructure prepared (Cargo.lock + rust-toolchain.toml)
  12. Thoughtful threat model (heap+mlock, deterministic Sign, no logging SK)

За что снимаю 2 (отрицательное):

  1. cargo fmt --check FAILS (F-2) — нарушение discipline которая декларируется
  2. AUDIT.md заявления не сверены с фактом (F-1, F-4, F-13, F-18) — документация устарела
  3. Stale references к pre-migration RustCrypto (F-3) — confusion для auditor
  4. Test-only keypair() использует weak entropy (F-6) — теоретический risk при misuse
  5. Best-effort mlock без runtime warning (F-5) — silent fallback на encrypted swap
  6. Coverage SigGen NIST KAT — только 1/15 cases в supported group (F-8)
  7. Нет fuzzing infrastructure (F-15) — FFI boundary не tested на malformed input
  8. Constant-time свойства не verified конструкцией (F-14)
  9. Нет внешней audit firm signature (требование regulator/insurer)
  10. Нет 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