RS256 privésleutel-formaatfout: één melding, zeven oorzaken
Een RS256 privésleutel-formaatfout noemt vrijwel nooit zijn eigen oorzaak. Op Node v25.8.2 levert elk van deze vergissingen exact dezelfde regel op:
code: ERR_OSSL_UNSUPPORTED
message: error:1E08010C:DECODER routines::unsupported
Vijf dingen die niets met elkaar te maken hebben zetten hem in gang: een OpenSSH-container waar een PEM-sleutel werd verwacht, een ingesprongen -----BEGIN-regel, een publieke sleutel die aan de ondertekening wordt doorgegeven, letterlijke \n-reeksen die niemand heeft teruggezet, en een bestand waarvan de regeleindes onderweg zijn gesneuveld. De geteste lijst in sectie 2 telt er zeven. Elke keer dezelfde regel. Zoek je op de fouttekst, dan beland je dus midden in iemand anders’ draadje over iemand anders’ oorzaak.
Splits het probleem eerst in tweeën:
- De library heeft nooit een sleutelobject gekregen. Blijf hier.
- De library laadde de sleutel en zei daarna
invalid signature. Ander soort fout, andere oorzaken. Ga naar JWT invalid signature: alle oorzaken en hoe je ze oplost.
Triage van dertig seconden voor het eerste geval:
openssl rsa -in key.pem -noout -text | head -1
Geeft dat een fout, dan zit het probleem in het bestand en vinden secties 3 tot en met 6 het. Lukt het wel, dan begreep OpenSSL de container en zit je probleem bij de library of bij wat je eraan hebt doorgegeven. Dat zijn secties 4 en 7.
Alles hieronder is op 2026-08-11 gemeten tegen OpenSSL 3.6.2 7 Apr 2026, Node v25.8.2, Go go1.26.1 darwin/arm64 en Java 1.8.0_162. Waar een bewering uit het lezen van broncode komt in plaats van uit het uitvoeren ervan, staat dat er expliciet bij.
1. Bepaal eerst welk soort fout je hebt
De scheidslijn is of er ooit een sleutelobject heeft bestaan. Inleesfouten treden op voordat er ook maar één cryptografische bewerking draait. De library leest je PEM, slaagt er niet in er een sleutel van te maken, en gooit een fout. Er is niets ondertekend en niets geverifieerd, en het token dat je aan het debuggen bent is nooit geproduceerd. Verificatiefouten zijn het omgekeerde: de sleutel laadde probleemloos, er werd een handtekening berekend, en die kwam niet overeen. Die komen voort uit verschillen op byteniveau tussen ondertekenaar en verifieerder, en de gids over invalid signature behandelt ze.
Het onderscheid maken kost één blik op de stacktrace. Een inleesfout noemt een decoder, een key spec of een ASN.1-structuur. Een verificatiefout noemt een handtekening.
Zo ziet een geweigerde privésleutel eruit in drie ecosystemen:
| Runtime | Geteste versie | Melding als de sleutel niet laadt |
|---|---|---|
Node crypto | v25.8.2 | error:1E08010C:DECODER routines::unsupported |
Go crypto/x509 | go1.26.1 darwin/arm64 | x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format) |
Java PKCS8EncodedKeySpec | 1.8.0_162 | InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence |
Ze lopen ver uiteen in behulpzaamheid. Go vertelt je precies welke functie je in plaats daarvan moet aanroepen. Java noemt een “algid” en een “sequence” en laat het aan jou over om te bedenken dat je sleutel in de verkeerde container zit. Node zegt helemaal niets bruikbaars.
Weet je niet zeker of het token dat je najaagt überhaupt RS256 is, plak het dan in de tool om JWT decoderen online en lees alg uit de header voordat je verdergaat. Een HS256-header betekent dat je een gedeeld secret nodig hebt en geen sleutelpaar, en dan wijst elk symptoom in dit artikel je de verkeerde kant op.
2. Van fouttekst naar oorzaak: de opzoektabel voor de RS256 privésleutel-formaatfout
Zoek je exacte tekenreeks op. De rechterkolom vertelt je waar je verder moet kijken.
| Fouttekst | Waar hij vandaan komt | Wat het werkelijk betekent |
|---|---|---|
error:1E08010C:DECODER routines::unsupported | Node v25.8.2 | Zeven mogelijke oorzaken, hieronder opgesomd |
error:07880109:common libcrypto routines::interrupted or cancelled | Node v25.8.2 | De sleutel is versleuteld en je gaf geen wachtwoordzin mee |
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format) | Go 1.26.1 | Je riep ParsePKCS1PrivateKey aan op een PKCS#8-bestand |
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format) | Go 1.26.1 | Je riep ParsePKCS8PrivateKey aan op een PKCS#1-bestand |
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...}) | Go 1.26.1 | Het eerste PEM-blok is EC PARAMETERS, niet de sleutel |
algid parse error, not a sequence | Java 1.8.0_162 | PKCS#1 aangeboden aan PKCS8EncodedKeySpec |
secretOrPrivateKey must have a value | jsonwebtoken, in de broncode | Het sleutelargument is falsy en alg is niet none |
secretOrPrivateKey is not valid key material | jsonwebtoken, in de broncode | Er kon noch een privésleutel noch een secret key worden opgebouwd |
secretOrPrivateKey must be a symmetric key when using ${header.alg} | jsonwebtoken, in de broncode | alg begint met HS, maar de sleutel is geen secret |
secretOrPrivateKey must be an asymmetric key when using ${header.alg} | jsonwebtoken, in de broncode | alg komt overeen met RS, PS of ES, maar de sleutel is geen privésleutel |
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg} | jsonwebtoken, in de broncode | RS of PS met een sleutel onder 2048 bits en allowInsecureKeySizes uit |
De vijf secretOrPrivateKey-teksten zijn gelezen uit sign.js op de master-branch van jsonwebtoken en zijn hier niet lokaal uitgevoerd. Behandel de voorwaarden dus als wat de broncode zegt, en niet als iets dat op deze machine is gereproduceerd. Het stuk ${header.alg} is een template-placeholder in die broncode; tijdens uitvoering staat daar je eigen algoritmenaam, en daarom levert zoeken op de letterlijke tekst met accolades niets op.
De zeven manieren om DECODER routines::unsupported op te roepen
Alle zeven zijn gereproduceerd tegen crypto.createPrivateKey() op Node v25.8.2, en alle zeven gaven dezelfde code en melding:
- Een OpenSSH-container. Het bestand begint met
-----BEGIN OPENSSH PRIVATE KEY-----en is helemaal geen PEM-sleutelstructuur. - Een ingesprongen
-----BEGIN-regel, of een ingesprongen-----END-regel. De regels ertussen vallen buiten schot; sectie 5 geeft de exacte grens. - Witruimte vóór de hele PEM. Een lege regel vooraf mag, een spatie vooraf niet.
- Regeleindes volledig verwijderd, zodat de header, de base64 en de footer op één regel aan elkaar plakken.
- Een publieke sleutel waar een privésleutel werd verwacht.
- Letterlijke backslash-n-reeksen die niemand heeft teruggezet — precies wat een omgevingsvariabele op één regel ervan maakt.
- Een scheidingsregel met het verkeerde aantal streepjes, of
begin/endin kleine letters geschreven.
Twee daarvan zijn containerproblemen, vier zijn problemen met verminkte tekst, en één is gewoon een verwisseling. De melding verklapt niet welke, dus je komt sneller vooruit door te elimineren dan door te lezen.
Wat Node wél accepteert, en waarom dat je zoektocht versmalt
De omgekeerde lijst is nuttiger, want elk item erop is een theorie die je meteen kunt laten vallen. Op Node v25.8.2 accepteerde crypto.createPrivateKey() dit allemaal zonder morren:
- PKCS#1- en PKCS#8-privésleutels
- EC SEC1-privésleutels
- CRLF-regeleindes
- Een ontbrekend afsluitend regeleinde
- Een base64-inhoud op één ongevouwen regel
- Ingesprongen regels in de body
- Een lege regel vóór de PEM
- Een UTF-8 BOM, geleverd als
'' + pemen alsBufferdie begint met0xEF 0xBB 0xBF - Een PKCS#1-header om een PKCS#8-body heen
Die laatste verdient even aandacht. De decoder leest de DER-structuur binnen de base64 en negeert het label aan de buitenkant, dus een bestand met BEGIN RSA PRIVATE KEY boven PKCS#8-inhoud laadt gewoon. Dat is meteen een waarschuwing voor sectie 3: de headerregel is een aanwijzing, geen garantie.
3. De PEM-headerregel: welke container heb je echt in handen
Elke PEM kondigt zichzelf aan op regel één. Dit zijn de headerwaarden die OpenSSL 3.6.2 wegschrijft:
| Inhoud | Eerste regel |
|---|---|
| PKCS#8-privésleutel | -----BEGIN PRIVATE KEY----- |
| PKCS#1-privésleutel | -----BEGIN RSA PRIVATE KEY----- |
| Versleutelde privésleutel | -----BEGIN ENCRYPTED PRIVATE KEY----- |
| OpenSSH-privésleutel | -----BEGIN OPENSSH PRIVATE KEY----- |
| EC SEC1-privésleutel | -----BEGIN EC PARAMETERS-----, daarna een tweede blok -----BEGIN EC PRIVATE KEY----- |
| SPKI-publieke sleutel | -----BEGIN PUBLIC KEY----- |
| PKCS#1-publieke sleutel | -----BEGIN RSA PUBLIC KEY----- |
| Ed25519-privésleutel | -----BEGIN PRIVATE KEY-----, en het hele bestand is drie regels |
head -1 key.pem beantwoordt dus de eerste vraag van elk onderzoek. Bij drie van die headers zit een addertje onder het gras.
ENCRYPTED PRIVATE KEY is geen formaatfout. Het is een wachtwoordzin die je vergeten bent mee te geven. Node meldt dit anders dan al het andere, met ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED en error:07880109:common libcrypto routines::interrupted or cancelled, omdat de library om een wachtwoordzin vroeg en niets terugkreeg. Gooi deze melding niet op één hoop met die van DECODER; ze hebben niets met elkaar te maken.
OPENSSH PRIVATE KEY is een andere wereld. OpenSSH schrijft zijn eigen container, en dat is geen PKCS#1 of PKCS#8, ook al staat hij tussen scheidingsregels die op PEM lijken. Node weigert hem botweg, en de parsers van Go’s crypto/x509 en de PKCS8EncodedKeySpec van de JDK doen dat ook. Komt de sleutel waarmee je JWT’s ondertekent uit ssh-keygen, dan is dat je bug.
EC SEC1-bestanden bevatten twee blokken. openssl ecparam -genkey schrijft eerst een EC PARAMETERS-blok en pas daarna de privésleutel. Alles wat alleen het eerste PEM-blok leest krijgt de parameters te pakken en faalt op een manier die geen van beide noemt. Sectie 4 heeft de Go-versie van die fout.
En omdat de header alleen een label is, telt de omgekeerde controle net zo goed: een bestand waarvan de header het één zegt en de DER het ander, wordt volgens de DER verwerkt. head -1 lezen is betrouwbaar voor bestanden die rechtstreeks uit OpenSSL komen, en onbetrouwbaar voor bestanden die langs een mens, een wikipagina of een script met zoek-en-vervang zijn gekomen.
4. Welke library wat accepteert: PKCS#1 versus PKCS#8 in drie ecosystemen
Dit is de matrix die de meeste formaatdiscussies tussen teams verklaart. Elke rij is gemeten op de versies die bovenaan dit artikel staan.
| Library | PKCS#1 | PKCS#8 | OpenSSH | Legt de foutmelding zichzelf uit? |
|---|---|---|---|---|
Node crypto | Ja | Ja | Nee | Nee. Veel oorzaken, één DECODER routines::unsupported |
Go crypto/x509 | Ja, eigen functie | Ja, eigen functie | Nee | Ja. Hij noemt de functie waarnaar je moet overstappen |
| Java-standaardlibrary | Nee | Ja | Nee | Nee. algid parse error, not a sequence is ronduit misleidend |
Lees de kolommen en de discussies lossen zichzelf op. Een Node-service en een Java-service die één sleutelbestand delen werkt prima, totdat de sleutel PKCS#1 is: dan blijft Node vrolijk ondertekenen en gooit Java een melding over ASN.1-sequences. Niemand verdenkt de sleutel, want die werkt aantoonbaar in productie op de andere service.
Node. Niets in te stellen. Is de container PKCS#1 of PKCS#8, dan neemt createPrivateKey() hem aan. Gooit hij tóch een fout, besteed je tijd dan aan de zeven oorzaken uit sectie 2 en niet aan het formaat.
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);
}
Voer dat uit tegen het bestand dat je applicatie inleest, niet tegen een kopie die je met de hand hebt gemaakt, en de catch-tak drukt het paar code en melding af dat je in sectie 2 kunt opzoeken.
Go. Twee containers, twee functies, en de verkeerde aanroepen is de meest voorkomende Go-fout. De melding vertelt je welke je wél moet gebruiken, dus de oplossing is mechanisch. Ze allebei op volgorde proberen haalt de keuze helemaal weg:
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
}
De EC-val moet je wel eerst afvangen. Tegen een bestand van openssl ecparam -genkey geeft pem.Decode een blok terug waarvan Type gelijk is aan EC PARAMETERS, en alle drie de parse-functies falen daarop met:
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})
Die melding noemt PEM-blokken met geen woord, dus de gebruikelijke reactie is de sleutel te verdenken. Sla in plaats daarvan het parameterblok over:
block, rest := pem.Decode(pemBytes)
if block == nil {
log.Fatal("no PEM block found")
}
if block.Type == "EC PARAMETERS" {
block, _ = pem.Decode(rest)
}
Of voorkom dat extra blok helemaal door -noout toe te voegen aan het ecparam-commando dat het bestand schrijft.
Java. De standaardlibrary leest PKCS#8 en verder niets. Geef PKCS8EncodedKeySpec een PKCS#1-sleutel op Java 1.8.0_162 en je krijgt:
InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
“algid” is de algorithm identifier, het veld dat PKCS#8 toevoegt en dat PKCS#1 niet heeft. De parser zocht ernaar, vond het begin van een RSA-modulus en gaf het op. De melding is even correct als nutteloos. Zet het bestand om en de fout verdwijnt:
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem
Zodra het bestand PKCS#8 is, is het werkende laadpad voor Java 8 kort genoeg om in een test te plakken terwijl je de oplossing bevestigt:
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));
Het alternatief voor omzetten is BouncyCastle toevoegen, dat PKCS#1 wél leest. Omzetten is één commando en nul extra afhankelijkheden, dus zet om, tenzij iets anders in je stack die library toch al nodig heeft.
5. De tekens die je niet ziet
Hier zit het populaire advies ernaast, en dat is meetbaar.
Inspringen: precies het omgekeerde van wat je is verteld
Een veelherhaalde instructie zegt dat elke regel van een PEM behalve de scheidingsregels op kolom nul moet beginnen. Getest op Node v25.8.2 is dat andersom:
| Wijziging in het bestand | Resultaat |
|---|---|
| Elke regel ingesprongen | Faalt |
Alleen de -----BEGIN-regel ingesprongen | Faalt |
Alleen de -----END-regel ingesprongen | Faalt |
| Alleen de base64-regels in de body ingesprongen | Geaccepteerd |
| Een spatie vóór de hele PEM | Faalt |
| Een lege regel vóór de hele PEM | Geaccepteerd |
De regel luidt dus: de -----BEGIN- en -----END-regels moeten op kolom nul beginnen, en inspringing van de body-regels maakt niets uit. Precies de twee regels waarvan mensen horen dat ze mogen inspringen zijn de twee die breken; de body, die volgens dat advies strak moet uitlijnen, heeft juist speling.
Dit doet ertoe vanwege de manier waarop privésleutels überhaupt ingesprongen raken. Niemand springt een PEM met de hand in. Het gebeurt als een sleutel in een YAML-blok wordt geplakt, in een Helm values-bestand, in een Terraform-heredoc of in een Python-string met drie aanhalingstekens binnen een class. Elk daarvan springt het hele blok uniform in, scheidingsregels incluis, en dat is de eerste rij van die tabel.
Letterlijke backslash-n uit een omgevingsvariabele op één regel
Een PEM heeft regeleindes en een omgevingsvariabele heeft die in de praktijk niet. Dus belanden sleutels in .env-bestanden als één regel waarin \n als twee losse tekens is uitgeschreven. Wat dat bestand ook inleest, het geeft je code een string met backslashes, en de parser ziet een scheidingsregel gevolgd door rommel. Op Node is dit oorzaak 6 uit sectie 2, met dezelfde melding DECODER routines::unsupported als al het andere.
Draai het terug op het punt van gebruik:
const pem = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');
Doe die vervanging alleen als de string die tweetekenreeks werkelijk bevat, dan blijft een waarde die via een andere loader al met echte regeleindes binnenkomt ongemoeid. Beter nog is base64, als je platform dat toelaat: sla één regel base64 op, decodeer die bij het opstarten, en de hele escape-vraag verdwijnt.
BOM: onschadelijk op Node, en elders niet getest
Een byte order mark bestaat uit drie bytes, EF BB BF, die sommige Windows-editors aan het begin van een UTF-8-bestand schrijven. Het advies om die weg te halen voordat je een sleutel inleest kom je vaak tegen. Op Node v25.8.2 maakte het geen enkel verschil: een PEM met de BOM ervoor werd probleemloos ingelezen, als string en als Buffer die met die drie bytes begint.
Baken dat resultaat wel af: het is uitsluitend op Node v25.8.2 gemeten. Java, Python en andere parsers zijn hier niet getest, en niets in dit artikel zegt hoe die zich gedragen. Debug je een Java-service, dan blijft de BOM een open vraag in plaats van een uitgesloten oorzaak.
De BOM sloopt wél andere dingen, en daar komt het sleuteladvies waarschijnlijk vandaan, bij associatie. JSON.parse op een string met een BOM ervoor is een echte en goed gedocumenteerde fout, behandeld in UTF-8 BOM: JSON-parsefouten en CSV-problemen oplossen. Een sleutelbestand dat binnen een JSON-configuratie is opgeslagen kan dus al falen lang voordat iemand naar de sleutel kijkt.
Regeleindes, afsluitend regeleinde en regelbreedte
Nog drie verdachten die Node v25.8.2 vrijpleitte:
- CRLF-regeleindes: geaccepteerd. Een sleutel die door Windows is gereisd is niet automatisch kapot.
- Een ontbrekend afsluitend regeleinde: geaccepteerd. Let wel, dit hangt van de parser af. Node accepteert het; andere parsers zijn hier niet getest.
- Een niet-gevouwen body: geaccepteerd. De base64 hoeft niet op 64 tekens te worden afgebroken.
Wat een base64-body wél sloopt is een verdwenen, ingevoegd of vervangen teken, en dat is een andere fout dan het afbreken van regels. Een chatprogramma dat een regeleinde in een spatie verandert, of een tekstveld dat een laatste teken opeet, levert een body op die niet meer te decoderen is. Kopiëren doe je met een kopieerknop en niet door met de muis te slepen.
6. OpenSSL 3.x heeft de standaard onder je vandaan veranderd
Gemeten op OpenSSL 3.6.2 7 Apr 2026:
| Commando | Container die het schrijft |
|---|---|
openssl genrsa -out k.pem 2048 | PKCS#8, header BEGIN PRIVATE KEY |
openssl genrsa -traditional -out k.pem 2048 | PKCS#1 |
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 | PKCS#8 |
openssl genpkey -algorithm ED25519 | PKCS#8 |
openssl pkcs8 -topk8 -nocrypt -in a.pem -out b.pem | Zet PKCS#1 om naar PKCS#8 |
openssl rsa -in b.pem -traditional -out a.pem | Zet PKCS#8 om naar PKCS#1 |
Lees die eerste twee rijen nog eens. genrsa geeft je op deze build standaard PKCS#8, en -traditional is wat het bestand met BEGIN RSA PRIVATE KEY oplevert. Genoeg handleidingen beschrijven genrsa nog steeds als het PKCS#1-commando en genpkey als het PKCS#8-commando, en wie die volgt weet zeker dat hij een formaat heeft gegenereerd dat hij niet heeft.
Het praktische gevolg duikt op bij migraties. Een team op Java krijgt een werkende sleutel van een collega met een oudere OpenSSL, alles gaat goed, en zes maanden later genereert iemand de sleutel opnieuw op een verse machine. Zelfde commando, zelfde documentatie, andere container, en nu gooit de JDK algid parse error, not a sequence naar een sleutel die “op precies dezelfde manier is gegenereerd”. Dat is niet zo.
Raad dus niet, controleer:
head -1 key.pem
Eén regel uitvoer, en de tabel uit sectie 3 vertelt je wat je in handen hebt. Doe dit vóór je een omzettingscommando draait, want een PKCS#8-bestand omzetten naar PKCS#8 is een lege operatie die op een oplossing lijkt en niets oplost.
Wil je liever helemaal niet over die vlaggen nadenken, dan kun je een RSA-sleutel online genereren: die tool geeft beide containers uit hetzelfde sleutelpaar met één schakelaar, zodat je een PKCS#1- en een PKCS#8-kopie van dezelfde sleutel kunt maken en ze allebei kunt aanbieden aan de library die je weigert.
7. De ondergrens van 2048 bits die een volkomen geldige sleutel weigert
Eén fout ziet eruit als een formaatprobleem zonder er een te zijn. In de broncode van jsonwebtoken gooit sign.js:
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}
De broncode werpt hem op wanneer alg een RS- of PS-algoritme is, de sleutel onder 2048 bits zit, en allowInsecureKeySizes niet is gezet. Die controle is van de library zelf en niet van de runtime. Node v25.8.2 leest een RSA-sleutel van 1024 bits gewoon in; modulusLength: 1024 levert een sleutelobject op als elk ander. De sleutel is dus structureel geldig, de container klopt, OpenSSL leest hem, en de aanroep om te ondertekenen faalt alsnog.
Het herkenningspunt is dat deze melding een getal noemt. Formaatfouten hebben het over decoders, sequences en key material; deze heeft het over bits. Zie je een grootte in de melding staan, stop dan met naar de PEM kijken.
Waar sleutels van 1024 bits vandaan komen is meestal geschiedenis: een sleutel die jaren geleden is gegenereerd tegen een standaard die sindsdien is opgeschoven, of een testfixture die niemand ooit heeft herzien omdat kleine sleutels sneller genereren. De oplossing is een nieuw paar van 2048 bits of meer genereren. De ontsnappingsklep zet een controle uit die er niet voor niets zit, dus laat hem staan.
Om te bevestigen dat de grootte het enige resterende probleem is, onderteken je dezelfde payload met een verse sleutel van de juiste grootte in de JWT-encoder & generator. Levert dat een token op en jouw eigen code niet, dan zit het verschil in je sleutel en niet in je claims of je configuratie.
8. Een herbruikbare werkwijze voor RS256-sleutelfouten
Voer deze stappen op volgorde uit. Elke stap vindt de oorzaak of schrapt een tak.
- Lees de headerregel.
head -1 key.pem, en leg die naast de tabel in sectie 3. Dit vertelt je de container, of het bestand versleuteld is, en of het een OpenSSH-sleutel is die nooit gaat werken. - Laat OpenSSL hem inlezen.
openssl rsa -in key.pem -noout -text | head -1voor RSA, ofopenssl pkey -in key.pem -nooutvoor elk algoritme. Slaagt dat, dan zijn de bytes een geldige sleutel en zit het probleem aan de kant van de library. Faalt het, dan is het bestand beschadigd en ga je door naar stap 4. - Zoek de rij van jouw library op in de matrix. Sectie 4. Zit je op Java met een PKCS#1-bestand, of roep je in Go de verkeerde parse-functie aan, dan ben je hier klaar.
- Kijk naar de onzichtbare tekens.
head -c 32 key.pem | xxdtoont de eerste bytes, wat in één oogopslag een BOM, een spatie vooraf en een ingesprongen scheidingsregel vangt. Controleer daarna of de-----BEGIN- en-----END-regels op kolom nul beginnen, zoals in sectie 5. - Bisecteer met een sleutel waarvan je zeker weet dat hij goed is. Laat een vers paar RSA-sleutels online genereren, wijs je code ernaartoe, en kijk of de fout overleeft. Blijft hij, dan zit de bug in je laadcode en niet in het sleutelbestand, en helpt geen enkele herformattering van het origineel. Verdwijnt hij, dan is het oorspronkelijke bestand de boosdoener en heb je nu een werkende sleutel om tegenaan te vergelijken.
- Controleer het algoritme en de grootte als laatste. Bevestig dat de header
RS256zegt, en dat de sleutel minstens 2048 bits telt, zoals in sectie 7.
Stap 5 slaan mensen het vaakst over, en juist die bespaart de meeste tijd. Een schone referentiesleutel verandert een vaag “de sleutel werkt niet” in een binair antwoord over welke kant stuk is.
FAQ
Wat is het verschil tussen BEGIN RSA PRIVATE KEY en BEGIN PRIVATE KEY?
Het zijn twee containers om dezelfde RSA-sleutel. BEGIN RSA PRIVATE KEY is PKCS#1 en bevat de RSA-getallen rechtstreeks; BEGIN PRIVATE KEY is PKCS#8 en voegt een algorithm identifier toe, en daarom kan die ook ECDSA- en Ed25519-sleutels dragen. Welke je nodig hebt hangt volledig van de library af, en je kunt een RSA-sleutel online genereren in beide vormen.
Waarom levert openssl genrsa een ander formaat op dan de handleiding laat zien?
Omdat de standaardwaarde is verschoven. Op OpenSSL 3.6.2 schrijft openssl genrsa -out k.pem 2048 PKCS#8 met een BEGIN PRIVATE KEY-header. Voeg -traditional toe voor de traditionele PKCS#1-indeling die oudere handleidingen beschrijven. Voer head -1 uit op de uitvoer in plaats van een handleiding te geloven over wat jouw build produceert.
Hoe los ik algid parse error, not a sequence op in Java?
Die melding op Java 1.8.0_162 betekent dat je PKCS8EncodedKeySpec een PKCS#1-sleutel hebt gegeven. De standaardlibrary leest PKCS#1 helemaal niet. Zet één keer om met openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem, of voeg BouncyCastle toe als iets anders in het project die library toch al nodig heeft.
Moet elke regel van een privésleutel op kolom nul beginnen?
Nee, en het gangbare advies heeft het omgekeerd. Getest op Node v25.8.2: alleen de base64-regels in de body laten inspringen wordt prima ingelezen, terwijl alleen de -----BEGIN-regel of alleen de -----END-regel laten inspringen faalt. Een lege regel vóór de PEM wordt geaccepteerd, een spatie ervoor niet.
Hoe hoor je een privésleutel op te slaan in een .env-bestand?
Ofwel als één regel tussen aanhalingstekens met \n-escapes die je bij het laden terugdraait met .replace(/\\n/g, '\n'), ofwel als één regel base64 die je bij het opstarten decodeert. Het tweede is veiliger, want er is dan geen escape-conventie die een configuratielader verkeerd kan uitleggen.
Kan ik een sleutel van 1024 bits gebruiken met RS256?
Node v25.8.2 leest een RSA-sleutel van 1024 bits zonder fout in, maar de broncode van jsonwebtoken weigert ermee te ondertekenen: secretOrPrivateKey has a minimum key size of 2048 bits, tenzij allowInsecureKeySizes is gezet. Genereer in plaats daarvan een sleutel van 2048 bits. De melding noemt een aantal bits, en zo onderscheid je hem van een formaatprobleem.
Waarom krijg ik een RS256 privésleutel-formaatfout die zegt dat er een asymmetrische sleutel nodig is, terwijl ik juist een bestand met een privésleutel heb doorgegeven?
In de broncode van jsonwebtoken slaat secretOrPrivateKey must be an asymmetric key when using ${header.alg} aan wanneer alg RS, PS of ES is en de sleutel geen privésleutel is. Meestal is de waarde een secret-string in HS256-stijl die van een eerdere configuratie is blijven staan. Een willekeurige string hoort bij HS256 en bij de JWT-secretgenerator; RS256 heeft een sleutelpaar nodig en geen secret.
Conclusie
Dit soort bugs is niet moeilijk, wel duur. Eén fouttekst op Node dekt zeven oorzaken af, de melding van Java wijst naar ASN.1 terwijl het echte antwoord “verkeerde container” luidt, en het meest herhaalde stukje formatteringsadvies over dit onderwerp is omgekeerd. Je leest je niet naar het antwoord toe, dus je elimineert: headerregel, controle door OpenSSL, matrix per library, onzichtbare tekens, en een sleutel waarvan je zeker weet dat hij goed is.
Noteer welke container elke service nodig heeft, náást de sleutel in je secret store, want die eis zit in de library en niet in de sleutel. En houd in je ontwikkelomgeving een sleutelpaar aan waarvan je zeker weet dat het goed is, puur als controle, zodat de eerste vraag bij elke sleutelfout binnen een minuut een ja-of-nee-antwoord krijgt.
Voor de bredere vraag hoe deze sleutels uitgegeven, geroteerd en afgebakend horen te worden zodra ze wel correct laden, zie JWT-beveiliging: best practices, aanvallen en verdediging.