Skip to content
Powrót do bloga
Bezpieczeństwo

Błąd formatu klucza prywatnego RS256: jeden komunikat, 7 przyczyn

Node zgłasza ten sam błąd DECODER przy złym kontenerze, wciętym nagłówku lub kluczu publicznym. Sprawdzone poprawki PKCS#1 i PKCS#8 oraz generator online.

13 min czytania

Błąd formatu klucza prywatnego RS256: jeden komunikat, 7 przyczyn

Błąd formatu klucza prywatnego RS256 prawie nigdy nie nazywa własnej przyczyny. Na Node v25.8.2 każda z poniższych pomyłek kończy się identyczną linijką:

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

Wyzwala ją pięć niepowiązanych ze sobą rzeczy: kontener OpenSSH tam, gdzie spodziewano się klucza PEM, wcięta linia -----BEGIN, klucz publiczny podany komponentowi podpisującemu, dosłowne sekwencje \n, których nikt nie odwrócił, oraz plik, któremu w tranzycie wycięto znaki nowej linii. Przetestowana lista w sekcji 2 liczy siedem pozycji. Za każdym razem ten sam komunikat co do znaku. Dlatego szukanie treści błędu wrzuca człowieka w cudzy wątek o cudzej przyczynie.

Najpierw trzeba podzielić problem na pół:

Trzydziestosekundowa segregacja dla pierwszego przypadku:

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

Jeśli polecenie zwróci błąd, winny jest plik, a znajdą go sekcje od 3 do 6. Jeśli się powiedzie, OpenSSL zrozumiał kontener, a problem leży po stronie biblioteki albo tego, co jej podano, czyli w sekcjach 4 i 7.

Wszystko poniżej zmierzono 11 sierpnia 2026 na OpenSSL 3.6.2 7 Apr 2026, Node v25.8.2, Go go1.26.1 darwin/arm64 i Javie 1.8.0_162. Tam, gdzie stwierdzenie pochodzi z lektury kodu źródłowego, a nie z uruchomienia, tekst mówi o tym wprost.

1. Która to awaria: parsowanie czy weryfikacja

Awarie na etapie parsowania zdarzają się, zanim ruszy jakakolwiek kryptografia. Biblioteka czyta PEM, nie potrafi zamienić go w klucz i rzuca wyjątek. Nic nie zostało podpisane, nic nie zostało zweryfikowane, a token, którego dotyczy debugowanie, nigdy nie powstał. Awarie na etapie weryfikacji są dokładną odwrotnością: klucz wczytał się czysto, podpis został policzony i nie zgodził się. Biorą się z rozbieżności na poziomie bajtów między stroną podpisującą a weryfikującą, a opisuje je przewodnik po błędzie invalid signature.

Do rozróżnienia wystarczy jedno spojrzenie na stack trace. Awaria na etapie parsowania wymienia dekoder, key spec albo strukturę ASN.1. Awaria na etapie weryfikacji wymienia podpis.

Tak wygląda odrzucony klucz prywatny w trzech ekosystemach:

ŚrodowiskoTestowana wersjaKomunikat, gdy klucz się nie wczytuje
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

Przydatność tych trzech komunikatów jest skrajnie różna. Go podaje dokładnie, którą funkcję wywołać w zamian. Java wspomina o „algid” i o „sequence”, a domyślenie się, że chodzi o zły kontener, zostawia czytelnikowi. Node nie mówi zupełnie nic, co dałoby się wykorzystać.

Jeśli nie ma pewności, że ścigany token to w ogóle RS256, wystarczy wkleić go do dekodera JWT i odczytać alg z nagłówka, zanim pójdzie się dalej. Nagłówek HS256 oznacza, że potrzebny jest współdzielony sekret, a nie para kluczy — i wtedy każdy objaw opisany w tym artykule prowadzi w złą stronę.

2. Od treści błędu do przyczyny: tablica dla błędu formatu klucza prywatnego RS256

