Skip to content
Zurück zum Blog
Sicherheit

RS256 Private-Key-Formatfehler: eine Meldung, sieben Ursachen

Node meldet denselben DECODER-Fehler bei falschem Container, eingerücktem Header oder Public Key. Getestete Lösungen für PKCS#1 und PKCS#8. Online-Generator.

13 Min. Lesezeit

RS256 Private-Key-Formatfehler: eine Meldung, sieben Ursachen

Ein RS256 Private-Key-Formatfehler nennt fast nie seine eigene Ursache. Unter Node v25.8.2 erzeugt jeder dieser Fehlgriffe exakt dieselbe Zeile:

code:    ERR_OSSL_UNSUPPORTED
message: error:1E08010C:DECODER routines::unsupported

Fünf voneinander unabhängige Dinge lösen sie aus: ein OpenSSH-Container dort, wo ein PEM-Schlüssel erwartet wurde, eine eingerückte -----BEGIN-Zeile, ein Public Key, den jemand an eine Signierfunktion gereicht hat, wörtliche \n-Sequenzen, die niemand aufgelöst hat, und eine Datei, deren Zeilenumbrüche unterwegs verloren gingen. Die getestete Liste in Abschnitt 2 kommt auf sieben. Jedes Mal dieselbe Zeile, und genau deshalb landen Sie beim Suchen nach dem Fehlertext im Thread eines Fremden über die Ursache eines Fremden.

Teilen Sie das Problem zuerst in zwei Hälften:

Die 30-Sekunden-Triage für den ersten Fall:

openssl rsa -in key.pem -noout -text | head -1

Wirft das einen Fehler, liegt es an der Datei, und die Abschnitte 3 bis 6 finden ihn. Läuft es durch, hat OpenSSL den Container verstanden, und Ihr Problem steckt in der Bibliothek oder in dem, was Sie ihr übergeben haben: Abschnitte 4 und 7.

Alles Folgende wurde am 11. August 2026 gegen OpenSSL 3.6.2 7 Apr 2026, Node v25.8.2, Go go1.26.1 darwin/arm64 und Java 1.8.0_162 gemessen. Wo eine Aussage aus gelesenem Quelltext statt aus einem Testlauf stammt, steht es dabei.

1. Klären Sie zuerst, welche Art von Fehler Sie haben

Die Trennlinie verläuft an der Frage, ob je ein Schlüsselobjekt existiert hat. Parse-Time-Fehler passieren, bevor irgendeine Kryptografie läuft. Die Bibliothek liest Ihr PEM, bekommt daraus keinen Schlüssel und wirft. Sie hat nichts signiert, nichts geprüft, und das Token, dem Sie hinterherjagen, ist nie entstanden. Verify-Time-Fehler sind das Gegenteil: Die Bibliothek hat den Schlüssel sauber geladen, eine Signatur berechnet, und die passte nicht. Solche Fehler entstehen aus Abweichungen auf Byte-Ebene zwischen Signierer und Prüfer, und der Leitfaden zu „invalid signature“ behandelt sie.

Die beiden auseinanderzuhalten kostet einen Blick in den Stacktrace. Ein Parse-Time-Fehler nennt einen Decoder, eine Key-Spec oder eine ASN.1-Struktur. Ein Verify-Time-Fehler nennt eine Signatur.

So sieht ein abgewiesener privater Schlüssel in drei Ökosystemen aus:

LaufzeitumgebungGetestete VersionMeldung, wenn der Schlüssel nicht lädt
Node cryptov25.8.2error:1E08010C:DECODER routines::unsupported
Go crypto/x509go1.26.1 darwin/arm64x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)
Java PKCS8EncodedKeySpec1.8.0_162InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence

Die drei sind unterschiedlich hilfreich. Go sagt Ihnen genau, welche Funktion Sie stattdessen aufrufen sollen. Java spricht von einer „algid“ und einer „sequence“ und überlässt Ihnen die Schlussfolgerung, dass Ihr Schlüssel im falschen Container steckt. Node sagt überhaupt nichts Brauchbares.

Wenn Sie nicht sicher sind, ob das Token, dem Sie nachgehen, überhaupt RS256 ist, fügen Sie es in den JWT-Dekodierer ein und lesen Sie alg aus dem Header, bevor Sie weitermachen. Ein HS256-Header bedeutet, dass Sie ein gemeinsames Secret brauchen und kein Schlüsselpaar, und dann weist Sie jedes Symptom aus diesem Artikel in die falsche Richtung.

