- **Комментарии в коде** (`// SAFETY:`, `// spec, раздел "..."`) — рассматривались как **claims-to-verify**, не как авторитетные утверждения
- **Хардкоженные тестовые expected hex значения** в `tests/` — где они self-derived (regression baseline), а не cross-checked против NIST/RFC, оценивались как regression-tests, не как conformance-proofs
### Источники ground truth, использованные в аудите
| Стандарт / источник | URL / ссылка | Применение |
| 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/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:
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)
Также **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, с проверкой против:
**Воздействие.** Аудитор может пропустить новый код добавленный после написания AUDIT.md. Внешний аудитор, читающий AUDIT.md как ground truth, получит неверную картину объёма работы.
**Рекомендация.** Установить CI gate: при любом PR верифицировать что line counts в AUDIT.md соответствуют реальным. Либо удалить hardcoded counts из AUDIT.md и оставить только команду для проверки.
Реальная 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"`.
| 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`):
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()` использует слабую энтропию
- Привязка через `#[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
-`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...`.
- 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) проверяет:
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.
**Описание.** В`mt-mnemonic/src/mnemonic.rs` line 41:
```rust
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
**Рекомендация.** Проверить design intent. Если строгость намеренна (anti-tampering) — задокументировать в API docs. Если нет — заменить на `split_whitespace()`.
Итого **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
**Описание.** Заметка по архитектуре: 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`:
```toml
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.
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.
`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 процесс + рамки времени):
Кодовая база 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 (одной строкой каждая):**