Eine Signatur besteht aus zwei ganzen Zahlen
Egal, welche Bytes du auf der Leitung siehst: Mathematisch ist eine ECDSA-Signatur nur ein Paar positiver ganzer Zahlen (r, s), jede zwischen 1 und n − 1, wobei n die Ordnung der Kurve ist (bei P-256 und secp256k1 eine Zahl nahe 2²⁵⁶). Alles andere ist Serialisierung – und das Ökosystem hat sich auf zwei inkompatible Wege dafür festgelegt. Keiner enthält eine Information, die dem anderen fehlt; die Umwandlung zwischen beiden ist verlustfrei und rein mechanisch.
Format 1: ASN.1 DER – das Erbe von X.509
Das Format, das OpenSSL, X.509-Zertifikate und TLS verwenden, stammt aus der ASN.1-Welt: Das Paar wird in DER als SEQUENCE aus zwei Werten vom Typ INTEGER kodiert. Ein kommentiertes Beispiel für P-256:
30 45 SEQUENCE, 0x45 = 69 bytes follow
02 21 INTEGER, 0x21 = 33 bytes (this is r)
00 c8 7f 3b ... 33 bytes: leading 00 because r's top bit is set
02 20 INTEGER, 0x20 = 32 bytes (this is s)
6a 12 09 ... 32 bytes: no padding needed, top bit is clear
Die Regeln von DER für ganze Zahlen machen das Format etwas knifflig:
- Ganze Zahlen sind Big-Endian, im Zweierkomplement und von minimaler Länge: Führende Nullbytes müssen entfernt werden.
- Die Werte sind aber positiv – ist also das höchstwertige Bit des ersten Bytes gesetzt (erstes Byte ≥
0x80), muss ein einzelnes0x00vorangestellt werden, damit die Zahl nicht als negativ gelesen wird.rundstragen dieses Padding-Byte jeweils unabhängig voneinander – oder eben nicht. - Das Ergebnis ist eine Kodierung variabler Länge: Bei P-256 sind es typischerweise 70, 71 oder 72 Byte (gelegentlich weniger, wenn
roderszufällig mit Nullbytes beginnt). Code, der eine feste Signaturlänge annimmt, ist ein Bug, der nur auf den falschen Zufallswert wartet. - DER ist die eindeutige („distinguished“) Kodierung: genau eine gültige Bytefolge pro Wert. Strikte Parser lehnen nicht minimale ganze Zahlen, Padding außerhalb der Spezifikation oder angehängten Datenmüll ab – Bitcoin hat striktes DER gerade deshalb zur Konsensregel gemacht (BIP-66), um Abweichungen zwischen Parsern auszuschließen.
Format 2: Raw r‖s – IEEE P1363, das Format mit fester Länge
Die andere Konvention verzichtet ganz auf ASN.1: r und s werden jeweils genau auf die Koordinatengröße der Kurve aufgefüllt, Big-Endian, und aneinandergehängt. Oft heißt das Format IEEE P1363 (nach dem Standard, der es verwendet hat), „raw“ oder „flaches“ r‖s:
| Kurve | Größe eines Körperelements | Größe der Raw-Signatur |
|---|---|---|
| P-256 / secp256k1 | 32 Byte | 64 Byte |
| P-384 | 48 Byte | 96 Byte |
| P-521 | 66 Byte | 132 Byte |
Feste Länge, keine Tags, keine Padding-Regeln – teile den Blob in der Mitte, und du hast deine Zahlen. Dieses Format gibt WebCrypto aus, RFC 7518 §3.4 schreibt es für JWS vor (und damit für JWTs – die praktischen Folgen behandelt ES256 vs. RS256 für JWTs), und mit dieser Form arbeiten die meisten Blockchain-Stacks intern: Ethereum-Signaturen haben die Form r‖s‖v mit 65 Byte (das Raw-Paar plus ein Recovery-Byte), während Bitcoin, wie oben erwähnt, Transaktionssignaturen in striktem DER kodiert.
Der Fehler tritt in beide Richtungen auf und ist extrem häufig: Eine Raw-Signatur mit 64 Byte, die an eine API geht, die DER erwartet, scheitert am ASN.1-Parser (0x30 erwartet), während ein DER-Blob mit ~71 Byte, der an WebCrypto oder einen JWS-Verifizierer geht, schlicht die falsche Länge hat und sauber mit einem generischen „invalid signature“ fehlschlägt. Wenn sich zwei Stacks nicht einig sind, wandle explizit um – der Konverter DER ⇄ Raw erledigt das im Browser in beide Richtungen und zeigt die dekodierten Werte r und s.
High-S, Low-S und warum eine gültige Signatur abgelehnt wird
Selbst bei übereinstimmender Kodierung lässt eine weitere Feinheit die Prüfung zwischen verschiedenen Stacks scheitern. ECDSA hat eine eingebaute Symmetrie: Ist (r, s) eine gültige Signatur, dann ist (r, n − s) eine gültige Signatur für dieselbe Nachricht unter demselben Schlüssel. Wer eine Signatur besitzt, kann sie ohne den privaten Schlüssel in die andere Form umklappen – die Signatur-Bytes sind formbar (malleable), obwohl der signierte Inhalt es nicht ist.
Für Protokolle, die Daten über einen Hash der Signatur-Bytes identifizieren – Bitcoin-Transaktions-IDs sind das klassische Beispiel –, ist diese Malleability durch Dritte ein echtes Problem. Die Lösung ist die kanonische Low-S-Form: Verlange s ≤ n/2 und normalisiere andernfalls, indem du s durch n − s ersetzt. Bitcoin hat Low-S als Relay-Regel übernommen (vorgeschlagen in BIP-62, festgelegt in BIP-146), und die Prüfung in libsecp256k1 akzeptiert nur Low-S-Signaturen, sofern der Aufrufer nicht vorher normalisiert. Einige auf Sicherheit ausgerichtete Bibliotheken in anderen Ökosystemen erzeugen ebenfalls standardmäßig Low-S und lassen sich so konfigurieren, dass sie High-S bei der Prüfung ablehnen.
Wer was spricht: Standardformate der Bibliotheken
| Bibliothek / API | Standardformat | Das andere Format |
|---|---|---|
| OpenSSL (CLI und EVP-API) | DER | Kein eingebauter Raw-Modus für ECDSA; extern umwandeln oder aus r und s von ECDSA_SIG zusammensetzen |
Node.js node:crypto | DER | Option dsaEncoding: "ieee-p1363" bei sign/verify (verfügbar seit der LTS-Linie Node 12) |
WebCrypto crypto.subtle | Nur Raw (P1363) | Keine DER-Unterstützung – vorher/nachher umwandeln |
Go crypto/ecdsa | Keines von beiden: Sign/Verify nehmen r, s als *big.Int | SignASN1 / VerifyASN1 für DER (Go 1.15+); Raw mit FillBytes zusammenbauen |
Python cryptography (pyca) | DER | Die Hilfsfunktionen decode_dss_signature / encode_dss_signature legen r und s offen |
Python-Paket ecdsa | Standardmäßig Raw | Argumente sigencode_der / sigdecode_der |
| Java (JCA Signature) | DER (SHA256withECDSA) | SHA256withECDSAinP1363Format in modernen JDKs (9+) |
Einige davon im Code. Node.js kann mit demselben Schlüssel beide Kodierungen ausgeben:
import { generateKeyPairSync, sign } from "node:crypto";
const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const msg = Buffer.from("same signature, two encodings");
const der = sign("sha256", msg, privateKey);
const raw = sign("sha256", msg, { key: privateKey, dsaEncoding: "ieee-p1363" });
console.log(der.length); // 70..72 (varies per signature)
console.log(raw.length); // always 64
Go macht die zugrunde liegenden ganzen Zahlen explizit, was auch die Umwandlung offensichtlich macht – beachte FillBytes für die mit Nullen aufgefüllten Raw-Hälften fester Breite:
der, err := ecdsa.SignASN1(rand.Reader, priv, digest) // ASN.1 DER
if err != nil { return err }
r, s, err := ecdsa.Sign(rand.Reader, priv, digest) // the integers themselves
if err != nil { return err }
raw := make([]byte, 64)
r.FillBytes(raw[:32]) // zero-pads on the left — critical for short values
s.FillBytes(raw[32:])
Die Python-Bibliothek pyca/cryptography signiert in DER und bietet Hilfsfunktionen, um an die ganzen Zahlen zu kommen:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256())) # DER bytes
r, s = decode_dss_signature(der_sig)
raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big") # 64-byte P1363
Checkliste zur Fehlersuche
Wenn sich eine ECDSA-Signatur zwischen zwei Systemen nicht prüfen lässt, geh diese Liste der Reihe nach durch:
- Längentest. Genau 64/96/132 Byte → Raw. Beginnt mit
0x30und hat ein plausibles Längenbyte → DER. Alles andere: falsche Dekodierung einer äußeren Schicht (base64, Hex) oder beim Kopieren abgeschnittene Daten. - Passendes Format. Erwartet die prüfende API das, was der Signierer erzeugt hat? Wandle mit dem Konverter DER ⇄ Raw um, statt neu zu signieren.
- Padding mit Nullen. Schlagen Raw-Signaturen sporadisch fehl (etwa in 1 von 128 Fällen:
rundsbeginnen jeweils in etwa 1 von 256 Fällen mit einem Nullbyte), steckt vermutlich eine Umwandlung dahinter, die führende Nullbytes weggelassen hat, stattrundslinks auf feste Breite aufzufüllen. - High-S. Treten die Fehler sporadisch in etwa 50 % der Fälle auf und ist auf einer Seite eine Blockchain oder ein besonders gehärteter Stack beteiligt, vergleiche
smitn/2und normalisiere. - Alles andere – falscher Hash, falsche Kurve, den Hash noch einmal hashen – gehört zur Algorithmus-Ebene, nicht zur Format-Ebene; für diesen Hintergrund fang mit ECDSA vs. RSA an.
Die ermutigende Bilanz: DER oder Raw ist reine Formsache. An der Umwandlung ist keine Kryptografie beteiligt, am Schlüssel ändert sich nichts, und sobald sich beide Seiten über die Anordnung der Bytes einig sind – und über Low-S, wo es darauf ankommt –, stellt sich die „invalid signature“, die einen Nachmittag gekostet hat, meist als von Anfang an gültig heraus.