2. Vom Fehlertext zur Ursache: die Nachschlagetabelle für den RS256 Private-Key-Formatfehler

Suchen Sie Ihre exakte Zeichenkette. Die rechte Spalte sagt, wo es weitergeht.

FehlertextWoher er kommtWas er tatsächlich bedeutet
error:1E08010C:DECODER routines::unsupportedNode v25.8.2Sieben mögliche Ursachen, unten aufgelistet
error:07880109:common libcrypto routines::interrupted or cancelledNode v25.8.2Der Schlüssel ist verschlüsselt und Sie haben keine Passphrase übergeben
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)Go 1.26.1Sie haben ParsePKCS1PrivateKey auf eine PKCS#8-Datei angewendet
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)Go 1.26.1Sie haben ParsePKCS8PrivateKey auf eine PKCS#1-Datei angewendet
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})Go 1.26.1Der erste PEM-Block ist EC PARAMETERS und nicht der Schlüssel
algid parse error, not a sequenceJava 1.8.0_162PKCS#1 an PKCS8EncodedKeySpec übergeben
secretOrPrivateKey must have a valuejsonwebtoken, im QuelltextDas Schlüsselargument ist falsy und alg ist nicht none
secretOrPrivateKey is not valid key materialjsonwebtoken, im QuelltextEs ließ sich weder ein Private Key noch ein Secret Key bauen
secretOrPrivateKey must be a symmetric key when using ${header.alg}jsonwebtoken, im Quelltextalg beginnt mit HS, der Schlüssel ist aber kein Secret
secretOrPrivateKey must be an asymmetric key when using ${header.alg}jsonwebtoken, im Quelltextalg passt auf RS, PS oder ES, der Schlüssel ist aber kein privater
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}jsonwebtoken, im QuelltextRS oder PS mit einem Schlüssel unter 2048 Bit und abgeschaltetem allowInsecureKeySizes

Die fünf secretOrPrivateKey-Zeichenketten stammen aus sign.js im master-Branch von jsonwebtoken und wurden nicht lokal ausgeführt. Die Auslösebedingungen geben also wieder, was der Quelltext sagt; nachgestellt hat sie auf dieser Maschine niemand. Der Teil ${header.alg} ist ein Template-Platzhalter in diesem Quelltext; zur Laufzeit steht dort Ihr eigener Algorithmusname, und deshalb findet die Suche nach der wörtlichen Zeichenkette samt geschweiften Klammern nichts.

Die sieben Wege, DECODER routines::unsupported zu erzeugen

Alle sieben wurden gegen crypto.createPrivateKey() unter Node v25.8.2 reproduziert, und alle sieben lieferten denselben Code und dieselbe Meldung:

  1. Ein OpenSSH-Container. Die Datei beginnt mit -----BEGIN OPENSSH PRIVATE KEY----- und ist überhaupt keine PEM-Schlüsselstruktur.
  2. Eine eingerückte -----BEGIN-Zeile oder eine eingerückte -----END-Zeile. Die Rumpfzeilen sind ausgenommen; Abschnitt 5 zeigt die genaue Grenze.
  3. Ein Leerzeichen vor dem gesamten PEM. Eine führende Leerzeile ist unbedenklich, ein führendes Leerzeichen nicht.
  4. Vollständig entfernte Zeilenumbrüche, sodass Header, Base64 und Footer auf einer einzigen Zeile zusammenlaufen.
  5. Ein Public Key dort, wo ein Private Key erwartet wurde.
  6. Wörtliche Backslash-n-Sequenzen, die niemand aufgelöst hat, also genau das, was aus einer einzeiligen Umgebungsvariablen wird.
  7. Eine Trennzeile mit der falschen Anzahl Bindestriche oder begin/end in Kleinschreibung.

Zwei davon sind Container-Probleme, vier sind Textverstümmelungen und eines ist eine schlichte Verwechslung. Die Meldung kann Ihnen nicht sagen, welches davon vorliegt, also ist Ausschließen schneller als Lesen.

Was Node akzeptiert, und warum das die Suche schneller eingrenzt

Die umgekehrte Liste ist nützlicher, denn jeder Punkt darauf ist eine Theorie, die Sie sofort fallen lassen können. Unter Node v25.8.2 nahm crypto.createPrivateKey() all das ohne Murren an:

  • PKCS#1- und PKCS#8-Private-Keys
  • EC-SEC1-Private-Keys
  • CRLF-Zeilenenden
  • einen fehlenden abschließenden Zeilenumbruch
  • einen Base64-Rumpf auf einer einzigen, nicht umbrochenen Zeile
  • eingerückte Rumpfzeilen
  • eine Leerzeile vor dem PEM
  • ein UTF-8-BOM, sowohl als '' + pem als auch als Buffer, der mit 0xEF 0xBB 0xBF beginnt
  • einen PKCS#1-Header um einen PKCS#8-Rumpf