Trzeba znaleźć swój dokładny ciąg znaków. Prawa kolumna mówi, co dalej.

Treść błęduSkąd pochodziCo naprawdę znaczy
error:1E08010C:DECODER routines::unsupportedNode v25.8.2Siedem możliwych przyczyn, wymienionych niżej
error:07880109:common libcrypto routines::interrupted or cancelledNode v25.8.2Klucz jest zaszyfrowany, a passphrase nie zostało podane
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)Go 1.26.1Wywołano ParsePKCS1PrivateKey na pliku PKCS#8
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)Go 1.26.1Wywołano ParsePKCS8PrivateKey na pliku PKCS#1
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})Go 1.26.1Pierwszy blok PEM to EC PARAMETERS, a nie klucz
algid parse error, not a sequenceJava 1.8.0_162PKCS#1 podany do PKCS8EncodedKeySpec
secretOrPrivateKey must have a valuejsonwebtoken, w kodzie źródłowymArgument z kluczem jest falsy, a alg nie jest none
secretOrPrivateKey is not valid key materialjsonwebtoken, w kodzie źródłowymNie udało się zbudować ani klucza prywatnego, ani sekretnego
secretOrPrivateKey must be a symmetric key when using ${header.alg}jsonwebtoken, w kodzie źródłowymalg zaczyna się od HS, ale klucz nie jest sekretem
secretOrPrivateKey must be an asymmetric key when using ${header.alg}jsonwebtoken, w kodzie źródłowymalg pasuje do RS, PS lub ES, ale klucz nie jest prywatny
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}jsonwebtoken, w kodzie źródłowymRS lub PS z kluczem poniżej 2048 bitów i wyłączonym allowInsecureKeySizes

Pięć ciągów secretOrPrivateKey odczytano z pliku sign.js na gałęzi master projektu jsonwebtoken, bez uruchamiania lokalnie. Warunki wyzwolenia należy więc traktować jako to, co mówi kod źródłowy, a nie jako coś odtworzonego na tej maszynie. Fragment ${header.alg} jest w tym kodzie placeholderem szablonu; w czasie działania pojawi się tam nazwa konkretnego algorytmu, i dlatego wyszukiwanie dosłownego ciągu razem z klamrami nic nie znajduje.

Siedem sposobów na wywołanie DECODER routines::unsupported

Wszystkie siedem odtworzono na crypto.createPrivateKey() w Node v25.8.2 i wszystkie siedem dało ten sam kod oraz komunikat:

  1. Kontener OpenSSH. Plik zaczyna się od -----BEGIN OPENSSH PRIVATE KEY----- i w ogóle nie jest strukturą klucza PEM.
  2. Wcięta linia -----BEGIN albo wcięta linia -----END. Linie treści są spod tej reguły wyjęte; dokładną granicę podaje sekcja 5.
  3. Biały znak przed całym blokiem PEM. Pusta linia na początku jest w porządku, spacja już nie.
  4. Całkowicie usunięte znaki nowej linii, przez co nagłówek, base64 i stopka zlewają się w jedną linię.
  5. Klucz publiczny tam, gdzie oczekiwano prywatnego.
  6. Dosłowne sekwencje backslash-n, których nikt nie odwrócił — w to właśnie zamienia się klucz zapisany w zmiennej środowiskowej w jednej linii.
  7. Ogranicznik z niewłaściwą liczbą myślników albo begin/end zapisane małymi literami.

Dwie z nich to problemy z kontenerem, cztery to zniekształcenia tekstu, a jedna to zwykła pomyłka. Komunikat nie rozstrzygnie która, więc najszybszą drogą jest eliminacja, a nie czytanie.

Co Node przyjmuje, czyli jak szybciej zawęzić poszukiwania

