Aller au contenu
ecdsa.com
Node.js · WebCrypto

@noble/curves

La bibliothèque de courbes elliptiques pour JavaScript de Paul Miller, auditée et sans dépendance : ECDSA sur les courbes NIST, secp256k1 et Brainpool, EdDSA, Schnorr et plus encore – déterministe, low-S et avec hachage du message par défaut depuis la version 2.
Licence
MIT
Version vérifiée
2.4.0 ·
Code source
github.com/paulmillr/noble-curves
Documentation
github.com/paulmillr/noble-curves#readme

Compatibilité en un coup d’œil

ECDSAEdDSA

Courbes ECDSA
P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed25519phEd25519ctxEd448Ed448ph
Formats de clé publique
sec1-uncompressedsec1-compressed

La bibliothèque @noble/curves implémente la cryptographie sur courbes elliptiques en TypeScript pur, sans aucune dépendance, et a été auditée à plusieurs reprises. C’est le moteur d’ethers v6, de viem et de nombreux outils Bitcoin et Nostr, et la réponse habituelle quand WebCrypto ne propose pas une courbe : elle couvre P-256, P-384, P-521, secp256k1 et les courbes Brainpool r1, Ed25519 et Ed448 avec leurs variantes de pré-hachage et de contexte, ainsi que les signatures Schnorr BIP-340.

Ses valeurs par défaut sont strictes. Une signature fait 64 octets de r‖s compact, le nonce suit la RFC 6979 (les essais ont reproduit le vecteur de test P-256 de la RFC) et s est toujours normalisé dans la moitié inférieure. La vérification est tout aussi stricte : chaque signature high-S produite par node:crypto a été rejetée, chaque signature low-S acceptée. Les signatures issues d’OpenSSL, de Java, de Go ou de WebCrypto sont high-S environ une fois sur deux : leur vérification nécessite donc lowS: false, et une entrée DER nécessite format: "der", sans quoi un bloc DER de 70 octets lève une erreur de longueur.

La version 2 a changé le sens du premier argument. En 1.x, sign() et verify() prenaient un condensat que vous aviez calculé ; en 2.x, elles hachent elles-mêmes le message avec la fonction de hachage de la courbe, et les essais ont confirmé que la 1.9.7 ne reproduit le vecteur de la RFC que si on lui fournit le SHA-256 du message. Le code mis à jour sans modification hache donc deux fois et échoue face à toutes les autres bibliothèques ; passez prehash: false là où vous détenez réellement un condensat. De plus, la version 1 ne normalisait pas les signatures P-256, si bien que des signatures qui passaient la vérification en 1.x peuvent échouer en 2.x.

Les clés sont uniquement des octets bruts : un scalaire secret de 32 octets, et des clés publiques sous forme de points SEC1, compressés (33 octets) par défaut, non compressés sur demande. SPKI, PKCS#8 et JWK ne sont pas lus, si bien que les clés provenant d’OpenSSL ou d’un JWKS doivent d’abord être extraites ; le convertisseur JWK et le bac à sable des courbes elliptiques y aident. Ce paquet ne contient aucun code post-quantique : le même auteur le publie séparément.

L’exemple montre les deux options (format et lowS) qui rendent noble interopérable avec un signataire de type OpenSSL, ainsi que la sortie déterministe de son propre signataire. Pour comparer manuellement une signature dans ses différents encodages, utilisez le convertisseur DER ⇄ brut ; pour les signatures de portefeuille, le vérificateur de signatures Ethereum s’appuie sur le même code de courbes.

Les faits et les vérifications qui les fondent

