Kurz gesagt. secp256k1 ist die elliptische Kurve y² = x³ + 7 über dem Primkörper mit p = 2²⁵⁶ − 2³² − 977, definiert im Jahr 2000 in SEC 2. Die Zahl der Punkte ihrer Gruppe ist eine Primzahl (Kofaktor 1), und die Sicherheit liegt bei etwa 128 Bit. Die Koeffizienten wurden auf Geschwindigkeit hin gewählt, nicht aus einem Seed erzeugt: a = 0 vereinfacht die Formeln, und die Kurve besitzt einen effizient berechenbaren Endomorphismus, der die Skalarmultiplikation beschleunigt. Bitcoin setzte sie ab dem ersten Commit 2009 ein, Ethereum folgte, und heute nutzen sie Nostr und die meisten EVM-Chains. In WebCrypto, TLS 1.3 und den meisten Arten sicherer Hardware fehlt sie; außerhalb von Blockchain-Protokollen solltest du P-256 oder Ed25519 bevorzugen. Über das reine ECDSA hinaus haben ihre Ökosysteme eigene Regeln: nur Low-S-Signaturen in der Relay-Policy von Bitcoin und in Ethereum-Transaktionen, Wiederherstellung des öffentlichen Schlüssels, statt Schlüssel zu übertragen, und Keccak-256 oder doppeltes SHA-256 als Hashfunktion.
Die Parameter
SEC 2 v2.0 §2.4.1 definiert die Kurve durch sechs Zahlen (SEC 2):
| Parameter | Wert | Bedeutung |
|---|---|---|
| p | 2²⁵⁶ − 2³² − 977 (0xffff…fffefffffc2f) | Primzahl des Körpers; ihre Form macht die Reduktion modulo p günstig |
| a, b | 0, 7 | Die Kurve y² = x³ + 7 |
| G | x = 0x79be667e…16f81798, y = 0x483ada77…fb10d4b8 | Basispunkt |
| n | 0xffff…fffebaaedce6af48a03bbfd25e8cd0364141 | Ordnung von G, eine Primzahl |
| h | 1 | Kofaktor: Jeder Punkt außer dem unendlich fernen Punkt erzeugt die ganze Gruppe |
Nichts davon musst du einfach glauben, und nichts stammt aus einem unerklärten Seed. Das folgende Skript prüft die Konstanten mit einfachen Python-Ganzzahlen:
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 und mul sind affine Punktaddition und Double-and-Add wie aus dem Lehrbuch (gut genug, um Konstanten zu prüfen, aber nicht für den Umgang mit Geheimnissen, denn zeitkonstant arbeiten beide nicht). Die Spur stimmt mit dem Wert überein, den SafeCurves veröffentlicht. Da n eine Primzahl ist und der Anzahl der Punkte entspricht, gibt es keine kleinen Untergruppen: Die Kofaktorprobleme, die Protokolle auf Curve25519 verkomplizieren, gibt es hier nicht.
Warum ihre Form sie schnell macht
Drei Eigenschaften dieser Zahlen sind gewollt:
- Die Primzahl ist 2²⁵⁶ minus eine kleine Zahl. Deshalb kostet die Reduktion eines 512-Bit-Produkts modulo p nur ein paar Multiplikationen mit 2³² + 977 statt einer allgemeinen Division.
- a = 0 spart bei jeder Punktverdopplung eine Multiplikation.
- Der Endomorphismus. Weil p ≡ 1 (mod 3) gilt, gibt es eine dritte Einheitswurzel β modulo p, und die Abbildung (x, y) → (βx, y) bildet jeden Punkt auf einen anderen Punkt der Kurve ab. Diese Abbildung entspricht der Multiplikation mit einer dritten Einheitswurzel λ modulo n. Ein Skalar k lässt sich dann in zwei Hälften von etwa 128 Bit zerlegen, k = k₁ + k₂λ, und kP als k₁P + k₂(λP) berechnen, wobei λP nur eine Multiplikation im Körper kostet. Das ist die GLV-Methode (Gallant, Lambert, Vanstone, 2001).
Dasselbe Skript bestimmt β und λ und prüft die Identität:
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
Das sind die Konstanten, die libsecp256k1 verwendet. Hal Finney beschrieb den Trick für Bitcoin in einem bitcointalk-Thread vom Februar 2011 und erwartete eine um 20 bis 25 % schnellere Prüfung; sein Code war der Ausgangspunkt von Pieter Wuilles libsecp256k1. Bitcoin Core schaltete die GLV-Optimierung mit dem Update von libsecp256k1 in PR #20147 (Bitcoin Core 0.21, 2021) standardmäßig ein; laut den Messungen dort wurde die Erstsynchronisierung mit aktivierter Signaturprüfung dadurch etwa 16 % schneller. Wie viel das alles bringt, hängt von der Bibliothek ab: OpenSSL hat keinen speziellen Code für secp256k1 und ist auf P-256 deutlich schneller (die Zahlen stehen in P-256 vs. secp256k1).
Warum Bitcoin und Ethereum sie verwenden
Der erste Commit von Bitcoin (August 2009) erzeugt Schlüssel mit EC_KEY_new_by_curve_name(NID_secp256k1) aus OpenSSL. Satoshi Nakamoto hat die Wahl nie begründet. Die üblicherweise genannten Gründe sind Eigenschaften der Kurve, keine Aussagen dessen, der sie einsetzte: Das Bitcoin-Wiki schreibt, sie sei „auf eine spezielle, nicht zufällige Weise konstruiert, die besonders effiziente Berechnungen ermöglicht“, und „anders als bei den populären NIST-Kurven wurden die Konstanten von secp256k1 auf vorhersagbare Weise gewählt“ (Bitcoin-Wiki). Ethereum behielt die Kurve von Bitcoin bei und tauschte die Hashfunktion aus: Transaktionen und Nachrichten werden mit Keccak-256 gehasht, und auch die Adresse wird damit aus dem Schlüssel abgeleitet.
Die Standardisierungsgremien zogen später nach. RFC 8812 (2020) registrierte ES256K für JOSE und COSE, und NIST SP 800-186 (2023) §H.2 hält fest, dass die Kurve „für Blockchain-bezogene Anwendungen verwendet werden darf“ (zu den für den allgemeinen Einsatz empfohlenen Kurven gehört sie nicht).
Wie stark sie ist
Der beste bekannte Angriff auf das Problem des diskreten Logarithmus in dieser Gruppe ist die Pollard-Rho-Methode. Laut SafeCurves braucht sie etwa 0,886·√n ≈ 2¹²⁷·⁸ Additionen, genauso viele wie bei P-256 – das ist die übliche „128-Bit-Sicherheit“. (Der Endomorphismus der Ordnung 3 kann Rho auf dieser Kurve noch etwas weiter beschleunigen, um etwa den Faktor √3, was am Gesamtbild nichts ändert.) SafeCurves stuft secp256k1 als nicht „sicher“ ein, allerdings aus Gründen, die Implementierungen betreffen, nicht den diskreten Logarithmus:
| SafeCurves-Kriterium | secp256k1 | Was es für dich bedeutet |
|---|---|---|
| Rho, Transfer, Starrheit (rigidity) | erfüllt | Der diskrete Logarithmus ist schwer, und die Konstanten sind erklärt |
| CM-Diskriminante | nicht erfüllt (D = −3) | Gerade die kleine Diskriminante erzeugt den Endomorphismus; ein Angriff folgt daraus nicht |
| Twist-Sicherheit | erfüllt, 2¹⁰⁹·⁵ | Niedriger als bei der Kurve selbst; relevant nur für Code, der die Punktvalidierung auslässt |
| Leiter (ladder), vollständige Formeln | nicht erfüllt | Zeitkonstanter Code ohne Ausnahmefälle verlangt mehr Sorgfalt als bei Curve25519 |
| Ununterscheidbarkeit | nicht erfüllt | Punkte sehen nicht wie zufällige Bitfolgen aus; das zählt bei zensurresistenten Protokollen, nicht bei Signaturen |
Die Konsequenz für die Praxis ist dieselbe wie bei P-256: Verwende eine Bibliothek, die für diese Kurve geschrieben wurde (libsecp256k1, @noble/curves, k256 in Rust, das Binding von go-ethereum), prüfe öffentliche Schlüssel bei der Eingabe und schreib nie deine eigene Skalarmultiplikation für geheime Werte. Kommt ein Schlüssel als Punkt an, musst du vor der Verwendung prüfen, ob er auf y² = x³ + 7 liegt; fehlt diese Prüfung, ermöglicht das den klassischen Invalid-Curve-Angriff, und genau dort spielt der Twist-Wert eine Rolle. Und wie bei jeder ECDSA-Kurve verrät eine wiederholte oder verzerrte Nonce den Schlüssel (ECDSA-Nonce-Wiederverwendung).
Schlüssel und Adressen
Ein privater Schlüssel ist eine Zahl d in [1, n − 1]; der öffentliche Schlüssel ist der Punkt Q = dG. Dasselbe Q lässt sich auf mehrere Arten schreiben, und Verwechslungen dabei sind eine häufige Ursache für Fehler der Art „falsche Adresse“. Ein Beispiel mit d = 1, dessen öffentlicher Schlüssel G selbst ist (ein Schlüssel, den jeder kennt – verwende ihn also nie):
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
- Komprimiert (SEC 1 §2.3.3):
02oder03für gerades oder ungerades y, danach x. Bitcoin verwendet komprimierte Schlüssel seit über einem Jahrzehnt standardmäßig. Zum Dekomprimieren braucht es eine Quadratwurzel modulo p, die für dieses p (p ≡ 3 mod 4) eine einzige Potenzierung ist, y = (x³ + 7)^((p+1)/4). - Unkomprimiert:
04 ‖ x ‖ y. Derselbe Schlüssel ergibt in dieser Form eine andere Bitcoin-Adresse; deshalb zeigen alte Wallets, die unkomprimierte Schlüssel verwendeten, für dasselbe Geheimnis andere Adressen an. - x-only (32 Byte): Taproot und Nostr verwenden BIP-340-Schnorr-Signaturen, deren Schlüssel die y-Koordinate weglassen und ein gerades y voraussetzen.
- Ethereum-Adresse: die letzten 20 Byte von Keccak-256 über das 64 Byte lange
x ‖ y, ohne das Präfix04. Keccak-256 ist nicht SHA3-256: Das Padding unterscheidet sich, und mithashlib.sha3_256erhältst du falsche Adressen.
Low-S: eine Signatur, zwei gültige Formen
Eine ECDSA-Signatur (r, s) hat einen Zwilling (r, n − s), der die Prüfung genauso besteht, weil die Negation des Nonce-Punkts r nicht verändert. Bei P-256 in TLS kümmert das niemanden. Bei Bitcoin war es wichtig: Der Zwilling ergab für dieselbe Transaktion eine andere Transaktions-ID, und Dienste, die Zahlungen anhand der ID verfolgten, ließen sich täuschen. Bitcoin Core 0.11.1 (2015) ließ den Knoten „beim Weiterleiten oder Mining die kanonische ‚Low-S‘-Kodierung für ECDSA-Signaturen verlangen“ (Release Notes) – eine Relay-Policy, keine Konsensregel. Ethereum ging mit EIP-2 (Homestead, 2016) noch weiter: „Alle Transaktionssignaturen, deren s-Wert größer als secp256k1n/2 ist, gelten nun als ungültig“, während „das ECDSA-Recover-Precompile unverändert bleibt und weiterhin High-S-Werte akzeptiert“.
Allgemeine Bibliotheken halten sich nicht an diese Regeln, Blockchain-Bibliotheken schon. Im Labor wurden 1.000 zufällige Nachrichten mit dem OpenSSL von Node.js signiert und anschließend eine High-S-Signatur und ihr Low-S-Zwilling mit drei Bibliotheken geprüft:
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
Etwa die Hälfte der OpenSSL-Signaturen ist High-S, OpenSSL akzeptiert beide Formen, @noble/curves lehnt High-S auf secp256k1 ab, sofern du es nicht anders einstellst, und ethers weigert sich sogar, aus einer solchen Signatur überhaupt ein Signaturobjekt zu erzeugen. Ein System, das mit einer allgemeinen Bibliothek oder einem HSM signiert und an eine Blockchain übermittelt, muss normalisieren: Ist s > n/2, ersetze es durch n − s und kehre das Recovery-Bit um. Google Cloud KMS dokumentiert, dass seine secp256k1-Schlüssel nur Low-S-Signaturen liefern; die Dokumentation von AWS KMS zu ECC_SECG_P256K1 verspricht das nicht, also normalisiere dessen Ausgabe selbst. Die DER-Kodierung und die rohe Kodierung, in denen diese Signaturen transportiert werden, behandelt der Artikel ECDSA-Signaturformate.
Wiederherstellung des öffentlichen Schlüssels
Aus einer Signatur, dem Hash der Nachricht und ein oder zwei zusätzlichen Bits lässt sich der öffentliche Schlüssel des Signierers berechnen. Deshalb enthalten Ethereum-Transaktionen überhaupt keinen öffentlichen Schlüssel, und signierte Bitcoin-Nachrichten werden gegen eine Adresse geprüft. Die zusätzlichen Bits sind die Recovery-ID (v, yParity oder das Header-Byte einer Bitcoin-Nachrichtensignatur). Wie die Wiederherstellung funktioniert, was dabei schiefgehen kann und wie sich ecrecover verhält, erklärt ECDSA-Public-Key-Recovery.
Wo sie unterstützt wird und wo nicht
| Plattform | secp256k1 |
|---|---|
WebCrypto (Browser, Deno, crypto.subtle in Node) | Nein; verwende @noble/curves oder einen WASM-Build von libsecp256k1 |
OpenSSL und crypto in Node.js | Ja, mit generischem (langsamerem) Code |
| BoringSSL | Nein: Eingebaut sind nur P-224, P-256, P-384 und P-521 |
| Java (SunEC) | In JDK 16 entfernt: Curve not supported: secp256k1; verwende Bouncy Castle |
| Apple CryptoKit, Secure Enclave, Android-Keystore, TPM 2.0, PIV-Karten | Nein |
| Cloud-KMS | AWS KMS ECC_SECG_P256K1; Google Cloud KMS EC_SIGN_SECP256K1_SHA256 (nur mit HSM-Schutz); Azure Key Vault P-256K mit ES256K |
| TLS 1.3, X.509 in der Web-PKI, WebAuthn | Nein |
Diese Lücke bei sicherer Hardware in Smartphones und Laptops ist der Grund, warum mobile Wallets secp256k1-Schlüssel in Software halten (geschützt durch einen Schlüssel, der sehr wohl in Hardware liegt) oder auf eine separate Hardware-Wallet setzen.
Häufige Fehler
- Hashen mit SHA3-256 statt Keccak-256 bei Ethereum oder mit einfachem SHA-256 statt des doppelten SHA-256, das Bitcoin für Nachrichten und Transaktionen verwendet. Die Signatur wird dann gegen eine andere Nachricht geprüft, und die Wiederherstellung liefert eine andere, gültig aussehende Adresse.
- High-S-Signaturen an Ethereum oder das Bitcoin-Netzwerk übermitteln, wie sie OpenSSL, ein HSM oder ein Cloud-KMS liefern. Normalisiere sie vorher.
- Komprimierte und unkomprimierte Schlüssel verwechseln, wenn du Bitcoin-Adressen ableitest.
- Rohe Bytelängen. Ein 64 Byte langes
r ‖ sund ein 65 Byte langesr ‖ s ‖ vsind verschiedene Formate; Bibliotheken melden das unmissverständlich, etwa ethers mit invalid raw signature length. - Plattformunterstützung voraussetzen. Code, der unter Java 11 lief, scheitert unter Java 17; WebCrypto hatte die Kurve nie.
- secp256k1 verwenden, wo nichts es verlangt. Für Tokens, TLS, Geräteidentität oder Passkeys bringt die Kurve keinen Vorteil und kostet die Unterstützung durch Hardware.
Quellen
- SEC 2 v2.0: Empfohlene Domainparameter elliptischer Kurven, §2.4.1; SEC 1 v2.0, §2.3.3 (Punktkodierung)
- NIST SP 800-186, Anhang H.2
- SafeCurves: Tabellen zu Rho, Diskriminante und Twist
- Gallant, Lambert, Vanstone, „Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms“, CRYPTO 2001; Hal Finney, Signaturprüfung beschleunigen (2011); Bitcoin Core PR #20147
- Release Notes zu Bitcoin Core 0.11.1 (Low-S-Relay-Policy); EIP-2
- RFC 8812 (
ES256K) - Schlüsseldienste: Schlüsselspezifikationen von AWS KMS, Algorithmen von Google Cloud KMS, Schlüsseltypen in Azure Key Vault