Pular para o conteúdo
ecdsa.com
Node.js · WebCrypto

@noble/curves

A biblioteca de curvas elípticas para JavaScript de Paul Miller, auditada e sem dependências: ECDSA nas curvas do NIST, secp256k1 e Brainpool, EdDSA, Schnorr e mais — determinística, low-S e com hash da mensagem por padrão desde a versão 2.
Licença
MIT
Versão verificada
2.4.0 ·
Código-fonte
github.com/paulmillr/noble-curves
Documentação
github.com/paulmillr/noble-curves#readme

Visão geral da compatibilidade

ECDSAEdDSA

Curvas ECDSA
P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed25519phEd25519ctxEd448Ed448ph
Formatos de chave pública
sec1-uncompressedsec1-compressed

A @noble/curves implementa criptografia de curvas elípticas em TypeScript puro, sem dependências, e passou por várias auditorias. É o motor por trás do ethers v6, do viem e de muitas ferramentas de Bitcoin e Nostr, e a resposta habitual quando falta uma curva no WebCrypto: cobre P-256, P-384, P-521, secp256k1 e as curvas Brainpool r1, Ed25519 e Ed448 com as variantes de pré-hash e de contexto, além de assinaturas Schnorr BIP-340.

Os padrões são os estritos. Uma assinatura tem 64 bytes de r‖s compacto, o nonce segue a RFC 6979 — o vetor de teste P-256 da RFC foi reproduzido — e o s é sempre normalizado para a metade inferior. A verificação é igualmente estrita: todas as assinaturas high-S produzidas pelo node:crypto foram rejeitadas, e todas as low-S, aceitas. Assinaturas do OpenSSL, do Java, do Go ou do WebCrypto são high-S em cerca de metade das vezes, então verificá-las exige lowS: false, e uma entrada DER exige format: "der" — sem isso, um blob DER de 70 bytes lança um erro de comprimento.

A versão 2 mudou o significado do primeiro argumento. Na 1.x, sign() e verify() recebiam um hash calculado por você; na 2.x, elas próprias calculam o hash da mensagem com a função de hash da curva, e o teste confirmou que a 1.9.7 só reproduz o vetor da RFC quando recebe o SHA-256 da mensagem. Por isso, um código atualizado sem mudanças calcula o hash duas vezes e falha contra todas as outras bibliotecas; passe prehash: false quando você realmente tiver um digest. A versão 1 também deixava as assinaturas P-256 sem normalização, então assinaturas que eram verificadas na 1.x podem falhar na 2.x.

As chaves são apenas bytes brutos: um escalar secreto de 32 bytes e chaves públicas como pontos SEC1 — comprimidos (33 bytes) por padrão, não comprimidos sob demanda. SPKI, PKCS#8 e JWK não são lidos, então chaves vindas do OpenSSL ou de um JWKS precisam ser desempacotadas antes; o conversor de JWK e o laboratório de curvas ajudam nisso. Não há código pós-quântico neste pacote; o mesmo autor o publica separadamente.

O exemplo mostra as duas opções que fazem o noble interoperar com um signatário no estilo do OpenSSL — format e lowS — e a saída determinística do seu próprio signatário. Para comparar manualmente uma assinatura em diferentes codificações, use o conversor DER ⇄ bruto; para assinaturas de carteiras, o verificador de assinaturas Ethereum roda sobre o mesmo código de curvas.