Lista odwrotna jest użyteczniejsza, bo każda pozycja na niej to hipoteza, którą można natychmiast odrzucić. W Node v25.8.2 crypto.createPrivateKey() przyjął bez słowa skargi wszystko z poniższych:

  • Klucze prywatne PKCS#1 i PKCS#8
  • Klucze prywatne EC SEC1
  • Zakończenia linii CRLF
  • Brak końcowego znaku nowej linii
  • Treść base64 w jednej, niezawiniętej linii
  • Wcięte linie treści
  • Pustą linię przed blokiem PEM
  • BOM UTF-8, zarówno jako '' + pem, jak i jako Buffer zaczynający się od 0xEF 0xBB 0xBF
  • Nagłówek PKCS#1 opakowujący treść PKCS#8

Ostatnia pozycja potrafi popsuć całe późniejsze śledztwo. Dekoder czyta strukturę DER ukrytą w base64 i ignoruje etykietę na zewnątrz, więc plik z napisem BEGIN RSA PRIVATE KEY nad treścią PKCS#8 i tak się wczyta. Jest to zarazem ostrzeżenie przed sekcją 3: linia nagłówka jest wskazówką, nie gwarancją.

3. Linia nagłówka PEM: który kontener naprawdę masz

Każdy PEM przedstawia się w pierwszej linii. Oto wartości nagłówków zapisywane przez OpenSSL 3.6.2:

ZawartośćPierwsza linia
Klucz prywatny PKCS#8-----BEGIN PRIVATE KEY-----
Klucz prywatny PKCS#1-----BEGIN RSA PRIVATE KEY-----
Zaszyfrowany klucz prywatny-----BEGIN ENCRYPTED PRIVATE KEY-----
Klucz prywatny OpenSSH-----BEGIN OPENSSH PRIVATE KEY-----
Klucz prywatny EC SEC1-----BEGIN EC PARAMETERS-----, a potem drugi blok -----BEGIN EC PRIVATE KEY-----
Klucz publiczny SPKI-----BEGIN PUBLIC KEY-----
Klucz publiczny PKCS#1-----BEGIN RSA PUBLIC KEY-----
Klucz prywatny Ed25519-----BEGIN PRIVATE KEY-----, a cały plik ma trzy linie

Dlatego head -1 key.pem odpowiada na pierwsze pytanie każdego śledztwa. Trzy wiersze tej tabeli mylą częściej niż pozostałe.

ENCRYPTED PRIVATE KEY nie jest błędem formatu. To passphrase, którego zapomniano podać. Node raportuje go inaczej niż całą resztę, przez ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED i error:07880109:common libcrypto routines::interrupted or cancelled, bo biblioteka poprosiła o passphrase i nic nie dostała. Nie należy mieszać tego komunikatu z tym od DECODER-a; nie mają ze sobą nic wspólnego.

OPENSSH PRIVATE KEY to zupełnie inny świat. OpenSSH zapisuje własny kontener, który nie jest ani PKCS#1, ani PKCS#8, mimo że siedzi między ogranicznikami wyglądającymi jak PEM. Node odrzuca go wprost, tak samo jak parsery crypto/x509 w Go i PKCS8EncodedKeySpec z JDK. Jeśli klucz podpisujący JWT wyszedł z ssh-keygen, to właśnie tam siedzi usterka.

Pliki EC SEC1 zawierają dwa bloki. openssl ecparam -genkey zapisuje najpierw blok EC PARAMETERS, a klucz prywatny dopiero jako drugi. Cokolwiek czyta wyłącznie pierwszy blok PEM, dostaje parametry i wywala się w sposób, który nie wspomina ani o jednym, ani o drugim. Wersję tej awarii w Go pokazuje sekcja 4.

A skoro nagłówek jest tylko etykietą, liczy się też sprawdzenie odwrotne: plik, którego nagłówek mówi jedno, a DER drugie, zostanie zinterpretowany zgodnie z DER. head -1 jest wiarygodny dla plików prosto z OpenSSL-a i niewiarygodny dla plików, które przeszły przez człowieka, stronę wiki albo skrypt podmieniający ciągi znaków.

4. Która biblioteka co przyjmuje: PKCS#1 vs PKCS#8 w trzech ekosystemach

