Zum Inhalt springen
ecdsa.com
Node.js · WebCrypto

@noble/curves

Paul Millers auditierte Bibliothek für elliptische Kurven in JavaScript, ohne Abhängigkeiten: ECDSA auf NIST-, secp256k1- und Brainpool-Kurven, EdDSA, Schnorr und mehr – seit Version 2 standardmäßig deterministisch, mit Low-S und mit Hashing der Nachricht.
Lizenz
MIT
Geprüfte Version
2.4.0 ·
Quellcode
github.com/paulmillr/noble-curves
Dokumentation
github.com/paulmillr/noble-curves#readme

Kompatibilität auf einen Blick

ECDSAEdDSA

ECDSA-Kurven
P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed25519phEd25519ctxEd448Ed448ph
Formate öffentlicher Schlüssel
sec1-uncompressedsec1-compressed

@noble/curves implementiert Kryptografie mit elliptischen Kurven in reinem TypeScript ohne Abhängigkeiten und wurde mehrfach auditiert. Die Bibliothek arbeitet unter der Haube von ethers v6, viem und vielen Bitcoin- und Nostr-Tools und ist die übliche Antwort, wenn WebCrypto eine Kurve fehlt: Abgedeckt sind P-256, P-384, P-521, secp256k1 und die Brainpool-r1-Kurven, Ed25519 und Ed448 mit ihren Prehash- und Kontextvarianten sowie Schnorr-Signaturen nach BIP-340.

Die Standardeinstellungen sind streng. Eine Signatur besteht aus 64 Byte kompaktem r‖s, die Nonce folgt RFC 6979 – der Test reproduzierte den P-256-Testvektor des RFC –, und s wird immer in die untere Hälfte normalisiert. Die Prüfung ist ebenso streng: Jede High-S-Signatur, die node:crypto erzeugt hatte, wurde abgelehnt, jede Low-S-Signatur akzeptiert. Signaturen aus OpenSSL, Java, Go oder WebCrypto sind etwa in der Hälfte der Fälle High-S; um sie zu prüfen, braucht es lowS: false, und DER-Eingaben brauchen format: "der" – ohne diese Option löst ein 70 Byte langer DER-Blob einen Längenfehler aus.

Version 2 hat die Bedeutung des ersten Arguments geändert. In 1.x erwarteten sign() und verify() einen Hash, den du selbst berechnet hast; in 2.x hashen sie die Nachricht selbst mit der Hashfunktion der Kurve, und der Test bestätigte, dass 1.9.7 den Vektor des RFC nur reproduziert, wenn die Funktion den SHA-256-Hash der Nachricht erhält. Code, der ohne Änderungen aktualisiert wird, hasht deshalb doppelt und scheitert im Zusammenspiel mit jeder anderen Bibliothek; übergib prehash: false, wo du wirklich einen Digest hast. Version 1 ließ außerdem P-256-Signaturen unnormalisiert, sodass Signaturen, die in 1.x die Prüfung bestanden, in 2.x scheitern können.

Schlüssel gibt es nur als rohe Bytes: ein geheimer 32-Byte-Skalar und öffentliche Schlüssel als SEC1-Punkte – standardmäßig komprimiert (33 Byte), auf Wunsch unkomprimiert. SPKI, PKCS#8 und JWK liest die Bibliothek nicht, Schlüssel aus OpenSSL oder einem JWKS müssen also zuerst entpackt werden; dabei helfen der JWK-Konverter und der Playground für elliptische Kurven. Post-Quanten-Code enthält dieses Paket nicht; derselbe Autor veröffentlicht ihn separat.

Das Beispiel zeigt die beiden Optionen, mit denen noble mit einem Signierer im Stil von OpenSSL zusammenarbeitet – format und lowS –, sowie die deterministische Ausgabe des eigenen Signierers. Um eine Signatur von Hand über Kodierungen hinweg zu vergleichen, nimm den Konverter DER ⇄ Raw; für Wallet-Signaturen läuft der Ethereum-Signatur-Verifizierer auf demselben Kurvencode.

Fakten und die Prüfungen dahinter

