5.6 KiB
Верификация: Dilithium Storm (Google/Gemini 3 Pro)
Верификатор: Claude Opus 4.5 (Anthropic)
Дата: 08.01.2026 UTC
Исходный отчёт: Council/Google/dilithium_storm_08.01.2026_12:00.md
Заявленная уязвимость
Название: Dilithium Storm (CPU Exhaustion on Bootstrap Nodes)
Severity (Gemini): CRITICAL → HIGH
Файл: protocol.rs:1175
Суть: Атакующий отправляет поток AuthChallenge сообщений к hardcoded node. Каждое сообщение триггерит синхронный вызов sign_mldsa65, блокируя async runtime и вызывая CPU exhaustion.
Верификация
Шаг 1: Код существует?
Файл: protocol.rs:1175-1205
Message::AuthChallenge(challenge) => {
if let Some(ref secret_key) = config.hardcoded_secret_key {
let sig = match crate::crypto::sign_mldsa65(secret_key, &challenge) {
Some(s) => s,
None => {
warn!("Failed to sign AuthChallenge (invalid secret key?)");
return Ok(false);
}
};
// ... send AuthResponse
}
}
Результат: ✓ КОД СУЩЕСТВУЕТ
Шаг 2: sign_mldsa65 синхронный?
Файл: crypto.rs:141-145
pub fn sign_mldsa65(secret_key: &[u8], message: &[u8]) -> Option<MlDsa65Signature> {
let sk = dilithium::SecretKey::from_bytes(secret_key).ok()?;
let sig = dilithium::detached_sign(message, &sk);
Some(sig.as_bytes().to_vec())
}
- Нет
async - Нет
spawn_blocking dilithium::detached_sign— CPU-интенсивная операция
Результат: ✓ СИНХРОННЫЙ, БЛОКИРУЕТ RUNTIME
Шаг 3: Rate limit для AuthChallenge?
Файл: rate_limit.rs:206-226
pub struct PeerRateLimits {
pub addr: AddrRateLimiter,
pub inv: InvRateLimiter,
pub getdata: GetDataRateLimiter,
pub headers: HeadersRateLimiter,
pub getslices: GetSlicesRateLimiter,
pub slice: SliceRateLimiter,
// НЕТ auth_challenge!
}
Результат: ✗ RATE LIMIT ОТСУТСТВУЕТ
Шаг 4: AuthChallenge pre_handshake?
Файл: message.rs:89-98
pub fn allowed_pre_handshake(&self) -> bool {
matches!(
self,
Message::Version(_)
| Message::Verack
| Message::Reject(_)
| Message::AuthChallenge(_) // ← РАЗРЕШЕНО
| Message::AuthResponse { .. }
)
}
Результат: ✓ РАЗРЕШЕНО ДО HANDSHAKE
Шаг 5: Защиты выше по стеку?
Noise handshake обязателен — protocol.rs:690-715:
let encrypted_result = if inbound {
tokio::time::timeout(noise_timeout, EncryptedStream::accept(stream, &keypair)).await
} else {
tokio::time::timeout(noise_timeout, EncryptedStream::connect(stream, &keypair)).await
};
Все сообщения идут через EncryptedStream. Атакующий ДОЛЖЕН завершить Noise XX handshake прежде чем отправлять любые сообщения.
Это добавляет стоимость атаке:
- ML-KEM-768 encapsulate/decapsulate
- X25519 key exchange
- 3-way message exchange
Результат: ⚠️ ЧАСТИЧНАЯ ЗАЩИТА (увеличивает стоимость атаки)
Шаг 6: Кто уязвим?
Только узлы с config.hardcoded_secret_key:
if let Some(ref secret_key) = config.hardcoded_secret_key {
// Только hardcoded nodes входят сюда
}
Обычные узлы не имеют secret_key и игнорируют AuthChallenge.
Результат: ⚠️ ТОЛЬКО HARDCODED NODES УЯЗВИМЫ
Итоговый вердикт
| Критерий | Gemini | Моя оценка |
|---|---|---|
| Код существует | ✓ | ✓ |
| Уязвимость реальна | ✓ | ✓ |
| Severity | CRITICAL → HIGH | HIGH |
Корректировки к отчёту Gemini
-
Noise handshake обязателен — Gemini написал "Не завершая хендшейк (или сразу после), атакующий отправляет поток сообщений". Это неточно. Noise handshake ОБЯЗАТЕЛЕН до любого сообщения.
-
Уязвимы только hardcoded nodes — это ограничивает impакт (1-10 узлов vs вся сеть).
-
Severity HIGH, не CRITICAL — атака не "уничтожает сеть", а временно DoS'ит bootstrap инфраструктуру.
Рекомендации
Рекомендации Gemini корректны:
-
Добавить rate limit —
auth_challenge: TokenBucket::new(5.0, 0.1)в PeerRateLimits -
spawn_blocking — вынести sign_mldsa65:
let sig = tokio::task::spawn_blocking(move || { crate::crypto::sign_mldsa65(&secret_key, &challenge) }).await.ok()??; -
Рассмотреть перенос AuthChallenge после handshake — это усложнит атаку, но изменит протокол bootstrap.
Финальный статус
[x] CONFIRMED — уязвимость реальна
[x] SEVERITY ADJUSTED — HIGH (не CRITICAL)
[ ] HALLUCINATED
[ ] ALREADY_PROTECTED
Действие: Требуется исправление до mainnet.