Hinter dem letzten Punkt steckt eine allgemeine Regel: Der Decoder liest die DER-Struktur innerhalb des Base64 und ignoriert das Etikett außen herum. Eine Datei, in der BEGIN RSA PRIVATE KEY über PKCS#8-Inhalt steht, lädt also trotzdem. Gut zu wissen, und zugleich eine Warnung für Abschnitt 3: Die Header-Zeile ist ein Hinweis, keine Garantie.

3. Die PEM-Header-Zeile: welchen Container Sie wirklich haben

Jedes PEM kündigt sich in Zeile eins an. Das sind die Header-Werte, die OpenSSL 3.6.2 schreibt:

InhaltErste Zeile
PKCS#8-Private-Key-----BEGIN PRIVATE KEY-----
PKCS#1-Private-Key-----BEGIN RSA PRIVATE KEY-----
Verschlüsselter Private Key-----BEGIN ENCRYPTED PRIVATE KEY-----
OpenSSH-Private-Key-----BEGIN OPENSSH PRIVATE KEY-----
EC-SEC1-Private-Key-----BEGIN EC PARAMETERS-----, danach ein zweiter Block -----BEGIN EC PRIVATE KEY-----
SPKI-Public-Key-----BEGIN PUBLIC KEY-----
PKCS#1-Public-Key-----BEGIN RSA PUBLIC KEY-----
Ed25519-Private-Key-----BEGIN PRIVATE KEY-----, und die ganze Datei hat drei Zeilen

head -1 key.pem beantwortet damit die erste Frage jeder Untersuchung.

ENCRYPTED PRIVATE KEY ist kein Formatfehler. Es ist eine Passphrase, die Sie zu übergeben vergessen haben. Node meldet das anders als alles andere, nämlich mit ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED und error:07880109:common libcrypto routines::interrupted or cancelled, weil die Bibliothek nach einer Passphrase gefragt und nichts zurückbekommen hat. Werfen Sie diese Meldung nicht mit der DECODER-Meldung in einen Topf; sie haben nichts miteinander zu tun.

OPENSSH PRIVATE KEY ist eine eigene Welt. OpenSSH schreibt seinen eigenen Container, der weder PKCS#1 noch PKCS#8 ist, obwohl er zwischen PEM-artigen Trennzeilen sitzt. Node weist ihn rundheraus ab, und die Parser aus Gos crypto/x509 sowie PKCS8EncodedKeySpec des JDK tun dasselbe. Wenn Ihr JWT-Signaturschlüssel aus ssh-keygen stammt, haben Sie Ihren Bug.

EC-SEC1-Dateien enthalten zwei Blöcke. openssl ecparam -genkey schreibt zuerst einen EC PARAMETERS-Block und danach den privaten Schlüssel. Alles, was nur den ersten PEM-Block liest, bekommt die Parameter und scheitert auf eine Weise, die weder das eine noch das andere erwähnt. Abschnitt 4 zeigt die Go-Variante dieses Fehlers.

Und weil der Header nur ein Etikett ist, zählt auch die Gegenprobe: Eine Datei, deren Header das eine sagt und deren DER etwas anderes enthält, wird nach dem DER geparst. head -1 zu lesen ist zuverlässig bei Dateien, die direkt aus OpenSSL kommen, und unzuverlässig bei Dateien, die durch einen Menschen, eine Wiki-Seite oder ein Skript mit String-Ersetzung gelaufen sind.

4. Welche Bibliothek was akzeptiert: PKCS#1 vs. PKCS#8 in drei Ökosystemen

Das ist die Matrix, die die meisten teamübergreifenden Formatstreitigkeiten erklärt. Jede Zeile wurde auf den oben genannten Versionen gemessen.

BibliothekPKCS#1PKCS#8OpenSSHErklärt sich der Fehler selbst?
Node cryptoJaJaNeinNein. Viele Ursachen, ein DECODER routines::unsupported
Go crypto/x509Ja, eigene FunktionJa, eigene FunktionNeinJa. Er nennt die Funktion, auf die Sie wechseln müssen
Java-StandardbibliothekNeinJaNeinNein. algid parse error, not a sequence führt aktiv in die Irre