Os fatos e as verificações por trás deles

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/noble-curves/run.sh executou @noble/curves 2.4.0 na imagem de contêiner node:24.22.0-bookworm-slim em 20 de outubro de 2026; a página precisa corresponder ao que a verificação imprimiu, e cada fato lista essas linhas.

  • Codificação da assinatura ECDSA

    raw

    sign() retorna 64 bytes de r‖s compacto; com format: "der", retorna ASN.1 DER, e verify() precisa da mesma opção para lê-lo.

    Registrado pela verificação
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: default 64 bytes; format "der" gives 71 bytes that parse as DER: true
  • Low-S ao assinar

    low-s

    Nenhum valor high-S apareceu em 64 assinaturas em nenhuma das sete curvas de Weierstrass: o s é sempre normalizado.

    Registrado pela verificação
    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 ao verificar

    rejects-high-s

    Com as opções padrão, todas as assinaturas high-S geradas pelo node:crypto foram rejeitadas, e todas as low-S, aceitas; com lowS: false, as 64 foram verificadas.

    Registrado pela verificação
    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 do ECDSA

    rfc6979

    RFC 6979: o vetor de teste P-256 do apêndice A.2.5 foi reproduzido exatamente, com s substituído por n − s. extraEntropy: true mistura aleatoriedade (assinaturas hedged).

    Registrado pela verificação
    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
  • Entrada do hash na chamada de assinatura

    hashes-message

    A 2.x calcula o hash da mensagem com a função de hash da curva (SHA-256 para P-256); prehash: false assina e verifica um digest calculado por você.

    Registrado pela verificação
    fact ecdsa-prehash = hashes-message
  • Entrada assinada na versão 1

    digest

    Na 1.9.7, sign() tratava a entrada como o digest: só sign(sha256(m)) reproduziu o vetor da RFC 6979. Atualizar da 1.x sem mudar as chamadas faz o hash ser calculado duas vezes.

    Registrado pela verificação
    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 em P-256 na versão 1

    either

    A 1.9.7 retornou sem alteração o valor high-S da RFC para a P-256; a normalização em todas as curvas é novidade da 2.x.

    Registrado pela verificação
    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
  • Assinatura ECDSA malformada

    throws

    Uma assinatura de comprimento errado — bytes DER sem format: "der" ou um blob de 3 bytes — lança expected Uint8Array of length 64; uma assinatura bem formada, mas errada, retorna false.

    Registrado pela verificação
    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
  • Curvas ECDSA

    P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-256, P-384 e P-521 em nist.js, secp256k1 em secp256k1.js e as curvas Brainpool r1 em misc.js.

    Registrado pela verificação
    fact ecdsa-curves = P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed25519phEd25519ctxEd448Ed448ph

    Assinatura e verificação funcionaram com Ed25519 e as variantes ph e ctx, Ed448 e Ed448ph.

    Registrado pela verificação
    fact eddsa = Ed25519, Ed25519ph, Ed25519ctx, Ed448, Ed448ph
  • Algoritmos pós-quânticos

    none

    Sem algoritmos pós-quânticos neste pacote.

    Registrado pela verificação
    fact pqc = none
    evidence pqc: exports of the package mention ML-DSA or ML-KEM: false
  • Formatos de chave pública

    sec1-uncompressedsec1-compressed

    As chaves públicas são pontos SEC1, comprimidos ou não comprimidos; um x‖y de 64 bytes e SPKI DER foram rejeitados.

    Registrado pela verificação
    fact public-key-formats = sec1-uncompressed, sec1-compressed
  • Formato padrão da chave pública

    sec1-compressed

    getPublicKey() retorna o ponto comprimido de 33 bytes, a menos que você passe false para obter a forma não comprimida.

    Registrado pela verificação
    fact default-public-key = sec1-compressed
  • Formatos de chave privada

    raw-scalar

    As chaves secretas são o escalar bruto de 32 bytes; PKCS#8 DER foi rejeitado.

    Registrado pela verificação
    fact private-key-formats = raw-scalar
  • Assinaturas Schnorr

    BIP-340

    schnorr em secp256k1.js implementa o BIP-340: assinaturas de 64 bytes sobre chaves públicas x-only.

    Registrado pela verificação
    fact schnorr = BIP-340

Uso

Verifique uma assinatura DER do node:crypto com as opções que tornam o noble tolerante o bastante para ela e, depois, assine de forma determinística com o próprio 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);

O executor de trechos de código roda cada exemplo literalmente na imagem de contêiner node:24.22.0-bookworm-slim, e a execução falha se a saída não corresponder.

Configuração
npm init -y >/dev/null && npm install --no-audit --no-fund @noble/curves@2.4.0
Comando
node usage.mjs
A saída deve corresponder a (expressão regular)
verified: true\ndeterministic: true 64

Bibliotecas