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

@noble/curves

Библиотека для эллиптических кривых на JavaScript от Paul Miller, прошедшая аудит и не имеющая зависимостей: ECDSA на кривых NIST, secp256k1 и Brainpool, EdDSA, Schnorr и не только; с версии 2 по умолчанию детерминированная подпись, low-S и хеширование сообщения.
Лицензия
MIT
Проверенная версия
2.4.0 ·
Исходный код
github.com/paulmillr/noble-curves
Документация
github.com/paulmillr/noble-curves#readme

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

ECDSAEdDSA

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

Библиотека @noble/curves реализует криптографию на эллиптических кривых на чистом TypeScript без зависимостей и несколько раз проходила аудит. Она служит движком для ethers v6, viem и многих инструментов для Bitcoin и Nostr и обычно выручает, когда в WebCrypto нет нужной кривой: в ней есть P-256, P-384, P-521, secp256k1 и кривые Brainpool r1, Ed25519 и Ed448 с вариантами с предварительным хешированием и с контекстом, а также подписи Schnorr по BIP-340.

Настройки по умолчанию у неё строгие. Подпись — 64 байта в компактном формате r‖s, nonce вычисляется по RFC 6979 (проверка воспроизвела тестовый вектор из RFC для P-256), а s всегда нормализуется в нижнюю половину. Проверка столь же строгая: все подписи high-S, созданные node:crypto, были отклонены, все подписи low-S приняты. Подписи из OpenSSL, Java, Go или WebCrypto примерно в половине случаев имеют high-S, поэтому для их проверки нужен lowS: false, а для входных данных в DER — format: "der": без него 70-байтовый фрагмент DER вызывает ошибку длины.

В версии 2 изменился смысл первого аргумента. В 1.x sign() и verify() принимали вычисленный вами хеш, а в 2.x они сами хешируют сообщение хеш-функцией кривой; проверка подтвердила, что 1.9.7 воспроизводит вектор из RFC, только если передать ей SHA-256 сообщения. Поэтому код, обновлённый без изменений, хеширует дважды и расходится со всеми остальными библиотеками; передавайте prehash: false там, где у вас действительно дайджест. Кроме того, версия 1 не нормализовала подписи на P-256, так что подписи, прошедшие проверку в 1.x, в 2.x могут её не пройти.

Ключи принимаются только в виде сырых байтов: 32-байтовый секретный скаляр и открытые ключи в виде точек SEC1, по умолчанию сжатых (33 байта), а по запросу несжатых. SPKI, PKCS#8 и JWK библиотека не читает, поэтому ключи из OpenSSL или JWKS сначала нужно распаковать; с этим помогут конвертер JWK и песочница эллиптических кривых. Постквантового кода в этом пакете нет: тот же автор публикует его отдельно.

