Тема 13

Эллиптические кривые

Эллиптические кривые

В 1985 году Виктор Миллер и Нил Коблиц независимо друг от друга предложили использовать для схем с открытым ключом не мультипликативную группу конечного поля, а группу точек эллиптической кривой 1 2. Идея оказалась на редкость удачной: та же стойкость достигается ключами в десять — двенадцать раз короче, а сами операции складываются из обычной модульной арифметики. Сегодня на эллиптических кривых стоит почти вся защищённая связь: TLS, SSH, мессенджеры, банковские приложения, электронные подписи и блокчейны.

Причина кроется в том, что в группе точек кривой нет известного алгоритма дискретного логарифмирования быстрее «универсального», требующего около √n операций. Для поля GF(p)* работают решето числового поля и индексный метод, которые используют гладкость чисел; для «общей» эллиптической группы такого понятия нет, и все известные алгоритмы упираются в √n. Отсюда и выигрыш: 256-битная кривая даёт те же 128 бит стойкости, что и 3072-битный RSA.

Вторая половина темы — про то, как это ломается. Математика эллиптических кривых прочна, а вот реализации — нет. Повтор одноразового числа в ECDSA выдаёт закрытый ключ из двух подписей; формулы сложения не содержат коэффициента b, и сервер, не проверяющий входную точку, послушно считает скалярное произведение на чужой кривой; ветвление по битам секрета утекает по времени. История знает и Sony PlayStation 3, и биткойн-кошельки на Android, потерявшие ключи именно так.

Порядок изложения такой: сначала кривая как группа и её геометрия, затем числовой пример на малой кривой и скалярное умножение с лестницей Монтгомери, потом задача ECDLP и выбор стандартных кривых, затем протоколы — ECDH, ECDSA, EdDSA, — и, наконец, ошибки реализации и сравнение с RSA.

Паспорт темы. Нужно знать: темы 4, 11. Освоите: складывать точки и объяснять группу кривой; выполнять ECDH и подписывать ECDSA и EdDSA; оценивать ECDLP и выбирать кривую; восстанавливать ключ при повторе nonce; защищаться от invalid curve. Проверяет: ЛР 13. Минимум: разделы 1–4. Углубление: разделы 5–6.

Кривая как группа

Уравнение и точки

Эллиптическая кривая в форме Вейерштрасса над полем K (характеристики не 2 и не 3) задаётся уравнением y² = x³ + a·x + b, где a, b ∈ K и 4a³ + 27b² ≠ 0. Условие на дискриминант означает, что кривая гладкая: у неё нет точек возврата и самопересечений, а значит, в каждой точке можно провести касательную. Именно гладкость обеспечивает групповой закон.

Над вещественными числами множество решений — знакомая кривая, симметричная относительно оси абсцисс. Над конечным полем GF(p) «кривая» превращается в конечный набор пар (x, y) с 0 ≤ x, y < p, удовлетворяющих уравнению, плюс одна особая точка. Никакой геометрии в привычном смысле там нет, но алгебраические формулы сложения остаются теми же, и этого достаточно.

Полезно сразу привыкнуть к масштабу: у кривой над 256-битным полем примерно 2^256 точек. Перебрать их нельзя, хранить нельзя, а работать с ними — можно, потому что все операции локальны.

Сложение точек

Закон сложения определяется геометрически: через точки P и Q проводится прямая, она пересекает кривую в третьей точке R', и сумма P + Q — это отражение R' относительно оси абсцисс. Отражение нужно, чтобы операция оказалась ассоциативной.

Сложение точек на эллиптической кривой
Секущая через P и Q даёт третью точку R′; сумма P + Q — отражение R′ относительно оси. Касательная в точке удвоения даёт 2P

В координатах это выглядит так. Для P ≠ ±Q наклон секущей равен λ = (y₂ − y₁)/(x₂ − x₁), а результат — x₃ = λ² − x₁ − x₂, y₃ = λ·(x₁ − x₃) − y₁. Когда P = Q, секущая вырождается в касательную, и в числителе появляется производная правой части: λ = (3x₁² + a)/(2y₁), x₃ = λ² − 2x₁. Обе формулы собраны в одном блоке.

сложение (P ≠ ±Q):        удвоение (P = Q):
λ = (y₂ − y₁)/(x₂ − x₁)   λ = (3x₁² + a)/(2y₁)
x₃ = λ² − x₁ − x₂          x₃ = λ² − 2x₁
y₃ = λ·(x₁ − x₃) − y₁      y₃ = λ·(x₁ − x₃) − y₁

Деление — это умножение на обратный элемент по модулю p. Каждое сложение требует одной инверсии; в реальных реализациях переходят в проективные координаты, где инверсия нужна только один раз в конце, а цена сложения — около десятка умножений. Инверсия в поле стоит примерно столько же, сколько возведение в степень, то есть сотни умножений, поэтому переход к проективным координатам даёт ощутимый выигрыш.

