Перейти к содержимому
ecdsa.com
Node.js

elliptic

elliptic от Fedor Indutny — библиотека ECDSA и EdDSA на чистом JavaScript, на которой целое десятилетие держались браузерные бандлы и кошельки: подписи детерминированные, но по умолчанию без low-S, на вход принимается только дайджест, и есть открытый бюллетень безопасности о неверных nonce на P-521.
Лицензия
MIT
Проверенная версия
6.6.1 ·
Исходный код
github.com/indutny/elliptic
Документация
github.com/indutny/elliptic#readme

Совместимость вкратце

ECDSAEdDSA

Кривые ECDSA
P-224P-256P-384P-521secp256k1
EdDSA
Ed25519
Форматы открытых ключей
sec1-uncompressedsec1-compressed

elliptic реализует на обычном JavaScript поверх bn.js ECDSA на кривых NIST и secp256k1, а также Ed25519. Библиотека распространилась через crypto-browserify, старые библиотеки для Ethereum и Bitcoin и полифилы Node.js, которые Webpack 4 встраивал в браузерные бандлы, поэтому она до сих пор встречается в lock-файлах. ethers v6 и viem вместо неё используют @noble/curves: эта библиотека поддерживает те же кривые и имеет более строгие настройки по умолчанию.

API библиотеки отличается от большинства других тремя особенностями, из-за которых возникают ошибки совместимости. sign() принимает дайджест, а не сообщение, и считает дайджестом любые переданные ему байты: при проверке подпись исходного сообщения отличалась от подписи его SHA-256. Метод возвращает объект Signature с полями r, s и recoveryParam, поэтому формат передачи зависит от того, как вы его сериализуете: toDER() для сторон в стиле OpenSSL или оба значения, дополненные до 32 байт, для raw r‖s. Наконец, s не нормализуется, если не передать canonical: true, а проверка принимает значения s из обеих половин диапазона.

Nonce вычисляются по RFC 6979 (тестовый вектор для P-256 воспроизвёлся точно), но бюллетень безопасности GHSA-848j-6mx2-7j84 (CVE-2025-14505), который для 6.6.1 всё ещё открыт, сообщает, что промежуточное значение усекается, если начинается с нулевых байтов. Проверка это подтвердила: на P-521 с SHA-512 для 9 из 2 048 фиксированных наборов входных данных nonce отличался от RFC 6979, тогда как для secp256k1 с SHA-256 и P-256 с SHA-512 расхождений не нашлось ни на одном из 4 096 наборов в каждом случае. Такие подписи всё равно проходят проверку, но если подписать то же сообщение тем же ключом другой реализацией RFC 6979, получатся связанные nonce, а это, по данным бюллетеня, может раскрыть ключ. Не используйте elliptic для P-521; почему связанные nonce опасны, объясняет проверка повторного использования nonce.

Проблемы из более ранних бюллетеней исправлены в версиях с 6.5.7 по 6.6.1, среди них приём подписей в кодировке BER и ошибка, позволявшая извлечь ключ, если вместо хеша подписывалась строка; в ходе проверки версия 6.6.1 отклонила подпись DER с лишним ведущим нулём. Ключи задаются строками hex или массивами байтов — это сжатые или несжатые точки SEC1 и скаляры в формате raw; PEM, SPKI и JWK не поддерживаются.

