Aller au contenu
ecdsa.com
← Tous les articles

secp256k1 expliquée : la courbe de Bitcoin et d’Ethereum

y² = x³ + 7 modulo un nombre premier de 256 bits. Nous vérifions chaque constante de SEC 2 en quelques lignes de Python, montrons le raccourci sur lequel repose libsecp256k1 et testons quelles bibliothèques acceptent une signature high-S, le tout exécuté le 20 octobre 2026.

En bref. secp256k1 est la courbe elliptique y² = x³ + 7 sur le corps premier p = 2²⁵⁶ − 2³² − 977, définie dans SEC 2 en 2000. Son groupe compte un nombre premier de points (cofacteur 1) et offre une sécurité d’environ 128 bits. Ses coefficients ont été choisis pour la vitesse, et non générés à partir d’une graine : a = 0 simplifie les formules, et la courbe possède un endomorphisme efficacement calculable qui accélère la multiplication scalaire. Bitcoin l’a utilisée dès son premier commit, en 2009 ; Ethereum a suivi, et Nostr et la plupart des chaînes EVM l’utilisent aujourd’hui. Elle ne figure ni dans WebCrypto, ni dans TLS 1.3, ni dans la plupart des matériels sécurisés ; en dehors des protocoles de blockchain, préférez P-256 ou Ed25519. Par-dessus l’ECDSA ordinaire, ses écosystèmes ajoutent leurs propres règles : uniquement des signatures low-S dans la politique de relais de Bitcoin et dans les transactions Ethereum, la récupération de la clé publique au lieu de la transmission des clés, et Keccak-256 ou un double SHA-256 comme fonction de hachage.

Les paramètres

SEC 2 v2.0 §2.4.1 définit la courbe par six nombres (SEC 2) :

ParamètreValeurSignification
p2²⁵⁶ − 2³² − 977 (0xffff…fffefffffc2f)Nombre premier du corps ; sa forme rend la réduction modulo p peu coûteuse
a, b0, 7La courbe y² = x³ + 7
Gx = 0x79be667e…16f81798, y = 0x483ada77…fb10d4b8Point de base
n0xffff…fffebaaedce6af48a03bbfd25e8cd0364141Ordre de G, premier
h1Cofacteur : tout point autre que le point à l’infini engendre le groupe entier

Rien ici n’a besoin d’être admis sur parole, et rien ne provient d’une graine inexpliquée. Le script ci-dessous vérifie les constantes avec de simples entiers Python :

curve.py (extrait) : vérifications des paramètres de SEC 2, Python 3.14.8
G = (Gx, Gy)
print("p == 2^256 - 2^32 - 977:", hex(p) == "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f")
print("G is on y^2 = x^3 + 7:  ", (Gy * Gy - Gx**3 - b) % p == 0)
print("n is a probable prime:  ", probable_prime(n))
print("n * G is infinity:      ", mul(n, G) is None)
t = p + 1 - n
print("trace t = p + 1 - n:    ", t)
print("Hasse bound |t| <= 2*sqrt(p):", t * t <= 4 * p)
print("n < p, p - n has", (p - n).bit_length(), "bits: the group has n points, cofactor 1")
Sortie (python:3.14.8-slim-trixie)
p == 2^256 - 2^32 - 977: True
G is on y^2 = x^3 + 7:   True
n is a probable prime:   True
n * G is infinity:       True
trace t = p + 1 - n:     432420386565659656852420866390673177327
Hasse bound |t| <= 2*sqrt(p): True
n < p, p - n has 129 bits: the group has n points, cofactor 1

add et mul sont, comme dans les manuels, l’addition de points en coordonnées affines et la multiplication par doublements et additions successifs (double-and-add) : elles suffisent pour vérifier des constantes, pas pour manipuler des secrets, car elles ne s’exécutent pas en temps constant. La trace correspond à la valeur publiée par SafeCurves. Comme n est premier et égal au nombre de points, il n’existe aucun petit sous-groupe : les problèmes de cofacteur qui compliquent les protocoles fondés sur Curve25519 n’existent pas ici.

Pourquoi sa forme la rend rapide

Trois propriétés de ces nombres sont délibérées :

  • Le nombre premier vaut 2²⁵⁶ moins un petit nombre : réduire un produit de 512 bits modulo p ne demande donc que quelques multiplications par 2³² + 977, au lieu d’une division générale.
  • a = 0 supprime une multiplication de chaque doublement de point.
  • L’endomorphisme. Comme p ≡ 1 (mod 3), il existe une racine cubique de l’unité β modulo p, et l’application (x, y) → (βx, y) envoie tout point sur un autre point de la courbe. Cette application revient à multiplier par une racine cubique de l’unité λ modulo n. Un scalaire k peut alors être découpé en deux moitiés d’environ 128 bits, k = k₁ + k₂λ, et kP calculé comme k₁P + k₂(λP), où λP coûte une seule multiplication dans le corps. C’est la méthode GLV (Gallant, Lambert, Vanstone, 2001).