Три свойства, которые нужно проверить, чтобы убедиться, что перед нами группа: коммутативность (очевидна из симметрии формулы), существование нейтрального элемента и ассоциативность (самая мучительная; геометрически она следует из теоремы Паскаля, алгебраически проверяется прямым счётом). Лабораторная работа проверяет ассоциативность на сотнях случайных троек точек.

Удвоение и точка на бесконечности

Нейтральный элемент — точка на бесконечности O, лежащая на каждой вертикальной прямой. Формально O = (0 : 1 : 0) в проективных координатах; в коде её удобно представлять как None. Складывать с ней просто: P + O = O + P = P, и это первый случай, который разбирается в любой реализации.

Противоположная точка — отражение: −P = (x, −y). Тогда P + (−P) = O, потому что вертикальная прямая пересекает кривую ровно в этих двух точках и уходит в бесконечность. Отсюда и особый случай в формуле сложения: если x₁ = x₂ и y₁ + y₂ ≡ 0 (mod p), результат — O, а не деление на ноль.

Обратите внимание на важную деталь: коэффициент b не участвует ни в одной формуле сложения. Он задаёт, какие точки лежат на кривой, но не то, как они складываются. Через несколько разделов эта деталь станет основой атаки на недопустимую кривую.

Порядок, кофактор и теорема Хассе

Порядок точки P — наименьшее n ≥ 1, при котором n·P = O. Порядок группы #E(GF(p)) — число всех точек, включая O. По теореме Лагранжа порядок точки делит порядок группы.

Теорема Хассе ограничивает разброс числа точек: |#E(GF(p)) − (p + 1)| ≤ 2·√p. То есть точек «примерно p», и отклонение не больше 2√p. Для криптографии это удобно: порядок группы известен с точностью до узкого интервала, и его можно посчитать алгоритмом Шуфа — Элкиса — Аткина (SEA) за полиномиальное время. Без этого нельзя было бы ни выбрать кривую, ни проверить её стойкость.

Порядок группы раскладывается как #E = n·h, где n — порядок большой подгруппы, в которой и работают протоколы, а h — кофактор. Для криптографических кривых хотят h = 1 (тогда вся группа циклическая простого порядка) или, как у Curve25519, h = 8 при простом n размером около 2^252.

Простой порядок n — это требование, а не украшение. Если n раскладывается на множители, то задача дискретного логарифма распадается на подзадачи по каждой подгруппе (алгоритм Полига — Хеллмана), и стойкость определяется наибольшим простым множителем, а не длиной ключа. Кривая с 256-битным порядком, у которого старший простой множитель всего 40 бит, ломается за 2^20 операций — быстрее, чем перебор пароля.

Кривые над конечным полем: числовой пример

Перечисление точек

Чтобы увидеть группу целиком, возьмём крошечную кривую y² = x³ + 2x + 2 над GF(17).

Наивный способ перечислить точки — перебрать все p² = 289 пар (x, y). Для p = 10007 это уже сто миллионов проверок, а для настоящих кривых — невозможно. Быстрее так: сначала построить словарь «квадрат → корни» за p шагов, затем для каждого x вычислить правую часть и посмотреть, есть ли она в словаре. Сложность падает с p² до p.

def enumerate_points(curve):
    p, a, b = curve
    squares = {}
    for y in range(p):
        squares.setdefault(y * y % p, []).append(y)
    points = []
    for x in range(p):
        rhs = (x * x * x + a * x + b) % p
        for y in squares.get(rhs, ()):
            points.append((x, y))
    points.sort()
    return points

Для нашей кривой находится 18 аффинных точек, то есть #E = 19 вместе с O. Проверим теорему Хассе: |19 − 18| = 1 ≤ 2√17 ≈ 8,25. Порядок группы 19 — простое число, значит, группа циклическая и любая точка, кроме O, порождает её целиком.

Таблица кратностей

Возьмём G = (5, 1) и выпишем все кратности. Такая таблица — самый быстрый способ увидеть, что перед нами действительно циклическая группа.

 k   kG        k   kG         k   kG
 1  (5, 1)    8  (13, 7)    15  (3, 16)
 2  (6, 3)    9  (7, 6)     16  (10, 11)
 3  (10, 6)  10  (7, 11)    17  (6, 14)
 4  (3, 1)   11  (13, 10)   18  (5, 16)
 5  (9, 16)  12  (0, 11)    19  O
 6  (16, 13) 13  (16, 4)    20  (5, 1)
 7  (0, 6)   14  (9, 1)

Читать её нужно так: 19·G = O, поэтому 20·G = G, и дальше всё повторяется с периодом 19. Обратите внимание на 18·G = (5, 16) = −G — как и должно быть, 18 ≡ −1 (mod 19). Пары точек с одинаковой абсциссой — это P и −P, они дают в сумме O.

Скалярное умножение

Считать k·P последовательным сложением — безнадёжно: для 256-битного k это 2^256 операций. Используют «удвоение и сложение» (double-and-add): заводим R = O и идём по битам k слева направо, на каждом шаге выполняя R = R + R, а если бит равен единице — ещё и R = R + P.

