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
Signieren mit OpenSSL-CLI → Prüfen mit WebCrypto (crypto.subtle)
2 Probleme zu beheben, bevor es funktioniertSignaturformate 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.
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.
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
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
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.
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.
| Bibliothek | Standard-Signaturformat | So stellst du um | Low-S-Richtlinie | RFC 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
- Konverter DER ⇄ Raw – füge die Signatur ein, die du schon hast, und erhalte die andere Kodierung, mit r, s und High-S-Prüfung.
- ECDSA-Signatur-Verifizierer – kläre die Streitfrage, ob die Signatur überhaupt gültig ist, unabhängig von beiden Bibliotheken.
- ECDSA-Signaturformate: DER vs. Raw (P1363) – die Hintergründe auf Byte-Ebene zu jeder Zeile der Tabelle.
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.