Erreur de format de clé privée RS256 : un message, sept causes
Une erreur de format de clé privée RS256 ne nomme presque jamais sa propre cause. Sur Node v25.8.2, chacune des fautes ci-dessous produit exactement la même ligne :
code: ERR_OSSL_UNSUPPORTED
message: error:1E08010C:DECODER routines::unsupported
Cinq choses sans rapport entre elles la déclenchent : un conteneur OpenSSH là où une clé PEM était attendue, une ligne -----BEGIN indentée, une clé publique remise à un signataire, des séquences \n littérales que personne n’a déséchappées, et un fichier dont les sauts de ligne ont été effacés en cours de route. La liste testée de la section 2 en compte sept. La même ligne à chaque fois : voilà pourquoi chercher le texte de l’erreur vous fait atterrir dans le fil de discussion de quelqu’un d’autre, à propos de la cause de quelqu’un d’autre.
Commencez par couper le problème en deux :
- La bibliothèque n’a jamais obtenu d’objet clé. Restez ici.
- La bibliothèque a chargé la clé, puis a répondu
invalid signature. Autre panne, autres causes : voyez JWT « invalid signature » : toutes les causes et comment les corriger.
Pour le premier cas, voici un tri en trente secondes :
openssl rsa -in key.pem -noout -text | head -1
Si la commande échoue, le fichier est en cause et les sections 3 à 6 vous diront pourquoi. Si elle réussit, OpenSSL a compris le conteneur : votre problème vient de la bibliothèque ou de ce que vous lui avez passé, c’est-à-dire des sections 4 et 7.
Tout ce qui suit a été mesuré le 11 août 2026 sur OpenSSL 3.6.2 7 Apr 2026, Node v25.8.2, Go go1.26.1 darwin/arm64 et Java 1.8.0_162. Quand une affirmation provient de la lecture du code source plutôt que d’une exécution, le texte le précise.
1. Commencez par identifier le type de panne
Tout tient à une question : un objet clé a-t-il existé, oui ou non ? Les pannes au chargement (parse-time) surviennent avant la moindre opération cryptographique. La bibliothèque lit votre PEM, n’arrive pas à en faire une clé, et lève une exception. Rien n’a été signé ni vérifié : le token que vous déboguez n’a jamais existé. Les pannes à la vérification (verify-time) sont l’inverse : la clé s’est chargée sans accroc, une signature a été calculée, et elle ne correspond pas. Celles-là viennent de désaccords au niveau des octets entre le signataire et le vérificateur, et le guide sur la signature invalide les traite.
Un coup d’œil à la pile d’appels suffit à les distinguer. Une panne au chargement cite un décodeur, une key spec ou une structure ASN.1. Une panne à la vérification cite une signature.
Une clé privée refusée produit ceci dans trois écosystèmes :
| Environnement | Version testée | Message quand la clé refuse de se charger |
|---|---|---|
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 |
Les trois messages ne se valent pas. Go vous dit exactement quelle fonction appeler à la place. Java évoque un « algid » et une « sequence », puis vous laisse deviner que cela veut dire que votre clé est dans le mauvais conteneur. Node, lui, ne dit strictement rien d’exploitable.
Si vous n’êtes pas certain que le token que vous poursuivez est bien en RS256, collez-le dans le décodeur JWT en ligne et lisez alg dans le header avant d’aller plus loin. Un header HS256 signifie qu’il vous faut un secret partagé et non une paire de clés : tous les symptômes décrits ici vous enverraient alors sur une fausse piste.
2. Du texte de l’erreur à la cause racine : la table de correspondance des erreurs de format de clé privée RS256
Repérez votre chaîne exacte. La colonne de droite indique la suite.
| Texte de l’erreur | D’où elle vient | Ce qu’elle veut vraiment dire |
|---|---|---|
error:1E08010C:DECODER routines::unsupported | Node v25.8.2 | Sept causes possibles, listées plus bas |
error:07880109:common libcrypto routines::interrupted or cancelled | Node v25.8.2 | La clé est chiffrée et vous n’avez fourni aucune passphrase |
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format) | Go 1.26.1 | Vous avez appelé ParsePKCS1PrivateKey sur un fichier PKCS#8 |
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format) | Go 1.26.1 | Vous avez appelé ParsePKCS8PrivateKey sur un fichier PKCS#1 |
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...}) | Go 1.26.1 | Le premier bloc PEM est EC PARAMETERS, pas la clé |
algid parse error, not a sequence | Java 1.8.0_162 | Un PKCS#1 remis à PKCS8EncodedKeySpec |
secretOrPrivateKey must have a value | jsonwebtoken, dans le code source | L’argument de clé est falsy et alg n’est pas none |
secretOrPrivateKey is not valid key material | jsonwebtoken, dans le code source | Impossible de construire ni une clé privée ni une clé secrète |
secretOrPrivateKey must be a symmetric key when using ${header.alg} | jsonwebtoken, dans le code source | alg commence par HS mais la clé n’est pas un secret |
secretOrPrivateKey must be an asymmetric key when using ${header.alg} | jsonwebtoken, dans le code source | alg correspond à RS, PS ou ES mais la clé n’est pas privée |
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg} | jsonwebtoken, dans le code source | RS ou PS avec une clé de moins de 2048 bits et allowInsecureKeySizes désactivé |
Les cinq chaînes secretOrPrivateKey ont été lues dans sign.js sur la branche master de jsonwebtoken, sans exécution locale : traitez donc les conditions de déclenchement comme ce que dit le code source, et non comme un comportement reproduit sur cette machine. Le fragment ${header.alg} est un emplacement de gabarit dans ce code source ; à l’exécution, vous y verrez le nom de votre propre algorithme, ce qui explique qu’une recherche sur la chaîne littérale avec les accolades ne donne rien.
Les sept façons de produire DECODER routines::unsupported
Les sept ont été reproduites contre crypto.createPrivateKey() sur Node v25.8.2, et les sept ont renvoyé le même code et le même message :
- Un conteneur OpenSSH. Le fichier commence par
-----BEGIN OPENSSH PRIVATE KEY-----et n’est pas du tout une structure de clé PEM. - Une ligne
-----BEGINindentée, ou une ligne-----ENDindentée. Les lignes du corps y échappent ; la section 5 donne la frontière exacte. - Une espace avant le PEM entier. Une ligne vide en tête passe, une espace en tête non.
- Les sauts de ligne entièrement supprimés : l’en-tête, le base64 et le pied de bloc se retrouvent collés sur une seule ligne.
- Une clé publique là où une clé privée était attendue.
- Des séquences antislash-n restées littérales, jamais déséchappées, ce que devient fatalement une variable d’environnement sur une seule ligne.
- Un délimiteur avec le mauvais nombre de tirets, ou
begin/endécrits en minuscules.
Deux d’entre elles relèvent du conteneur, quatre du texte abîmé, une d’une simple confusion. Le message est incapable de vous dire laquelle, et vous irez donc plus vite en éliminant les hypothèses qu’en relisant l’erreur.
Ce que Node accepte, et les hypothèses que cela élimine
La liste inverse est plus utile, car chacun de ses éléments est une hypothèse que vous pouvez abandonner sur-le-champ. Sur Node v25.8.2, crypto.createPrivateKey() a accepté tout ce qui suit sans broncher :
- Les clés privées PKCS#1 et PKCS#8
- Les clés privées EC SEC1
- Les fins de ligne CRLF
- L’absence de saut de ligne final
- Un corps base64 sur une seule ligne, sans repli
- Des lignes de corps indentées
- Une ligne vide avant le PEM
- Un BOM UTF-8, aussi bien sous la forme
'' + pemque sous celle d’unBuffercommençant par0xEF 0xBB 0xBF - Un en-tête PKCS#1 enroulé autour d’un corps PKCS#8
Le décodeur lit la structure DER contenue dans le base64 et ignore l’étiquette posée à l’extérieur : un fichier qui annonce BEGIN RSA PRIVATE KEY au-dessus d’un contenu PKCS#8 se charge quand même. Retenez-le pour la section 3 : la ligne d’en-tête est un indice, elle ne garantit rien.
3. La ligne d’en-tête PEM : quel conteneur avez-vous réellement en main
Tout PEM s’annonce dès sa première ligne. Voici les en-têtes écrits par OpenSSL 3.6.2 :
| Contenu | Première ligne |
|---|---|
| Clé privée PKCS#8 | -----BEGIN PRIVATE KEY----- |
| Clé privée PKCS#1 | -----BEGIN RSA PRIVATE KEY----- |
| Clé privée chiffrée | -----BEGIN ENCRYPTED PRIVATE KEY----- |
| Clé privée OpenSSH | -----BEGIN OPENSSH PRIVATE KEY----- |
| Clé privée EC SEC1 | -----BEGIN EC PARAMETERS-----, puis un second bloc -----BEGIN EC PRIVATE KEY----- |
| Clé publique SPKI | -----BEGIN PUBLIC KEY----- |
| Clé publique PKCS#1 | -----BEGIN RSA PUBLIC KEY----- |
| Clé privée Ed25519 | -----BEGIN PRIVATE KEY-----, et le fichier entier tient en trois lignes |
head -1 key.pem répond donc à la première question de toute investigation.
ENCRYPTED PRIVATE KEY n’est pas une erreur de format. C’est une passphrase que vous avez oublié de fournir. Node la signale autrement que le reste, avec ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED et error:07880109:common libcrypto routines::interrupted or cancelled, parce que la bibliothèque a réclamé une passphrase et n’a rien reçu en retour. Ne mélangez pas ce message avec celui du DECODER : ils n’ont rien à voir l’un avec l’autre.
OPENSSH PRIVATE KEY est un autre monde. OpenSSH écrit son propre conteneur, qui n’est ni du PKCS#1 ni du PKCS#8 malgré des délimiteurs qui ressemblent à du PEM. Node le refuse sans discuter, tout comme les parseurs crypto/x509 de Go et le PKCS8EncodedKeySpec du JDK. Si votre clé de signature JWT sort de ssh-keygen, votre bug est là.
Les fichiers EC SEC1 contiennent deux blocs. openssl ecparam -genkey écrit d’abord un bloc EC PARAMETERS, la clé privée ensuite. Tout code qui ne lit que le premier bloc PEM récupère les paramètres et échoue sans mentionner ni l’un ni l’autre. La section 4 donne la version Go de cette panne.
Et comme l’en-tête n’est qu’une étiquette, la vérification inverse compte tout autant : un fichier dont l’en-tête dit une chose et le DER une autre sera interprété selon le DER. Lire head -1 est fiable pour les fichiers sortis directement d’OpenSSL, et peu fiable pour ceux qui sont passés entre les mains d’un humain, par une page de wiki ou par un script de remplacement de chaînes.
4. Qui accepte quoi : PKCS#1 contre PKCS#8 dans trois écosystèmes
Cette matrice explique la plupart des disputes de format entre équipes. Chaque ligne est mesurée sur les versions listées en tête d’article.
| Bibliothèque | PKCS#1 | PKCS#8 | OpenSSH | L’erreur s’explique-t-elle elle-même ? |
|---|---|---|---|---|
Node crypto | Oui | Oui | Non | Non. Plusieurs causes, un seul DECODER routines::unsupported |
Go crypto/x509 | Oui, fonction dédiée | Oui, fonction dédiée | Non | Oui. Elle nomme la fonction à utiliser à la place |
| Bibliothèque standard Java | Non | Oui | Non | Non. algid parse error, not a sequence est franchement trompeur |
Lisez les colonnes et les disputes se règlent d’elles-mêmes. Un service Node et un service Java qui partagent le même fichier de clé fonctionnent très bien jusqu’au jour où la clé est en PKCS#1 : Node continue de signer, Java lève un message parlant de séquences ASN.1. Personne ne soupçonne la clé, puisqu’elle marche visiblement en production sur l’autre service.
Sur Node, il n’y a rien à configurer : si le conteneur est du PKCS#1 ou du PKCS#8, createPrivateKey() le prend. Quand la fonction lève bel et bien une exception, passez votre temps sur les sept causes de la section 2 plutôt que sur le 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);
}
Lancez ce script sur le fichier que charge réellement votre application, pas sur une copie faite à la main : la branche catch affiche le couple code et message que vous retrouverez dans la table de la section 2.
Go sépare les deux conteneurs en deux fonctions, et appeler la mauvaise est la panne Go la plus fréquente. Le message vous dit laquelle utiliser, donc le correctif est mécanique. Essayer les deux dans l’ordre vous dispense même de choisir :
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
}
Le piège EC, lui, doit être traité en amont. Sur un fichier produit par openssl ecparam -genkey, pem.Decode renvoie un bloc dont le Type vaut EC PARAMETERS, et les trois fonctions de parsing échouent dessus avec :
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})
Ce message ne parle jamais de blocs PEM, d’où le réflexe habituel : soupçonner la clé. Sautez plutôt le bloc de paramètres :
block, rest := pem.Decode(pemBytes)
if block == nil {
log.Fatal("no PEM block found")
}
if block.Type == "EC PARAMETERS" {
block, _ = pem.Decode(rest)
}
Ou évitez purement et simplement de produire le bloc superflu en ajoutant -noout à la commande ecparam qui écrit le fichier.
En Java, la bibliothèque standard lit le PKCS#8 et rien d’autre. Donnez une clé PKCS#1 à PKCS8EncodedKeySpec sur Java 1.8.0_162 et vous obtenez :
InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
« algid » désigne l’identifiant d’algorithme, le champ que PKCS#8 ajoute et que PKCS#1 ne possède pas. Le parseur l’a cherché, est tombé sur le début d’un module RSA, et a renoncé. Le message est exact et inutile à parts égales. Convertissez le fichier et l’erreur disparaît :
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem
Le chemin de chargement Java 8 qui fonctionne, une fois le fichier en PKCS#8, est assez court pour être collé tel quel dans un test le temps de confirmer le correctif :
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’alternative à la conversion, c’est d’ajouter BouncyCastle, qui, lui, lit le PKCS#1. La conversion tient en une commande et n’ajoute aucune dépendance : convertissez, sauf si autre chose dans votre stack réclame déjà cette bibliothèque.
5. Les caractères que vous ne voyez pas
Un PEM peut paraître correct à l’écran et rester illisible pour le parseur : ce qui le casse se trouve dans les octets qu’aucun éditeur n’affiche.
L’indentation : l’inverse de ce qu’on vous a dit
Une consigne largement recopiée affirme que toutes les lignes d’un PEM, sauf les délimiteurs, doivent commencer en colonne zéro. Testée sur Node v25.8.2, elle est à l’envers :
| Modification du fichier | Résultat |
|---|---|
| Toutes les lignes indentées | Échec |
Seule la ligne -----BEGIN indentée | Échec |
Seule la ligne -----END indentée | Échec |
| Seules les lignes du corps base64 indentées | Accepté |
| Une espace avant le PEM entier | Échec |
| Une ligne vide avant le PEM entier | Accepté |
La règle est donc la suivante : les lignes -----BEGIN et -----END doivent commencer en colonne zéro, et l’indentation des lignes du corps n’a aucune importance. Les deux lignes précises qu’on vous autorise à indenter sont exactement celles qui cassent, et celles qu’on vous demande d’aligner sont celles qui ont du jeu.
Ce détail compte parce que personne n’indente un PEM à la main. Cela arrive quand une clé est collée dans un bloc YAML, un fichier de values Helm, un heredoc Terraform ou une chaîne Python à triples guillemets à l’intérieur d’une classe. Chacun de ces contextes indente l’ensemble uniformément, délimiteurs compris, ce qui correspond à la première ligne de ce tableau.
L’antislash-n littéral venu d’une variable d’environnement sur une ligne
Un PEM contient des sauts de ligne ; une variable d’environnement, en pratique, non. Les clés atterrissent donc dans les fichiers .env sur une seule ligne, avec \n écrit sous forme de deux caractères. Ce qui lit ce fichier passe alors à votre code une chaîne pleine d’antislashs, et le parseur voit un délimiteur suivi de charabia. Sur Node, c’est la cause 6 de la section 2, avec le même message DECODER routines::unsupported que tout le reste.
Défaites-le au point d’utilisation :
const pem = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');
Entourez cette ligne de deux précautions. D’abord, n’appliquez le remplacement que si la chaîne contient réellement la séquence de deux caractères ; ainsi une valeur véritablement multiligne, passée par un autre chargeur, restera de toute façon intacte. Ensuite, préférez le base64 pour le PEM entier si votre plateforme le permet : vous stockez une ligne de base64, vous la décodez au démarrage, et la question de l’échappement ne se pose tout simplement plus.
Le BOM : inoffensif sur Node, non testé ailleurs
Une marque d’ordre des octets (BOM) tient en trois octets, EF BB BF, que certains éditeurs Windows écrivent en tête d’un fichier UTF-8. Le conseil de la retirer avant de charger une clé circule beaucoup. Sur Node v25.8.2, elle n’a rien changé : un PEM préfixé du BOM s’est analysé sans erreur, aussi bien sous forme de chaîne que sous forme de Buffer commençant par ces trois octets.
Ce résultat a des bornes strictes. Il a été mesuré sur Node v25.8.2 uniquement. Java, Python et les autres parseurs n’ont pas été testés ici, et rien dans cet article ne dit comment ils se comportent. Si vous déboguez un service Java, le BOM reste une question ouverte, pas une piste écartée.
Le BOM casse en revanche d’autres choses, ce qui explique sans doute par association l’origine du conseil sur les clés. JSON.parse sur une chaîne préfixée d’un BOM est une panne réelle et bien documentée, traitée dans BOM UTF-8 : corriger les erreurs JSON.parse et CSV. Un fichier de clé stocké à l’intérieur d’une configuration JSON peut donc échouer bien avant que quoi que ce soit regarde la clé.
Fins de ligne, saut de ligne final et largeur de repli
Trois autres suspects que Node v25.8.2 a mis hors de cause :
- Les fins de ligne CRLF. Acceptées. Une clé passée par Windows n’est pas cassée d’office.
- L’absence de saut de ligne final. Acceptée. Attention, ce point-là dépend du parseur : Node l’accepte, les autres n’ont pas été testés ici.
- Un corps non replié. Accepté. Le base64 n’a pas besoin d’être plié à 64 caractères.
Ce qui casse vraiment un corps base64, c’est un caractère perdu, inséré ou substitué, une panne bien différente de celle du repli. Un client de messagerie qui transforme un saut de ligne en espace, ou un champ de texte qui avale le dernier caractère, produit un corps qui ne se décode plus. Copiez avec un bouton de copie plutôt qu’avec un cliqué-glissé à la souris.
6. OpenSSL 3.x a changé la valeur par défaut dans votre dos
Mesuré sur OpenSSL 3.6.2 7 Apr 2026 :
| Commande | Conteneur écrit |
|---|---|
openssl genrsa -out k.pem 2048 | PKCS#8, en-tête 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 | Convertit PKCS#1 en PKCS#8 |
openssl rsa -in b.pem -traditional -out a.pem | Convertit PKCS#8 en PKCS#1 |
Relisez les deux premières lignes. Sur cette build, genrsa vous donne du PKCS#8 par défaut, et c’est -traditional qui produit le fichier BEGIN RSA PRIVATE KEY. Quantité de guides décrivent encore genrsa comme la commande PKCS#1 et genpkey comme celle du PKCS#8 : les suivre vous laissera convaincu d’avoir généré un format que vous n’avez pas généré.
La conséquence pratique se voit lors des migrations. Une équipe sous Java reçoit une clé fonctionnelle d’un collègue équipé d’un OpenSSL plus ancien, tout va bien, et six mois plus tard quelqu’un régénère la clé sur une machine neuve. Même commande, même documentation, conteneur différent : le JDK jette maintenant algid parse error, not a sequence à la figure d’une clé « générée exactement de la même façon ». Non, elle ne l’a pas été.
Ne présumez donc jamais. Vérifiez :
head -1 key.pem
Une ligne de sortie, et le tableau de la section 3 vous dit ce que vous tenez entre les mains. Faites-le avant toute commande de conversion, car convertir un fichier PKCS#8 en PKCS#8 est une opération neutre qui ressemble à un correctif et ne corrige rien.
Si vous préférez ne pas penser aux options du tout, le générateur de clés RSA en ligne produit les deux conteneurs à partir de la même paire de clés via un simple sélecteur : vous obtenez une copie PKCS#1 et une copie PKCS#8 d’une même clé, et vous essayez chacune contre la bibliothèque qui vous refuse.
7. Le plancher de 2048 bits qui rejette une clé parfaitement valide
Une panne ressemble à un problème de format sans en être un. Dans le code source de jsonwebtoken, sign.js lève :
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}
Le code source la déclenche quand alg est un algorithme RS ou PS, que la clé fait moins de 2048 bits et que allowInsecureKeySizes n’a pas été activé. Ce contrôle appartient à la bibliothèque, pas au runtime. Node v25.8.2 analyse une clé RSA de 1024 bits sans broncher ; modulusLength: 1024 produit un objet clé comme un autre. La clé est donc structurellement valide, le conteneur est le bon, OpenSSL la lit, et l’appel de signature échoue quand même.
L’indice, c’est que ce message-là cite un nombre. Les erreurs de format parlent de décodeurs, de séquences et de matériel de clé ; celle-ci parle de bits. Si vous voyez une taille dans le message, arrêtez d’inspecter le PEM.
L’origine des clés de 1024 bits est généralement historique : une clé générée il y a des années contre une valeur par défaut qui a depuis bougé, ou un jeu de test que personne n’a revisité parce que les petites clés se génèrent plus vite. Le correctif consiste à générer une nouvelle paire de 2048 bits ou plus. allowInsecureKeySizes existe, mais l’activer revient à désactiver un contrôle qui a sa raison d’être.
Pour confirmer que la taille est le seul problème restant, signez la même charge utile avec une clé neuve de la bonne taille dans l’encodeur JWT. Si l’outil produit un token et que votre code n’y arrive pas, la différence tient à votre clé, pas à vos claims ni à votre configuration.
8. Une procédure reproductible pour les pannes de clé RS256
Exécutez ces étapes dans l’ordre. Chacune trouve la cause ou élimine une branche.
- Lisez la ligne d’en-tête.
head -1 key.pem, puis comparez avec le tableau de la section 3. Vous saurez quel est le conteneur, si le fichier est chiffré, et s’il s’agit d’une clé OpenSSH qui ne fonctionnera jamais. - Demandez à OpenSSL de l’analyser.
openssl rsa -in key.pem -noout -text | head -1pour RSA, ouopenssl pkey -in key.pem -nooutpour n’importe quel algorithme. Un succès signifie que les octets forment une clé valide et que le problème est du côté de la bibliothèque. Un échec signifie que le fichier est abîmé : continuez à l’étape 4. - Consultez la ligne de votre bibliothèque dans la matrice. Section 4. Si vous êtes sous Java avec un fichier PKCS#1, ou sous Go en appelant la mauvaise fonction de parsing, vous en avez terminé ici.
- Regardez les caractères invisibles.
head -c 32 key.pem | xxdaffiche les premiers octets, ce qui attrape d’un seul coup d’œil un BOM, une espace en tête et un délimiteur indenté. Vérifiez ensuite que les lignes-----BEGINet-----ENDcommencent en colonne zéro, comme l’explique la section 5. - Dichotomisez avec une clé connue comme bonne. Générez une paire neuve dans le générateur de clés RSA en ligne, pointez votre code dessus, et regardez si l’erreur survit. Si oui, le bug est dans votre code de chargement et non dans le fichier de clé, et reformater l’original n’y changera rien. Si elle disparaît, l’original est fautif et vous disposez désormais d’une clé fonctionnelle à comparer avec lui.
- Vérifiez l’algorithme et la taille en dernier. Confirmez que le header annonce
RS256, et que la clé fait au moins 2048 bits, comme l’explique la section 7.
L’étape 5 est celle que tout le monde saute, et celle qui fait gagner le plus de temps. Une clé de référence propre transforme un vague « la clé ne marche pas » en une réponse binaire sur le côté qui est cassé.
FAQ
Quelle est la différence entre BEGIN RSA PRIVATE KEY et BEGIN PRIVATE KEY ?
Ce sont deux conteneurs autour de la même clé RSA. BEGIN RSA PRIVATE KEY, c’est du PKCS#1 : il contient directement les nombres RSA. BEGIN PRIVATE KEY, c’est du PKCS#8 : il ajoute un identifiant d’algorithme, ce qui lui permet de transporter aussi des clés ECDSA et Ed25519. Celui dont vous avez besoin dépend entièrement de la bibliothèque, et le générateur de clés RSA en ligne écrit l’un comme l’autre.
Pourquoi openssl genrsa produit-il un format différent de celui du tutoriel ?
Parce que la valeur par défaut a bougé. Sur OpenSSL 3.6.2, openssl genrsa -out k.pem 2048 écrit du PKCS#8 avec un en-tête BEGIN PRIVATE KEY. Pour obtenir la disposition PKCS#1 traditionnelle que décrivent les guides plus anciens, ajoutez -traditional. Lancez head -1 sur la sortie plutôt que de croire un tutoriel sur ce que produit votre build.
Comment corriger algid parse error, not a sequence en Java ?
Ce message, sur Java 1.8.0_162, signifie que vous avez donné une clé PKCS#1 à PKCS8EncodedKeySpec. La bibliothèque standard ne lit pas du tout le PKCS#1. Convertissez une bonne fois avec openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem, ou ajoutez BouncyCastle si autre chose dans le projet en a déjà besoin.
Chaque ligne d’une clé privée doit-elle commencer en colonne zéro ?
Non, et le conseil courant dit l’exact inverse de la réalité. Testé sur Node v25.8.2 : n’indenter que les lignes du corps base64 s’analyse très bien, tandis que n’indenter que la ligne -----BEGIN ou que la ligne -----END échoue. Une ligne vide devant le PEM est acceptée ; une espace en tête ne l’est pas.
Comment faut-il stocker une clé privée dans un fichier .env ?
Soit sur une seule ligne entre guillemets avec des échappements \n que vous défaites au chargement via .replace(/\\n/g, '\n'), soit sur une ligne de base64 que vous décodez au démarrage. La seconde option est plus sûre, car il ne reste aucune convention d’échappement qu’un chargeur de configuration puisse mal interpréter.
Puis-je utiliser une clé de 1024 bits avec RS256 ?
Node v25.8.2 analyse une clé RSA de 1024 bits sans erreur, mais le code source de jsonwebtoken refuse de signer avec : secretOrPrivateKey has a minimum key size of 2048 bits, sauf si allowInsecureKeySizes est activé. Générez plutôt une clé de 2048 bits. Le message cite un nombre de bits, et c’est ainsi qu’on le distingue d’un problème de format.
Pourquoi une erreur de format de clé privée RS256 réclame-t-elle une clé asymétrique alors que je passe bien un fichier de clé privée ?
Dans le code source de jsonwebtoken, secretOrPrivateKey must be an asymmetric key when using ${header.alg} se déclenche quand alg vaut RS, PS ou ES et que la clé n’est pas une clé privée. La valeur passée est le plus souvent une chaîne secrète de style HS256, vestige d’une configuration antérieure. Une chaîne aléatoire va de pair avec HS256 et le générateur de secret JWT ; RS256 réclame une paire de clés, pas un secret.
Conclusion
Cette catégorie de bug coûte cher pour une raison bête : une seule chaîne d’erreur recouvre sept causes sur Node, le message de Java pointe vers ASN.1 alors que la vraie réponse est « mauvais conteneur », et le conseil de formatage le plus recopié sur le sujet est inversé. La réponse s’obtient par élimination : ligne d’en-tête, analyse OpenSSL, matrice des bibliothèques, caractères invisibles, clé de référence saine.
Deux habitudes évitent la récidive. Notez quel conteneur exige chaque service, à côté de la clé dans votre coffre à secrets, parce que la contrainte vit dans la bibliothèque et non dans la clé. Et gardez dans votre environnement de développement une paire de clés connue comme bonne, uniquement à titre de témoin, pour que la première question posée devant n’importe quelle panne de clé obtienne une réponse par oui ou par non en une minute.
Une fois que ces clés se chargent correctement, la question suivante est celle de leur émission et de leur rotation, traitée dans Bonnes pratiques de sécurité JWT : attaques et défenses.