Содержание:

Здесь нужна математика из статьи 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 и однострочники для быстрой проверки. Минимальный набор из трех инструментов - и расширенный для сложных случаев.