225 lines
14 KiB
Markdown
225 lines
14 KiB
Markdown
|
|
# Security Audit: Montana Network Layer (net/)
|
|||
|
|
|
|||
|
|
**Модель:** GPT-5.2
|
|||
|
|
**Компания:** OpenAI
|
|||
|
|
**Дата:** 08.01.2026 14:54 UTC
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Понимание архитектуры
|
|||
|
|
|
|||
|
|
Montana — это ACP: сеть фиксирует «присутствие во времени» через регулярные подписи (τ₁/τ₂), которые распространяются в реальном времени по P2P. Безопасность консенсуса опирается на (a) криптографическую неподделываемость, (b) привязку к текущему τ₂ (prev_hash / сетевое окно), (c) eventual consistency через fork-choice.
|
|||
|
|
|
|||
|
|
Следствие для сетевого слоя: ключевая цель сети — обеспечить доставку и валидацию внешних данных (подписей/слайсов/tx) в bounded-режиме и не дать атакующему:
|
|||
|
|
- изолировать узел на старте (bootstrap) или во время работы (eclipse/addrman poisoning)
|
|||
|
|
- исчерпать ресурсы (mem/cpu/bandwidth) через сообщения, очереди, инвентори
|
|||
|
|
- подменить доверенные источники на старте (hardcoded bootstrap anchors)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Изученные файлы
|
|||
|
|
|
|||
|
|
Сетевой слой `montana/src/net/` (12,330 LOC):
|
|||
|
|
- `net/message.rs` (168)
|
|||
|
|
- `net/encrypted.rs` (570)
|
|||
|
|
- `net/types.rs` (658)
|
|||
|
|
- `net/serde_safe.rs` (266)
|
|||
|
|
- `net/protocol.rs` (1506)
|
|||
|
|
- `net/verification.rs` (848)
|
|||
|
|
- `net/dns.rs` (266)
|
|||
|
|
- `net/feeler.rs` (256)
|
|||
|
|
- `net/discouraged.rs` (286)
|
|||
|
|
- `net/sync.rs` (665)
|
|||
|
|
- `net/bootstrap.rs` (1413)
|
|||
|
|
- `net/startup.rs` (184)
|
|||
|
|
- `net/subnet.rs` (444)
|
|||
|
|
- `net/eviction.rs` (347)
|
|||
|
|
- `net/mod.rs` (82)
|
|||
|
|
- `net/inventory.rs` (705)
|
|||
|
|
- `net/hardcoded_identity.rs` (203)
|
|||
|
|
- `net/noise.rs` (898)
|
|||
|
|
- `net/rate_limit.rs` (879)
|
|||
|
|
- `net/connection.rs` (509)
|
|||
|
|
- `net/peer.rs` (438)
|
|||
|
|
- `net/addrman.rs` (739)
|
|||
|
|
|
|||
|
|
Связанные файлы:
|
|||
|
|
- `src/types.rs`, `src/crypto.rs`, `src/db.rs`, `src/nmi.rs`, `src/nts.rs`
|
|||
|
|
|
|||
|
|
Инструменты/утилиты:
|
|||
|
|
- `src/bin/attacker.rs` (stress-тест)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Attack Surface
|
|||
|
|
|
|||
|
|
- **Bootstrap / Startup verification**: `net/startup.rs` → `net/verification.rs` → `net/bootstrap.rs`.
|
|||
|
|
- hardcoded challenge-response
|
|||
|
|
- сбор P2P адресов через GetAddr / Addr
|
|||
|
|
- принятие network time (median) и median height
|
|||
|
|
|
|||
|
|
- **Долгоживущая P2P-сеть**: `net/protocol.rs`.
|
|||
|
|
- inbound/outbound TCP
|
|||
|
|
- Noise XX + ML-KEM (шифрование/аутентификация транспорта)
|
|||
|
|
- обработка сообщений (Version/Verack/Addr/Inv/GetData/Slice/Tx/Presence)
|
|||
|
|
|
|||
|
|
- **Механизмы устойчивости**:
|
|||
|
|
- connection limits / netgroup / per-IP (`net/connection.rs`)
|
|||
|
|
- eviction защищённых слотов (`net/eviction.rs`)
|
|||
|
|
- AddrMan bucket system (`net/addrman.rs`) + feeler (`net/feeler.rs`)
|
|||
|
|
- rate limit per peer + subnet-level (fast/slow) (`net/rate_limit.rs`)
|
|||
|
|
- bounded collections (`net/serde_safe.rs`, `net/message.rs`)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Найденные уязвимости
|
|||
|
|
|
|||
|
|
### [CRITICAL] Bootstrap MITM: подпись hardcoded узла не привязана к `VersionPayload`
|
|||
|
|
|
|||
|
|
**Суть:** протокол `AuthChallenge`/`AuthResponse` предполагает, что hardcoded узел доказывает идентичность подписью, но в коде проверяется подпись только над `challenge`, а поля `version` (высота, timestamp, best_slice, etc.) не входят в подписанное сообщение.
|
|||
|
|
|
|||
|
|
**Уязвимый код (проверка подписи по одному challenge):**
|
|||
|
|
- `montana/src/net/verification.rs:493-515` — подпись проверяется функцией `verify_hardcoded_response(&challenge, &signature)` и затем используется `version`.
|
|||
|
|
- `montana/src/net/hardcoded_identity.rs:85-112` — `verify_hardcoded_response()` проверяет `verify_mldsa65(pubkey, challenge, signature)`.
|
|||
|
|
|
|||
|
|
**Уязвимый код (hardcoded узел подписывает только challenge):**
|
|||
|
|
- `montana/src/net/protocol.rs:1175-1204` — `sign_mldsa65(secret_key, &challenge)` и отправка `AuthResponse { version, signature }`.
|
|||
|
|
|
|||
|
|
**Почему это важно:** на старте именно bootstrap-верификация определяет, чему узел доверяет как «правде» о сети (высота/время/seed peers). Если подпись не связывает `version` с `challenge`, атакующий «на проводе» может сохранить валидную подпись на challenge, но подменить `VersionPayload` (с высотой/временем/параметрами) в транзите.
|
|||
|
|
|
|||
|
|
**Импакт:** высокий риск некорректного bootstrap (неверная высота/время/peer-discovery), что может привести к:
|
|||
|
|
- изоляции узла (eclipse-like) на этапе запуска
|
|||
|
|
- выбору неправильного chain-tip/сегмента сети
|
|||
|
|
- отказу в запуске из‑за «clock divergence» или «height divergence» (liveness)
|
|||
|
|
|
|||
|
|
**Сложность:** средняя для сетевого противника на пути (ISP/BGP/локальная сеть) или при наличии вредоносного прокси; не требует компрометации приватного ключа hardcoded узла.
|
|||
|
|
|
|||
|
|
**Проверка председателя:** ПОДТВЕРЖДЕНО по коду: подпись проверяется только над `challenge` и никак не включает `version`.
|
|||
|
|
|
|||
|
|
**Рекомендация исправления:**
|
|||
|
|
- Подписывать и проверять **контекст + challenge + сериализованный VersionPayload** (и/или transcript):
|
|||
|
|
- `sig_msg = "Montana.HardcodedAuth.v1" || challenge || postcard(version)`
|
|||
|
|
- проверять `verify_mldsa65(pubkey, sig_msg, signature)`
|
|||
|
|
- Дополнительно: рассмотреть перенос bootstrap-канала поверх `EncryptedStream` (Noise), чтобы исключить MITM в принципе.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### [HIGH] Централизация bootstrap: фактически 1 hardcoded IP и пустые DNS seeds
|
|||
|
|
|
|||
|
|
**Суть:** документы декларируют набор seed/hardcoded узлов и их разнообразие. В текущем коде:
|
|||
|
|
- DNS seeds пусты (комментарии-заглушки)
|
|||
|
|
- fallback IP содержит 1 адрес
|
|||
|
|
- hardcoded идентичности mainnet содержат 1 узел
|
|||
|
|
|
|||
|
|
**Доказательства:**
|
|||
|
|
- `montana/src/net/dns.rs:8-32` — `DNS_SEEDS` пустой, `FALLBACK_IPS` содержит один адрес.
|
|||
|
|
- `montana/src/net/hardcoded_identity.rs:40-66` — `MAINNET_HARDCODED` содержит один узел.
|
|||
|
|
|
|||
|
|
**Импакт:** при таком наборе «trusted core» становится фактически единственной точкой отказа:
|
|||
|
|
- DoS/недоступность узла → bootstrap деградирует/ломается
|
|||
|
|
- регуляторные/инфраструктурные события вокруг одного провайдера/географии → системный риск
|
|||
|
|
|
|||
|
|
**Сложность:** низкая (противнику достаточно бить в доступность одного узла).
|
|||
|
|
|
|||
|
|
**Проверка председателя:** ПОДТВЕРЖДЕНО по коду.
|
|||
|
|
|
|||
|
|
**Рекомендация:** расширить список hardcoded узлов (минимум 5–10), реально включить DNS seeds, обеспечить юрисдикционное/AS/гео разнообразие.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### [MEDIUM] Несогласованность лимитов размера сообщений: в `protocol.rs` глобальный лимит = `MAX_TX_SIZE` (1MB), а в `net/types.rs` заявлен 2MB и `inv` до ~1.8MB
|
|||
|
|
|
|||
|
|
**Суть:** `net/types.rs` декларирует `MESSAGE_SIZE_LIMIT = 2MB` и `MAX_INV_MSG_SIZE ≈ 1.8MB`, но `protocol.rs` делает ранний reject любых payload > `MAX_TX_SIZE` (1MB) независимо от команды.
|
|||
|
|
|
|||
|
|
**Доказательства:**
|
|||
|
|
- `montana/src/net/types.rs:111-140` — 2MB лимит и `MAX_INV_MSG_SIZE`.
|
|||
|
|
- `montana/src/net/protocol.rs:1249-1263` и `1329-1334` — ранняя проверка `if len > MAX_TX_SIZE`.
|
|||
|
|
|
|||
|
|
**Импакт:** функциональная несовместимость со «спецификацией» внутри самого репо:
|
|||
|
|
- крупные `inv` сообщения по верхней границе фактически не проходят
|
|||
|
|
- возможна деградация синхронизации/relay при больших батчах
|
|||
|
|
|
|||
|
|
**Сложность:** низкая (это детерминированное поведение кода).
|
|||
|
|
|
|||
|
|
**Проверка председателя:** ПОДТВЕРЖДЕНО по коду.
|
|||
|
|
|
|||
|
|
**Рекомендация:**
|
|||
|
|
- заменить ранний лимит на `MESSAGE_SIZE_LIMIT`, либо
|
|||
|
|
- привести `MAX_*` константы к тому, что реально разрешено ранней проверкой.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### [MEDIUM] `ip_votes` заявлен как bounded, но не очищается на disconnect → потенциальный рост памяти
|
|||
|
|
|
|||
|
|
**Суть:** комментарий говорит «max size = MAX_OUTBOUND», но ключи `SocketAddr` добавляются при каждом (outbound) Version и нигде не удаляются при разрыве соединения.
|
|||
|
|
|
|||
|
|
**Доказательства:**
|
|||
|
|
- Определение + заявление bounded: `montana/src/net/protocol.rs:210-216`.
|
|||
|
|
- Вставка голоса: `montana/src/net/protocol.rs:884-914`.
|
|||
|
|
- Cleanup на disconnect удаляет только `sent_nonces`, но не `ip_votes`: `montana/src/net/protocol.rs:837-844`.
|
|||
|
|
|
|||
|
|
**Импакт:** потенциальный медленный рост HashMap при длительной работе и смене outbound адресов.
|
|||
|
|
|
|||
|
|
**Сложность:** низкая (естественная динамика сети + перезапросы).
|
|||
|
|
|
|||
|
|
**Проверка председателя:** ПОДТВЕРЖДЕНО по коду.
|
|||
|
|
|
|||
|
|
**Рекомендация:**
|
|||
|
|
- удалять `ip_votes[peer.addr]` при `PeerDisconnected`/disconnect,
|
|||
|
|
- либо хранить bounded LRU на `MAX_OUTBOUND` последних адресов.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### [LOW] Хранение Noise static secret key не соответствует комментарию («encrypted»), хранится как raw 32 bytes
|
|||
|
|
|
|||
|
|
**Суть:** в комментарии указано «encrypted with a derived key», но фактически ключ пишется как `std::fs::write(noise_key.bin, keypair.secret)`.
|
|||
|
|
|
|||
|
|
**Доказательства:**
|
|||
|
|
- `montana/src/net/encrypted.rs:406-435`.
|
|||
|
|
|
|||
|
|
**Импакт:** риск при локальной компрометации/backup leaks; смягчается правами `0600`.
|
|||
|
|
|
|||
|
|
**Сложность:** зависит от локального противника.
|
|||
|
|
|
|||
|
|
**Проверка председателя:** ПОДТВЕРЖДЕНО по коду.
|
|||
|
|
|
|||
|
|
**Рекомендация:** либо зашифровать ключ на диске (например, OS keychain), либо поправить комментарий, чтобы не вводил в заблуждение.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Атаки, которые НЕ работают (или работают хуже ожидаемого)
|
|||
|
|
|
|||
|
|
- **«Просто спамить большие inv до 1.8MB»**: верхняя граница `MAX_INV_MSG_SIZE` теоретически есть, но `protocol.rs` режет всё >1MB ранней проверкой (`MAX_TX_SIZE`).
|
|||
|
|
|
|||
|
|
- **`src/bin/attacker.rs` как готовый эксплойт**: этот бинарь сериализует сообщения через `bincode` и работает без Noise, тогда как текущая сеть в `protocol.rs` использует `postcard` поверх `EncryptedStream` (Noise). В текущем виде это скорее исторический stress-тест/заготовка и не отражает реальный on-wire протокол.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Рекомендации
|
|||
|
|
|
|||
|
|
- **Закрыть CRITICAL bootstrap MITM**:
|
|||
|
|
- изменить `AuthResponse` так, чтобы подпись покрывала `challenge || VersionPayload` с domain separation.
|
|||
|
|
- (опционально, сильнее) проводить `VerificationClient` поверх `EncryptedStream`.
|
|||
|
|
|
|||
|
|
- **Убрать single point of failure в bootstrap**:
|
|||
|
|
- заполнить `DNS_SEEDS`, расширить `FALLBACK_IPS`, добавить 5–10+ hardcoded identities.
|
|||
|
|
|
|||
|
|
- **Согласовать лимиты сообщений**:
|
|||
|
|
- использовать `MESSAGE_SIZE_LIMIT` как ранний лимит в `protocol.rs`/`verification.rs` или уменьшить max-команды.
|
|||
|
|
|
|||
|
|
- **Сделать `ip_votes` реально bounded**:
|
|||
|
|
- удаление на disconnect или bounded LRU.
|
|||
|
|
|
|||
|
|
- **Уточнить/исправить хранение Noise secret**:
|
|||
|
|
- либо реализовать шифрование на диске, либо исправить комментарий.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Вердикт
|
|||
|
|
|
|||
|
|
[x] **CRITICAL — есть уязвимость, позволяющая сломать доверие bootstrap (MITM при старте)**
|
|||
|
|
[ ] HIGH — есть серьёзные уязвимости
|
|||
|
|
[ ] MEDIUM — есть уязвимости среднего риска
|
|||
|
|
[ ] LOW — только minor issues
|
|||
|
|
[ ] SECURE — уязвимостей не найдено
|