Пример показывает два параметра, format и lowS, с которыми noble совместима с библиотеками подписи в стиле OpenSSL, и детерминированный результат её собственной функции подписи. Чтобы вручную сравнить подпись в разных кодировках, воспользуйтесь конвертером DER ⇄ raw; для подписей кошельков есть проверка подписей Ethereum, которая работает на том же коде кривых.

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

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

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

    raw

    sign() возвращает 64 байта в компактном формате r‖s; с format: "der" вместо этого возвращается ASN.1 DER, а чтобы прочитать такую подпись, verify() нужен тот же параметр.

    Что записала проверка
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: default 64 bytes; format "der" gives 71 bytes that parse as DER: true
  • Low-S при подписи

    low-s

    Ни на одной из 7 кривых Вейерштрасса среди 64 подписей не встретилось значения high-S: s всегда нормализуется.

    Что записала проверка
    fact ecdsa-low-s-sign = low-s
    evidence ecdsa-low-s-sign: no high-S in 64 signatures on any of the seven curves
  • High-S при проверке

    rejects-high-s

    С параметрами по умолчанию все подписи high-S, созданные node:crypto, были отклонены, а все подписи low-S приняты; с lowS: false проверку прошли все 64.

    Что записала проверка
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: 64 node:crypto signatures: every high-S one rejected: true; every low-S one accepted: true; all 64 accepted with lowS false: true
  • Nonce в ECDSA

    rfc6979

    RFC 6979: тестовый вектор для P-256 из приложения A.2.5 воспроизвёлся точно, с заменой s на n − s. extraEntropy: true подмешивает случайность (рандомизированные подписи, hedged).

    Что записала проверка
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r matches RFC 6979 A.2.5: true; s equals n - s(RFC): true; extraEntropy true changes the bytes: true
  • Что хеширует вызов подписи

    hashes-message

    Версия 2.x хеширует сообщение хеш-функцией кривой (SHA-256 для P-256); с prehash: false подписывается и проверяется вычисленный вами дайджест.

    Что записала проверка
    fact ecdsa-prehash = hashes-message
  • Входные данные подписи в версии 1

    digest

    В 1.9.7 функция sign() считала входные данные дайджестом: только вызов sign(sha256(m)) воспроизвёл вектор RFC 6979. При обновлении с 1.x без изменения мест вызова сообщение хешируется дважды.

    Что записала проверка
    fact v1-sign-input = digest
    evidence v1-sign-input: 1.x sign(sha256(m)) gives the RFC 6979 r: true; 1.x sign(m) does: false
  • Low-S для P-256 в версии 1

    either

    Версия 1.9.7 возвращала для P-256 значение high-S из RFC без изменений; нормализация на всех кривых появилась в 2.x.

    Что записала проверка
    fact v1-p256-low-s-sign = either
    evidence v1-p256-low-s-sign: 1.x P-256 returned the RFC's high s unchanged: true
  • Некорректная подпись ECDSA

    throws

    Подпись неправильной длины (байты DER без format: "der" или 3-байтовый фрагмент данных) вызывает исключение expected Uint8Array of length 64; корректно сформированная, но неверная подпись даёт false.

    Что записала проверка
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: DER without format: throws expected Uint8Array of length 64, got length=71
    evidence ecdsa-malformed-signature: 3 bytes: throws expected Uint8Array of length 64, got length=3
    evidence ecdsa-malformed-signature: 64 zero bytes: false
  • Кривые ECDSA

    P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-256, P-384 и P-521 из nist.js, secp256k1 из secp256k1.js и кривые Brainpool r1 из misc.js.

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

    Ed25519Ed25519phEd25519ctxEd448Ed448ph

    Подпись и проверка работают для Ed25519 с вариантами ph и ctx, Ed448 и Ed448ph.

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

    none

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

    Что записала проверка
    fact pqc = none
    evidence pqc: exports of the package mention ML-DSA or ML-KEM: false
  • Форматы открытых ключей

    sec1-uncompressedsec1-compressed

    Открытые ключи — точки SEC1, сжатые или несжатые; 64-байтовое значение x‖y и SPKI DER были отклонены.

    Что записала проверка
    fact public-key-formats = sec1-uncompressed, sec1-compressed
  • Формат открытого ключа по умолчанию

    sec1-compressed

    getPublicKey() возвращает 33-байтовую сжатую точку, если не передать false, чтобы получить несжатую форму.

    Что записала проверка
    fact default-public-key = sec1-compressed
  • Форматы закрытых ключей

    raw-scalar

    Секретные ключи — 32-байтовый скаляр в формате raw; PKCS#8 DER был отклонён.

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

    BIP-340

    schnorr из secp256k1.js реализует BIP-340: 64-байтовые подписи с открытыми ключами x-only.

    Что записала проверка
    fact schnorr = BIP-340

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

Проверьте подпись DER из node:crypto с параметрами, при которых noble её принимает, а затем создайте детерминированную подпись самой noble.
import { generateKeyPairSync, sign } from "node:crypto";
import { p256 } from "@noble/curves/nist.js";

// A signature made elsewhere: node:crypto (OpenSSL) emits DER, with s in either half.
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const message = new TextEncoder().encode("order #1042 approved");
const der = sign("sha256", message, privateKey);
const jwk = publicKey.export({ format: "jwk" });
const point = Uint8Array.from([4, ...Buffer.from(jwk.x, "base64url"), ...Buffer.from(jwk.y, "base64url")]);

// noble 2.x hashes the message itself and rejects high-S unless told otherwise.
console.log("verified:", p256.verify(der, message, point, { format: "der", lowS: false }));

// Its own signatures are compact r||s, RFC 6979 and low-S: the same bytes every time.
const secretKey = p256.utils.randomSecretKey();
const first = p256.sign(message, secretKey);
const second = p256.sign(message, secretKey);
console.log("deterministic:", Buffer.from(first).equals(Buffer.from(second)), first.length);

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

Подготовка
npm init -y >/dev/null && npm install --no-audit --no-fund @noble/curves@2.4.0
Команда
node usage.mjs
Вывод должен совпадать (регулярное выражение)
verified: true\ndeterministic: true 64

Библиотеки