Стоимость — log₂ k удвоений и в среднем log₂ k / 2 сложений, то есть около 384 операций для 256-битного скаляра вместо 10^77. Это и есть причина, по которой эллиптические кривые вообще применимы на практике: скалярное умножение логарифмично по числу битов ключа.

У метода есть неприятное свойство: последовательность операций зависит от битов k. Если k — закрытый ключ, то по времени работы и по профилю энергопотребления можно восстановить его побитно (атаки Кохера и подобные). Поэтому в реальных библиотеках используют схемы с постоянным временем.

Лестница Монтгомери

Лестница Монтгомери выполняет на каждом бите одну и ту же пару операций — сложение и удвоение — независимо от значения бита. Меняются только входы, и то через условный обмен cswap, который реализуется маской и исключающим ИЛИ, без ветвления.

def cswap(swap, a, b):
    # в Python — ветвление; в реальной реализации маска и XOR:
    # mask = -swap; t = mask & (a ^ b); a ^= t; b ^= t
    return (b, a) if swap else (a, b)

# на каждом бите t скаляра k:
#   swap ^= bit;  (x2,x3) = cswap(swap, x2, x3);  (z2,z3) = cswap(swap, z2, z3)
#   swap = bit
#   A = x2+z2;  AA = A²;  B = x2−z2;  BB = B²;  E = AA−BB
#   C = x3+z3;  D = x3−z3;  DA = D·A;  CB = C·B
#   x3 = (DA+CB)²;  z3 = x1·(DA−CB)²;  x2 = AA·BB;  z2 = E·(AA+a24·E)
# в конце: cswap, затем x2 · z2^(p−2) mod p

Именно так устроен X25519: 255 итераций, на каждой — фиксированный набор умножений и сложений по модулю 2^255 − 19. Никаких ветвлений по секрету, никаких обращений к таблицам по индексу, зависящему от секрета. Цена — примерно вдвое больше операций, чем у оптимального double-and-add, и это осознанный обмен: константное время важнее скорости.

Два способа умножить точку на скаляр
Сравнение алгоритмов: double-and-add ветвится в зависимости от значения бита секрета; лестница Монтгомери исключает ветвление и защищает от атак по времени

Стоит честно сказать: на Python постоянного времени добиться нельзя в принципе — интерпретатор, сборщик мусора и длинная арифметика вносят свой вклад. Учебная реализация показывает структуру алгоритма, а не обеспечивает защиту. Для настоящей работы берут cryptography, libsodium или OpenSSL.

Задача ECDLP и выбор кривых

Почему дискретный логарифм сложнее

Задача дискретного логарифмирования на эллиптической кривой (ECDLP) формулируется так: даны P и Q = k·P, найти k. Для 256-битной кривой k лежит в интервале порядка 2^256.

Лучший универсальный алгоритм — ро-метод Полларда с параллелизацией, он требует около √n ≈ 2^128 операций. Это число астрономически велико: даже при 10^12 операций в секунду перебор займёт порядка 10^19 лет. Никакого субэкспоненциального алгоритма для общей эллиптической группы не известно, и это принципиально: индексный метод, который ломает GF(p)* за субэкспоненциальное время, опирается на понятие гладкости, а в группе точек кривой такого понятия нет.

