Skip to content
Retour au blog
Sécurité

Erreur de format de clé privée RS256 : un message, sept causes

La même erreur DECODER surgit pour un mauvais conteneur, un en-tête indenté ou une clé publique. Correctifs testés PKCS#1 et PKCS#8. Générateur en ligne.

13 min de lecture

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 :

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 :

EnvironnementVersion testéeMessage quand la clé refuse de se charger
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

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’erreurD’où elle vientCe qu’elle veut vraiment dire
error:1E08010C:DECODER routines::unsupportedNode v25.8.2Sept causes possibles, listées plus bas
error:07880109:common libcrypto routines::interrupted or cancelledNode v25.8.2La 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.1Vous avez appelé ParsePKCS1PrivateKey sur un fichier PKCS#8
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)Go 1.26.1Vous avez appelé ParsePKCS8PrivateKey sur un fichier PKCS#1
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})Go 1.26.1Le premier bloc PEM est EC PARAMETERS, pas la clé
algid parse error, not a sequenceJava 1.8.0_162Un PKCS#1 remis à PKCS8EncodedKeySpec
secretOrPrivateKey must have a valuejsonwebtoken, dans le code sourceL’argument de clé est falsy et alg n’est pas none
secretOrPrivateKey is not valid key materialjsonwebtoken, dans le code sourceImpossible 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 sourcealg 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 sourcealg 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 sourceRS 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 :

  1. Un conteneur OpenSSH. Le fichier commence par -----BEGIN OPENSSH PRIVATE KEY----- et n’est pas du tout une structure de clé PEM.
  2. Une ligne -----BEGIN indentée, ou une ligne -----END indentée. Les lignes du corps y échappent ; la section 5 donne la frontière exacte.
  3. Une espace avant le PEM entier. Une ligne vide en tête passe, une espace en tête non.
  4. 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.
  5. Une clé publique là où une clé privée était attendue.
  6. 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.
  7. 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 '' + pem que sous celle d’un Buffer commençant par 0xEF 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 :

ContenuPremiè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èquePKCS#1PKCS#8OpenSSHL’erreur s’explique-t-elle elle-même ?
Node cryptoOuiOuiNonNon. Plusieurs causes, un seul DECODER routines::unsupported
Go crypto/x509Oui, fonction dédiéeOui, fonction dédiéeNonOui. Elle nomme la fonction à utiliser à la place
Bibliothèque standard JavaNonOuiNonNon. 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 fichierRé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éesAccepté
Une espace avant le PEM entierÉchec
Une ligne vide avant le PEM entierAccepté

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 :

CommandeConteneur écrit
openssl genrsa -out k.pem 2048PKCS#8, en-tête 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.pemConvertit PKCS#1 en PKCS#8
openssl rsa -in b.pem -traditional -out a.pemConvertit 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.

  1. 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.
  2. Demandez à OpenSSL de l’analyser. openssl rsa -in key.pem -noout -text | head -1 pour RSA, ou openssl pkey -in key.pem -noout pour 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.
  3. 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.
  4. Regardez les caractères invisibles. head -c 32 key.pem | xxd affiche 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 -----BEGIN et -----END commencent en colonne zéro, comme l’explique la section 5.
  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.
  6. 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.

Tags: jwt rsa pem openssl debugging security

Articles connexes

Voir tous les articles