Коротко. secp256k1 — эллиптическая кривая y² = x³ + 7 над простым полем порядка p = 2²⁵⁶ − 2³² − 977, определённая в SEC 2 в 2000 году. Её группа содержит простое число точек (кофактор 1) и обеспечивает стойкость около 128 бит. Коэффициенты подобраны ради скорости, а не получены из seed: a = 0 упрощает формулы, а у кривой есть эффективно вычислимый эндоморфизм, который ускоряет умножение точки на скаляр. Bitcoin использует её с первого коммита в 2009 году, затем к нему присоединился Ethereum, а сегодня на ней работают Nostr и большинство EVM-сетей. Её нет ни в WebCrypto, ни в TLS 1.3, ни в большей части защищённого оборудования; вне блокчейн-протоколов выбирайте P-256 или Ed25519. Поверх обычного ECDSA её экосистемы вводят собственные правила: только подписи в форме low-S в политике ретрансляции Bitcoin и в транзакциях Ethereum, восстановление открытого ключа вместо передачи ключей, а в качестве хеша — Keccak-256 или двойной SHA-256.
Параметры
В SEC 2 v2.0 (§ 2.4.1) кривая задана шестью числами (SEC 2):
| Параметр | Значение | Смысл |
|---|---|---|
| p | 2²⁵⁶ − 2³² − 977 (0xffff…fffefffffc2f) | Простой модуль поля; благодаря его виду приведение по модулю p обходится дёшево |
| a, b | 0, 7 | Кривая y² = x³ + 7 |
| G | x = 0x79be667e…16f81798, y = 0x483ada77…fb10d4b8 | Базовая точка |
| n | 0xffff…fffebaaedce6af48a03bbfd25e8cd0364141 | Порядок G, простое число |
| h | 1 | Кофактор: каждая точка, кроме бесконечно удалённой, порождает всю группу |
Здесь ничего не нужно принимать на веру, и ничто не получено из seed неизвестного происхождения. Скрипт ниже проверяет константы на обычных целых числах Python:
G = (Gx, Gy)
print("p == 2^256 - 2^32 - 977:", hex(p) == "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f")
print("G is on y^2 = x^3 + 7: ", (Gy * Gy - Gx**3 - b) % p == 0)
print("n is a probable prime: ", probable_prime(n))
print("n * G is infinity: ", mul(n, G) is None)
t = p + 1 - n
print("trace t = p + 1 - n: ", t)
print("Hasse bound |t| <= 2*sqrt(p):", t * t <= 4 * p)
print("n < p, p - n has", (p - n).bit_length(), "bits: the group has n points, cofactor 1")
p == 2^256 - 2^32 - 977: True
G is on y^2 = x^3 + 7: True
n is a probable prime: True
n * G is infinity: True
trace t = p + 1 - n: 432420386565659656852420866390673177327
Hasse bound |t| <= 2*sqrt(p): True
n < p, p - n has 129 bits: the group has n points, cofactor 1
add и mul — учебные реализации сложения точек в аффинных координатах и умножения методом удвоения и сложения (double-and-add): для проверки констант они годятся, а для работы с секретами нет, потому что не выполняются за постоянное время. След t совпадает со значением, опубликованным на сайте SafeCurves. Поскольку n простое и совпадает с числом точек, малых подгрупп нет: проблем с кофактором, которые усложняют протоколы на Curve25519, здесь не возникает.
Почему эти числа делают кривую быстрой
Три свойства этих чисел выбраны намеренно:
- Простой модуль равен 2²⁵⁶ минус небольшое число, поэтому приведение 512-битного произведения по модулю p сводится к нескольким умножениям на 2³² + 977 вместо деления общего вида.
- a = 0 убирает одно умножение из каждого удвоения точки.
- Эндоморфизм. Поскольку p ≡ 1 (mod 3), существует кубический корень из единицы β по модулю p, и отображение (x, y) → (βx, y) переводит каждую точку в другую точку той же кривой. Это отображение равносильно умножению на кубический корень из единицы λ по модулю n. Тогда скаляр k можно разбить на две половины длиной около 128 бит, k = k₁ + k₂λ, и вычислить kP как k₁P + k₂(λP), где λP обходится в одно умножение в поле. Это метод GLV (Gallant, Lambert, Vanstone, 2001).
Тот же скрипт находит β и λ и проверяет это тождество:
print("p % 3 == 1 (cube roots of unity exist mod p):", p % 3 == 1)
beta = pow(2, (p - 1) // 3, p)
lam = pow(3, (n - 1) // 3, n)
# Pick the lambda that matches this beta (there are two non-trivial cube roots of unity mod n).
if mul(lam, G) != (beta * Gx % p, Gy):
lam = lam * lam % n
print("beta^3 == 1 mod p, lambda^3 == 1 mod n:", pow(beta, 3, p) == 1, pow(lam, 3, n) == 1)
print("lambda * G == (beta * Gx, Gy): ", mul(lam, G) == (beta * Gx % p, Gy))
print("beta =", hex(beta))
print("lambda =", hex(lam))
p % 3 == 1 (cube roots of unity exist mod p): True
beta^3 == 1 mod p, lambda^3 == 1 mod n: True True
lambda * G == (beta * Gx, Gy): True
beta = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee
lambda = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72
Именно эти константы использует libsecp256k1. Хэл Финни описал этот приём применительно к Bitcoin в ветке форума bitcointalk от февраля 2011 года и рассчитывал ускорить проверку подписей на 20–25 %; его код стал отправной точкой для libsecp256k1, которую написал Pieter Wuille. Bitcoin Core включил оптимизацию GLV по умолчанию вместе с обновлением libsecp256k1 в PR #20147 (Bitcoin Core 0.21, 2021), и там же замерили, что начальная синхронизация с включённой проверкой подписей ускорилась примерно на 16 %. Насколько всё это важно, зависит от библиотеки: OpenSSL не содержит специального кода для secp256k1 и на P-256 работает намного быстрее (цифры приведены в сравнении P-256 или secp256k1).
Почему её используют Bitcoin и Ethereum
Первый коммит Bitcoin (август 2009 года) создаёт ключи вызовом EC_KEY_new_by_curve_name(NID_secp256k1) из OpenSSL. Сатоси Накамото так и не объяснил этот выбор. Обычно называют причины, которые относятся к свойствам кривой, а не к словам того, кто её выбрал: в Bitcoin wiki отмечается, что она «построена особым неслучайным способом, который позволяет выполнять вычисления особенно эффективно» и что «в отличие от популярных кривых NIST, константы secp256k1 выбраны предсказуемым образом» (Bitcoin wiki). Ethereum сохранил кривую Bitcoin, но заменил хеш-функцию: транзакции и сообщения хешируются с помощью Keccak-256, и с её же помощью из ключа выводится адрес.
Органы стандартизации подтянулись позже. RFC 8812 (2020) зарегистрировал ES256K для JOSE и COSE, а в NIST SP 800-186 (2023), § H.2, сказано, что кривую «разрешено использовать в приложениях, связанных с блокчейном» (в число кривых, рекомендованных для общего применения, она не входит).
Насколько она стойкая
Лучшая из известных атак на задачу дискретного логарифмирования в этой группе — ро-метод Полларда: по данным SafeCurves, ему нужно около 0,886·√n ≈ 2¹²⁷·⁸ сложений. Это та же оценка, что и для P-256, и та самая привычная «стойкость 128 бит». (Эндоморфизм порядка 3 позволяет на этой кривой ещё немного ускорить ро-метод, примерно в √3 раза, но картину это не меняет.) SafeCurves не признаёт secp256k1 «безопасной», но по причинам, которые касаются реализаций, а не дискретного логарифма:
| Критерий SafeCurves | secp256k1 | Что это значит для вас |
|---|---|---|
| ро-метод, перенос, жёсткость (rho, transfer, rigidity) | пройден | Дискретный логарифм вычислить трудно, а происхождение констант объяснено |
| дискриминант комплексного умножения (CM discriminant) | не пройден (D = −3) | Именно малый дискриминант даёт эндоморфизм; никакой атаки из этого не следует |
| стойкость кручения (twist security) | пройден, 2¹⁰⁹·⁵ | Ниже, чем у самой кривой; важно только для кода, который пропускает проверку точек |
| лестница, полные формулы (ladder, complete formulas) | не пройден | Код с постоянным временем выполнения и без исключительных случаев приходится писать аккуратнее, чем для Curve25519 |
| неотличимость (indistinguishability) | не пройден | Точки не выглядят как случайные строки; это важно для протоколов, устойчивых к цензуре, а не для подписей |
Практический вывод тот же, что и для P-256: используйте библиотеку, написанную для этой кривой (libsecp256k1, @noble/curves, k256 для Rust, привязку из go-ethereum), проверяйте открытые ключи на входе и никогда не пишите собственное умножение на скаляр для секретных значений. Если ключ приходит в виде точки, перед использованием обязательно проверьте, что она лежит на кривой y² = x³ + 7; без этой проверки возможна классическая атака с недопустимой кривой, и именно здесь играет роль стойкость кручения. И, как на любой кривой ECDSA, повторный или смещённый nonce раскрывает ключ (повторное использование nonce в ECDSA).
Ключи и адреса
Закрытый ключ — число d из диапазона [1, n − 1], открытый ключ — точка Q = dG. Одну и ту же Q записывают несколькими способами, и путаница между ними — частая причина ошибок вида «не тот адрес». Пример для d = 1, у которого открытый ключ — сама точка G (этот ключ известен всем, поэтому никогда его не используйте):
compressed (33 bytes): 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
uncompressed (65 bytes): 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
x-only, BIP-340 (32 bytes): 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Ethereum address, keccak256(x || y)[12:]: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
Bitcoin P2PKH from the compressed key: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
Bitcoin P2PKH from the uncompressed key: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
- Сжатая форма (SEC 1, § 2.3.3):
02или03для чётной или нечётной координаты y, затем x. Bitcoin по умолчанию использует сжатые ключи уже больше десятилетия. Для распаковки нужен квадратный корень по модулю p, а для этого p (p ≡ 3 mod 4) он вычисляется одним возведением в степень: y = (x³ + 7)^((p+1)/4). - Несжатая форма:
04 ‖ x ‖ y. В этой форме тот же ключ даёт другой адрес Bitcoin, поэтому старые кошельки, которые использовали несжатые ключи, показывают для того же секрета другие адреса. - x-only (32 байта): Taproot и Nostr используют подписи Schnorr по BIP-340, ключи которых опускают координату y и подразумевают, что она чётная.
- Адрес Ethereum: последние 20 байт хеша Keccak-256 от 64-байтного
x ‖ y, без префикса04. Keccak-256 — не SHA3-256: у них разное дополнение, иhashlib.sha3_256даёт неверные адреса.
Low-S: одна подпись, две действительные формы
У подписи ECDSA (r, s) есть двойник (r, n − s), который проходит проверку точно так же, потому что при замене точки nonce на противоположную значение r не меняется. Для P-256 в TLS это никого не волнует. Для Bitcoin это было важно: двойник давал той же транзакции другой идентификатор, и сервисы, которые отслеживали платежи по идентификатору, можно было обмануть. Bitcoin Core 0.11.1 (2015) обязал узел «требовать каноническую кодировку „low-s“ для подписей ECDSA при ретрансляции или майнинге» (примечания к выпуску), и это политика ретрансляции, а не правило консенсуса. Ethereum пошёл дальше в EIP-2 (Homestead, 2016): «Все подписи транзакций, у которых значение s больше secp256k1n/2, теперь считаются недействительными», при этом «прекомпилированный контракт восстановления ECDSA остаётся без изменений и по-прежнему будет принимать большие значения s».
Универсальные библиотеки этих правил не соблюдают, а блокчейн-библиотеки соблюдают. Мы подписали 1 000 случайных сообщений с помощью OpenSSL из Node.js, а затем проверили одну подпись с high-S и её двойника с low-S в трёх библиотеках:
const { msg, sig, s } = sample;
const low = Buffer.concat([sig.subarray(0, 32), Buffer.from((n - s).toString(16).padStart(64, "0"), "hex")]);
const opensslOk = (x) => verify("sha256", msg, { key: publicKey, dsaEncoding: "ieee-p1363" }, x);
console.log(`node:crypto verify high-S: ${opensslOk(sig)} low-S twin: ${opensslOk(low)}`);
console.log(`noble default high-S: ${secp256k1.verify(sig, msg, pub)} low-S twin: ${secp256k1.verify(low, msg, pub)}`);
console.log(`noble lowS: false high-S: ${secp256k1.verify(sig, msg, pub, { lowS: false })}`);
OpenSSL 3.5.8: 494 of 1000 signatures have s > n/2
node:crypto verify high-S: true low-S twin: true
noble default high-S: false low-S twin: true
noble lowS: false high-S: true
ethers 6.17.0 v=27 high-S: non-canonical s; use ._s
Примерно половина подписей OpenSSL имеет форму high-S; OpenSSL принимает обе формы, @noble/curves на secp256k1 отклоняет high-S, если явно не настроить её иначе, а ethers отказывается даже создавать из такой подписи объект подписи. Система, которая подписывает универсальной библиотекой или в HSM, а подписи отправляет в блокчейн, обязана их нормализовать: если s > n/2, замените s на n − s и инвертируйте бит восстановления. Google Cloud KMS указывает в документации, что ключи secp256k1 в нём выдают только подписи в форме low-S; документация AWS KMS по ECC_SECG_P256K1 такого не обещает, поэтому нормализуйте его подписи сами. Кодировки DER и raw, в которых передаются эти подписи, разобраны в статье Форматы подписей ECDSA.
Восстановление открытого ключа
По подписи, хешу сообщения и одному или двум дополнительным битам можно вычислить открытый ключ подписанта, поэтому транзакции Ethereum вообще не содержат открытого ключа, а подписанные сообщения Bitcoin проверяют по адресу. Эти дополнительные биты — идентификатор восстановления (v, yParity или байт заголовка в подписи сообщения Bitcoin). Как работает восстановление, что при нём может пойти не так и как ведёт себя ecrecover, рассказано в статье Восстановление открытого ключа ECDSA.
Где она поддерживается, а где нет
| Платформа | secp256k1 |
|---|---|
WebCrypto (браузеры, Deno, crypto.subtle в Node) | Нет; используйте @noble/curves или WASM-сборку libsecp256k1 |
OpenSSL и crypto в Node.js | Да, с универсальным (более медленным) кодом |
| BoringSSL | Нет: в нём встроены только P-224, P-256, P-384 и P-521 |
| Java (SunEC) | Удалена в JDK 16: Curve not supported: secp256k1; используйте Bouncy Castle |
| Apple CryptoKit, Secure Enclave, Android Keystore, TPM 2.0, карты PIV | Нет |
| Облачные KMS | AWS KMS ECC_SECG_P256K1; Google Cloud KMS EC_SIGN_SECP256K1_SHA256 (только с уровнем защиты HSM); Azure Key Vault P-256K с ES256K |
| TLS 1.3, X.509 в Web PKI, WebAuthn | Нет |
Поскольку защищённое оборудование телефонов и ноутбуков эту кривую не поддерживает, мобильные кошельки хранят ключи secp256k1 программно (под защитой ключа, который как раз хранится в оборудовании) или полагаются на отдельный аппаратный кошелёк.
Частые ошибки
- Хеширование с SHA3-256 вместо Keccak-256 для Ethereum или с одинарным SHA-256 вместо двойного SHA-256, который Bitcoin использует в сообщениях и транзакциях. Подпись проверяется уже для другого сообщения, а восстановление выдаёт другой адрес, с виду вполне корректный.
- Отправка подписей с high-S, полученных от OpenSSL, HSM или облачного KMS, в Ethereum или в сеть Bitcoin. Сначала нормализуйте их.
- Смешение сжатых и несжатых ключей при выводе адресов Bitcoin.
- Длина подписи в формате raw. 64-байтная подпись
r ‖ sи 65-байтнаяr ‖ s ‖ v— разные форматы, и библиотеки прямо сообщают об этом, как, например, ethers с ошибкой invalid raw signature length. - Расчёт на поддержку платформы. Код, который работал на Java 11, перестаёт работать на Java 17, а в WebCrypto этой кривой не было никогда.
- Использование secp256k1 там, где её ничто не требует. Для токенов, TLS, удостоверений устройств или ключей доступа она не даёт никаких преимуществ, а аппаратная поддержка теряется.
Источники
- SEC 2 v2.0: рекомендуемые параметры эллиптических кривых, § 2.4.1; SEC 1 v2.0, § 2.3.3 (кодирование точек)
- NIST SP 800-186, приложение H.2
- SafeCurves: таблицы по ро-методу, дискриминанту и кручению
- Gallant, Lambert, Vanstone, «Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms», CRYPTO 2001; Хэл Финни, Ускорение проверки подписей (2011); Bitcoin Core PR #20147
- Примечания к выпуску Bitcoin Core 0.11.1 (политика ретрансляции low-S); EIP-2
- RFC 8812 (
ES256K) - Сервисы управления ключами: спецификации ключей AWS KMS, алгоритмы Google Cloud KMS, типы ключей Azure Key Vault