Zum Inhalt springen
ecdsa.com

Interop-Cheatsheet für Bibliotheken

Du signierst in Bibliothek A und prüfst in Bibliothek B – was geht dabei kaputt? Wähle die beiden Seiten und erhalte eine Checkliste der konkreten Abweichungen: Signaturkodierung, Umgang mit High-S, Format des öffentlichen Schlüssels und deterministische Nonces, jeweils mit der Codezeile, die das Problem behebt.

Statische Referenzdaten – keine Eingaben, keine Netzwerkaufrufe, nichts wird irgendwohin gesendet

openssl dgst / openssl pkeyutl

Browser, Deno, webcrypto von Node

Signierer
Verifizierer
Bibliothek
OpenSSL-CLI
WebCrypto (crypto.subtle)
Signatur
ASN.1 DER
r‖s im Raw-Format (P1363)
Low-S
Akzeptiert beide · signiert beides
Akzeptiert beide · signiert beides
RFC 6979
Optional aktivierbar
Nein – zufällige Nonce
Öffentlicher Schlüssel
SPKI PEM
JWK
Kurven
P-256, P-384, P-521, secp256k1
P-256, P-384, P-521

Signieren mit OpenSSL-CLI → Prüfen mit WebCrypto (crypto.subtle)

2 Probleme zu beheben, bevor es funktioniert
Scheitert

Signaturformate passen nicht: ASN.1 DER erzeugt, r‖s im Raw-Format erwartet

OpenSSL-CLI erzeugt standardmäßig ASN.1 DER, und WebCrypto (crypto.subtle) erwartet r‖s im Raw-Format. Am Schlüssel und an der Mathematik ist nichts falsch – der Verifizierer kann die Bytes schlicht nicht lesen, und ein DER-Blob variabler Länge hat für einen Parser mit fester Breite die falsche Länge.

Behebe es auf einer der beiden Seiten – beide Kodierungen tragen identische Informationen, die Konvertierung ist also verlustfrei und kommt ohne Kryptografie aus.

Seite des Signierersr‖s im Raw-Format aus OpenSSL-CLI ausgeben

Keine eingebaute Option

Die CLI hat keine P1363-Ausgabe für ECDSA. Nimm die beiden INTEGERs aus der DER-Struktur und fülle jeden links auf die Kurvengröße auf (32 Byte bei P-256) – `openssl asn1parse -inform DER -in sig.der` gibt r und s aus, sodass du die Konvertierung mit bloßem Auge prüfen kannst.

Seite des VerifizierersASN.1 DER in WebCrypto (crypto.subtle) akzeptieren

Keine eingebaute Option

Es gibt keinen DER-Modus. Teile die Ausgabe fester Länge in zwei Hälften, entferne bei jeder Hälfte führende Nullen und verpacke die beiden Ganzzahlen in eine DER-SEQUENCE, bevor du sie an einen Verifizierer im Stil von OpenSSL übergibst.

† Das Verhalten unterscheidet sich je nach Version oder Build – prüfe es in deiner Version.

Siehe auch: Konverter DER ⇄ Raw · ECDSA-Signaturformate: DER vs. Raw

Zu prüfen

Formate des öffentlichen Schlüssels passen nicht: SPKI PEM vs. JWK

OpenSSL-CLI exportiert den öffentlichen Schlüssel als SPKI PEM, und WebCrypto (crypto.subtle) erwartet ihn als JWK. Das sind tatsächlich verschiedene Container: SPKI verpackt den Punkt zusammen mit der Kurvenkennung, ein JWK schreibt die x- und y-Koordinate als base64url-Felder aus, und ein SEC1-Punkt besteht nur aus den Koordinatenbytes. Das Schlüsselmaterial darunter ist in allen drei identisch.

OpenSSL-CLI: `openssl ec -pubin -in pub.pem -pubout -outform DER` liefert denselben Schlüssel als rohes SPKI DER; `openssl ec -pubin -in pub.pem -text -noout` gibt den unkomprimierten Punkt im SEC1-Format in Hex aus.

WebCrypto (crypto.subtle): importKey("spki", derBytes, …) erwartet SPKI *DER*: Entferne zuerst PEM-Header und -Footer und dekodiere das base64, denn PEM-Text selbst wird abgelehnt.

Siehe auch: ECDSA-Signatur-Verifizierer

Gut zu wissen

Bleib bei P-256 / P-384 / P-521 – den Kurven, die beide Seiten unterstützen