Ta macierz rozstrzyga większość sporów o format toczonych między zespołami. Każdy wiersz zmierzono na wersjach wymienionych na początku artykułu.

BibliotekaPKCS#1PKCS#8OpenSSHCzy błąd tłumaczy się sam?
Node cryptoTakTakNieNie. Wiele przyczyn, jeden DECODER routines::unsupported
Go crypto/x509Tak, osobna funkcjaTak, osobna funkcjaNieTak. Wskazuje funkcję, na którą trzeba się przełączyć
Biblioteka standardowa JavyNieTakNieNie. algid parse error, not a sequence czynnie wprowadza w błąd

Usługa w Node i usługa w Javie współdzielące jeden plik klucza działają bez zarzutu do momentu, w którym klucz jest w PKCS#1 — wtedy Node podpisuje dalej, a Java rzuca komunikatem o sekwencjach ASN.1. Nikt nie podejrzewa klucza, bo ten w widoczny sposób działa na produkcji w drugiej usłudze.

Node. Nie ma czego konfigurować. Jeśli kontener to PKCS#1 albo PKCS#8, createPrivateKey() go przyjmie. A kiedy już rzuci wyjątkiem, szukać trzeba wśród siedmiu przyczyn z sekcji 2, nie w formacie.

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);
}

Uruchomić to trzeba na pliku, który wczytuje aplikacja, a nie na kopii zrobionej ręcznie. Gałąź catch wypisze parę kod plus komunikat, którą można odszukać w sekcji 2.

Go. Dwa kontenery, dwie funkcje, a wywołanie nie tej co trzeba to najczęstsza awaria po stronie Go. Komunikat podaje, której użyć, więc poprawka jest mechaniczna. Próba obu po kolei zdejmuje konieczność decyzji:

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
}

Wcześniej trzeba jednak obsłużyć pułapkę EC. Dla pliku z openssl ecparam -genkey pem.Decode zwraca blok, którego Type to EC PARAMETERS, i wszystkie trzy funkcje parsujące wywalają się na nim z:

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

Ten komunikat nigdy nie wspomina o blokach PEM, więc podejrzenie zwykle pada na klucz. Zamiast tego wystarczy pominąć blok z parametrami:

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

Albo w ogóle nie tworzyć dodatkowego bloku, dodając -noout do polecenia ecparam, które zapisuje plik.

Java. Biblioteka standardowa czyta PKCS#8 i nic poza tym. Podanie PKCS8EncodedKeySpec klucza PKCS#1 na Javie 1.8.0_162 daje:

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

„algid” to identyfikator algorytmu, czyli pole, które PKCS#8 dokłada, a którego PKCS#1 nie ma. Parser go szukał, trafił na początek modułu RSA i się poddał. Komunikat jest w równych proporcjach poprawny i bezużyteczny. Konwersja pliku sprawia, że błąd znika:

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

Działająca ścieżka wczytania w Javie 8, już dla pliku w PKCS#8, jest na tyle krótka, że da się ją wkleić wprost do testu potwierdzającego poprawkę:

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));

Alternatywą dla konwersji jest dorzucenie BouncyCastle, które PKCS#1 czyta. Konwersja to jedno polecenie i zero zależności, więc lepiej konwertować, chyba że coś innego w stosie i tak tej biblioteki potrzebuje.

5. Znaki, których nie widać

Tu popularna porada jest błędna, i to w sposób dający się wykazać.

Wcięcia: dokładna odwrotność tego, co się powszechnie słyszy

Szeroko powielana instrukcja mówi, że każda linia PEM poza ogranicznikami musi zaczynać się w zerowej kolumnie. Test na Node v25.8.2 pokazuje, że jest odwrotnie:

Zmiana w plikuWynik
Każda linia wciętaBłąd
Wcięta tylko linia -----BEGINBłąd
Wcięta tylko linia -----ENDBłąd
Wcięte tylko linie treści base64Przyjęte
Spacja przed całym blokiem PEMBłąd
Pusta linia przed całym blokiem PEMPrzyjęte