Пример сначала хеширует сообщение, запрашивает каноническую подпись low-S и показывает обе сериализации. Если подпись из elliptic не проходит проверку в другом месте, сравните две её кодировки в конвертере DER ⇄ raw.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/elliptic/run.sh запустила elliptic 6.6.1 в среде Node.js 24.22.0 в образе контейнера node:24.22.0-bookworm-slim; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Кодировка подписи ECDSA

    structured

    sign() возвращает объект с r, s и recoveryParam; toDER() выдаёт ASN.1 DER, а raw r‖s нужно собирать из двух значений, дополненных до размера кривой.

    Что записала проверка
    fact ecdsa-signature-encoding = structured
    evidence ecdsa-signature-encoding: sign() returns an object with r, s and recoveryParam 0; toDER() gives 72 bytes that parse as DER: true
  • Что хеширует вызов подписи

    takes-digest

    Входные данные sign() и verify() используются как дайджест: подпись исходного сообщения отличалась от подписи его SHA-256.

    Что записала проверка
    fact ecdsa-prehash = takes-digest
  • Nonce в ECDSA

    rfc6979

    RFC 6979 с HMAC-DRBG: тестовый вектор для P-256 из приложения A.2.5 воспроизвёлся точно, вместе с его значением s из верхней половины (high-S).

    Что записала проверка
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r and s equal RFC 6979 A.2.5 (high s kept): true
  • Nonce, отличающиеся от RFC 6979

    P-521/SHA-512

    На P-521 с SHA-512 для 9 из 2 048 фиксированных наборов входных данных nonce отличался от RFC 6979 (эталоном служила @noble/curves); на secp256k1/SHA-256 и P-256/SHA-512 расхождений не нашлось ни в одном из 4 096 наборов для каждой пары. Это соответствует бюллетеню GHSA-848j-6mx2-7j84, открытому для всех версий до 6.6.1 включительно.

    Что записала проверка
    fact rfc6979-divergence = P-521/SHA-512
    evidence rfc6979-divergence: secp256k1 with SHA-256, 4096 fixed inputs: 0 nonces differ from RFC 6979, 0 of those signatures still verify
    evidence rfc6979-divergence: P-256 with SHA-512, 4096 fixed inputs: 0 nonces differ from RFC 6979, 0 of those signatures still verify
    evidence rfc6979-divergence: P-521 with SHA-512, 2048 fixed inputs: 9 nonces differ from RFC 6979, 9 of those signatures still verify
  • Low-S при подписи

    either

    По умолчанию s не нормализуется: тестовый вектор из RFC дал подпись с high-S. С canonical: true вместо s возвращается n − s.

    Что записала проверка
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: canonical: true returns n - s: true
  • High-S при проверке

    accepts-high-s

    Обе формы одной и той же подписи, high-S и low-S, прошли проверку.

    Что записала проверка
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: the RFC signature (high s) and its low-s twin both verify: true
  • Строгость разбора DER

    rejects-non-minimal

    Подпись DER с лишним ведущим нулём в r была отклонена с ошибкой Signature without r or s, а минимальная кодировка из toDER() прошла проверку.

    Что записала проверка
    fact der-strictness = rejects-non-minimal
    evidence der-strictness: the minimal DER from toDER() verifies when passed back as bytes: true
    evidence der-strictness: non-minimal INTEGER encoding: throws Signature without r or s
  • Некорректная подпись ECDSA

    throws

    Если передать в verify() 64-байтовый фрагмент raw r‖s или 3-байтовый массив, метод выбрасывает Signature without r or s вместо того, чтобы вернуть false.

    Что записала проверка
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: 64-byte raw r||s: throws Signature without r or s
    evidence ecdsa-malformed-signature: 3 bytes: throws Signature without r or s
  • Кривые ECDSA

    P-224P-256P-384P-521secp256k1

    Подпись и проверка работают на P-224, P-256, P-384, P-521 и secp256k1; кривых Brainpool нет.

    Что записала проверка
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1
  • EdDSA

    Ed25519

    Ed25519 через new elliptic.eddsa("ed25519").

    Что записала проверка
    fact eddsa = Ed25519
  • Постквантовые алгоритмы

    none

    Постквантовых алгоритмов нет.

    Что записала проверка
    fact pqc = none
  • Форматы открытых ключей

    sec1-uncompressedsec1-compressed

    keyFromPublic() прочитал точки SEC1 в hex, сжатые и несжатые, а также объект с координатами x и y в hex; 64-байтовое значение x‖y без префикса 0x04 было отклонено.

    Что записала проверка
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: an object of hex x and y coordinates also works: true; there is no PEM, SPKI, PKCS#8 or JWK parser
  • Форматы закрытых ключей

    raw-scalar

    Закрытые ключи — скаляр в формате raw в виде hex или байтов; парсера PEM, PKCS#8 или JWK нет.

    Что записала проверка
    fact private-key-formats = raw-scalar

Использование

Захешируйте сообщение, подпишите его на secp256k1 канонической подписью low-S и сериализуйте подпись в DER и в raw r‖s.
import { createHash } from "node:crypto";
import elliptic from "elliptic";

const ec = new elliptic.ec("secp256k1");
const key = ec.genKeyPair();

// elliptic signs exactly the bytes it is given: hash the message yourself,
// and pass bytes — never a string.
const digest = createHash("sha256").update("order #1042 approved").digest();

// canonical: true keeps s in the lower half, which Bitcoin and noble verifiers require.
const signature = key.sign(digest, { canonical: true });

const der = Buffer.from(signature.toDER());
const raw = Buffer.concat([signature.r.toArrayLike(Buffer, "be", 32), signature.s.toArrayLike(Buffer, "be", 32)]);
console.log("low-S:", signature.s.cmp(ec.nh) <= 0);
console.log("verified:", key.verify(digest, der), "raw bytes:", raw.length);

Тестовый стенд для фрагментов кода запускает каждый пример без изменений в образе контейнера node:24.22.0-bookworm-slim, и запуск завершается ошибкой, если вывод не совпадает.

Подготовка
npm init -y >/dev/null && npm install --no-audit --no-fund elliptic@6.6.1
Команда
node usage.mjs
Вывод должен совпадать (регулярное выражение)
low-S: true\nverified: true raw bytes: 64

Библиотеки