Lesen Sie die Spalten, und die Streitigkeiten lösen sich von selbst. Ein Node-Dienst und ein Java-Dienst, die sich eine Schlüsseldatei teilen, laufen problemlos, bis der Schlüssel PKCS#1 ist. Ab da signiert Node weiter, und Java wirft eine Meldung über ASN.1-Sequenzen. Niemand verdächtigt den Schlüssel, weil er im anderen Dienst nachweislich in der Produktion funktioniert.

Node. Hier gibt es nichts zu konfigurieren. Ist der Container PKCS#1 oder PKCS#8, nimmt createPrivateKey() ihn an. Wenn es doch wirft, investieren Sie Ihre Zeit in die sieben Ursachen aus Abschnitt 2 statt in das Format.

const fs = require('node:fs');
const { createPrivateKey } = require('node:crypto');

try {
  const key = createPrivateKey(fs.readFileSync('key.pem'));
  console.log('parsed:', key.asymmetricKeyType);
} catch (err) {
  console.log(err.code, '/', err.message);
}

Lassen Sie das gegen die Datei laufen, die Ihre Anwendung tatsächlich lädt, und nicht gegen eine von Hand erstellte Kopie. Dann gibt der catch-Zweig genau das Paar aus Code und Meldung aus, das Sie in Abschnitt 2 nachschlagen können.

Go. Zwei Container, zwei Funktionen, und die falsche aufzurufen ist der häufigste Go-Fehler. Die Meldung sagt Ihnen, welche die richtige ist, die Korrektur ist also mechanisch. Beide der Reihe nach zu probieren nimmt die Entscheidung ganz heraus:

priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
	rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err2 != nil {
		log.Fatalf("neither container parsed: %v / %v", err, err2)
	}
	priv = rsaKey
}

Die EC-Falle müssen Sie allerdings vorher abfangen. Gegen eine Datei aus openssl ecparam -genkey liefert pem.Decode einen Block, dessen Type EC PARAMETERS lautet, und alle drei Parse-Funktionen scheitern daran mit:

asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})

Diese Meldung erwähnt PEM-Blöcke mit keinem Wort, also verdächtigt man reflexhaft den Schlüssel. Überspringen Sie stattdessen den Parameterblock:

block, rest := pem.Decode(pemBytes)
if block == nil {
	log.Fatal("no PEM block found")
}
if block.Type == "EC PARAMETERS" {
	block, _ = pem.Decode(rest)
}

Oder erzeugen Sie den zusätzlichen Block gar nicht erst, indem Sie dem ecparam-Aufruf, der die Datei schreibt, ein -noout mitgeben.

Java. Die Standardbibliothek liest PKCS#8 und sonst nichts. Geben Sie PKCS8EncodedKeySpec unter Java 1.8.0_162 einen PKCS#1-Schlüssel, bekommen Sie:

InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence

„algid“ ist der Algorithm Identifier, also das Feld, das PKCS#8 ergänzt und das PKCS#1 nicht hat. Der Parser hat danach gesucht, den Anfang eines RSA-Modulus gefunden und aufgegeben. Die Meldung ist zu gleichen Teilen korrekt und nutzlos. Konvertieren Sie die Datei, und der Fehler verschwindet:

openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem

Der funktionierende Ladepfad unter Java 8 ist, sobald die Datei PKCS#8 ist, kurz genug, um ihn zur Bestätigung direkt in einen Test zu schreiben:

String pem = new String(Files.readAllBytes(Paths.get("key.pem")), StandardCharsets.UTF_8)
        .replace("-----BEGIN PRIVATE KEY-----", "")
        .replace("-----END PRIVATE KEY-----", "")
        .replaceAll("\\s+", "");
byte[] der = Base64.getDecoder().decode(pem);
PrivateKey key = KeyFactory.getInstance("RSA")
        .generatePrivate(new PKCS8EncodedKeySpec(der));

Die Alternative zum Konvertieren ist BouncyCastle, das PKCS#1 tatsächlich liest. Konvertieren ist ein Befehl und keine Abhängigkeit, also konvertieren Sie, sofern nicht ohnehin etwas anderes in Ihrem Stack die Bibliothek braucht.

5. Die Zeichen, die Sie nicht sehen können

Hier ist der verbreitete Rat nachweislich falsch.

Einrückung: das Gegenteil dessen, was man Ihnen erzählt hat