Rien de ce qui suit n’est copié d’une documentation. La vérification content/libraries/verify/noble-curves/run.sh a exécuté @noble/curves 2.4.0 dans l’image de conteneur node:24.22.0-bookworm-slim le 20 octobre 2026 ; la page doit correspondre à ce que la vérification a affiché, et chaque fait cite les lignes en question.

  • Encodage des signatures ECDSA

    raw

    sign() renvoie 64 octets de r‖s compact ; avec format: "der", elle renvoie plutôt de l’ASN.1 DER, et verify() a besoin de la même option pour le lire.

    Relevé par la vérification
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: default 64 bytes; format "der" gives 71 bytes that parse as DER: true
  • Low-S à la signature

    low-s

    Aucune valeur high-S n’est apparue sur 64 signatures, sur aucune des sept courbes de Weierstrass : s est toujours normalisé.

    Relevé par la vérification
    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 à la vérification

    rejects-high-s

    Avec les options par défaut, chaque signature high-S produite par node:crypto a été rejetée et chaque signature low-S acceptée ; avec lowS: false, les 64 ont toutes passé la vérification.

    Relevé par la vérification
    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 : le vecteur de test P-256 de l’annexe A.2.5 a été reproduit exactement, avec s remplacé par n − s. extraEntropy: true y mêle de l’aléa (signatures dites « hedged »).

    Relevé par la vérification
    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
  • Ce que l’appel de signature hache

    hashes-message

    La 2.x hache le message avec la fonction de hachage de la courbe (SHA-256 pour P-256) ; prehash: false signe et vérifie un condensat que vous avez calculé.

    Relevé par la vérification
    fact ecdsa-prehash = hashes-message
  • Entrée de signature en version 1

    digest

    En 1.9.7, sign() traitait son entrée comme le condensat : seul sign(sha256(m)) a reproduit le vecteur de la RFC 6979. Mettre à jour depuis la 1.x sans modifier les points d’appel conduit à un double hachage.

    Relevé par la vérification
    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 sur P-256 en version 1

    either

    La 1.9.7 renvoyait telle quelle la valeur high-S de la RFC pour P-256 ; la normalisation sur toutes les courbes est une nouveauté de la 2.x.

    Relevé par la vérification
    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
  • Signature ECDSA mal formée

    throws

    Une signature de mauvaise longueur (des octets DER sans format: "der", ou un bloc de 3 octets) lève expected Uint8Array of length 64 ; une signature bien formée mais incorrecte renvoie false.

    Relevé par la vérification
    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
  • Courbes ECDSA

    P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-256, P-384 et P-521 dans nist.js, secp256k1 dans secp256k1.js, et les courbes Brainpool r1 dans misc.js.

    Relevé par la vérification
    fact ecdsa-curves = P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed25519phEd25519ctxEd448Ed448ph

    Signature et vérification réussies avec Ed25519 et ses variantes ph et ctx, Ed448 et Ed448ph.

    Relevé par la vérification
    fact eddsa = Ed25519, Ed25519ph, Ed25519ctx, Ed448, Ed448ph
  • Algorithmes post-quantiques

    none

    Aucun algorithme post-quantique dans ce paquet.

    Relevé par la vérification
    fact pqc = none
    evidence pqc: exports of the package mention ML-DSA or ML-KEM: false
  • Formats de clé publique

    sec1-uncompressedsec1-compressed

    Les clés publiques sont des points SEC1, compressés ou non compressés ; un x‖y de 64 octets et le SPKI DER ont été rejetés.

    Relevé par la vérification
    fact public-key-formats = sec1-uncompressed, sec1-compressed
  • Format de clé publique par défaut

    sec1-compressed

    getPublicKey() renvoie le point compressé de 33 octets, sauf si vous passez false pour obtenir la forme non compressée.

    Relevé par la vérification
    fact default-public-key = sec1-compressed
  • Formats de clé privée

    raw-scalar

    Les clés secrètes sont le scalaire brut de 32 octets ; le PKCS#8 DER a été rejeté.

    Relevé par la vérification
    fact private-key-formats = raw-scalar
  • Signatures Schnorr

    BIP-340

    schnorr, dans secp256k1.js, implémente BIP-340 : des signatures de 64 octets sur des clés publiques x-only.

    Relevé par la vérification
    fact schnorr = BIP-340

Utilisation

Vérifier une signature DER issue de node:crypto avec les options qui rendent noble assez permissive pour l’accepter, puis signer de façon déterministe avec noble elle-même.
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);

Le banc de test des extraits de code exécute chaque exemple tel quel dans l’image de conteneur node:24.22.0-bookworm-slim, et l’exécution échoue si la sortie ne correspond pas.

Mise en place
npm init -y >/dev/null && npm install --no-audit --no-fund @noble/curves@2.4.0
Commande
node usage.mjs
La sortie doit correspondre à cette expression régulière
verified: true\ndeterministic: true 64

Bibliothèques