Errore formato chiave privata RS256: un messaggio, sette cause
Un errore di formato della chiave privata RS256 non dice quasi mai quale sia la sua causa. Su Node v25.8.2 cause diverse producono esattamente la stessa riga:
code: ERR_OSSL_UNSUPPORTED
message: error:1E08010C:DECODER routines::unsupported
A scatenarlo sono cinque guasti che non hanno niente in comune: un container OpenSSH dove ci si aspettava una chiave PEM, una riga -----BEGIN con un rientro, una chiave pubblica passata a chi deve firmare, sequenze \n letterali che nessuno ha riconvertito e un file a cui il trasporto ha tolto gli a capo. L’elenco testato nella sezione 2 arriva a sette. Sempre la stessa riga, ed è per questo che cercare il testo dell’errore ti fa finire nel thread di qualcun altro, su una causa diversa dalla tua.
Per prima cosa, taglia il problema a metà:
- La libreria non ha mai ottenuto un oggetto chiave. Resta qui.
- La libreria ha caricato la chiave e poi ha detto
invalid signature. Guasto diverso, cause diverse. Vai a JWT invalid signature: tutte le cause e come risolverle.
Triage da trenta secondi per il primo caso:
openssl rsa -in key.pem -noout -text | head -1
Se dà errore, il problema è il file e le sezioni da 3 a 6 lo troveranno. Se va a buon fine, OpenSSL ha capito il container e il tuo problema è la libreria o quello che le hai passato: sezioni 4 e 7.
Tutto quello che segue è stato misurato l’11 agosto 2026 su OpenSSL 3.6.2 7 Apr 2026, Node v25.8.2, Go go1.26.1 darwin/arm64 e Java 1.8.0_162. Dove un’affermazione viene dalla lettura del sorgente e non dall’averlo eseguito, il testo lo dice.
1. Prima di tutto, stabilisci che tipo di guasto hai
La linea di demarcazione è una sola: è mai esistito un oggetto chiave?
I guasti in fase di parsing avvengono prima che parta qualsiasi operazione crittografica. La libreria legge il tuo PEM, non riesce a trasformarlo in una chiave e solleva un’eccezione. Non ha firmato niente e il token su cui stai facendo debug non è mai esistito. I guasti in fase di verifica sono l’opposto: la chiave si è caricata senza problemi, la libreria ha calcolato una firma e quella firma non corrisponde. Quelli nascono da disaccordi a livello di byte tra chi firma e chi verifica, e li tratta la guida su invalid signature.
Per distinguerli basta un’occhiata allo stack trace. Un guasto in fase di parsing nomina un decoder, una key spec o una struttura ASN.1. Un guasto in fase di verifica nomina una firma.
Ecco che aspetto ha una chiave privata rifiutata in tre ecosistemi:
| Runtime | Versione testata | Messaggio quando la chiave non si carica |
|---|---|---|
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 |
L’utilità cambia parecchio da riga a riga. Go ti dice esattamente quale funzione chiamare al suo posto. Java tira in ballo un “algid” e una “sequence” e ti lascia dedurre da solo che la chiave è nel container sbagliato. Node non dice niente di utilizzabile.
Se non hai la certezza che il token che stai inseguendo sia davvero RS256, incollalo nel decodificatore JWT online e leggi alg nell’intestazione prima di andare avanti. Un’intestazione HS256 significa che ti serve un segreto condiviso e non una coppia di chiavi, e ogni sintomo descritto in questo articolo ti porterà fuori strada.
2. Dal testo dell’errore alla causa: la tabella di lookup dell’errore formato chiave privata RS256
Trova la tua stringa esatta. La colonna di destra dice dove andare dopo.
| Testo dell’errore | Da dove viene | Che cosa significa davvero |
|---|---|---|
error:1E08010C:DECODER routines::unsupported | Node v25.8.2 | Sette cause possibili, elencate qui sotto |
error:07880109:common libcrypto routines::interrupted or cancelled | Node v25.8.2 | La chiave è cifrata e non hai fornito nessuna passphrase |
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format) | Go 1.26.1 | Hai chiamato ParsePKCS1PrivateKey su un file PKCS#8 |
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format) | Go 1.26.1 | Hai chiamato ParsePKCS8PrivateKey su un file PKCS#1 |
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...}) | Go 1.26.1 | Il primo blocco PEM è EC PARAMETERS, non la chiave |
algid parse error, not a sequence | Java 1.8.0_162 | Un PKCS#1 passato a PKCS8EncodedKeySpec |
secretOrPrivateKey must have a value | jsonwebtoken, nel sorgente | L’argomento della chiave è falsy e alg non è none |
secretOrPrivateKey is not valid key material | jsonwebtoken, nel sorgente | Non è stato possibile costruire né una chiave privata né una chiave segreta |
secretOrPrivateKey must be a symmetric key when using ${header.alg} | jsonwebtoken, nel sorgente | alg inizia per HS ma la chiave non è un segreto |
secretOrPrivateKey must be an asymmetric key when using ${header.alg} | jsonwebtoken, nel sorgente | alg corrisponde a RS, PS o ES ma la chiave non è privata |
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg} | jsonwebtoken, nel sorgente | RS o PS con una chiave sotto i 2048 bit e allowInsecureKeySizes disattivato |
Le cinque stringhe secretOrPrivateKey sono state lette da sign.js sul branch master di jsonwebtoken e non eseguite in locale: considera quindi le condizioni di innesco come le descrive il sorgente, non come qualcosa riprodotto su questa macchina. La parte ${header.alg} è un segnaposto di template in quel sorgente; a runtime al suo posto vedrai il nome del tuo algoritmo, ed è il motivo per cui cercare la stringa letterale con le graffe non trova niente.
Le sette strade che portano a DECODER routines::unsupported
Tutte e sette sono state riprodotte con crypto.createPrivateKey() su Node v25.8.2, e tutte hanno dato lo stesso codice e lo stesso messaggio:
- Un container OpenSSH. Il file inizia con
-----BEGIN OPENSSH PRIVATE KEY-----e non è affatto una struttura di chiave PEM. - Un rientro sulla riga
-----BEGIN, oppure un rientro sulla riga-----END. Le righe del corpo sono esenti; il confine esatto è nella sezione 5. - Spazio bianco prima dell’intero PEM. Una riga vuota iniziale va bene, uno spazio iniziale no.
- Gli a capo eliminati del tutto, con intestazione, corpo base64 e riga di chiusura attaccati su una riga sola.
- Una chiave pubblica dove ci si aspettava una chiave privata.
- Sequenze backslash-n letterali rimaste senza conversione: è in questo che si trasforma una variabile d’ambiente su riga singola.
- Un delimitatore con il numero di trattini sbagliato, oppure
begin/endscritti in minuscolo.
Due sono problemi di container, quattro sono problemi di testo rovinato e uno è un banale scambio di file. Il messaggio non può dirti quale dei sette, quindi la via più rapida è l’eliminazione e non la lettura.
Quello che Node accetta, e che restringe la ricerca più in fretta
L’elenco inverso è più utile, perché ogni voce è un’ipotesi che puoi scartare subito. Su Node v25.8.2 crypto.createPrivateKey() ha accettato senza protestare tutto quanto segue:
- chiavi private PKCS#1 e PKCS#8
- chiavi private EC SEC1
- fine riga CRLF
- l’a capo finale mancante
- un corpo base64 su una sola riga non spezzata
- righe del corpo con un rientro
- una riga vuota prima del PEM
- un BOM UTF-8, sia come
'' + pemsia comeBufferche inizia con0xEF 0xBB 0xBF - un’intestazione PKCS#1 avvolta attorno a un corpo PKCS#8
L’ultima voce merita una nota. Il decoder legge la struttura DER dentro il base64 e ignora l’etichetta all’esterno, quindi un file che dice BEGIN RSA PRIVATE KEY sopra un contenuto PKCS#8 si carica lo stesso. Tienilo presente quando leggi la sezione 3: la riga di intestazione è un indizio, non una garanzia.
3. La riga di intestazione PEM: quale container hai davvero in mano
Ogni PEM si annuncia sulla prima riga. Questi sono i valori di intestazione scritti da OpenSSL 3.6.2:
| Contenuto | Prima riga |
|---|---|
| Chiave privata PKCS#8 | -----BEGIN PRIVATE KEY----- |
| Chiave privata PKCS#1 | -----BEGIN RSA PRIVATE KEY----- |
| Chiave privata cifrata | -----BEGIN ENCRYPTED PRIVATE KEY----- |
| Chiave privata OpenSSH | -----BEGIN OPENSSH PRIVATE KEY----- |
| Chiave privata EC SEC1 | -----BEGIN EC PARAMETERS-----, poi un secondo blocco -----BEGIN EC PRIVATE KEY----- |
| Chiave pubblica SPKI | -----BEGIN PUBLIC KEY----- |
| Chiave pubblica PKCS#1 | -----BEGIN RSA PUBLIC KEY----- |
| Chiave privata Ed25519 | -----BEGIN PRIVATE KEY-----, e l’intero file è di tre righe |
Quindi head -1 key.pem risponde alla prima domanda di qualsiasi indagine. Restano tre dettagli da fissare.
ENCRYPTED PRIVATE KEY non è un errore di formato. È una passphrase che hai dimenticato di passare. Node lo segnala in modo diverso da tutto il resto, con ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED e error:07880109:common libcrypto routines::interrupted or cancelled, perché la libreria ha chiesto una passphrase e non ha ricevuto niente. Non mescolare questo messaggio con quello DECODER: non hanno nulla a che vedere l’uno con l’altro.
OPENSSH PRIVATE KEY è un mondo a parte. OpenSSH scrive un container tutto suo, che non è né PKCS#1 né PKCS#8 nonostante stia tra delimitatori dall’aria PEM. Node lo rifiuta senza appello, e lo stesso fanno i parser crypto/x509 di Go e il PKCS8EncodedKeySpec della JDK. Se la tua chiave di firma JWT è uscita da ssh-keygen, il bug è quello.
I file EC SEC1 contengono due blocchi. openssl ecparam -genkey scrive prima un blocco EC PARAMETERS e poi la chiave privata. Tutto ciò che legge solo il primo blocco PEM si ritrova i parametri in mano e fallisce con un messaggio che non nomina nessuno dei due. La versione Go di questo guasto è nella sezione 4.
E siccome l’intestazione è solo un’etichetta, vale anche il controllo opposto: un file la cui intestazione dice una cosa e il cui DER ne dice un’altra viene analizzato secondo il DER. Leggere head -1 è affidabile per i file usciti direttamente da OpenSSL e inaffidabile per i file passati per le mani di una persona, per una pagina wiki o per uno script che fa sostituzioni di stringhe.
4. Quale libreria accetta cosa: PKCS#1 vs PKCS#8 in tre ecosistemi
Questa è la matrice che spiega la maggior parte delle discussioni sui formati tra team diversi. Ogni riga è misurata sulle versioni elencate all’inizio dell’articolo.
| Libreria | PKCS#1 | PKCS#8 | OpenSSH | L’errore si spiega da solo? |
|---|---|---|---|---|
Node crypto | Sì | Sì | No | No. Molte cause, un solo DECODER routines::unsupported |
Go crypto/x509 | Sì, funzione dedicata | Sì, funzione dedicata | No | Sì. Nomina la funzione a cui passare |
| Libreria standard Java | No | Sì | No | No. algid parse error, not a sequence è attivamente fuorviante |
Leggi le colonne e le discussioni si risolvono da sole. Un servizio Node e un servizio Java che condividono lo stesso file di chiave funzionano benissimo finché la chiave non è PKCS#1: a quel punto Node continua a firmare e Java solleva un messaggio sulle sequenze ASN.1. Nessuno sospetta della chiave, perché sull’altro servizio è in produzione e funziona.
Node. Niente da configurare. Se il container è PKCS#1 o PKCS#8, createPrivateKey() lo accetta. Quando invece solleva un’eccezione, spendi il tuo tempo sulle sette cause della sezione 2 e non sul formato.
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);
}
Eseguilo sul file che la tua applicazione carica davvero, non su una copia fatta a mano: il ramo catch stampa la coppia codice/messaggio che puoi cercare nella sezione 2.
Go. Due container, due funzioni, e chiamare quella sbagliata è il guasto più comune in Go. Il messaggio ti dice quale usare, quindi la correzione è meccanica. Provarle entrambe in sequenza elimina del tutto la decisione:
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
}
Prima, però, devi togliere di mezzo la trappola EC. Su un file prodotto da openssl ecparam -genkey, pem.Decode restituisce un blocco il cui Type è EC PARAMETERS, e su quello tutte e tre le funzioni di parsing falliscono con:
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})
Quel messaggio non nomina mai i blocchi PEM, quindi la reazione tipica è sospettare della chiave. Salta invece il blocco dei parametri:
block, rest := pem.Decode(pemBytes)
if block == nil {
log.Fatal("no PEM block found")
}
if block.Type == "EC PARAMETERS" {
block, _ = pem.Decode(rest)
}
Oppure evita del tutto di produrre il blocco in più aggiungendo -noout al comando ecparam che scrive il file.
Java. La libreria standard legge PKCS#8 e nient’altro. Passa a PKCS8EncodedKeySpec una chiave PKCS#1 su Java 1.8.0_162 e ottieni:
InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
“algid” è l’algorithm identifier, il campo che PKCS#8 aggiunge e che PKCS#1 non ha. Il parser lo ha cercato, ha trovato l’inizio di un modulo RSA e ha rinunciato. Il messaggio è corretto e inutile in egual misura. Converti il file e l’errore sparisce:
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem
Una volta che il file è PKCS#8, il percorso di caricamento che funziona su Java 8 è abbastanza corto da stare dentro un test mentre verifichi la correzione:
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));
L’alternativa alla conversione è aggiungere BouncyCastle, che PKCS#1 lo legge. Convertire è un comando e zero dipendenze, quindi converti, a meno che qualcos’altro nel tuo stack non abbia già bisogno di quella libreria.
5. I caratteri che non riesci a vedere
È qui che il consiglio più diffuso sbaglia, e in modo dimostrabile.
Il rientro: l’esatto contrario di quello che ti hanno detto
Un’istruzione ripetuta ovunque dice che ogni riga di un PEM tranne i delimitatori deve iniziare alla colonna zero. Su Node v25.8.2 le misure dicono il contrario:
| Modifica al file | Risultato |
|---|---|
| Tutte le righe con un rientro | Fallisce |
Solo la riga -----BEGIN con un rientro | Fallisce |
Solo la riga -----END con un rientro | Fallisce |
| Solo le righe base64 del corpo con un rientro | Accettato |
| Uno spazio prima dell’intero PEM | Fallisce |
| Una riga vuota prima dell’intero PEM | Accettato |
Quindi la regola è questa: le righe -----BEGIN e -----END devono iniziare alla colonna zero, e il rientro delle righe del corpo non conta. Proprio le due righe che tutti ti dicono di poter far rientrare sono le due che rompono tutto, e le righe che ti dicono di allineare sono quelle che hanno margine.
Nessuno mette un rientro a un PEM a mano. Succede quando una chiave viene incollata in un blocco YAML, in un file values di Helm, in un heredoc Terraform o in una stringa Python tra tripli apici dentro il corpo di una classe. Ognuno di questi fa rientrare tutto quanto in modo uniforme, delimitatori compresi, che è la prima riga di quella tabella.
\n letterali da una variabile d’ambiente su riga singola
Un PEM ha degli a capo e una variabile d’ambiente, in pratica, no. Così le chiavi finiscono nei file .env su una riga sola, con \n scritto come due caratteri. Qualunque cosa legga quel file passa al tuo codice una stringa piena di backslash, e il parser vede un delimitatore seguito da spazzatura. Su Node questa è la causa 6 della sezione 2, con lo stesso messaggio DECODER routines::unsupported di tutte le altre.
Annulla la trasformazione nel punto in cui la usi:
const pem = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');
Due protezioni da aggiungere lì attorno. Primo, applica la sostituzione solo quando la stringa contiene davvero la sequenza di due caratteri, così un valore genuinamente multiriga arrivato da un loader diverso resta comunque intatto. Secondo, se la tua piattaforma lo consente preferisci il base64 per l’intero PEM: memorizzi una riga di base64, la decodifichi all’avvio e la questione dell’escaping non si pone proprio.
BOM: innocuo su Node, e non testato altrove
Un byte order mark è una sequenza di tre byte, EF BB BF, che alcuni editor Windows scrivono all’inizio di un file UTF-8. Il consiglio di rimuoverlo prima di caricare una chiave è molto diffuso. Su Node v25.8.2 non ha fatto alcuna differenza: il parser ha letto senza problemi un PEM preceduto dal BOM, sia come stringa sia come Buffer che inizia con quei tre byte.
Questo risultato è stato misurato solo su Node v25.8.2. Java, Python e gli altri parser qui non sono stati testati, e niente in questo articolo dice come si comportino. Se stai facendo debug di un servizio Java, il BOM resta una domanda aperta e non un’ipotesi esclusa.
Il BOM rompe però altre cose, ed è probabilmente da lì che il consiglio sulle chiavi è arrivato per associazione. JSON.parse su una stringa preceduta dal BOM è un guasto reale e ben documentato, trattato nella guida agli errori di JSON.parse causati dal BOM UTF-8. Un file di chiave conservato dentro una configurazione JSON può quindi fallire molto prima che qualcosa guardi la chiave.
Fine riga, a capo finale e larghezza delle righe
Altri tre sospettati che Node v25.8.2 ha scagionato:
- Fine riga CRLF. Accettato. Una chiave passata per Windows non è automaticamente rotta.
- A capo finale mancante. Accettato, ma dipende dal parser: altri parser potrebbero rifiutare un PEM privo dell’a capo finale, e qui non sono stati testati. Node lo accetta.
- Corpo non spezzato in righe. Accettato. Il base64 non deve per forza essere piegato a 64 caratteri.
Quello che rompe davvero un corpo base64 è un carattere perso, inserito o sostituito, che è un guasto diverso da come sono spezzate le righe. Un client di chat che trasforma un a capo in uno spazio, o un campo di testo che si mangia l’ultimo carattere, producono un corpo che non si decodifica più. Copia con un pulsante di copia invece che trascinando il mouse.
6. OpenSSL 3.x ti ha cambiato il default alle spalle
Misurato su OpenSSL 3.6.2 7 Apr 2026:
| Comando | Container che scrive |
|---|---|
openssl genrsa -out k.pem 2048 | PKCS#8, intestazione 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 | Converte PKCS#1 in PKCS#8 |
openssl rsa -in b.pem -traditional -out a.pem | Converte PKCS#8 in PKCS#1 |
Rileggi le prime due righe. Su questa build genrsa ti dà PKCS#8 per impostazione predefinita, ed è -traditional a produrre il file BEGIN RSA PRIVATE KEY. Moltissime guide descrivono ancora genrsa come il comando PKCS#1 e genpkey come quello PKCS#8, e seguirle ti lascerà convinto di aver generato un formato che non hai generato.
La conseguenza pratica si vede nelle migrazioni. Un team che lavora in Java riceve una chiave funzionante da un collega con una versione più vecchia di OpenSSL, va tutto bene, e sei mesi dopo qualcuno rigenera la chiave su una macchina nuova. Stesso comando, stessa documentazione, container diverso, e adesso la JDK lancia algid parse error, not a sequence contro una chiave che “è stata generata esattamente allo stesso modo”. Non è vero.
Quindi controlla il container, invece di dedurlo dal comando che hai lanciato:
head -1 key.pem
Una riga di output, e la tabella della sezione 3 ti dice che cosa hai in mano. Fallo prima di lanciare qualsiasi comando di conversione, perché convertire un file PKCS#8 in PKCS#8 è un’operazione a vuoto che sembra una correzione e non corregge niente.
Se preferisci non pensare affatto ai flag, il generatore di chiavi RSA online emette entrambi i container dalla stessa coppia di chiavi con un interruttore, così puoi produrre una copia PKCS#1 e una copia PKCS#8 della stessa chiave e provarle una alla volta contro la libreria che ti sta rifiutando.
7. Il limite di 2048 bit che rifiuta una chiave perfettamente valida
Un guasto sembra un problema di formato e non lo è. Nel sorgente di jsonwebtoken, sign.js solleva:
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}
Il sorgente lo lancia quando alg è un algoritmo RS o PS, la chiave è sotto i 2048 bit e allowInsecureKeySizes non è stato impostato. Quel controllo è della libreria, non del runtime. Node v25.8.2 analizza una chiave RSA da 1024 bit senza protestare; modulusLength: 1024 produce un oggetto chiave come tutti gli altri. Quindi la chiave è strutturalmente valida, il container è quello giusto, OpenSSL la legge, e la chiamata di firma fallisce lo stesso.
L’indizio è che questo messaggio nomina un numero. Gli errori di formato parlano di decoder, di sequenze e di key material; questo parla di bit. Se nel messaggio vedi una dimensione, smetti di guardare il PEM.
Da dove arrivino le chiavi da 1024 bit di solito è una questione di storia: una chiave generata anni fa contro un default che nel frattempo si è spostato, oppure una fixture di test che nessuno ha più rivisto perché le chiavi piccole si generano più in fretta. La correzione è generare una nuova coppia da 2048 bit o più, non attivare la via di fuga: quel flag disattiva un controllo che serve.
Per confermare che la dimensione è l’unico problema rimasto, firma lo stesso payload con una chiave nuova della misura giusta nel codificatore JWT online. Se lì viene fuori un token e dal tuo codice no, la differenza è la tua chiave, non i tuoi claim né la tua configurazione.
8. Un flusso di lavoro ripetibile per i guasti delle chiavi RS256
Esegui questi passi in ordine. Ogni passo o trova la causa o elimina un ramo.
- Leggi la riga di intestazione.
head -1 key.pem, poi confrontala con la tabella della sezione 3. Ti dice qual è il container, se il file è cifrato e se è una chiave OpenSSH che non funzionerà mai. - Chiedi a OpenSSL di analizzarla.
openssl rsa -in key.pem -noout -text | head -1per RSA, oppureopenssl pkey -in key.pem -nooutper qualsiasi algoritmo. Se va a buon fine, i byte sono una chiave valida e il problema sta dalla parte della libreria. Se fallisce, il file è danneggiato e passi al punto 4. - Controlla la riga della tua libreria nella matrice. Sezione 4. Se sei su Java con un file PKCS#1, o su Go con la funzione di parsing sbagliata, qui hai già finito.
- Guarda i caratteri invisibili.
head -c 32 key.pem | xxdmostra i primi byte e in un colpo d’occhio becca un BOM, uno spazio iniziale e un delimitatore con un rientro. Poi verifica che le righe-----BEGINe-----ENDinizino alla colonna zero, come dice la sezione 5. - Fai una bisezione con una chiave sicuramente buona. Genera una coppia nuova nel generatore di chiavi RSA online, fai puntare il tuo codice a quella e guarda se l’errore sopravvive. Se sopravvive, il bug è nel tuo codice di caricamento e non nel file di chiave, e nessuna riformattazione dell’originale servirà a qualcosa. Se sparisce, la colpa è del file originale e adesso hai una chiave funzionante con cui fare il confronto.
- Controlla per ultimi l’algoritmo e la dimensione. Verifica che l’intestazione dica
RS256e che la chiave sia di almeno 2048 bit, come dice la sezione 7.
Il passo 5 è quello che tutti saltano ed è quello che fa risparmiare più tempo. Una chiave di riferimento pulita trasforma un vago “la chiave non funziona” in una risposta binaria su quale dei due lati sia rotto.
FAQ
Che differenza c’è tra BEGIN RSA PRIVATE KEY e BEGIN PRIVATE KEY?
Sono due container attorno alla stessa chiave RSA. BEGIN RSA PRIVATE KEY è PKCS#1 e contiene direttamente i numeri RSA; BEGIN PRIVATE KEY è PKCS#8 e aggiunge un algorithm identifier, ed è per questo che può trasportare anche chiavi ECDSA ed Ed25519. Quale ti serve dipende interamente dalla libreria, e il generatore di chiavi RSA online scrive l’uno o l’altro.
Perché openssl genrsa produce un formato diverso da quello del tutorial?
Perché il default è cambiato. Su OpenSSL 3.6.2, openssl genrsa -out k.pem 2048 scrive PKCS#8 con l’intestazione BEGIN PRIVATE KEY. Per ottenere il layout PKCS#1 tradizionale descritto dalle guide più vecchie, aggiungi -traditional. Esegui head -1 sull’output invece di fidarti di un tutorial qualsiasi su ciò che produce la tua build.
Come si risolve algid parse error, not a sequence in Java?
Quel messaggio su Java 1.8.0_162 significa che hai passato a PKCS8EncodedKeySpec una chiave PKCS#1. La libreria standard non legge PKCS#1, punto. Converti una volta sola con openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem, oppure aggiungi BouncyCastle se qualcos’altro nel progetto ne ha già bisogno.
Ogni riga di una chiave privata deve iniziare alla colonna zero?
No, e il consiglio più comune ha le cose al rovescio. Provato su Node v25.8.2: un rientro solo sulle righe base64 del corpo si analizza senza problemi, mentre un rientro solo sulla riga -----BEGIN o solo sulla riga -----END fallisce. Una riga vuota prima del PEM viene accettata; uno spazio iniziale no.
Come va scritta una chiave privata dentro un file .env?
O come una riga singola tra virgolette con gli escape \n che annulli al caricamento con .replace(/\\n/g, '\n'), oppure come una riga di base64 che decodifichi all’avvio. La seconda è più sicura perché non c’è nessuna convenzione di escaping che un loader di configurazione possa sbagliare.
Posso usare una chiave da 1024 bit con RS256?
Node v25.8.2 analizza una chiave RSA da 1024 bit senza errori, ma il sorgente di jsonwebtoken si rifiuta di firmare con essa: secretOrPrivateKey has a minimum key size of 2048 bits, a meno che allowInsecureKeySizes non sia impostato. Genera invece una chiave da 2048 bit. Il messaggio nomina un numero di bit, ed è così che lo distingui da un problema di formato.
Perché ricevo un errore di formato della chiave privata RS256 che dice di volere una chiave asimmetrica, se ho passato proprio un file di chiave privata?
Nel sorgente di jsonwebtoken, secretOrPrivateKey must be an asymmetric key when using ${header.alg} scatta quando alg è RS, PS o ES e la chiave non è una chiave privata. Di solito quel valore è una stringa segreta in stile HS256 rimasta da una configurazione precedente. Una stringa casuale va con HS256 e con il generatore di segreti JWT; RS256 ha bisogno di una coppia di chiavi, non di un segreto.
Conclusione
Questa classe di bug costa tempo non perché sia difficile, ma perché su Node una sola stringa di errore copre sette cause, il messaggio di Java punta ad ASN.1 quando la risposta vera è “container sbagliato” e il consiglio di formattazione più ripetuto sull’argomento è rovesciato. Alla risposta non ci arrivi leggendo, quindi elimini: riga di intestazione, parsing con OpenSSL, matrice delle librerie, caratteri invisibili, chiave sicuramente buona.
Due abitudini evitano che si ripeta. Annota quale container richiede ciascun servizio accanto alla chiave nel tuo secret store, perché il vincolo vive nella libreria e non nella chiave. E tieni una coppia di chiavi sicuramente buona nel tuo ambiente di sviluppo puramente come controllo, così la prima domanda su qualsiasi guasto di una chiave ottiene una risposta sì o no in un minuto.
Per la questione più ampia di come queste chiavi vadano emesse, ruotate e limitate nel loro ambito una volta che si caricano correttamente, vedi Sicurezza JWT sul web: best practice, attacchi e difese.