Die Kurvenmengen stimmen nicht überein: secp256k1 nur in OpenSSL-CLI. Wähle einen Schlüssel auf einer gemeinsamen Kurve, dann stellt sich die Frage nie.

OpenSSL-CLI: Alles, was der Build aktiviert: die NIST-Primkurven, secp256k1, Brainpool und mehr – maßgeblich für dein Binary ist `openssl ecparam -list_curves`.

WebCrypto (crypto.subtle): Nur die drei NIST-Kurven. secp256k1 gehört nicht zur Spezifikation, daher brauchen Bitcoin- und Ethereum-Schlüssel stattdessen eine JavaScript-Bibliothek.

Gut zu wissen

OpenSSL-CLI nutzt eine zufällige Nonce, sofern du RFC 6979 nicht ausdrücklich aktivierst

Der übliche Signierweg nutzt eine zufällige Nonce. Seit Version 3.2 wählt der Signaturparameter `nonce-type:1` deterministisches ECDSA (RFC 6979), mit explizit angegebenem Digest: `openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` oder `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`.

Das ist korrektes Verhalten, kein Bug – es bedeutet aber, dass derselbe Schlüssel und dieselbe Nachricht bei jedem Lauf andere Bytes ergeben, sodass ein Test, der Signaturen Byte für Byte vergleicht, scheitert. Vergleiche stattdessen die Ergebnisse der Prüfung.

† Das Verhalten unterscheidet sich je nach Version oder Build – prüfe es in deiner Version.

Alle Bibliotheken nebeneinander

Nur Standardeinstellungen – jede Zeile lässt sich mit einem Flag oder einem anderen Aufruf ändern. Scrolle seitlich, um alle Spalten zu sehen.