Reguła brzmi więc tak: linie -----BEGIN i -----END muszą zaczynać się w zerowej kolumnie, a wcięcie linii treści nie ma znaczenia. Dokładnie te dwie linie, o których wszędzie się słyszy, że wolno je wciąć, są tymi, które psują wczytywanie — a linie, które każe się wyrównywać, mają luz.

Ma to znaczenie ze względu na to, skąd wcięcia w kluczach prywatnych w ogóle się biorą. Nikt nie wcina PEM-u ręcznie. Dzieje się to wtedy, gdy klucz trafia do bloku YAML, do pliku values Helma, do heredoku w Terraformie albo do pythonowego stringa w potrójnym cudzysłowie wewnątrz ciała klasy. Każde z tych miejsc wcina całość równomiernie, razem z ogranicznikami, czyli dokładnie tak, jak w pierwszym wierszu powyższej tabeli.

Dosłowne backslash-n ze zmiennej środowiskowej zapisanej w jednej linii

PEM ma znaki nowej linii, a zmienna środowiskowa ich nie ma. Klucze lądują więc w plikach .env jako jedna linia, w której \n jest zapisane jako dwa znaki. Cokolwiek czyta ten plik, podaje kodowi ciąg zawierający backslashe, a parser widzi ogranicznik, po którym idą śmieci. W Node to przyczyna 6 z sekcji 2, z tym samym komunikatem DECODER routines::unsupported co wszystko inne.

Odwrócić to trzeba w miejscu użycia:

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

Wokół tego przydają się dwa zabezpieczenia. Po pierwsze, podmieniać tylko wtedy, gdy ciąg faktycznie zawiera tę dwuznakową sekwencję; prawdziwie wielolinijkowa wartość, która przeszła przez inny loader, zostanie wtedy nietknięta. Po drugie, jeśli platforma na to pozwala, lepiej trzymać cały PEM w base64: jedna linia base64 w konfiguracji, dekodowanie przy starcie i kwestia escapowania znika całkowicie.

BOM: nieszkodliwy w Node, nieprzetestowany gdzie indziej

Byte order mark to trzy bajty, EF BB BF, które niektóre edytory windowsowe zapisują na początku pliku UTF-8. Rada, żeby usunąć go przed wczytaniem klucza, jest bardzo częsta. W Node v25.8.2 nie robił on żadnej różnicy: PEM poprzedzony BOM-em wczytał się poprawnie zarówno jako string, jak i jako Buffer zaczynający się od tych trzech bajtów.

Zakres tego wyniku jest wąski. Zmierzono go wyłącznie na Node v25.8.2. Javy, Pythona ani innych parserów tutaj nie testowano i nic w tym artykule nie mówi, jak się zachowują. Przy debugowaniu usługi w Javie BOM pozostaje pytaniem otwartym, a nie wykluczoną hipotezą.

BOM psuje natomiast inne rzeczy i stamtąd zapewne, przez skojarzenie, wzięła się ta rada o kluczach. JSON.parse na ciągu poprzedzonym BOM-em to awaria realna i dobrze udokumentowana, opisana w tekście BOM UTF-8: napraw błędy JSON.parse i problemy z CSV. Plik klucza zapisany wewnątrz konfiguracji JSON może więc wywalić się na długo przed tym, zanim cokolwiek spojrzy na sam klucz.

Zakończenia linii, końcowy znak nowej linii i szerokość zawijania

Trzej kolejni podejrzani, których Node v25.8.2 oczyścił z zarzutów:

  • Zakończenia linii CRLF. Przyjęte. Klucz, który przeszedł przez Windowsa, nie jest automatycznie zepsuty.
  • Brak końcowego znaku nowej linii. Przyjęte. Akurat to zależy od parsera: mówi się, że niektóre parsery odrzucają PEM bez końcowego znaku nowej linii, ale Node do nich nie należy. Node to przyjmuje; innych parserów tutaj nie testowano.
  • Niezawinięta treść. Przyjęte. Base64 nie musi być łamane co 64 znaki.

