Zum Inhalt springen
ecdsa.com
← Alle Artikel

ECDSA-Signaturformate: DER vs. Raw (P1363), High-S und Stolperfallen zwischen Bibliotheken

Dein Schlüssel stimmt, deine Nachricht stimmt, dein Algorithmus stimmt – und die Prüfung schlägt trotzdem fehl. Meistens sind sich die beiden Seiten schlicht nicht einig, wie die Bytes der Signatur angeordnet sind. Dieser Leitfaden erklärt auf Byte-Ebene die beiden Kodierungen einer ECDSA-Signatur und die High-S-Feinheit, an der die Interoperabilität selbst dann scheitert, wenn die Kodierung übereinstimmt.

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:

DER-kodierte ECDSA-Signatur, kommentiert
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 einzelnes 0x00 vorangestellt werden, damit die Zahl nicht als negativ gelesen wird. r und s tragen 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 r oder s zufä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:

KurveGröße eines KörperelementsGröße der Raw-Signatur
P-256 / secp256k132 Byte64 Byte
P-38448 Byte96 Byte
P-52166 Byte132 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 / APIStandardformatDas andere Format
OpenSSL (CLI und EVP-API)DERKein eingebauter Raw-Modus für ECDSA; extern umwandeln oder aus r und s von ECDSA_SIG zusammensetzen
Node.js node:cryptoDEROption dsaEncoding: "ieee-p1363" bei sign/verify (verfügbar seit der LTS-Linie Node 12)
WebCrypto crypto.subtleNur Raw (P1363)Keine DER-Unterstützung – vorher/nachher umwandeln
Go crypto/ecdsaKeines von beiden: Sign/Verify nehmen r, s als *big.IntSignASN1 / VerifyASN1 für DER (Go 1.15+); Raw mit FillBytes zusammenbauen
Python cryptography (pyca)DERDie Hilfsfunktionen decode_dss_signature / encode_dss_signature legen r und s offen
Python-Paket ecdsaStandardmäßig RawArgumente 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:

node – ein Schlüssel, beide Formate
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:

go – DER und Raw ausgeben
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:

python – von DER zu Raw mit pyca/cryptography
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 0x30 und 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: r und s beginnen jeweils in etwa 1 von 256 Fällen mit einem Nullbyte), steckt vermutlich eine Umwandlung dahinter, die führende Nullbytes weggelassen hat, statt r und s links 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 s mit n/2 und 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.