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

elliptic

O elliptic, de Fedor Indutny, a biblioteca de ECDSA e EdDSA em JavaScript puro por trás de uma década de bundles de navegador e carteiras — determinística, mas sem low-S por padrão, entrada apenas como digest e um alerta de segurança em aberto sobre nonces incorretos na P-521.
Licença
MIT
Versão verificada
6.6.1 ·
Código-fonte
github.com/indutny/elliptic
Documentação
github.com/indutny/elliptic#readme

Visão geral da compatibilidade

ECDSAEdDSA

Curvas ECDSA
P-224P-256P-384P-521secp256k1
EdDSA
Ed25519
Formatos de chave pública
sec1-uncompressedsec1-compressed

O elliptic implementa ECDSA sobre as curvas do NIST e a secp256k1, além do Ed25519, em JavaScript puro sobre o bn.js. Ele se espalhou por meio do crypto-browserify, de bibliotecas mais antigas de Ethereum e Bitcoin e dos polyfills do Node.js que o Webpack 4 injetava nos bundles de navegador, e é por isso que ainda aparece em arquivos de lock. O ethers v6 e o viem usam, em vez dele, a @noble/curves, que cobre as mesmas curvas com padrões mais rigorosos.

A API difere da maioria das bibliotecas em três pontos que causam bugs de interoperabilidade. sign() recebe o digest, não a mensagem, e trata quaisquer bytes que receber como esse digest — o teste assinou uma mensagem bruta e obteve uma assinatura diferente da obtida para o SHA-256 dela. Ele retorna um objeto Signature com r, s e recoveryParam, então o formato de transmissão é o que você serializar: toDER() para sistemas no estilo do OpenSSL, ou os dois valores preenchidos até 32 bytes para r‖s bruto. E o s não é normalizado, a menos que você passe canonical: true, enquanto a verificação aceita as duas metades.

Os nonces seguem a RFC 6979 — o vetor de teste da P-256 foi reproduzido exatamente —, mas o alerta de segurança GHSA-848j-6mx2-7j84 (CVE-2025-14505), ainda em aberto para a versão 6.6.1, relata que um valor intermediário é truncado quando tem bytes zero à esquerda. O teste reproduziu o problema: na P-521 com SHA-512, 9 de 2.048 entradas fixas produziram um nonce diferente do da RFC 6979, enquanto secp256k1 com SHA-256 e P-256 com SHA-512 não mostraram diferença em 4.096 entradas cada. Essas assinaturas continuam passando na verificação, mas assinar a mesma mensagem com a mesma chave em outra implementação da RFC 6979 gera nonces relacionados, o que, segundo o alerta, pode expor a chave. Evite o elliptic para P-521 e veja no verificador de reutilização de nonce por que nonces relacionados importam.

Alertas anteriores foram corrigidos da 6.5.7 à 6.6.1, entre eles a aceitação de assinaturas com codificação BER e um bug de extração da chave quando uma string era assinada no lugar de um hash; no teste, a 6.6.1 rejeitou uma assinatura DER com um zero à esquerda redundante. As chaves são strings hexadecimais ou arrays de bytes — pontos SEC1, comprimidos ou não comprimidos, e escalares brutos —, sem suporte a PEM, SPKI ou JWK.

O exemplo calcula o hash primeiro, pede uma assinatura low-S canônica e mostra as duas serializações. Quando uma assinatura do elliptic falhar em outro lugar, compare as duas codificações dela no conversor DER ⇄ bruto.

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

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/elliptic/run.sh executou elliptic 6.6.1 em Node.js 24.22.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

    structured

    sign() retorna um objeto com r, s e recoveryParam; toDER() produz ASN.1 DER, e o r‖s bruto precisa ser montado a partir dos dois valores preenchidos até o tamanho da curva.

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

    takes-digest

    A entrada de sign() e verify() é usada como digest; assinar a mensagem bruta produziu uma assinatura diferente da obtida ao assinar o SHA-256 dela.

    Registrado pela verificação
    fact ecdsa-prehash = takes-digest
  • Nonce do ECDSA

    rfc6979

    RFC 6979 com HMAC-DRBG: o vetor de teste da P-256 do apêndice A.2.5 foi reproduzido exatamente, inclusive o s high-S do vetor.

    Registrado pela verificação
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r and s equal RFC 6979 A.2.5 (high s kept): true
  • Nonces que diferem da RFC 6979

    P-521/SHA-512

    Na P-521 com SHA-512, 9 de 2.048 entradas fixas usaram um nonce diferente do da RFC 6979 (a referência foi a @noble/curves); nenhuma diferença apareceu em secp256k1/SHA-256 ou P-256/SHA-512 em 4.096 entradas cada. Isso corresponde ao alerta de segurança GHSA-848j-6mx2-7j84, em aberto para todas as versões até a 6.6.1.

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

    either

    Não normalizado por padrão: o vetor de teste da RFC voltou com seu s high-S. Com canonical: true, o resultado traz n − s em vez de s.

    Registrado pela verificação
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: canonical: true returns n - s: true
  • High-S ao verificar

    accepts-high-s

    Tanto a forma high-S quanto a forma low-S da mesma assinatura foram verificadas.

    Registrado pela verificação
    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
  • Rigor com a codificação DER

    rejects-non-minimal

    Uma assinatura DER cujo r tinha um zero à esquerda redundante foi recusada com Signature without r or s, enquanto a codificação mínima de toDER() foi verificada.

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

    throws

    Um blob r‖s bruto de 64 bytes ou um array de 3 bytes passado a verify() lança Signature without r or s em vez de retornar false.

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

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

    Assinatura e verificação funcionaram com P-224, P-256, P-384, P-521 e secp256k1; não há curvas Brainpool.

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

    Ed25519

    Ed25519 por meio de new elliptic.eddsa("ed25519").

    Registrado pela verificação
    fact eddsa = Ed25519
  • Algoritmos pós-quânticos

    none

    Sem algoritmos pós-quânticos.

    Registrado pela verificação
    fact pqc = none
  • Formatos de chave pública

    sec1-uncompressedsec1-compressed

    keyFromPublic() leu pontos SEC1 em hex, comprimidos ou não comprimidos, e um objeto com as coordenadas x e y em hex; um x‖y de 64 bytes sem o prefixo 0x04 foi recusado.

    Registrado pela verificação
    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
  • Formatos de chave privada

    raw-scalar

    As chaves privadas são o escalar bruto em hex ou em bytes; não há parser de PEM, PKCS#8 nem JWK.

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

Uso

Calcule o hash da mensagem, assine-a como uma assinatura low-S canônica na secp256k1 e serialize a assinatura em DER e em r‖s bruto.
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);

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 elliptic@6.6.1
Comando
node usage.mjs
A saída deve corresponder a (expressão regular)
low-S: true\nverified: true raw bytes: 64

Bibliotecas