Eine vielfach wiederholte Anweisung besagt, jede Zeile eines PEM außer den Trennzeilen müsse in Spalte null beginnen. Unter Node v25.8.2 getestet, ist das genau verkehrt herum:

Änderung an der DateiErgebnis
Jede Zeile eingerücktSchlägt fehl
Nur die -----BEGIN-Zeile eingerücktSchlägt fehl
Nur die -----END-Zeile eingerücktSchlägt fehl
Nur die Base64-Rumpfzeilen eingerücktAkzeptiert
Ein Leerzeichen vor dem gesamten PEMSchlägt fehl
Eine Leerzeile vor dem gesamten PEMAkzeptiert

Die Regel lautet also: Die Zeilen -----BEGIN und -----END müssen in Spalte null beginnen, und die Einrückung der Rumpfzeilen spielt keine Rolle. Ausgerechnet die beiden Zeilen, von denen es heißt, man dürfe sie einrücken, sind die beiden, die brechen, und die Zeilen, die man ausrichten soll, sind die mit dem Spielraum.

Wichtig ist das wegen der Art, wie private Schlüssel überhaupt erst zu ihrer Einrückung kommen. Niemand rückt ein PEM von Hand ein. Es passiert, wenn jemand einen Schlüssel in einen YAML-Block, eine Helm-values-Datei, ein Terraform-Heredoc oder einen dreifach zitierten Python-String innerhalb eines Klassenkörpers einfügt. Jedes davon rückt das Ganze gleichmäßig ein, Trennzeilen inklusive, und das ist die erste Zeile jener Tabelle.

Wörtliches Backslash-n aus einer einzeiligen Umgebungsvariablen

Ein PEM hat Zeilenumbrüche, eine Umgebungsvariable in der Praxis nicht. Also landen Schlüssel als eine Zeile in .env-Dateien, mit \n ausgeschrieben als zwei Zeichen. Was diese Datei liest, reicht Ihrem Code eine Zeichenkette voller Backslashes, und der Parser sieht eine Trennzeile gefolgt von Datenmüll. Unter Node ist das Ursache 6 aus Abschnitt 2, mit derselben Meldung DECODER routines::unsupported wie alles andere.

Machen Sie es an der Stelle rückgängig, an der Sie den Wert benutzen:

const pem = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');

Zwei Absicherungen lohnen sich drumherum. Erstens: Wenden Sie die Ersetzung nur an, wenn die Zeichenkette die Zwei-Zeichen-Folge tatsächlich enthält; ein echt mehrzeiliger Wert aus einem anderen Loader bleibt dann ohnehin unangetastet. Zweitens: Bevorzugen Sie Base64 für das gesamte PEM, wenn Ihre Plattform das zulässt. Eine Zeile Base64 speichern, beim Start dekodieren, und die Frage nach dem Escaping stellt sich gar nicht erst.

BOM: unter Node harmlos und anderswo ungetestet

Eine Byte Order Mark besteht aus drei Bytes, EF BB BF, die manche Windows-Editoren an den Anfang einer UTF-8-Datei schreiben. Der Rat, sie vor dem Laden eines Schlüssels zu entfernen, ist verbreitet. Unter Node v25.8.2 machte sie keinen Unterschied: Node parste ein PEM mit vorangestelltem BOM fehlerfrei, sowohl als Zeichenkette als auch als Buffer, der mit diesen drei Bytes beginnt.

Grenzen Sie dieses Ergebnis sorgfältig ein: Es stammt ausschließlich aus Node v25.8.2. Java, Python und andere Parser wurden hier nicht getestet, und nichts in diesem Artikel sagt etwas über ihr Verhalten aus. Wenn Sie einen Java-Dienst debuggen, bleibt das BOM eine offene Frage und keine ausgeschlossene.

Andere Dinge macht das BOM sehr wohl kaputt, und daher stammt der Rat zum Schlüssel vermutlich durch Assoziation. JSON.parse auf einer Zeichenkette mit BOM ist ein realer und gut dokumentierter Fehler, behandelt im Leitfaden zum UTF-8-BOM-JSON-Parse-Fehler. Eine Schlüsseldatei, die in einer JSON-Konfiguration steckt, kann deshalb längst scheitern, bevor überhaupt jemand den Schlüssel ansieht.

Zeilenenden, abschließender Zeilenumbruch und Umbruchbreite

