Эллиптические кривые
В 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 наклон секущей равен λ = (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, и это осознанный обмен: константное время важнее скорости.
Стоит честно сказать: на 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: сырой общий секрет не равномерен и не привязан к контексту.
Обязательные проверки на принимаемой точке 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
Всё. Двух подписей достаточно, чтобы полностью восстановить ключ и подписывать что угодно от имени жертвы. Никакого перебора, никакой криптоаналитической сложности — только модульная арифметика.
Проверим на учебной кривой из этой темы. Параметры: 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, выглядит так:
- Атакующий выбирает
b' ≠ bи берёт кривуюy² = x³ + a·x + b'с тем жеa. - Считает число точек
N'этой кривой и находит у него малый простой делительq. - Находит точку
Qпорядкаqна «чужой» кривой. - Отправляет
Qсерверу и получаетR = d·Q. - Перебирает
t ∈ [0, q)и находит такое, чтоt·Q = R. Этоd mod q. - Повторяет для нескольких
q, пока произведение не превысит порядок группы, и собираетdпо китайской теореме об остатках.
Каждый запрос даёт остаток по модулю q, а перебор внутри q дешёвый — q специально выбирается маленьким. Число запросов логарифмически зависит от размера d.
На учебной кривой лабораторной работы (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-3072 | ECDSA P-256 | Ed25519 |
|---|---|---|---|
| Открытый ключ (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-3072 | ECDSA P-256 | Ed25519 |
|---|---|---|---|
| Подпись | 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 кривые дают ключи и подписи в разы короче и подпись в десятки раз быстрее, но проверку — медленнее и с меньшим запасом стойкости на бит ключа. Следующая тема собирает всё это в инфраструктуру: сертификаты, удостоверяющие центры, цепочки доверия и отзыв — то, без чего открытый ключ остаётся просто числом.
Литература
- Koblitz N. Elliptic Curve Cryptosystems. — Mathematics of Computation, 1987, Т. 48, № 177, С. 203–209. DOI: 10.1090/S0025-5718-1987-0866109-5
- 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
- Langley A., Hamburg M., Turner S. Elliptic Curves for Security. — RFC 7748, IETF, 2016. https://www.rfc-editor.org/rfc/rfc7748
- Josefsson S., Liusvaara I. Edwards-Curve Digital Signature Algorithm (EdDSA). — RFC 8032, IETF, 2017. https://www.rfc-editor.org/rfc/rfc8032
- NIST. Digital Signature Standard (DSS). — FIPS 186-5, 2023. DOI: 10.6028/NIST.FIPS.186-5
- NIST. Recommendation for Key Management, Part 1: General. — SP 800-57 Part 1 Rev. 5, 2020. DOI: 10.6028/NIST.SP.800-57pt1r5
- Bernstein D. J. Curve25519: New Diffie-Hellman Speed Records. — Public Key Cryptography — PKC 2006, Springer, 2006, С. 207–228. DOI: 10.1007/11745853_14
- 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
- Bernstein D. J., Lange T. SafeCurves: Choosing Safe Curves for Elliptic-Curve Cryptography. — 2014. https://safecurves.cr.yp.to
- 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
- 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
- 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
- ГОСТ Р 34.10-2012. Информационная технология. Криптографическая защита информации. Процессы формирования и проверки электронной цифровой подписи. — М.: Стандартинформ, 2013.
- 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