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ètre | Valeur | Signification |
|---|---|---|
| p | 2²⁵⁶ − 2³² − 977 (0xffff…fffefffffc2f) | Nombre premier du corps ; sa forme rend la réduction modulo p peu coûteuse |
| a, b | 0, 7 | La courbe y² = x³ + 7 |
| G | x = 0x79be667e…16f81798, y = 0x483ada77…fb10d4b8 | Point de base |
| n | 0xffff…fffebaaedce6af48a03bbfd25e8cd0364141 | Ordre de G, premier |
| h | 1 | Cofacteur : 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 :
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")
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é :
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))
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 SafeCurves | secp256k1 | Ce que cela signifie pour vous |
|---|---|---|
| rho, transfert, rigidité | réussi | Le 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 | échec | Un code en temps constant et sans cas particuliers demande plus de soin que sur Curve25519 |
| indiscernabilité | échec | Les 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) :
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) :
02ou03selon 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éfixe04. Keccak-256 n’est pas SHA3-256 : le remplissage diffère, et utiliserhashlib.sha3_256donne 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 :
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 })}`);
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
| Plateforme | secp256k1 |
|---|---|
WebCrypto (navigateurs, Deno, crypto.subtle de Node) | Non ; utilisez @noble/curves ou un build WASM de libsecp256k1 |
OpenSSL et crypto de Node.js | Oui, avec du code générique (plus lent) |
| BoringSSL | Non : 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 PIV | Non |
| KMS cloud | AWS 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, WebAuthn | Non |
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 ‖ sde 64 octets et unr ‖ s ‖ vde 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
- SEC 2 v2.0 : paramètres de domaine recommandés pour les courbes elliptiques, §2.4.1 ; SEC 1 v2.0, §2.3.3 (encodage des points)
- NIST SP 800-186, annexe H.2
- SafeCurves : tableaux rho, discriminant et tordue (twist)
- Gallant, Lambert, Vanstone, « Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms », CRYPTO 2001 ; Hal Finney, Accélérer la vérification des signatures (2011) ; PR #20147 de Bitcoin Core
- Notes de version de Bitcoin Core 0.11.1 (politique de relais low-S) ; EIP-2
- RFC 8812 (
ES256K) - Services de clés : spécifications de clés AWS KMS, algorithmes de Google Cloud KMS, types de clés Azure Key Vault