Drei weitere Verdächtige, die Node v25.8.2 entlastet hat:

  • CRLF-Zeilenenden. Akzeptiert. Ein Schlüssel, der durch Windows gereist ist, ist nicht automatisch kaputt.
  • Ein fehlender abschließender Zeilenumbruch. Akzeptiert, aber parserabhängig: Manche Parser sollen ein PEM ohne abschließenden Zeilenumbruch ablehnen, Node tut es nicht. Andere Parser wurden hier nicht getestet.
  • Ein nicht umbrochener Rumpf. Akzeptiert. Das Base64 muss nicht auf 64 Zeichen gefaltet sein.

Was einen Base64-Rumpf tatsächlich zerstört, ist ein verlorenes, eingefügtes oder ersetztes Zeichen, und das ist ein anderer Fehler als der Umbruch. Ein Chat-Client, der einen Zeilenumbruch in ein Leerzeichen verwandelt, oder ein Textfeld, das ein Zeichen am Ende schluckt, erzeugt einen Rumpf, der sich nicht mehr dekodieren lässt. Kopieren Sie mit einem Kopier-Button statt mit gezogener Maus.

6. OpenSSL 3.x hat den Standard unter Ihnen ausgetauscht

Gemessen unter OpenSSL 3.6.2 7 Apr 2026:

BefehlContainer, den er schreibt
openssl genrsa -out k.pem 2048PKCS#8, Header BEGIN PRIVATE KEY
openssl genrsa -traditional -out k.pem 2048PKCS#1
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048PKCS#8
openssl genpkey -algorithm ED25519PKCS#8
openssl pkcs8 -topk8 -nocrypt -in a.pem -out b.pemKonvertiert PKCS#1 nach PKCS#8
openssl rsa -in b.pem -traditional -out a.pemKonvertiert PKCS#8 nach PKCS#1

Lesen Sie die ersten beiden Zeilen noch einmal. genrsa liefert Ihnen auf diesem Build standardmäßig PKCS#8, und -traditional ist das, was die BEGIN RSA PRIVATE KEY-Datei erzeugt. Viele Anleitungen beschreiben genrsa weiterhin als den PKCS#1-Befehl und genpkey als den für PKCS#8. Wer ihnen folgt, ist sich anschließend sicher, ein Format erzeugt zu haben, das er nicht erzeugt hat.

Die praktische Folge zeigt sich bei Migrationen. Ein Java-Team bekommt einen funktionierenden Schlüssel von einer Kollegin mit älterem OpenSSL, alles läuft, und ein halbes Jahr später erzeugt jemand den Schlüssel auf einer frischen Maschine neu. Gleicher Befehl, gleiche Dokumentation, anderer Container, und jetzt wirft das JDK algid parse error, not a sequence gegen einen Schlüssel, der „exakt genauso erzeugt wurde“. Wurde er nicht.

Also nie annehmen, sondern nachsehen:

head -1 key.pem

Eine Zeile Ausgabe, und die Tabelle aus Abschnitt 3 sagt Ihnen, was Sie in der Hand halten. Tun Sie das, bevor Sie irgendeinen Konvertierungsbefehl absetzen, denn eine PKCS#8-Datei nach PKCS#8 zu konvertieren ist ein Leerlauf, der wie eine Reparatur aussieht und nichts repariert.

Wenn Sie über die Flags lieber gar nicht nachdenken wollen: Der RSA-Schlüsselgenerator gibt beide Container aus demselben Schlüsselpaar aus, umschaltbar. So erzeugen Sie eine PKCS#1- und eine PKCS#8-Kopie desselben Schlüssels und probieren jede gegen die Bibliothek, die Sie abweist.

7. Die 2048-Bit-Untergrenze, die einen völlig gültigen Schlüssel abweist

Ein Fehlschlag sieht aus wie ein Formatproblem und ist keines. Im Quelltext von jsonwebtoken wirft sign.js:

secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}

Der Quelltext löst das aus, wenn alg ein RS- oder PS-Algorithmus ist, der Schlüssel unter 2048 Bit liegt und allowInsecureKeySizes nicht gesetzt wurde. Diese Prüfung sitzt in der Bibliothek, eine Ebene über der Laufzeitumgebung. Node v25.8.2 parst einen 1024-Bit-RSA-Schlüssel anstandslos; modulusLength: 1024 erzeugt ein Schlüsselobjekt wie jedes andere. Der Schlüssel ist also strukturell gültig, der Container stimmt, OpenSSL liest ihn, und der Signieraufruf scheitert trotzdem.

