Содержание:
- Напоминание: почему nonce в ECDSA критичен
- Откуда утекают биты nonce
- Hidden Number Problem: даже частичная утечка фатальна
- Minerva: утечка длины nonce
- TPM-FAIL: ключ VPN-сервера за 5 часов
- EUCLEAK: клонирование YubiKey
- Fault injection: Rowhammer и RSA-CRT
- Nonce reuse detection: что проверять при пентесте
- Что делать для защиты
- Что дальше
Здесь нужна математика из статьи 9. Утечка трех бит nonce из каждой ECDSA-подписи - и 100 подписей спустя у тебя полный приватный ключ. Minerva, TPM-FAIL, EUCLEAK - реальные атаки на реальные устройства.
На стандартном веб-пентесте ты не будешь строить решетки. Но три вещи из этой статьи пригодятся: (1) проверка nonce reuse в ECDSA-подписях - это 10 строк Python и реальный вектор, (2) определение версии криптобиблиотеки на сервере и сверка с CVE, (3) понимание, почему EdDSA безопаснее ECDSA и когда это не так.
Напоминание: почему nonce в ECDSA критичен
В статье 9 я разбирал формулу подписи ECDSA (ES256 в JWT):
s = k^(-1) * (H(m) + r * d) mod n
Где k - случайный nonce, d - приватный ключ. Там же я показывал nonce reuse на примере Sony PS3: если k повторится в двух подписях, приватный ключ вычисляется в две формулы. Функция get_r() из той статьи позволяет обнаружить повторение - одинаковый r в двух токенах означает одинаковый k.
Полный nonce reuse - это идеальный случай. А что если утекает не весь nonce, а только несколько бит?
Откуда утекают биты nonce
Через timing side-channel (атака по времени выполнения). Скалярное умножение k * G на эллиптической кривой выполняется итеративно - количество операций зависит от длины nonce в битах (позиции старшего значащего бита). Nonce с ведущими нулевыми битами короче и обрабатывается быстрее - меньше бит, меньше итераций, меньше времени. Разница измерима: микросекунды на локальной машине, но через сеть (HTTPS, CDN, load balancer) шум на порядки перекрывает сигнал.
В контексте JWT источники утечки:
- IdP подписывает JWT ES256 на сервере с уязвимой криптобиблиотекой
- HSM/TPM с уязвимой прошивкой подписывает токены
- Смарт-карта подписывает device-bound JWT
Атакующий собирает JWT с ECDSA-подписями, замеряет время ответа сервера. По timing классифицирует nonce по длине в битах. Строит решетку. Запускает LLL. Получает приватный ключ. На практике timing-измерения через HTTPS с CDN и load balancers крайне зашумлены - реалистичный сценарий требует либо локального доступа, либо сетевой близости к signing-серверу. Определить библиотеку/версию и сверить с CVE - более практичный подход для веб-пентеста.
Hidden Number Problem: даже частичная утечка фатальна
В статье 9 мы восстанавливали ключ из повторения nonce (два уравнения, два неизвестных). Здесь задача сложнее: мы не знаем nonce целиком, но знаем несколько бит каждого. Это даёт не точные уравнения, а приближённые - и для их решения нужен другой математический аппарат.
Hidden Number Problem (HNP) - задача, сформулированная Boneh и Venkatesan в 1996 году. Суть: из формулы ECDSA s = k^(-1)(H(m) + r*d) mod n, приблизительное знание k позволяет составить уравнение с одним неизвестным d. Каждая подпись с утечкой бит nonce даёт одно приближённое модулярное уравнение. Когда уравнений достаточно, приватный ключ оказывается единственной точкой решетки, ближайшей к известным данным.
Представь себе решетку как сетку точек в многомерном пространстве. У тебя есть точка, которая “почти” совпадает с одной из точек решетки. Задача - найти ближайшую точку решетки. Это задача Closest Vector Problem (CVP), и она решается алгоритмами LLL и BKZ.
LLL (Lenstra-Lenstra-Lovász) - полиномиальный алгоритм редукции решёток, который делает базисные векторы более ортогональными и короткими. Для практических размерностей HNP из ECDSA его обычно достаточно.
BKZ (Block Korkine-Zolotarev) - более мощный алгоритм, использующий LLL как подпроцедуру, но дополнительно решающий задачу кратчайшего вектора (SVP) в блоках размера beta. С параметром beta = 20-40 даёт лучшие результаты для сложных случаев.
Сколько нужно подписей и бит утечки:
- 3 бита на подпись, кривая P-256: ~90 подписей
- 1 бит (только длина nonce), P-256: ~500-2100 подписей
- 5 бит, P-384: ~150-300 подписей
Ядро атаки - построение решетки и LLL - укладывается менее чем в 100 строк Python с библиотекой fpylll (полная атака требует ещё сбор подписей, timing-анализ и классификацию nonce):
from fpylll import IntegerMatrix, LLL, CVP
# P-256 curve order
n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
# N - количество собранных подписей
# l - количество известных старших бит nonce
# t[i] = r_i * s_i^(-1) mod n (из каждой подписи)
# u[i] = -H(m_i) * s_i^(-1) mod n
# Вычисление t[i], u[i] из JWT:
# r_i, s_i = первые и вторые 32 байта подписи (ES256, IEEE P1363 формат)
# z_i = int.from_bytes(sha256(header.payload), 'big')
# t[i] = (r_i * pow(s_i, -1, n)) % n # Python 3.8+
# u[i] = (-z_i * pow(s_i, -1, n)) % n
B = IntegerMatrix(N+1, N+1)
for i in range(N):
B[i, i] = n # порядок группы кривой
B[N, i] = t[i] # вычислено из подписи
B[N, N] = n // (2 ** (l + 1)) # scaling factor: граница bias
LLL.reduction(B)
target = [u[i] for i in range(N)] + [0]
closest = CVP.closest_vector(B, target)
private_key = (closest[N] * (2 ** (l + 1))) % n
На момент публикации задокументированных lattice-атак конкретно на JWT-инфраструктуру нет, но все компоненты атаки продемонстрированы по отдельности - уязвимые библиотеки, timing-каналы, lattice-reduction. Инструменты: SageMath (де-факто стандарт для lattice cryptanalysis), fpylll (Python-обертка над fplll), minerva-tool (GitHub crocs-muni/minerva), lattice-attack (готовые HNP solvers).
Minerva: утечка длины nonce
Исследование Minerva (2019, Ján Jancar et al.) выявило timing side-channel в нескольких криптобиблиотеках и смарт-картах. Утечка минимальная: только длина nonce в битах. ~50% nonce имеют полную длину (256 бит для P-256), ~25% на бит короче, ~12.5% на два бита короче. Каждый недостающий бит - одна итерация меньше в скалярном умножении, и подпись вычисляется быстрее. Атакующий замеряет время: быстрая подпись - короткий nonce.
Затронутые реализации (каждая имеет свой CVE):
- libgcrypt (CVE-2019-13627)
- wolfSSL (CVE-2019-13628)
- SunEC/OpenJDK (CVE-2019-2894) - дефолтный ECDSA-провайдер Java, то есть любая Java-библиотека JWT (Nimbus, Auth0 java-jwt) на стандартном SunEC была уязвима
- Crypto++ (CVE-2019-14318)
- MatrixSSL (CVE-2019-13629) - не пропатчено
- Смарт-карты Athena IDProtect и SafeNet eToken (CVE-2019-15809)
Сколько подписей нужно: ~500 в симуляции, ~1200 для реальной библиотеки, ~2100 для смарт-карты (больше шума в timing).
Сценарий для JWT: IdP работает на сервере с уязвимым libgcrypt (до версии 1.8.5) и подписывает JWT алгоритмом ES256. Атакующий с сетевой близостью к серверу (в одном датацентре, без CDN) собирает ~1200 подписей, замеряя время ответа с микросекундной точностью. Классифицирует подписи по timing, строит решетку, запускает LLL - и восстанавливает signing key. Через HTTPS с CDN точность timing недостаточна - на стандартном веб-пентесте практичнее определить версию библиотеки и свериться с CVE.
TPM-FAIL (CVE-2019-11090, CVE-2019-16863): ключ VPN-сервера за 5 часов
Timing leakage в Intel fTPM (firmware-based TPM) и STMicro TPM при ECDSA-подписях. Время выполнения TPM2_Sign зависит от nonce.
Цифры:
- Intel fTPM, локально: ~1300 подписей, менее двух минут на восстановление ключа
- Intel fTPM, удаленно по сети: ~5 часов (сбор + анализ)
- STMicro TPM: ~40000 подписей (аппаратный TPM зашумленнее)
Исследователи продемонстрировали извлечение ключа VPN-сервера удаленно за 5 часов. Если TPM используется для подписи JWT (что рекомендуется как “безопасное хранение ключей”) - та же атака.
EUCLEAK (CVE-2024-45678): клонирование YubiKey
Side-channel в YubiKey 5 Series. Non-constant-time модулярное обращение (Extended Euclidean Algorithm) в библиотеке Infineon при ECDSA-подписи. Электромагнитные эманации при подписании раскрывают информацию о nonce.
Для эксплуатации нужен физический доступ к YubiKey и EM-probe (электромагнитный зонд). Firmware ниже 5.7 не обновляется - устройство перманентно уязвимо (это by design: Yubico считает non-updatable firmware защитой от supply chain атак на механизм обновления).
Для JWT: если YubiKey используется как фактор FIDO2 в системе, выдающей JWT-токены, клонирование YubiKey дает прохождение FIDO-аутентификации и получение легитимных JWT.
Fault injection: Rowhammer и RSA-CRT
Отдельная категория атак - не пассивное наблюдение за timing, а активное воздействие на процесс подписания.
Rowhammer на ECDSA. Bit-flip-ы в DRAM-памяти. Атакующий на соседней виртуальной машине (co-tenant) в облаке может вызвать fault в процессе подписания. Одна дефектная подпись (с неправильным nonce из-за bit-flip) + одна нормальная подпись = восстановление ключа через те же lattice-методы. Для стандартного веб-пентеста fault injection неприменим - это threat model для облачных провайдеров и аппаратных вендоров, требующий co-location на одном физическом хосте и точного попадания в наносекундное окно вычисления nonce.
Свежие работы (2025):
- SLasH-DSA: forgery SLH-DSA подписи (постквантовый стандарт NIST, подробнее в статье 20) за 1-8 часов через Rowhammer
- ECC.fail: обход ECC-защиты памяти через targeted miscorrection на DDR4 серверах
- Phoenix: bypass TRR (Target Row Refresh) на DDR5 за 109 секунд
RSA-CRT fault (Boneh-DeMillo-Lipton, 1997). Для RS256/PS256 JWT есть аналогичная угроза. Если RSA подпись вычисляется через Chinese Remainder Theorem (как в большинстве реализаций) и одно из двух промежуточных вычислений corrupted через fault, одна дефектная подпись раскрывает приватный ключ через GCD. Это проще lattice-атак на ECDSA (одна подпись вместо десятков-сотен).
Важный момент: детерминированные подписи не спасают от fault injection. RFC 6979 и EdDSA (которую я рекомендовал в статье 9) вычисляют nonce детерминистически из приватного ключа и сообщения. Но если fault меняет состояние при вычислении nonce, одно и то же сообщение получит два разных nonce в двух попытках подписания. Атакующий провоцирует fault, получает дефектную подпись, сравнивает с нормальной - и lattice-атака работает. На детерминированных схемах fault injection даже опаснее: атакующий может заставить сервер переподписать то же сообщение и сравнить результаты.
Nonce reuse detection: что проверять при пентесте
Первый шаг - убедиться что цель использует ECDSA. Проверь alg в заголовке JWT: ES256, ES384, ES512. Если видишь RS256 или EdDSA - nonce reuse и timing-атаки из этой статьи не применимы (но RSA-CRT fault применим к RS256).
import base64
def get_r(token):
sig = base64.urlsafe_b64decode(
token.split('.')[2] + '==')
return sig[:32] # первые 32 байта для ES256 (JWS IEEE P1363 формат)
# Собираем N токенов от одного сервера
# authenticate_and_get_jwt() - ваша функция аутентификации
# Учитывайте rate limiting: большинство IdP ограничивают ~100-300 req/min
tokens = [authenticate_and_get_jwt() for _ in range(100)]
# Ищем совпадение r-компонента
rs = {}
for t in tokens:
r = get_r(t).hex()
if r in rs:
print(f"NONCE REUSE DETECTED!")
print(f"Token 1: {rs[r][:50]}...")
print(f"Token 2: {t[:50]}...")
# Два токена с одним nonce = приватный ключ:
# d = (z1 - z2) * pow(s1 - s2, -1, n) % n
# где z = SHA256(header.payload), s из подписи
rs.setdefault(r, t)
На практике nonce reuse в JWT-подписях встречается редко - современные библиотеки используют RFC 6979 (детерминированный nonce). Но в кастомных реализациях, старых библиотеках и embedded-системах - вполне реально.
Что делать для защиты
EdDSA вместо ECDSA - детерминированные nonce устраняют проблемы random nonce reuse и length bias. Но: fault injection на детерминированных схемах опаснее (две подписи одного сообщения с разными nonce = восстановление ключа). JWT алгоритм EdDSA, JWK тип OKP, кривая Ed25519. Поддержка: jose (Node.js), PyJWT (>=2.4), golang-jwt, nimbus-jose-jwt (Java). Если EdDSA недоступна, PS256 (RSA-PSS) полностью устраняет nonce-проблему - у RSA-PSS нет nonce.
Hedged signatures - комбинация детерминированного nonce (RFC 6979) с дополнительной энтропией: k = HMAC(d, m || random). Если RNG сломан - детерминированный fallback. Если fault injection - рандом предотвращает предсказуемость. Защита от обоих классов атак. Реализовано: Go crypto/ecdsa (>=1.20), BoringSSL, libsodium Ed25519 hedged mode. Спецификация: draft-irtf-cfrg-det-sigs-with-noise (CFRG). Для детерминированных схем дополнительно: sign-twice-and-compare (подписать дважды, сравнить результаты; если отличаются - fault, не выпускать ни одну подпись).
Constant-time библиотеки - устраняют timing side-channel. Конкретно: OpenSSL (>=1.1.0), BoringSSL, libsodium, ring (Rust), Go crypto/ecdsa (>=1.20). Проверить constant-time поведение: инструменты dudect, ctgrind, timecop. Minerva и EUCLEAK были в реализациях, которые считались constant-time - доверяй, но проверяй.
Ротация ключей - ограничивает окно для lattice-атак. Если библиотека constant-time, количество подписей нерелевантно. Если есть подозрение на timing leakage - ротация каждые 24-72 часа через JWKS endpoint с kid в заголовке JWT. Overlap period: публиковать текущий и предыдущий ключ одновременно для валидации уже выданных токенов.
Мониторинг timing - если гистограмма времени подписания показывает несколько отдельных пиков (кластеры вокруг разных значений) вместо одного, это указывает на утечку длины nonce. Актуально для HSM/TPM-backed signing где нельзя инспектировать исходный код. При обнаружении - немедленная ротация ключа.
Что дальше
За четырнадцать частей получились десятки PoC и техник - от alg:none до lattice-атак. В следующей статье соберу инструменты: jwt_tool, hashcat, Burp JWT Editor и однострочники для быстрой проверки. Минимальный набор из трех инструментов - и расширенный для сложных случаев.