To, co faktycznie psuje treść base64, to zgubiony, wstawiony albo podmieniony znak. Jest to zupełnie inna awaria niż zawijanie. Komunikator, który zamienia znak nowej linii na spację, albo pole tekstowe, które zjada ostatni znak, dają treść przestającą się dekodować. Kopiować przyciskiem kopiowania, a nie przeciągnięciem myszy.

6. OpenSSL 3.x zmienił domyślne zachowanie za plecami

Zmierzone na OpenSSL 3.6.2 7 Apr 2026:

PolecenieZapisywany kontener
openssl genrsa -out k.pem 2048PKCS#8, nagłówek 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.pemKonwertuje PKCS#1 na PKCS#8
openssl rsa -in b.pem -traditional -out a.pemKonwertuje PKCS#8 na PKCS#1

Dwa pierwsze wiersze trzeba przeczytać jeszcze raz. genrsa w tej wersji domyślnie daje PKCS#8, a plik BEGIN RSA PRIVATE KEY powstaje dopiero po dodaniu -traditional. Mnóstwo poradników wciąż opisuje genrsa jako polecenie od PKCS#1, a genpkey jako to od PKCS#8. Kto się nimi kieruje, zostaje z niezachwianą pewnością, że wygenerował format, którego nie wygenerował.

Praktyczna konsekwencja wychodzi przy migracjach. Zespół pracujący w Javie dostaje działający klucz od kolegi ze starszym OpenSSL-em, wszystko jest w porządku, a pół roku później ktoś generuje klucz od nowa na świeżej maszynie. To samo polecenie, ta sama dokumentacja, inny kontener — i JDK rzuca algid parse error, not a sequence w klucz, który „powstał dokładnie tak samo”. Nie powstał.

Nigdy nie zakładać, tylko sprawdzić:

head -1 key.pem

Jedna linia wyjścia, a tabela z sekcji 3 mówi, co jest w ręku. Sprawdzenie idzie przed jakimkolwiek poleceniem konwertującym, bo konwersja pliku PKCS#8 do PKCS#8 to operacja pusta, która wygląda jak poprawka i nie naprawia niczego.

Kto woli w ogóle nie myśleć o flagach, ma generator kluczy RSA online, który jednym przełącznikiem wypuszcza oba kontenery z tej samej pary kluczy. Można więc zrobić kopię PKCS#1 i kopię PKCS#8 tego samego klucza i sprawdzić każdą z nich w bibliotece, która odmawia współpracy.

7. Dolny limit 2048 bitów, który odrzuca całkowicie poprawny klucz

Jedna awaria wygląda na problem z formatem, a nim nie jest. W kodzie źródłowym jsonwebtoken plik sign.js rzuca:

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

Kod źródłowy podnosi ten wyjątek, gdy alg jest algorytmem RS lub PS, klucz ma poniżej 2048 bitów, a allowInsecureKeySizes nie zostało ustawione. To sprawdzenie należy do samej biblioteki, nie do środowiska uruchomieniowego. Node v25.8.2 wczytuje 1024-bitowy klucz RSA bez słowa sprzeciwu; modulusLength: 1024 daje obiekt klucza jak każdy inny. Klucz jest więc strukturalnie poprawny i kontener się zgadza, a wywołanie podpisujące i tak kończy się błędem.

Rozpoznaje się to po tym, że komunikat podaje liczbę. Błędy formatu mówią o dekoderach, sekwencjach i materiale klucza; ten mówi o bitach. Jeśli w komunikacie pojawia się rozmiar, trzeba przestać patrzeć na PEM.