Das Erkennungsmerkmal ist, dass diese Meldung eine Zahl nennt. Formatfehler sprechen von Decodern, Sequenzen und Key Material; diese hier spricht von Bits. Wenn in der Meldung eine Größe steht, hören Sie auf, das PEM anzusehen.

Woher 1024-Bit-Schlüssel kommen, ist meist Geschichte: ein Schlüssel, den jemand vor Jahren gegen einen inzwischen verschobenen Standardwert erzeugt hat, oder eine Test-Fixture, die niemand angefasst hat, weil kleine Schlüssel schneller entstehen. Die Lösung ist ein neues Schlüsselpaar mit 2048 Bit oder mehr und nicht der Griff zur Notluke, denn die Notluke schaltet eine Prüfung ab, die es aus gutem Grund gibt.

Um zu bestätigen, dass die Größe das einzige verbleibende Problem ist, signieren Sie dieselbe Payload mit einem frischen Schlüssel der richtigen Größe im JWT-Kodierer. Entsteht dort ein Token und in Ihrem eigenen Code nicht, liegt der Unterschied an Ihrem Schlüssel und nicht an Ihren Claims oder Ihrer Konfiguration.

8. Ein wiederholbarer Ablauf für RS256-Schlüsselfehler

Führen Sie die Schritte der Reihe nach aus. Jeder findet entweder die Ursache oder streicht einen Zweig.

  1. Die Header-Zeile lesen. head -1 key.pem, dann mit der Tabelle aus Abschnitt 3 abgleichen. Das verrät Ihnen den Container, ob die Datei verschlüsselt ist und ob es ein OpenSSH-Schlüssel ist, der nie funktionieren wird.
  2. OpenSSL bitten, sie zu parsen. openssl rsa -in key.pem -noout -text | head -1 für RSA oder openssl pkey -in key.pem -noout für beliebige Algorithmen. Erfolg heißt, die Bytes sind ein gültiger Schlüssel und das Problem liegt auf der Bibliotheksseite. Fehlschlag heißt, die Datei ist beschädigt, und Sie machen bei Schritt 4 weiter.
  3. Die Zeile Ihrer Bibliothek in der Matrix prüfen. Abschnitt 4. Wenn Sie unter Java eine PKCS#1-Datei haben oder unter Go die falsche Parse-Funktion aufrufen, sind Sie hier fertig.
  4. Auf die unsichtbaren Zeichen sehen. head -c 32 key.pem | xxd zeigt die ersten Bytes und fängt ein BOM, ein führendes Leerzeichen und eine eingerückte Trennzeile in einem einzigen Blick ab. Bestätigen Sie danach gemäß Abschnitt 5, dass die Zeilen -----BEGIN und -----END in Spalte null beginnen.
  5. Mit einem bekannt guten Schlüssel halbieren. Erzeugen Sie ein frisches Schlüsselpaar im RSA-Schlüsselgenerator, richten Sie Ihren Code darauf und sehen Sie, ob der Fehler überlebt. Überlebt er, steckt der Bug in Ihrem Ladecode statt in der Schlüsseldatei, und kein noch so gründliches Umformatieren des Originals hilft. Verschwindet er, ist die Originaldatei schuld, und Sie haben jetzt einen funktionierenden Schlüssel zum Vergleichen.
  6. Algorithmus und Schlüssellänge zuletzt prüfen. Bestätigen Sie, dass im Header RS256 steht, und dass der Schlüssel mindestens 2048 Bit hat, gemäß Abschnitt 7.

Schritt 5 ist der, den man überspringt, und zugleich der, der am meisten Zeit spart. Ein sauberer Referenzschlüssel verwandelt ein vages „der Schlüssel geht nicht“ in eine binäre Antwort darauf, welche Seite kaputt ist.

FAQ

Was ist der Unterschied zwischen BEGIN RSA PRIVATE KEY und BEGIN PRIVATE KEY?

Es sind zwei Container um denselben RSA-Schlüssel. BEGIN RSA PRIVATE KEY ist PKCS#1 und enthält die RSA-Zahlen direkt; BEGIN PRIVATE KEY ist PKCS#8 und ergänzt einen Algorithm Identifier, weshalb es auch ECDSA- und Ed25519-Schlüssel tragen kann. Welchen Sie brauchen, entscheidet ausschließlich die Bibliothek, und der RSA-Schlüsselgenerator schreibt beide.

Warum erzeugt openssl genrsa ein anderes Format als im Tutorial?