Из этого правила есть важные исключения — и все они про неправильно выбранные кривые.

  • Аномальные кривые (#E = p) ломаются за линейное время алгоритмом Смарта через p-адические числа.
  • Кривые с малой степенью вложения допускают MOV-атаку: спаривание переносит задачу в GF(p^k), где работает индексный метод. Так ломаются суперсингулярные кривые.
  • Кривые с гладким порядком ломаются Полигом — Хеллманом по кусочкам.
  • Кривые с малым кофактором и без проверки точек ломаются атакой на недопустимую кривую, о которой ниже.

Отсюда практическое правило: кривую нельзя придумать самому. Её нужно брать из стандарта, где порядок посчитан, проверен на простоту и на все перечисленные исключения.

P-256 и вопрос о происхождении параметров

Стандартная кривая NIST P-256 (она же secp256r1, prime256v1) задаётся так 5:

p  = 2^256 − 2^224 + 2^192 + 2^96 − 1
a  = p − 3
b  = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
Gx = 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296
Gy = 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5
n  = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
h  = 1

Модуль p объясним: это ближайшее к 2^256 число вида 2^256 − 2^224 + 2^192 + 2^96 − 1, удобное для вычислений с точки зрения редукции. Сложнее с b и координатами G. Они не выбраны произвольно: по процедуре ANSI X9.62 они получены из затравочного значения хешированием (так называемая «проверяемо случайная» генерация, verifiably random), и проверить соответствие параметров заявленной процедуре можно, пересчитав хеш.

Проблема в другом: проверяемость здесь означает лишь воспроизводимость вычисления, а не обоснованность выбора. Затравочное значение и причина, по которой оно взято именно таким, не объяснены; кроме того, сама процедура допускает достаточно свободы, чтобы перебором затравочных значений получить кривую с нужными свойствами. Поэтому «параметры проверяемы» и «параметры заслуживают доверия» — разные утверждения, и второе из первого не следует.

Бернстайн и Ланге посвятили этому проект SafeCurves и подробный разбор 9, а в отдельной работе показали, как именно параметры кривых можно подбирать в интересах атакующего 14. Их аргумент не в том, что P-256 точно сломана, а в том, что её происхождение нельзя обосновать так же строго, как происхождение Curve25519, где каждый параметр выбран по правилу «наименьшее подходящее». История с Dual_EC_DRBG, где в константах действительно оказался скрытый механизм, доверия не добавила.

Отсюда два лагеря. Регуляторы (NIST, FIPS, большинство корпоративных политик) требуют P-256 и P-384. Криптографы-практики всё чаще берут Curve25519 и Ed25519, у которых параметры выбраны по понятному правилу и всё поведение описано в RFC.

Curve25519

Кривая предложена Дэниелом Бернстайном в 2006 году как «быстрая и простая» 7. Она задаётся в форме Монтгомери v² = u³ + 486662·u² + u над полем GF(2^255 − 19); базовая точка — u = 9, порядок большой подгруппы — L = 2^252 + 27742317777372353535851937790883648493, кофактор — h = 8.

Почему именно так: 2^255 − 19 — простое число, допускающее быструю редукцию; коэффициент 486662 выбран как наименьший, при котором кривая и её скрученная форма имеют простой порядок большой подгруппы; базовая точка u = 9 — наименьшая подходящая. Все параметры объяснимы одной фразой «наименьшее подходящее», и это и есть проверяемость.

Функция X25519 описана в RFC 7748 3. Она принимает 32-байтный скаляр и 32-байтную u-координату и возвращает 32 байта. Скаляр «зажимается» (clamping): три младших бита обнуляются (скаляр становится кратным 8), старший бит обнуляется, а бит 254 устанавливается — длина скаляра фиксируется на 255 битах.

Зажим решает сразу три задачи: убирает зависимость от малых подгрупп (скаляр кратен кофактору 8, поэтому точка порядка 2, 4 или 8 обнуляется), фиксирует длину и исключает вырожденные случаи. Кроме того, X25519 не различает точки на кривой и на её скрученной форме — это сделано намеренно, чтобы атака на недопустимую кривую не работала, а поведение не зависело от проверок.

from cryptography.hazmat.primitives.asymmetric.x25519 import (
    X25519PrivateKey, X25519PublicKey)
from cryptography.hazmat.primitives import serialization

raw = (serialization.Encoding.Raw, serialization.PublicFormat.Raw)
alice, bob = X25519PrivateKey.generate(), X25519PrivateKey.generate()
shared_a = alice.exchange(bob.public_key())
shared_b = bob.exchange(alice.public_key())
assert shared_a == shared_b

Тестовые векторы RFC 7748 стоит проверить руками: пара «скаляр, u → результат» из раздела 5.2 и обмен ключами из раздела 6.1. Студенческая реализация на Python проходит их все, но тратит около миллисекунды на вызов против микросекунд у библиотеки.

secp256k1 и ГОСТ

secp256k1 задаётся уравнением y² = x³ + 7 над другим полем: p = 2^256 − 2^32 − 977, тогда как у P-256 p = 2^256 − 2^224 + 2^192 + 2^96 − 1. Совпадает только длина координаты — 256 бит; сами поля, а значит и все вычисления, различны. Порядок группы у secp256k1 простой, кофактор равен 1. Известна тем, что её выбрал Биткойн. У неё есть инженерное преимущество: эффективный эндоморфизм позволяет ускорить скалярное умножение по методу GLV. С точки зрения стойкости она равноценна P-256; с точки зрения доверия к параметрам — та же проблема, что и у NIST-кривых, только параметры ещё проще подобрать «под себя».

В России действует ГОСТ Р 34.10-2012 13, который определяет и параметры кривых (256 и 512 бит), и совсем другую схему подписи: подпись — пара (r, s), где r получается из x-координаты точки k·G, а s — из уравнения с обратным по модулю q. Схема не является ECDSA и несовместима с ней; для применения в регулируемой сфере нужны сертифицированные реализации, а не самостоятельная имплементация.

Протоколы: ECDH и подписи

ECDH

Обмен ключей Диффи — Хеллмана на кривой — прямое переложение схемы из темы 11. Алиса и Боб договариваются о кривой и базовой точке G. Каждый выбирает секретный скаляр d и публикует Q = d·G. Общий секрет — x-координата точки S = d_A · Q_B = d_A · d_B · G = d_B · Q_A.

Дальше x-координата (32 байта у X25519) не используется как ключ напрямую: из неё выводят ключи через HKDF, как в теме 10. Причина та же, что и в обычном DH: сырой общий секрет не равномерен и не привязан к контексту.

Обмен ключами ECDH
Алиса и Боб публикуют d·G, общий секрет — d_A·d_B·G; из x-координаты выводятся ключи через HKDF

Обязательные проверки на принимаемой точке Q: она не O, лежит на кривой, и n·Q = O (то есть принадлежит подгруппе нужного порядка). Для X25519 эти проверки не нужны и вредны: зажим скаляра и отсутствие различия между кривой и скрученной формой делают атаку бессмысленной, а RFC 7748 прямо требует не отвергать «неканоничные» входы.

Требования к схемам установления ключей собраны в NIST SP 800-56A 12: там же описаны режимы с одноразовыми и долговременными ключами, подтверждение ключа и вывод ключевого материала. Для учебных целей достаточно запомнить: ECDH даёт общий секрет, но не аутентификацию; без подписи или сертификата он уязвим к атаке «человек посередине», разобранной в теме 11.

ECDSA

ECDSA — схема подписи, стандартизованная в FIPS 186 5. Параметры: кривая, базовая точка G порядка n, хеш-функция H.

Подпись сообщения M закрытым ключом d вычисляется так: z = H(M) (берутся левые min(bitlen(n), bitlen(H)) бит), выбирается одноразовое k ∈ [1, n−1], считается R = k·G, r = R.x mod n, затем s = k⁻¹·(z + r·d) mod n. Если r или s оказались нулём, выбирается новое k. Подпись — пара (r, s).

Проверка открытым ключом Q = d·G: w = s⁻¹ mod n, u₁ = z·w mod n, u₂ = r·w mod n, X = u₁·G + u₂·Q; подпись верна, если X ≠ O и X.x mod n = r.

Корректность проверяется подстановкой: u₁·G + u₂·Q = (z·w + r·w·d)·G = w·(z + r·d)·G = k·G, потому что w = s⁻¹ = k·(z + r·d)⁻¹. Значит, X = R, и абсциссы совпадают.

Две технические детали, которые часто упускают. Первая: (r, s) кодируется в DER, и парсеры DER — отдельная поверхность атаки; именно так появились уязвимости в проверке подписей у нескольких библиотек. Вторая: подпись (r, s) и (r, n − s) обе корректны, поэтому ECDSA «ковка» (malleable) — в Биткойне это лечат требованием s ≤ n/2 (low-s).

EdDSA решает обе проблемы: подпись фиксированной длины, ровно один допустимый вид.

EdDSA и детерминированный nonce

Ed25519 определена в RFC 8032 4 и построена на скрученной кривой Эдвардса −x² + y² = 1 + d·x²·y² с d = −121665/121666 над GF(2^255 − 19). У неё есть полная формула сложения (работает для всех пар точек, без исключений) и эффективная операция удвоения. Ключи: 32-байтный секретный, 32-байтный открытый, подпись — 64 байта.

Главное отличие от ECDSA — детерминированное вычисление одноразового числа. Секретный ключ расширяется хешем: h = SHA-512(seed), из первых 32 байт получается скаляр a, из вторых — префикс. Затем r = SHA-512(prefix ‖ M) mod L, R = r·B, S = (r + SHA-512(R ‖ A ‖ M)·a) mod L, подпись — (R, S).

Одноразовое число r — функция сообщения и секретного префикса. Оно никогда не повторяется для разных сообщений (если не повторяется сообщение) и не зависит от генератора случайных чисел. Именно поэтому утечка через повтор nonce, погубившая PS3 и Android-кошельки, для Ed25519 невозможна by design.

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

key = ec.generate_private_key(ec.SECP256R1())
sig = key.sign(b"document", ec.ECDSA(hashes.SHA256()))
key.public_key().verify(sig, b"document", ec.ECDSA(hashes.SHA256()))

sk = bytes.fromhex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
ed = Ed25519PrivateKey.from_private_bytes(sk)
assert ed.sign(b"") == ed.sign(b"")                        # детерминированно
assert ed.sign(b"").hex().startswith("e5564300c360ac72")   # вектор RFC 8032, TEST 1

Детерминированность — не только защита, но и удобство: подпись воспроизводима, тесты стабильны, а аудит проще. Цена — отсутствие рандомизации: если злоумышленник научится предсказывать хеш, он получит r. Поэтому seed Ed25519 должен быть по-настоящему случайным.

Для ECDSA аналог есть — RFC 6979 11, детерминированный k через HMAC-DRBG от закрытого ключа и сообщения. Это не делает ECDSA детерминированной схемой в смысле EdDSA (проверка всё равно принимает любую корректную пару), но убирает зависимость от качества RNG. Многие библиотеки, включая OpenSSL и cryptography, используют RFC 6979 по умолчанию.

Ошибки реализации

Повтор одноразового числа

Пусть один и тот же k использован для двух сообщений M_1 и M_2 с хешами z_1, z_2. Тогда r у подписей совпадает, а s_1 = k^{-1}(z_1 + r \cdot d) и s_2 = k^{-1}(z_2 + r \cdot d). Вычтем второе из первого и получим k, а из него — закрытый ключ d:

s_1 - s_2 = k^{-1} (z_1 - z_2) \implies k = (z_1 - z_2)(s_1 - s_2)^{-1} \pmod n

d = (s_1 k - z_1) r^{-1} \pmod n

Всё. Двух подписей достаточно, чтобы полностью восстановить ключ и подписывать что угодно от имени жертвы. Никакого перебора, никакой криптоаналитической сложности — только модульная арифметика.

Повтор nonce в ECDSA
Одинаковый k даёт одинаковый r; разность s выдаёт k, из k — закрытый ключ d

Проверим на учебной кривой из этой темы. Параметры: n = 19, G = (5, 1), ключ d = 7, Q = 7G = (0, 6). Подпишем два «сообщения» с хешами z₁ = 4 и z₂ = 9 одним k = 3. Тогда kG = 3G = (10, 6), значит r = 10; s₁ = 3⁻¹·(4 + 10·7) = 13·74 ≡ 12 (mod 19) и s₂ = 3⁻¹·(9 + 10·7) = 13·79 ≡ 1 (mod 19). Восстановление: k = (4 − 9)·(12 − 1)⁻¹ = (−5)·7 ≡ 3, d = (12·3 − 4)·10⁻¹ = 32·2 ≡ 7. Исходный ключ d = 7 получен. На настоящей 256-битной кривой формулы те же, только числа длиннее.

История вопроса поучительна. В 2010 году группа fail0verflow обнаружила, что Sony использовала в подписи прошивки PlayStation 3 постоянное k. Консоль проверяла подпись корректно, кривая была настоящая, а закрытый ключ Sony оказался восстановлен и опубликован. В 2013 году похожая история произошла с Android-кошельками для Биткойна: дефект SecureRandom приводил к повторам k, и ключи уводили с кошельков. В обоих случаях алгоритм был исправен, а генератор — нет.

Хуже того, k не обязан повторяться целиком. Если он лишь немного смещён — скажем, старшие биты фиксированы, — задача восстановления ключа превращается в задачу о скрытом числе и решается решёткой (LLL). Работа Брайтнера и Хенингера показала, что такое смещение реально встречается в блокчейн-подписях и позволяет восстанавливать ключи массово 10. Отсюда вывод: k должен быть либо полноценно случайным, либо детерминированным по RFC 6979. «Почти случайный» — негоден.

Атака на недопустимую кривую

Вернёмся к детали из первого раздела: формулы сложения не содержат b. Значит, если сервер получает точку и просто умножает её на секретный скаляр d, он посчитает d·Q корректно по формулам — но эта точка может лежать не на той кривой, о которой договаривались.

Атака, описанная Билем, Мейером и Мюллером 8, выглядит так:

  1. Атакующий выбирает b' ≠ b и берёт кривую y² = x³ + a·x + b' с тем же a.
  2. Считает число точек N' этой кривой и находит у него малый простой делитель q.
  3. Находит точку Q порядка q на «чужой» кривой.
  4. Отправляет Q серверу и получает R = d·Q.
  5. Перебирает t ∈ [0, q) и находит такое, что t·Q = R. Это d mod q.
  6. Повторяет для нескольких q, пока произведение не превысит порядок группы, и собирает d по китайской теореме об остатках.

Каждый запрос даёт остаток по модулю q, а перебор внутри q дешёвый — q специально выбирается маленьким. Число запросов логарифмически зависит от размера d.

Атака на недопустимую кривую
Точка с чужой кривой (то же a, другое b) имеет малый порядок q; ответ оракула даёт d mod q, остатки собираются по КТО

На учебной кривой лабораторной работы (p = 10007, a = 3, b = 6, N = 10039) хватает пяти запросов: остатки по модулям 3, 5, 7, 41, 71 дают произведение 305 655 > 10 039, и d восстанавливается однозначно. Защита — одна строка: проверить, что Q лежит на кривой, и отвергнуть иначе.

Именно поэтому в X25519 проверка «на кривой» не делается: там скаляр кратен кофактору, а скрученная кривая имеет такой же большой простой порядок, так что точка с неё ничего не выдаёт. Это редкий случай, когда отсутствие проверки — осознанное решение, а не ошибка.

Малые подгруппы, кофактор и скрученные кривые

Даже на правильной кривой порядок группы может иметь малые множители. Если #E = n·h и h > 1, то существует точка T порядка h (или делителя h). Отправив её серверу, атакующий получает d·T, из чего узнаёт d mod h. При h = 8 это три бита; одного раза мало, но в протоколе с повторными обменами остатки накапливаются.

Лечится умножением результата на кофактор: h·(d·Q) обнуляет любую компоненту малого порядка. В X25519 это уже сделано зажимом скаляра, в протоколах на произвольных кривых кофактор нужно учитывать явно.

Отдельный сюжет — скрученные кривые (twists). У каждой кривой есть «квадратичный твист» — кривая с тем же a, но другим b, и точки твиста часто принимаются за точки исходной кривой при небрежной валидации. Если порядок твиста гладкий, атака становится ещё проще. Поэтому при выборе кривой смотрят не только на #E, но и на порядок твиста — свойство, которое SafeCurves называет twist security 9.

Валидация точек и сжатие

Точку можно передавать в двух видах. Несжатый — 65 байт для P-256: префикс 04 и две 32-байтные координаты. Сжатый — 33 байта: префикс 02 или 03 (чётность y) и только x. Восстановление y требует извлечения квадратного корня по модулю p; для p ≡ 3 (mod 4) работает y = (x³ + ax + b)^((p+1)/4) mod p, и остаётся выбрать нужный знак.

Сжатие экономит место, но добавляет поверхность атаки: нужно проверить, что x < p, что подкоренное выражение — квадратичный вычет, что префикс допустим. Неканоничные кодировки (например, x + p вместо x) в некоторых реализациях проходили проверку и приводили к приёму разных байтов как одной точки — а значит, к расхождению в вычислении хеша транскрипта.

Короткий чек-лист валидации для ECDH на произвольной кривой:

  • точка не O;
  • координаты в диапазоне [0, p−1];
  • уравнение кривой выполняется;
  • n·Q = O (принадлежность подгруппе порядка n);
  • при h > 1 — умножение результата на h.

Для проверки подписи ECDSA добавляется: r, s ∈ [1, n−1], открытый ключ прошёл те же проверки, X ≠ O. Пропуск любого из этих пунктов — не теоретическая мелочь, а реальная уязвимость; библиотеки прошлых лет ломались именно на этом.

Сравнение с RSA

Длины ключей и подписей

Главный практический выигрыш эллиптических кривых — размер. Сравним при одинаковом уровне стойкости 128 бит (NIST SP 800-57 6):

ПараметрRSA-3072ECDSA P-256Ed25519
Открытый ключ (DER, SubjectPublicKeyInfo)422 байта91 байт44 байта
Подпись384 байта70–72 байта (DER)64 байта
Закрытый ключ (PEM, PKCS#8)~2,5 КБ~0,24 КБ~0,12 КБ
Уровень стойкости128 бит128 бит~128 бит

Разница в десять раз по открытому ключу и в шесть — по подписи. Для сертификатов, для TLS-рукопожатий, для встроенных устройств с парой килобайт памяти это решающе. Для блокчейна, где подпись хранится в каждой транзакции навсегда, — тем более.

Обратная сторона: 256-битная кривая даёт 128 бит стойкости, то есть вдвое меньше, чем длина ключа. У RSA «запас» другой: 3072-битный модуль даёт те же 128 бит, но факторизация не имеет известного субэкспоненциального ускорения, поэтому рост длины ключа даёт больше. Парадокс: у ECC хуже «запас прочности» на бит ключа, но лучше — на байт хранилища.

Скорость

Замеры на одном ядре (openssl speed -seconds 2, OpenSSL 3.6.4, x86-64):

ОперацияRSA-3072ECDSA P-256Ed25519
Подпись1,28 мс (780/с)16 мкс (62 700/с)25 мкс (40 100/с)
Проверка27 мкс (37 300/с)51 мкс (19 650/с)70 мкс (14 350/с)

Подпись на кривой быстрее RSA примерно в 80 раз. Проверка, наоборот, у RSA в два раза быстрее, чем у ECDSA: у RSA проверка — это возведение в степень с малым показателем e = 65537, а у ECDSA — два скалярных умножения. Асимметрия важна при выборе: там, где подписывают часто, а проверяют редко (сервер, выдающий токены), кривые выигрывают с огромным отрывом; там, где проверяют много и часто (валидация сертификатов в браузере), выигрыш меньше, а иногда и отрицательный.

Ed25519 подписывает медленнее ECDSA (25 против 16 мкс), потому что вычисляет два хеша SHA-512, зато проверяет предсказуемо и не требует обращения к RNG.

Что выбирать

Короткие правила, которые стоит запомнить.

  • Новые протоколы, свой код — X25519 для обмена ключами и Ed25519 для подписи. Просто, быстро, детерминированно, параметры проверяемы.
  • Совместимость и регуляторика — P-256. Она есть везде, её требуют FIPS и большинство политик.
  • Биткойн и совместимость с ним — secp256k1, но только потому, что так исторически сложилось.
  • Регулируемая сфера в России — ГОСТ Р 34.10-2012 и сертифицированная реализация, не самостоятельная.
  • Долгосрочная защита от квантового компьютера — ни то, ни другое: и RSA, и ECC ломаются алгоритмом Шора. Нужны постквантовые схемы (тема 16), а на переходный период — гибридные схемы, где ECDH комбинируется с ML-KEM.

И последнее. Какую бы кривую ни выбрали, нельзя писать её реализацию самому для рабочего кода. Арифметика проста ровно настолько, чтобы казаться безопасной, и ровно настолько коварна, чтобы утечь по времени, по кэшу, по ошибкам в обработке краевых случаев. Учебная реализация в этой теме нужна, чтобы понять устройство; рабочая — чтобы не изобретать её заново.

Итоги темы

Эллиптическая кривая — это множество точек, на котором определено сложение по формулам хорды и касательной. Вместе с точкой на бесконечности оно образует абелеву группу: коммутативную, ассоциативную, с нейтральным элементом O и противоположной точкой −P = (x, −y). Коэффициент b задаёт, какие точки лежат на кривой, но не участвует в сложении — и это наблюдение открывает дорогу атаке на недопустимую кривую.

Число точек группы #E лежит в узком интервале вокруг p + 1 по теореме Хассе, раскладывается как n·h и для криптографических кривых имеет простой большой множитель n и малый кофактор. Скалярное умножение выполняется за log₂ k удвоений и сложений, а лестница Монтгомери делает время работы независимым от битов секрета.

Стойкость опирается на сложность ECDLP: лучший универсальный алгоритм — ро-метод Полларда с √n операций, что даёт 128 бит для 256-битной кривой. Исключения — аномальные кривые, малая степень вложения и гладкий порядок — закрываются выбором стандартной кривой. P-256 удобна и распространена, но её параметры необъяснимы; Curve25519 и Ed25519 выбраны по проверяемым правилам и описаны в RFC.

Протоколы: ECDH даёт общий секрет из d_A·d_B·G, ECDSA — подпись (r, s) с одноразовым k, EdDSA — детерминированную подпись, где k выводится из ключа и сообщения. Именно детерминированность делает Ed25519 невосприимчивой к повтору nonce, который погубил PS3 и Android-кошельки: k = (z₁ − z₂)/(s₁ − s₂) и d = (s₁k − z₁)/r восстанавливают ключ из двух подписей за секунды.

Реализации ломаются чаще, чем математика: повтор или смещение nonce, отсутствие проверки точки, игнорирование кофактора, приём неканоничных кодировок, ветвления по секрету. Чек-лист валидации короткий, и его пропуск — не стилистическая погрешность, а уязвимость.

По сравнению с RSA кривые дают ключи и подписи в разы короче и подпись в десятки раз быстрее, но проверку — медленнее и с меньшим запасом стойкости на бит ключа. Следующая тема собирает всё это в инфраструктуру: сертификаты, удостоверяющие центры, цепочки доверия и отзыв — то, без чего открытый ключ остаётся просто числом.

Литература

  1. Koblitz N. Elliptic Curve Cryptosystems. — Mathematics of Computation, 1987, Т. 48, № 177, С. 203–209. DOI: 10.1090/S0025-5718-1987-0866109-5
  2. Miller V. S. Use of Elliptic Curves in Cryptography. — Advances in Cryptology — CRYPTO '85, Springer, 1986, С. 417–426. DOI: 10.1007/3-540-39799-X_31
  3. Langley A., Hamburg M., Turner S. Elliptic Curves for Security. — RFC 7748, IETF, 2016. https://www.rfc-editor.org/rfc/rfc7748
  4. Josefsson S., Liusvaara I. Edwards-Curve Digital Signature Algorithm (EdDSA). — RFC 8032, IETF, 2017. https://www.rfc-editor.org/rfc/rfc8032
  5. NIST. Digital Signature Standard (DSS). — FIPS 186-5, 2023. DOI: 10.6028/NIST.FIPS.186-5
  6. NIST. Recommendation for Key Management, Part 1: General. — SP 800-57 Part 1 Rev. 5, 2020. DOI: 10.6028/NIST.SP.800-57pt1r5
  7. Bernstein D. J. Curve25519: New Diffie-Hellman Speed Records. — Public Key Cryptography — PKC 2006, Springer, 2006, С. 207–228. DOI: 10.1007/11745853_14
  8. Biehl I., Meyer B., Müller V. Differential Fault Attacks on Elliptic Curve Cryptosystems. — Advances in Cryptology — CRYPTO 2000, Springer, 2000, С. 131–146. DOI: 10.1007/3-540-44598-6_8
  9. Bernstein D. J., Lange T. SafeCurves: Choosing Safe Curves for Elliptic-Curve Cryptography. — 2014. https://safecurves.cr.yp.to
  10. Breitner J., Heninger N. Biased Nonce Sense: Lattice Attacks against Weak ECDSA Signatures in Cryptocurrencies. — Financial Cryptography and Data Security — FC 2019, Springer, 2019, С. 3–20. DOI: 10.1007/978-3-030-32101-7_1
  11. Pornin T. Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA). — RFC 6979, IETF, 2013. https://www.rfc-editor.org/rfc/rfc6979
  12. Barker E., Chen L., Roginsky A., Vassilev A., Davis R. Recommendation for Pair-Wise Key-Establishment Schemes Using Discrete Logarithm Cryptography. — NIST SP 800-56A Rev. 3, 2018. DOI: 10.6028/NIST.SP.800-56Ar3
  13. ГОСТ Р 34.10-2012. Информационная технология. Криптографическая защита информации. Процессы формирования и проверки электронной цифровой подписи. — М.: Стандартинформ, 2013.
  14. Bernstein D. J., Chou T., Chuengsatiansup C., Heninger N., Lange T., van Lunteren J., Niederhagen R., Raman C., Ronen E., Sattler P. How to Manipulate Curve Standards: A White Paper for the Black Hat. — Security Standardisation Research — SSR 2014, Springer, 2014, С. 109–139. DOI: 10.1007/978-3-319-14054-4_7