Nichts im Folgenden ist aus einer Dokumentation abgeschrieben. Das Prüfskript content/libraries/verify/noble-curves/run.sh hat @noble/curves 2.4.0 am 20. Oktober 2026 im Container-Image node:24.22.0-bookworm-slim ausgeführt; die Seite muss mit seiner Ausgabe übereinstimmen, und jeder Fakt führt die zugehörigen Zeilen auf.

  • Kodierung von ECDSA-Signaturen

    raw

    sign() liefert 64 Byte kompaktes r‖s; mit format: "der" liefert die Funktion stattdessen ASN.1-DER, und verify() braucht dieselbe Option, um es zu lesen.

    Von der Prüfung aufgezeichnet
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: default 64 bytes; format "der" gives 71 bytes that parse as DER: true
  • Low-S beim Signieren

    low-s

    Auf keiner der sieben Weierstraß-Kurven trat unter 64 Signaturen ein High-S-Wert auf: s wird immer normalisiert.

    Von der Prüfung aufgezeichnet
    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 bei der Prüfung

    rejects-high-s

    Mit den Standardoptionen wurde jede von node:crypto erzeugte High-S-Signatur abgelehnt und jede Low-S-Signatur akzeptiert; mit lowS: false bestanden alle 64 die Prüfung.

    Von der Prüfung aufgezeichnet
    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
  • ECDSA-Nonce

    rfc6979

    RFC 6979: Der P-256-Testvektor aus Anhang A.2.5 wurde exakt reproduziert, wobei s durch n − s ersetzt ist. extraEntropy: true mischt Zufall bei (Hedged-Signaturen).

    Von der Prüfung aufgezeichnet
    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
  • Was der Signaturaufruf hasht

    hashes-message

    2.x hasht die Nachricht mit der Hashfunktion der Kurve (SHA-256 bei P-256); mit prehash: false signiert und prüft die Bibliothek einen Digest, den du selbst berechnet hast.

    Von der Prüfung aufgezeichnet
    fact ecdsa-prehash = hashes-message
  • Signatureingabe in Version 1

    digest

    In 1.9.7 behandelte sign() seine Eingabe als Digest: Nur sign(sha256(m)) reproduzierte den Vektor aus RFC 6979. Wer von 1.x aktualisiert, ohne die Aufrufstellen zu ändern, hasht doppelt.

    Von der Prüfung aufgezeichnet
    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 für P-256 in Version 1

    either

    1.9.7 gab für P-256 den High-S-Wert des RFC unverändert zurück; die Normalisierung auf allen Kurven ist neu in 2.x.

    Von der Prüfung aufgezeichnet
    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
  • Fehlerhafte ECDSA-Signatur

    throws

    Eine Signatur mit falscher Länge – DER-Bytes ohne format: "der" oder ein 3-Byte-Blob – löst expected Uint8Array of length 64 aus; eine wohlgeformte, aber falsche Signatur ergibt false.

    Von der Prüfung aufgezeichnet
    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-Kurven

    P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-256, P-384 und P-521 aus nist.js, secp256k1 aus secp256k1.js und die Brainpool-r1-Kurven aus misc.js.

    Von der Prüfung aufgezeichnet
    fact ecdsa-curves = P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed25519phEd25519ctxEd448Ed448ph

    Signieren und Prüfen funktionierten mit Ed25519 samt den Varianten ph und ctx, mit Ed448 und mit Ed448ph.

    Von der Prüfung aufgezeichnet
    fact eddsa = Ed25519, Ed25519ph, Ed25519ctx, Ed448, Ed448ph
  • Post-Quanten-Algorithmen

    none

    Keine Post-Quanten-Algorithmen in diesem Paket.

    Von der Prüfung aufgezeichnet
    fact pqc = none
    evidence pqc: exports of the package mention ML-DSA or ML-KEM: false
  • Formate öffentlicher Schlüssel

    sec1-uncompressedsec1-compressed

    Öffentliche Schlüssel sind SEC1-Punkte, komprimiert oder unkomprimiert; ein 64 Byte langes x‖y und SPKI-DER wurden abgelehnt.

    Von der Prüfung aufgezeichnet
    fact public-key-formats = sec1-uncompressed, sec1-compressed
  • Standardformat öffentlicher Schlüssel

    sec1-compressed

    getPublicKey() liefert den komprimierten Punkt mit 33 Byte, sofern du für die unkomprimierte Form nicht false übergibst.

    Von der Prüfung aufgezeichnet
    fact default-public-key = sec1-compressed
  • Formate privater Schlüssel

    raw-scalar

    Geheime Schlüssel sind der rohe 32-Byte-Skalar; PKCS#8-DER wurde abgelehnt.

    Von der Prüfung aufgezeichnet
    fact private-key-formats = raw-scalar
  • Schnorr-Signaturen

    BIP-340

    schnorr in secp256k1.js implementiert BIP-340: 64 Byte lange Signaturen über öffentliche Schlüssel in x-only-Form.

    Von der Prüfung aufgezeichnet
    fact schnorr = BIP-340

Verwendung

Prüfe eine DER-Signatur aus node:crypto mit den Optionen, die noble dafür nachsichtig genug machen, und signiere dann mit noble selbst deterministisch.
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);

Die Snippet-Testumgebung führt jedes Beispiel unverändert im Container-Image node:24.22.0-bookworm-slim aus, und der Lauf schlägt fehl, wenn die Ausgabe nicht übereinstimmt.

Einrichtung
npm init -y >/dev/null && npm install --no-audit --no-fund @noble/curves@2.4.0
Befehl
node usage.mjs
Ausgabe muss passen (regulärer Ausdruck)
verified: true\ndeterministic: true 64

Bibliotheken