Le même script trouve β et λ, puis vérifie l’identité :

curve.py (extrait) : l’endomorphisme
print("p % 3 == 1 (cube roots of unity exist mod p):", p % 3 == 1)
beta = pow(2, (p - 1) // 3, p)
lam = pow(3, (n - 1) // 3, n)
# Pick the lambda that matches this beta (there are two non-trivial cube roots of unity mod n).
if mul(lam, G) != (beta * Gx % p, Gy):
    lam = lam * lam % n
print("beta^3 == 1 mod p, lambda^3 == 1 mod n:", pow(beta, 3, p) == 1, pow(lam, 3, n) == 1)
print("lambda * G == (beta * Gx, Gy):        ", mul(lam, G) == (beta * Gx % p, Gy))
print("beta   =", hex(beta))
print("lambda =", hex(lam))
Sortie
p % 3 == 1 (cube roots of unity exist mod p): True
beta^3 == 1 mod p, lambda^3 == 1 mod n: True True
lambda * G == (beta * Gx, Gy):         True
beta   = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee
lambda = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72

Ce sont les constantes qu’utilise libsecp256k1. Hal Finney a décrit l’astuce pour Bitcoin dans un fil bitcointalk de février 2011 et s’attendait à une vérification plus rapide de 20 à 25 % ; son code a été le point de départ de libsecp256k1, la bibliothèque de Pieter Wuille. Bitcoin Core a activé par défaut l’optimisation GLV avec la mise à jour de libsecp256k1 de la PR #20147 (Bitcoin Core 0.21, 2021), où le gain mesuré atteint environ 16 % sur la synchronisation initiale, vérification des signatures activée. L’importance de tout cela dépend de la bibliothèque : OpenSSL n’a pas de code spécifique à secp256k1 et se montre bien plus rapide sur P-256 (les chiffres figurent dans P-256 vs secp256k1).

Pourquoi Bitcoin et Ethereum l’utilisent

Le premier commit de Bitcoin (août 2009) crée les clés avec EC_KEY_new_by_curve_name(NID_secp256k1) d’OpenSSL. Satoshi Nakamoto n’a jamais expliqué ce choix. Les raisons généralement avancées sont des propriétés de la courbe plutôt que des explications de celui qui l’a adoptée : le wiki Bitcoin note qu’elle a été « construite d’une manière particulière, non aléatoire, qui permet un calcul particulièrement efficace » et que, « contrairement aux courbes NIST répandues, les constantes de secp256k1 ont été choisies de manière prévisible » (wiki Bitcoin). Ethereum a conservé la courbe de Bitcoin et remplacé la fonction de hachage : les transactions et les messages sont hachés avec Keccak-256, et l’adresse est dérivée de la clé avec cette même fonction.

Les organismes de normalisation ont suivi plus tard. La RFC 8812 (2020) a enregistré ES256K pour JOSE et COSE, et NIST SP 800-186 (2023) §H.2 indique que la courbe « est autorisée pour des applications liées à la blockchain » (elle ne fait pas partie des courbes recommandées pour un usage général).

Quel niveau de sécurité elle offre

La meilleure attaque connue contre le problème du logarithme discret dans ce groupe est l’algorithme rho de Pollard, qui demande environ 0,886·√n ≈ 2¹²⁷·⁸ additions selon SafeCurves, soit le même chiffre que pour P-256 et la « sécurité de 128 bits » habituelle. (L’endomorphisme d’ordre 3 permet d’accélérer encore un peu rho sur cette courbe, d’un facteur d’environ √3, ce qui ne change pas le constat.) SafeCurves ne classe pas secp256k1 parmi les courbes « sûres », mais pour des raisons qui concernent les implémentations, pas le logarithme discret :

Critère SafeCurvessecp256k1Ce que cela signifie pour vous
rho, transfert, rigiditéréussiLe logarithme discret est difficile et les constantes sont expliquées
discriminant CMéchec (D = −3)C’est ce petit discriminant qui crée l’endomorphisme ; il n’en découle aucune attaque
sécurité de la tordue (twist)réussi, 2¹⁰⁹·⁵Inférieure à celle de la courbe elle-même ; ne compte que pour du code qui omet la validation des points
échelle (ladder), formules complèteséchecUn code en temps constant et sans cas particuliers demande plus de soin que sur Curve25519
indiscernabilitééchecLes points ne ressemblent pas à des chaînes aléatoires, ce qui compte pour les protocoles résistants à la censure, pas pour les signatures

La leçon pratique est la même que pour P-256 : utilisez une bibliothèque écrite pour cette courbe (libsecp256k1, @noble/curves, k256 en Rust, le binding de go-ethereum), validez les clés publiques en entrée et n’écrivez jamais votre propre multiplication scalaire pour des secrets. Une clé reçue sous forme de point doit être vérifiée avant usage : elle doit se trouver sur y² = x³ + 7. Omettre ce contrôle, c’est s’exposer à la classique attaque par courbe invalide, et c’est là qu’intervient le chiffre de la tordue. Enfin, comme pour toute courbe ECDSA, un nonce répété ou biaisé révèle la clé (Réutilisation de nonce ECDSA).

Clés et adresses

Une clé privée est un nombre d dans [1, n − 1] ; la clé publique est le point Q = dG. Le même Q s’écrit de plusieurs façons, et les confondre est une source fréquente de bogues du type « mauvaise adresse ». Avec d = 1, dont la clé publique est G lui-même (une clé que tout le monde connaît, donc à ne jamais utiliser) :

keys.py : une clé, plusieurs encodages (pycryptodome 3.23.0 pour Keccak et RIPEMD-160)
compressed   (33 bytes): 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
uncompressed (65 bytes): 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
x-only, BIP-340 (32 bytes): 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Ethereum address, keccak256(x || y)[12:]: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
Bitcoin P2PKH from the compressed key: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
Bitcoin P2PKH from the uncompressed key: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
  • Forme compressée (SEC 1 §2.3.3) : 02 ou 03 selon que y est pair ou impair, puis x. Bitcoin utilise des clés compressées par défaut depuis plus de dix ans. La décompression exige une racine carrée modulo p, qui, pour ce p (p ≡ 3 mod 4), se calcule en une seule exponentiation, y = (x³ + 7)^((p+1)/4).
  • Forme non compressée : 04 ‖ x ‖ y. Sous cette forme, la même clé donne une adresse Bitcoin différente, ce qui explique pourquoi les anciens portefeuilles qui utilisaient des clés non compressées affichent d’autres adresses pour le même secret.
  • x-only (32 octets) : Taproot et Nostr utilisent les signatures Schnorr BIP-340, dont les clés omettent la coordonnée y et supposent un y pair.
  • Adresse Ethereum : les 20 derniers octets du condensat Keccak-256 des 64 octets x ‖ y, sans le préfixe 04. Keccak-256 n’est pas SHA3-256 : le remplissage diffère, et utiliser hashlib.sha3_256 donne de mauvaises adresses.

Low-S : une signature, deux formes valides

Une signature ECDSA (r, s) a une jumelle (r, n − s) qui se vérifie tout aussi bien, car prendre l’opposé du point de nonce ne change pas r. Pour P-256 dans TLS, cela n’intéresse personne. Pour Bitcoin, cela comptait : la jumelle produisait un autre identifiant de transaction pour la même transaction, et les services qui suivaient les paiements par identifiant pouvaient être trompés. Avec Bitcoin Core 0.11.1 (2015), le nœud a commencé à « exiger l’encodage canonique “low-s” des signatures ECDSA lors du relais ou du minage » (notes de version), une politique de relais plutôt qu’une règle de consensus. Ethereum est allé plus loin avec EIP-2 (Homestead, 2016) : « Toutes les signatures de transaction dont la valeur s est supérieure à secp256k1n/2 sont désormais considérées comme invalides », tandis que « le contrat précompilé de récupération ECDSA reste inchangé et continuera d’accepter les valeurs de s élevées ».

Les bibliothèques généralistes ne suivent pas ces règles, contrairement aux bibliothèques de blockchain. Le laboratoire a signé 1000 messages aléatoires avec l’OpenSSL de Node.js, puis vérifié une signature high-S et sa jumelle low-S avec trois bibliothèques :

lows.mjs (extrait) : node 24.22.0, @noble/curves 2.4.0, ethers 6.17.0
const { msg, sig, s } = sample;
const low = Buffer.concat([sig.subarray(0, 32), Buffer.from((n - s).toString(16).padStart(64, "0"), "hex")]);
const opensslOk = (x) => verify("sha256", msg, { key: publicKey, dsaEncoding: "ieee-p1363" }, x);
console.log(`node:crypto verify  high-S: ${opensslOk(sig)}  low-S twin: ${opensslOk(low)}`);
console.log(`noble default       high-S: ${secp256k1.verify(sig, msg, pub)}  low-S twin: ${secp256k1.verify(low, msg, pub)}`);
console.log(`noble lowS: false   high-S: ${secp256k1.verify(sig, msg, pub, { lowS: false })}`);
Sortie (node:24.22.0-bookworm-slim)
OpenSSL 3.5.8: 494 of 1000 signatures have s > n/2
node:crypto verify  high-S: true  low-S twin: true
noble default       high-S: false  low-S twin: true
noble lowS: false   high-S: true
ethers 6.17.0 v=27 high-S: non-canonical s; use ._s

Environ la moitié des signatures d’OpenSSL sont high-S ; OpenSSL accepte les deux formes, @noble/curves rejette le high-S sur secp256k1 sauf indication contraire, et ethers refuse même de construire un objet signature à partir d’une telle signature. Un système qui signe avec une bibliothèque généraliste ou un HSM et soumet le résultat à une blockchain doit normaliser : si s > n/2, remplacez s par n − s et inversez le bit de récupération. Google Cloud KMS indique dans sa documentation que ses clés secp256k1 ne renvoient que des signatures low-S ; la documentation de ECC_SECG_P256K1 chez AWS KMS ne fait aucune promesse de ce genre : normalisez donc vous-même sa sortie. Les encodages DER et brut dans lesquels circulent ces signatures sont traités dans Formats de signature ECDSA.

Récupération de la clé publique

À partir d’une signature, du condensat du message et d’un ou deux bits supplémentaires, on peut calculer la clé publique du signataire : c’est pourquoi les transactions Ethereum ne transportent aucune clé publique et les messages signés Bitcoin se vérifient par rapport à une adresse. Ces bits supplémentaires forment l’identifiant de récupération (v, yParity ou l’octet d’en-tête d’une signature de message Bitcoin). Le fonctionnement de la récupération, ce qui peut mal tourner et le comportement d’ecrecover sont expliqués dans Récupération de clé publique ECDSA.

Où elle est prise en charge, et où elle ne l’est pas

Plateformesecp256k1
WebCrypto (navigateurs, Deno, crypto.subtle de Node)Non ; utilisez @noble/curves ou un build WASM de libsecp256k1
OpenSSL et crypto de Node.jsOui, avec du code générique (plus lent)
BoringSSLNon : elle n’intègre que P-224, P-256, P-384 et P-521
Java (SunEC)Supprimée dans le JDK 16 : Curve not supported: secp256k1 ; utilisez Bouncy Castle
Apple CryptoKit, Secure Enclave, Android Keystore, TPM 2.0, cartes PIVNon
KMS cloudAWS KMS ECC_SECG_P256K1 ; Google Cloud KMS EC_SIGN_SECP256K1_SHA256 (protection HSM uniquement) ; Azure Key Vault P-256K avec ES256K
TLS 1.3, X.509 dans la PKI du Web, WebAuthnNon

C’est cette lacune du matériel sécurisé des téléphones et des ordinateurs portables qui explique pourquoi les portefeuilles mobiles conservent les clés secp256k1 de façon logicielle (protégées par une clé qui, elle, réside dans le matériel) ou s’appuient sur un portefeuille matériel distinct.

Erreurs fréquentes

  • Hacher avec SHA3-256 au lieu de Keccak-256 pour Ethereum, ou avec un simple SHA-256 au lieu du double SHA-256 qu’utilise Bitcoin pour les messages et les transactions. La signature se vérifie alors par rapport à un autre message, et la récupération produit une autre adresse, d’apparence valide.
  • Soumettre des signatures high-S produites par OpenSSL, un HSM ou un KMS cloud à Ethereum ou au réseau Bitcoin. Normalisez d’abord.
  • Mélanger clés compressées et non compressées lors de la dérivation d’adresses Bitcoin.
  • Longueur en octets des signatures brutes. Un r ‖ s de 64 octets et un r ‖ s ‖ v de 65 octets sont des formats différents ; les bibliothèques le font savoir haut et fort, comme ethers avec invalid raw signature length.
  • Supposer la prise en charge par la plateforme. Du code qui fonctionnait sous Java 11 échoue sous Java 17 ; WebCrypto n’a jamais eu cette courbe.
  • Utiliser secp256k1 là où rien ne l’impose. Pour les jetons, TLS, l’identité des appareils ou les clés d’accès, elle n’apporte aucun avantage et fait perdre la prise en charge matérielle.

Sources