Klucze 1024-bitowe biorą się zwykle z historii: klucz wygenerowany lata temu przy domyślnym ustawieniu, które od tamtej pory się przesunęło, albo fixture testowy, do którego nikt nie wrócił, bo małe klucze generują się szybciej. Poprawka polega na wygenerowaniu nowej pary o długości 2048 bitów lub większej. Furtka allowInsecureKeySizes wyłącza sprawdzenie, które istnieje nie bez powodu.

Żeby potwierdzić, że rozmiar jest jedynym pozostałym problemem, wystarczy podpisać ten sam payload świeżym kluczem o właściwej długości w koderze i generatorze JWT. Jeśli tam token powstaje, a we własnym kodzie nie, różnicą jest klucz, a nie claims ani konfiguracja.

8. Powtarzalny proces dla awarii klucza RS256

Wykonać po kolei. Każdy krok albo znajduje przyczynę, albo odcina gałąź.

  1. Odczytać linię nagłówka. head -1 key.pem, a potem dopasować wynik do tabeli z sekcji 3. To mówi, jaki jest kontener, czy plik jest zaszyfrowany i czy nie jest to klucz OpenSSH, który nigdy nie zadziała.
  2. Poprosić OpenSSL o wczytanie pliku. openssl rsa -in key.pem -noout -text | head -1 dla RSA albo openssl pkey -in key.pem -noout dla dowolnego algorytmu. Sukces oznacza, że bajty są poprawnym kluczem, a problem leży po stronie biblioteki. Niepowodzenie oznacza uszkodzony plik i przejście do kroku 4.
  3. Sprawdzić wiersz swojej biblioteki w macierzy. Sekcja 4. Java z plikiem PKCS#1 albo Go z wywołaniem niewłaściwej funkcji parsującej: na tym kroku sprawa się kończy.
  4. Przyjrzeć się niewidocznym znakom. head -c 32 key.pem | xxd pokazuje pierwsze bajty, co jednym spojrzeniem wyłapuje BOM, wiodącą spację i wcięty ogranicznik. Potem trzeba potwierdzić, że linie -----BEGIN i -----END zaczynają się w zerowej kolumnie, zgodnie z sekcją 5.
  5. Zawęzić problem kluczem o znanej poprawności. Wystarczy wygenerować świeżą parę w generatorze kluczy RSA online, wskazać ją kodowi i sprawdzić, czy błąd przetrwa. Jeśli przetrwa, wina leży po stronie kodu wczytującego, a nie pliku klucza, i żadne formatowanie oryginału nie pomoże. Jeśli zniknie, winny jest oryginalny plik, a do porównania jest już działający klucz.
  6. Algorytm i rozmiar sprawdzić na końcu. Potwierdzić, że nagłówek mówi RS256 i że klucz ma co najmniej 2048 bitów, zgodnie z sekcją 7.

Krok 5 pomija się najczęściej, a oszczędza najwięcej czasu. Czysty klucz referencyjny zamienia mgliste „klucz nie działa” w binarną odpowiedź na pytanie, która strona jest zepsuta.

FAQ

Czym różni się BEGIN RSA PRIVATE KEY od BEGIN PRIVATE KEY?

To dwa kontenery wokół tego samego klucza RSA. BEGIN RSA PRIVATE KEY to PKCS#1 i trzyma liczby RSA bezpośrednio; BEGIN PRIVATE KEY to PKCS#8, który dokłada identyfikator algorytmu i dlatego może nieść także klucze ECDSA oraz Ed25519. To, który jest potrzebny, zależy wyłącznie od biblioteki, a generator kluczy RSA online zapisuje jeden i drugi.

Dlaczego openssl genrsa daje inny format, niż pokazuje poradnik?

Bo przesunęła się wartość domyślna. W OpenSSL 3.6.2 openssl genrsa -out k.pem 2048 zapisuje PKCS#8 z nagłówkiem BEGIN PRIVATE KEY. Żeby dostać tradycyjny układ PKCS#1 opisywany w starszych przewodnikach, trzeba dodać -traditional. Zamiast wierzyć jakiemukolwiek poradnikowi w kwestii tego, co produkuje dana kompilacja, lepiej uruchomić head -1 na wyniku.