Weil der Standardwert gewechselt hat. Unter OpenSSL 3.6.2 schreibt openssl genrsa -out k.pem 2048 PKCS#8 mit einem BEGIN PRIVATE KEY-Header. Für das traditionelle PKCS#1-Layout aus älteren Anleitungen ergänzen Sie -traditional. Führen Sie head -1 auf der Ausgabe aus, statt einem Tutorial darüber zu vertrauen, was Ihr Build erzeugt.

Wie behebe ich algid parse error, not a sequence in Java?

Diese Meldung bedeutet unter Java 1.8.0_162, dass Sie PKCS8EncodedKeySpec einen PKCS#1-Schlüssel gegeben haben. Die Standardbibliothek liest PKCS#1 überhaupt nicht. Konvertieren Sie einmalig mit openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem, oder ergänzen Sie BouncyCastle, falls etwas anderes im Projekt die Bibliothek ohnehin braucht.

Muss jede Zeile eines privaten Schlüssels in Spalte null beginnen?

Nein, und der verbreitete Rat hat es genau verkehrt herum. Unter Node v25.8.2 getestet: Node parst eine Datei sauber, in der nur die Base64-Rumpfzeilen eingerückt sind, und lehnt sie ab, sobald nur die -----BEGIN-Zeile oder nur die -----END-Zeile eingerückt ist. Eine führende Leerzeile vor dem PEM akzeptiert Node, ein führendes Leerzeichen nicht.

Wie lege ich einen privaten Schlüssel in einer .env-Datei ab?

Entweder als eine Zeile in Anführungszeichen mit \n-Escapes, die Sie beim Laden mit .replace(/\\n/g, '\n') rückgängig machen, oder als eine Zeile Base64, die Sie beim Start dekodieren. Die zweite Variante ist sicherer, weil es keine Escaping-Konvention gibt, die ein Config-Loader falsch verstehen könnte.

Kann ich einen 1024-Bit-Schlüssel mit RS256 verwenden?

Node v25.8.2 parst einen 1024-Bit-RSA-Schlüssel fehlerfrei, aber der Quelltext von jsonwebtoken verweigert das Signieren damit: secretOrPrivateKey has a minimum key size of 2048 bits, sofern allowInsecureKeySizes nicht gesetzt ist. Erzeugen Sie stattdessen einen 2048-Bit-Schlüssel. Die Meldung nennt eine Bitzahl, und daran unterscheiden Sie sie von einem Formatproblem.

Warum bekomme ich einen RS256 Private-Key-Formatfehler, der einen asymmetrischen Schlüssel verlangt, obwohl ich eine Private-Key-Datei übergeben habe?

Im Quelltext von jsonwebtoken feuert secretOrPrivateKey must be an asymmetric key when using ${header.alg}, wenn alg RS, PS oder ES ist und der Schlüssel kein privater Schlüssel ist. Meist ist der Wert eine HS256-artige Secret-Zeichenkette aus einer früheren Konfiguration. Eine zufällige Zeichenkette gehört zu HS256 und zum JWT-Secret-Generator; RS256 braucht ein Schlüsselpaar und kein Secret.

Fazit

Teuer an dieser Fehlerklasse ist nicht die Schwierigkeit. Teuer ist, dass eine einzige Fehlerzeichenkette unter Node sieben Ursachen abdeckt, dass Javas Meldung auf ASN.1 zeigt, während die eigentliche Antwort „falscher Container“ lautet, und dass der meistwiederholte Formatierungsratschlag zu diesem Thema invertiert ist. Sie können sich die Antwort nicht anlesen, also schließen Sie stattdessen aus: Header-Zeile, OpenSSL-Parse, Bibliotheksmatrix, unsichtbare Zeichen, bekannt guter Schlüssel.

Zwei Gewohnheiten verhindern die Wiederholung. Notieren Sie neben dem Schlüssel in Ihrem Secret Store, welchen Container jeder Dienst verlangt, denn diese Einschränkung stammt von der Bibliothek, während die Datei selbst dazu schweigt. Und halten Sie ein bekannt gutes Schlüsselpaar in Ihrer Entwicklungsumgebung bereit, rein als Kontrolle, damit die erste Frage bei jedem Schlüsselfehler binnen einer Minute eine Ja-oder-Nein-Antwort bekommt.

Wie Sie diese Schlüssel ausstellen, rotieren und in ihrer Reichweite begrenzen, sobald sie korrekt laden, behandelt JWT-Sicherheit: Best Practices, Angriffe und Abwehr (2026).

Tags: jwt rsa pem openssl debugging security

Verwandte Artikel

Alle Artikel anzeigen