BibliothekStandard-SignaturformatSo stellst du umLow-S-RichtlinieRFC 6979Öffentlicher Schlüssel (Standard)
OpenSSL-CLI
openssl dgst / openssl pkeyutl
Signierer
ASN.1 DER
ASN.1 DER – eine SEQUENCE aus zwei INTEGERs, daher ist eine P-256-Signatur meist 70–72 Byte lang, und ihre Länge ändert sich von Signatur zu Signatur.
Kein Raw-Modus für r‖s in der CLI – konvertiere die DER-Ausgabe extern.†
Akzeptiert beide
Keine Low-S-Richtlinie, weder beim Signieren noch beim Prüfen: Etwa die Hälfte der Signaturen hat s in der oberen Hälfte, und die Prüfung stört sich nie daran.
Optional aktivierbar
Der übliche Signierweg nutzt eine zufällige Nonce. Seit Version 3.2 wählt der Signaturparameter `nonce-type:1` deterministisches ECDSA (RFC 6979), mit explizit angegebenem Digest: `openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` oder `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`.†
SPKI PEM
SPKI PEM: der Block `-----BEGIN PUBLIC KEY-----`, den `openssl ec -pubout` schreibt. Beachte: `-----BEGIN EC PUBLIC KEY-----` gibt es nicht – ein Block mit EC-Label ist ein privater Schlüssel.
Node.js node:crypto
crypto.sign / crypto.verify / KeyObject
ASN.1 DER
ASN.1 DER, sofern du nicht ausdrücklich die andere Kodierung anforderst.
`dsaEncoding: "ieee-p1363"` am Schlüsselobjekt – setze es beim Signieren *und* beim Prüfen.
Akzeptiert beide
Akzeptiert beide Hälften und normalisiert nichts – darunter steckt OpenSSL.
Nein – zufällige Nonce
Zufällige Nonce: Derselbe Schlüssel und dieselbe Nachricht ergeben bei jedem Aufruf andere Signaturbytes.
SPKI PEM
`publicKey.export({ type: "spki", format: "pem" })` ist die übliche Form; JWK ist nur einen Aufruf entfernt: `export({ format: "jwk" })`.
WebCrypto (crypto.subtle)
Browser, Deno, webcrypto von Node
Verifizierer
r‖s im Raw-Format (P1363)
Nur r‖s im Raw-Format, mit fester Länge: 64 Byte bei P-256, 96 bei P-384, 132 bei P-521. Die Spezifikation definiert keine andere Kodierung.
Nichts umzustellen – WebCrypto spricht nie DER. Konvertiere außerhalb der API.
Akzeptiert beide
Keine Low-S-Regel beim Signieren oder Prüfen.
Nein – zufällige Nonce
Zufällige Nonce, und die API bietet keine Möglichkeit, eine eigene zu übergeben.
JWK
Beim Import und Export gibst du immer ein Format an: „jwk“, „spki“ (DER-Bytes) oder „raw“ (der unkomprimierte Punkt im SEC1-Format). JWK ist die Form, die der meiste Webcode weiterreicht.
Go crypto/ecdsa
SignASN1 / VerifyASN1 oder das big.Int-Paar
ASN.1 DER
`SignASN1` – der Weg, den `crypto.Signer` nutzt – gibt ASN.1 DER zurück. Das tiefer liegende `Sign` liefert r und s als `*big.Int`, was keinem der beiden Übertragungsformate entspricht, bis du es kodierst.
`SignASN1`/`VerifyASN1` für DER; `Sign`/`Verify` plus `FillBytes` für das Raw-Format mit fester Breite.
Akzeptiert beide
Überhaupt keine Low-S-Behandlung: Die Prüfung akzeptiert beide Hälften des Wertebereichs.
Nein – zufällige Nonce
Nicht kompatibel mit RFC 6979. Neuere Versionen leiten die Nonce aus Schlüssel, Nachricht und frischem Zufall gemeinsam ab, daher unterscheiden sich die Signaturbytes bei jedem Lauf und lassen sich in Tests nicht festschreiben.†
SPKI DER
`x509.MarshalPKIXPublicKey` erzeugt SPKI DER; zum PEM-Block fehlt nur noch ein `pem.EncodeToMemory`.
Python cryptography (pyca)
hazmat.primitives.asymmetric.ec
ASN.1 DER
DER – `private_key.sign(...)` gibt die ASN.1-SEQUENCE zurück.
`decode_dss_signature` / `encode_dss_signature` wandeln zwischen DER und dem Ganzzahlpaar um; das Padding auf feste Breite musst du selbst ergänzen.
Akzeptiert beide
Akzeptiert beide Hälften; nichts normalisiert s.
Optional aktivierbar
Standardmäßig eine zufällige Nonce. Neuere Releases akzeptieren am ECDSA-Objekt ein Flag für deterministisches Signieren, und dieser Weg braucht darunter ein aktuelles OpenSSL – prüfe, gegen welches OpenSSL dein Wheel gebaut wurde, bevor du dich darauf verlässt.†
SPKI PEM
`public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)` – derselbe PEM-Block, den OpenSSL schreibt.
Python ecdsa (reines Python)
SigningKey / VerifyingKey, sigencode= und sigdecode=
r‖s im Raw-Format (P1363)
r‖s im Raw-Format: `sign()` gibt die beiden Werte verkettet und mit Nullen auf die Kurvengröße aufgefüllt zurück.
Die Argumente `sigencode=` und `sigdecode=` wählen bei jedem Signier- und Prüfaufruf die Kodierung.
Akzeptiert beide
Beide Hälften werden akzeptiert. Die Bibliothek bringt außerdem kanonisierende Encoder mit, die s in die untere Hälfte zwingen – es gibt sie neben den einfachen, und sie müssen explizit ausgewählt werden.†
Optional aktivierbar
Deterministisches Signieren nach RFC 6979 gibt es als eigene Signiermethode; der einfache Aufruf sign() nutzt Zufall.
SEC1-Punkt
Schlüssel werden als nackter Punkt weitergegeben: `VerifyingKey.to_string()` liefert standardmäßig x‖y ohne Präfix 0x04, was Stacks auf SPKI-Basis so nicht akzeptieren.†
Java JCA (SunEC)
java.security.Signature
ASN.1 DER
`SHA256withECDSA` erzeugt ASN.1 DER.
Hänge `inP1363Format` an den Namen des Algorithmus an – gleiche Klasse, anderes Übertragungsformat.
Akzeptiert beide
Keine Low-S-Richtlinie, weder beim Signieren noch beim Prüfen.
Nein – zufällige Nonce
SunEC signiert mit einer zufälligen Nonce. Deterministisches ECDSA bekommst du über einen Provider eines Drittanbieters wie Bouncy Castle.†
SPKI DER
`publicKey.getEncoded()` liefert X.509 SPKI DER. Das JDK unterstützt kein PEM – das Verpacken in base64 ist deine Sache.
Rust p256 / k256 (RustCrypto)
ecdsa::Signature, SigningKey, VerifyingKey
r‖s im Raw-Format (P1363)
Der Standardtyp `Signature` ist das Paar mit fester Breite – 64 Byte bei P-256 und secp256k1. DER ist eine separate, explizite Konvertierung.†
Das Paar mit fester Breite ist der Standardtyp; die DER-Konvertierung ist in beiden Richtungen explizit.†
Akzeptiert beide
Das secp256k1-Crate normalisiert s beim Signieren in die untere Hälfte, passend zur Bitcoin-Konvention; die Crates für NIST-Kurven tun das nicht. Die Prüfung akzeptiert beide, also normalisiere auf der Signierseite, wenn die Gegenseite strikt ist.†
Ja, standardmäßig
Standardmäßig deterministische Nonces nach RFC 6979 – derselbe Schlüssel und dieselbe Nachricht ergeben immer dieselben Bytes. Eine randomisierte Variante lässt sich optional aktivieren.
SEC1-Punkt
Ein `VerifyingKey` wird zu einem SEC1-kodierten Punkt serialisiert – 33 Byte komprimiert, 65 unkomprimiert. SPKI DER und PEM gibt es über die zugehörigen SPKI/PKCS#8-Traits.†
libsecp256k1 (Bitcoin Core)
secp256k1_ecdsa_sign / _verify
Frei wählbar
Keines von beiden, bis du serialisierst: Eine Signatur steckt in einem opaken internen Struct, und erst bei der Ausgabe wählst du Compact (r‖s im Raw-Format) oder DER.
Serialisiere als Compact für r‖s im Raw-Format, als DER für das Bitcoin-Übertragungsformat – die Wahl gilt pro Aufruf.
Erzwingt Low-S
Die strikte Bibliothek. Beim Signieren entsteht immer Low-S, und die Prüfung lehnt eine High-S-Signatur rundweg ab – die Bibliothek bietet genau deshalb eine Normalisierungsfunktion, damit du Signaturen von einem Allzweck-Signierer reparieren kannst.
Ja, standardmäßig
Standardmäßig RFC 6979; bei Bedarf lässt sich eine andere Nonce-Funktion übergeben.
SEC1-Punkt
SEC1-Punkte: 33 Byte komprimiert oder 65 Byte unkomprimiert. Kein SPKI, kein PEM, kein JWK irgendwo in der API.
PHP openssl_*
openssl_sign / openssl_verify
ASN.1 DER
DER – `openssl_sign()` liefert genau das, was OpenSSL erzeugt hat.
Überhaupt kein Raw-Modus: Konvertiere im Userland oder nutze eine JOSE-Bibliothek, die das bereits erledigt.
Akzeptiert beide
Keine Low-S-Richtlinie – das Verhalten von OpenSSL wird unverändert übernommen.
Nein – zufällige Nonce
Zufällige Nonce; die Extension bietet keinen deterministischen Modus.
SPKI PEM
PEM-Strings rein und raus: `openssl_pkey_get_public()` nimmt einen Block `-----BEGIN PUBLIC KEY-----` oder ein ganzes Zertifikat entgegen.
Ruby OpenSSL
OpenSSL::PKey::EC
ASN.1 DER
DER – `key.sign(digest, data)` gibt die ASN.1-SEQUENCE zurück.
Kein eingebauter Raw-Modus: Konvertiere von Hand über OpenSSL::ASN1 oder überlass das einem JOSE-Gem.
Akzeptiert beide
Keine Low-S-Richtlinie – das Verhalten von OpenSSL, unverändert.
Nein – zufällige Nonce
Zufällige Nonce; eine deterministische Option gibt es nicht.
SPKI PEM
SPKI PEM. Neuere Versionen bieten eine eigene Exportmethode für den öffentlichen Schlüssel – es lohnt sich, sie zu nutzen, denn ein einfaches `to_pem` auf einem Schlüssel mit privatem Teil schreibt den privaten Schlüssel.†

† Das Verhalten unterscheidet sich je nach Version oder Build – prüfe es in deiner Version.

Weiterführendes

How it works

A signature that verifies in one library and fails in another is almost never wrong mathematically: the two sides simply disagree about packaging. An ECDSA signature is a pair of integers (r, s), and each ecosystem picked its own way to serialize them — ASN.1 DER in the OpenSSL and X.509 lineage, fixed-width r‖s in WebCrypto, JWS and most blockchain stacks. On top of that sit two more sources of disagreement: whether s must be in the lower half of the range (the low-S rule that Bitcoin enforces and general-purpose libraries ignore), and how the public key itself is wrapped — an SPKI structure, a JWK object, or the bare SEC1 point.

This page compares those defaults for a pair of libraries and lists only the differences that actually cause a failure, together with the switch or conversion that removes each one. Every row describes what a library does when you call the obvious API without extra arguments — not what it is capable of. Nearly all of it is configurable, which is exactly why two teams reading the same documentation end up incompatible, and why the fix is usually one option rather than new code.