Jak naprawić algid parse error, not a sequence w Javie?

Ten komunikat na Javie 1.8.0_162 oznacza, że do PKCS8EncodedKeySpec trafił klucz PKCS#1. Biblioteka standardowa w ogóle nie czyta PKCS#1. Wystarczy jednorazowa konwersja poleceniem openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem albo dodanie BouncyCastle, jeśli coś innego w projekcie i tak go potrzebuje.

Czy każda linia klucza prywatnego musi zaczynać się w zerowej kolumnie?

Nie, a powszechna porada mówi coś dokładnie odwrotnego. W teście na Node v25.8.2 wcięcie wyłącznie linii treści base64 wczytuje się bez problemu, natomiast wcięcie samej linii -----BEGIN albo samej linii -----END kończy się błędem. Pusta linia przed blokiem PEM jest przyjmowana; wiodąca spacja nie.

Jak zapisać klucz prywatny w pliku .env, żeby się nie zepsuł?

Albo jako jedną linię w cudzysłowie z escapowanymi \n, które przy wczytywaniu odwraca się przez .replace(/\\n/g, '\n'), albo jako jedną linię base64 dekodowaną przy starcie. Drugi wariant jest bezpieczniejszy, bo nie ma tam żadnej konwencji escapowania, którą loader konfiguracji mógłby źle zrozumieć.

Czy można użyć klucza 1024-bitowego z RS256?

Node v25.8.2 wczytuje 1024-bitowy klucz RSA bez błędu, ale kod źródłowy jsonwebtoken odmawia podpisania nim: secretOrPrivateKey has a minimum key size of 2048 bits, chyba że ustawiono allowInsecureKeySizes. Zamiast tego trzeba wygenerować klucz 2048-bitowy. Komunikat podaje liczbę bitów i po tym odróżnia się go od problemu z formatem.

Dlaczego błąd formatu klucza prywatnego RS256 mówi, że potrzebny jest klucz asymetryczny, skoro podano plik z kluczem prywatnym?

W kodzie źródłowym jsonwebtoken komunikat secretOrPrivateKey must be an asymmetric key when using ${header.alg} odpala się, gdy alg to RS, PS lub ES, a klucz nie jest kluczem prywatnym. Zwykle pod tą wartością siedzi sekret w stylu HS256, pozostały po wcześniejszej konfiguracji. Losowy ciąg znaków należy do HS256 i do generatora sekretu JWT; RS256 potrzebuje pary kluczy, a nie sekretu.

Podsumowanie

Koszt tej klasy błędów bierze się z rozjazdu między komunikatem a przyczyną. Jeden ciąg błędu obejmuje w Node siedem przyczyn, komunikat Javy wskazuje na ASN.1, podczas gdy prawdziwa odpowiedź brzmi „zły kontener”, a najczęściej powtarzana porada o formatowaniu jest w tej sprawie odwrócona. Do odpowiedzi nie da się dojść czytaniem, więc zostaje eliminacja: linia nagłówka, próba wczytania przez OpenSSL, macierz bibliotek, niewidoczne znaki, klucz o znanej poprawności.

Powtórce zapobiegają dwa nawyki. Po pierwsze, zapisać obok klucza w magazynie sekretów, jakiego kontenera wymaga każda usługa; ograniczenie mieszka w bibliotece, a nie w kluczu. Po drugie, trzymać w środowisku deweloperskim parę kluczy o znanej poprawności wyłącznie jako próbę kontrolną, żeby pierwsze pytanie przy każdej awarii klucza dostawało odpowiedź tak/nie w minutę.

Szersze pytanie o to, jak te klucze wydawać, rotować i ograniczać zakresem, kiedy już wczytują się poprawnie, opisuje tekst Bezpieczeństwo JWT: najlepsze praktyki, ataki i obrona.

Tagi: jwt rsa pem openssl debugging security

Powiązane artykuły

Zobacz wszystkie artykuły