Skip to content
ecdsa.com
Node.js

elliptic

Fedor Indutny's elliptic, the pure-JavaScript ECDSA and EdDSA library behind a decade of browser bundles and wallets — deterministic but not low-S by default, digest-only input, and an open advisory about wrong nonces on P-521.
License
MIT
Checked version
6.6.1 ·
Source
github.com/indutny/elliptic
Documentation
github.com/indutny/elliptic#readme

Compatibility at a glance

ECDSAEdDSA

ECDSA curves
P-224P-256P-384P-521secp256k1
EdDSA
Ed25519
Public key formats
sec1-uncompressedsec1-compressed

elliptic implements ECDSA over the NIST curves and secp256k1, and Ed25519, in plain JavaScript on top of bn.js. It spread through crypto-browserify, older Ethereum and Bitcoin libraries and the Node.js polyfills that Webpack 4 injected into browser bundles, which is why it still turns up in lockfiles. ethers v6 and viem build on @noble/curves instead, which covers the same curves with stricter defaults.

The API differs from most libraries in three ways that cause interop bugs. sign() takes the digest, not the message, and treats whatever bytes it receives as that digest — the check signed a raw message and got a different signature than for its SHA-256. It returns a Signature object with r, s and recoveryParam, so the wire format is whatever you serialize: toDER() for OpenSSL-style peers, or both values padded to 32 bytes for raw r‖s. And s is not normalized unless you pass canonical: true, while verification accepts both halves.

Nonces follow RFC 6979 — the P-256 test vector reproduced exactly — but advisory GHSA-848j-6mx2-7j84 (CVE-2025-14505), still open for 6.6.1, reports that an interim value is truncated when it has leading zero bytes. The check reproduced it: on P-521 with SHA-512, 9 of 2,048 fixed inputs produced a nonce different from RFC 6979, while secp256k1 with SHA-256 and P-256 with SHA-512 showed no difference in 4,096 inputs each. Those signatures still verify, but signing the same message for the same key with another RFC 6979 implementation yields related nonces, which the advisory says can expose the key. Avoid elliptic for P-521, and see the nonce reuse checker for why related nonces matter.

Earlier advisories were fixed in 6.5.7 through 6.6.1, among them BER-encoded signatures being accepted and a key-extraction bug when a string was signed instead of a hash; 6.6.1 rejected a DER signature with a redundant leading zero in the check. Keys are hex strings or byte arrays — SEC1 points, compressed or uncompressed, and raw scalars — with no PEM, SPKI or JWK support.

The sample hashes first, asks for a canonical low-S signature and shows both serializations. When a signature from elliptic fails elsewhere, compare its two encodings in the DER ⇄ raw converter.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/elliptic/run.sh ran elliptic 6.6.1 on Node.js 24.22.0 in the container image node:24.22.0-bookworm-slim on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    structured

    sign() returns an object with r, s and recoveryParam; toDER() produces ASN.1 DER, and raw r‖s has to be assembled from the two values padded to the curve size.

    Recorded by the check
    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
  • What the signing call hashes

    takes-digest

    The input to sign() and verify() is used as the digest; signing the raw message produced a different signature than signing its SHA-256.

    Recorded by the check
    fact ecdsa-prehash = takes-digest
  • ECDSA nonce

    rfc6979

    RFC 6979 with HMAC-DRBG: the P-256 test vector from appendix A.2.5 reproduced exactly, including its high s.

    Recorded by the check
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r and s equal RFC 6979 A.2.5 (high s kept): true
  • Nonces that differ from RFC 6979

    P-521/SHA-512

    On P-521 with SHA-512, 9 of 2,048 fixed inputs used a nonce different from RFC 6979 (the reference was @noble/curves); no difference showed on secp256k1/SHA-256 or P-256/SHA-512 in 4,096 inputs each. This matches advisory GHSA-848j-6mx2-7j84, open for all versions up to 6.6.1.

    Recorded by the check
    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 when signing

    either

    Not normalized by default: the RFC test vector came back with its high s. canonical: true returns n − s instead.

    Recorded by the check
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: canonical: true returns n - s: true
  • High-S when verifying

    accepts-high-s

    Both the high-S and the low-S form of the same signature verified.

    Recorded by the check
    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 strictness

    rejects-non-minimal

    A DER signature whose r carried a redundant leading zero was refused with Signature without r or s, while the minimal encoding from toDER() verified.

    Recorded by the check
    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
  • Malformed ECDSA signature

    throws

    A 64-byte raw r‖s blob or a 3-byte array passed to verify() throws Signature without r or s instead of returning false.

    Recorded by the check
    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 curves

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

    P-224, P-256, P-384, P-521 and secp256k1 signed and verified; there are no Brainpool curves.

    Recorded by the check
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1
  • EdDSA

    Ed25519

    Ed25519 through new elliptic.eddsa("ed25519").

    Recorded by the check
    fact eddsa = Ed25519
  • Post-quantum algorithms

    none

    No post-quantum algorithms.

    Recorded by the check
    fact pqc = none
  • Public key formats

    sec1-uncompressedsec1-compressed

    keyFromPublic() read hex SEC1 points, compressed or uncompressed, and an object of hex x and y coordinates; a 64-byte x‖y without the 0x04 prefix was refused.

    Recorded by the check
    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
  • Private key formats

    raw-scalar

    Private keys are the raw scalar as hex or bytes; there is no PEM, PKCS#8 or JWK parser.

    Recorded by the check
    fact private-key-formats = raw-scalar

Usage

Hash the message, sign it as a canonical low-S signature on secp256k1, and serialize it as DER and as 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);

The snippet harness runs every sample verbatim in the container image node:24.22.0-bookworm-slim, and the run fails unless the output matches.

Setup
npm init -y >/dev/null && npm install --no-audit --no-fund elliptic@6.6.1
Command
node usage.mjs
Output must match (regular expression)
low-S: true\nverified: true raw bytes: 64

Libraries