Skip to content
Volver al blog
Seguridad

Error de formato de clave privada RS256: un mensaje, siete causas

El mismo error DECODER surge por un contenedor erróneo, una cabecera indentada o una clave pública. Correcciones probadas de PKCS#1 y PKCS#8. Generador online.

13 min de lectura

Error de formato de clave privada RS256: un mensaje, siete causas

Un error de formato de clave privada RS256 casi nunca nombra su propia causa. En Node v25.8.2, cada uno de estos descuidos produce exactamente la misma línea:

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

Cinco cosas sin relación entre sí lo disparan: un contenedor OpenSSH donde se esperaba una clave PEM, una línea -----BEGIN indentada, una clave pública entregada a un firmante, secuencias \n literales que nadie desescapó y un archivo al que le quitaron los saltos de línea en el camino. La lista probada de la Sección 2 llega a siete. Siempre la misma línea, y por eso buscar el texto del error te deja dentro del hilo de otra persona hablando de la causa de otra persona.

Antes que nada, parte el problema en dos:

Triaje de treinta segundos para el primer caso:

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

Si eso da error, el archivo es el problema y las Secciones 3 a 6 lo encuentran. Si funciona, OpenSSL entendió el contenedor y tu problema está en la biblioteca o en lo que le pasaste, es decir, las Secciones 4 y 7.

Todo lo que sigue se midió el 2026-08-11 contra OpenSSL 3.6.2 7 Apr 2026, Node v25.8.2, Go go1.26.1 darwin/arm64 y Java 1.8.0_162. Donde una afirmación venga de leer el código fuente y no de ejecutarlo, el texto lo dice.

1. Primero, averigua qué tipo de fallo tienes

La línea divisoria es si alguna vez existió un objeto de clave. Los fallos en tiempo de análisis (parse-time) ocurren antes de que corra nada de criptografía. La biblioteca lee tu PEM, no logra convertirlo en una clave y lanza la excepción. No se firmó nada, no se verificó nada y el token que estás depurando nunca llegó a producirse. Los fallos en tiempo de verificación (verify-time) son lo contrario: la clave cargó sin problemas, se calculó una firma y no coincidió. Esos vienen de desacuerdos a nivel de bytes entre firmante y verificador, y la guía de invalid signature los cubre.

Distinguirlos toma una sola mirada a la traza de pila. Un fallo de análisis nombra un decodificador, un key spec o una estructura ASN.1. Un fallo de verificación nombra una firma.

Así se ve una clave privada rechazada en tres ecosistemas:

RuntimeVersión probadaMensaje cuando la clave no carga
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

Fíjate en lo distinto que es su nivel de ayuda. Go te dice exactamente qué función llamar en su lugar. Java menciona un «algid» y una «sequence», y te deja a ti deducir que tu clave está en el contenedor equivocado. Node no dice nada aprovechable.

Si no tienes la certeza de que el token que persigues sea siquiera RS256, pégalo en el decodificador JWT y lee alg en la cabecera antes de seguir. Una cabecera HS256 significa que necesitas un secreto compartido y no un par de claves, y todos los síntomas de este artículo te van a apuntar en la dirección equivocada.

2. Del texto del error a la causa raíz: la tabla de búsqueda del error de formato de clave privada RS256

Busca tu cadena exacta. La columna de la derecha te dice a dónde ir después.

Texto del errorDe dónde vieneQué significa en realidad
error:1E08010C:DECODER routines::unsupportedNode v25.8.2Siete causas posibles, listadas abajo
error:07880109:common libcrypto routines::interrupted or cancelledNode v25.8.2La clave está cifrada y no diste ninguna contraseña
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)Go 1.26.1Llamaste a ParsePKCS1PrivateKey sobre un archivo PKCS#8
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)Go 1.26.1Llamaste a ParsePKCS8PrivateKey sobre un archivo PKCS#1
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})Go 1.26.1El primer bloque PEM es EC PARAMETERS, no la clave
algid parse error, not a sequenceJava 1.8.0_162PKCS#1 entregado a PKCS8EncodedKeySpec
secretOrPrivateKey must have a valuejsonwebtoken, en el código fuenteEl argumento de la clave es falsy y alg no es none
secretOrPrivateKey is not valid key materialjsonwebtoken, en el código fuenteNo se pudo construir ni una clave privada ni una clave secreta
secretOrPrivateKey must be a symmetric key when using ${header.alg}jsonwebtoken, en el código fuentealg empieza con HS pero la clave no es un secreto
secretOrPrivateKey must be an asymmetric key when using ${header.alg}jsonwebtoken, en el código fuentealg coincide con RS, PS o ES pero la clave no es privada
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}jsonwebtoken, en el código fuenteRS o PS con una clave de menos de 2048 bits y allowInsecureKeySizes desactivado

Las cinco cadenas de secretOrPrivateKey se leyeron de sign.js en la rama master de jsonwebtoken; no se ejecutaron localmente, así que toma las condiciones de disparo como lo que dice el código fuente y no como algo reproducido en esta máquina. La parte ${header.alg} es un marcador de plantilla en ese código: en tiempo de ejecución vas a ver ahí el nombre de tu propio algoritmo, y por eso buscar la cadena literal con las llaves no encuentra nada.

Las siete maneras de producir DECODER routines::unsupported

Las siete se reprodujeron contra crypto.createPrivateKey() en Node v25.8.2, y las siete dieron el mismo código y el mismo mensaje:

  1. Un contenedor OpenSSH. El archivo empieza con -----BEGIN OPENSSH PRIVATE KEY----- y no es una estructura de clave PEM en absoluto.
  2. Una línea -----BEGIN indentada, o una línea -----END indentada. Las líneas del cuerpo están exentas; la Sección 5 tiene el límite exacto.
  3. Espacios en blanco antes de todo el PEM. Una línea vacía al principio no molesta; un espacio al principio sí.
  4. Saltos de línea eliminados por completo, de modo que la cabecera, el base64 y el pie quedan pegados en una sola línea.
  5. Una clave pública donde se esperaba una clave privada.
  6. Secuencias de barra invertida y n literales que nadie desescapó, que es en lo que se convierte una variable de entorno de una sola línea.
  7. Un delimitador con la cantidad equivocada de guiones, o begin/end escritos en minúsculas.

Dos de esos son problemas de contenedor, cuatro son problemas de texto estropeado y uno es una simple confusión. El mensaje no puede decirte cuál, así que el camino más rápido es eliminar, no leer.

Lo que Node sí acepta, que acota la búsqueda más rápido

La lista inversa resulta más útil, porque cada elemento es una teoría que puedes descartar de inmediato. En Node v25.8.2, crypto.createPrivateKey() aceptó todo lo siguiente sin quejarse:

  • Claves privadas PKCS#1 y PKCS#8
  • Claves privadas EC SEC1
  • Finales de línea CRLF
  • Un salto de línea final ausente
  • Un cuerpo base64 en una sola línea sin plegar
  • Líneas del cuerpo indentadas
  • Una línea vacía antes del PEM
  • Una BOM UTF-8, tanto en la forma '' + pem como en un Buffer que empieza con 0xEF 0xBB 0xBF
  • Una cabecera PKCS#1 envolviendo un cuerpo PKCS#8

Ese último merece un momento. El decodificador lee la estructura DER que hay dentro del base64 e ignora la etiqueta de afuera, así que un archivo que dice BEGIN RSA PRIVATE KEY sobre contenido PKCS#8 carga igual. Útil de saber, y una advertencia sobre la Sección 3: la línea de cabecera es una pista, no una garantía.

3. La línea de cabecera PEM: qué contenedor tienes realmente

Todo PEM se anuncia en su primera línea. Estos son los valores de cabecera que escribe OpenSSL 3.6.2:

ContenidoPrimera línea
Clave privada PKCS#8-----BEGIN PRIVATE KEY-----
Clave privada PKCS#1-----BEGIN RSA PRIVATE KEY-----
Clave privada cifrada-----BEGIN ENCRYPTED PRIVATE KEY-----
Clave privada OpenSSH-----BEGIN OPENSSH PRIVATE KEY-----
Clave privada EC SEC1-----BEGIN EC PARAMETERS-----, y después un segundo bloque -----BEGIN EC PRIVATE KEY-----
Clave pública SPKI-----BEGIN PUBLIC KEY-----
Clave pública PKCS#1-----BEGIN RSA PUBLIC KEY-----
Clave privada Ed25519-----BEGIN PRIVATE KEY-----, y el archivo entero son tres líneas

Así que head -1 key.pem responde la primera pregunta de cualquier investigación. Quedan tres detalles por fijar.

ENCRYPTED PRIVATE KEY no es un error de formato, sino una contraseña que olvidaste pasar. Node lo reporta distinto de todo lo demás, con ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED y error:07880109:common libcrypto routines::interrupted or cancelled, porque la biblioteca pidió una contraseña y no recibió nada. No mezcles este mensaje con el de DECODER: no tienen nada que ver el uno con el otro.

OPENSSH PRIVATE KEY es otro mundo. OpenSSH escribe su propio contenedor, que no es PKCS#1 ni PKCS#8 pese a vivir entre delimitadores con aspecto de PEM. Node lo rechaza de plano, y lo mismo hacen los parsers de crypto/x509 de Go y el PKCS8EncodedKeySpec del JDK. Si tu clave de firma JWT salió de ssh-keygen, ahí está tu bug.

Los archivos EC SEC1 llevan dos bloques. openssl ecparam -genkey escribe primero un bloque EC PARAMETERS y la clave privada después. Cualquier cosa que lea solo el primer bloque PEM se queda con los parámetros y falla de una forma que no menciona ni lo uno ni lo otro. La Sección 4 tiene la versión de Go de ese fallo.

Y como la cabecera es solo una etiqueta, la comprobación inversa también importa: un archivo cuya cabecera dice una cosa y cuyo DER dice otra se analiza según el DER. Leer head -1 es confiable para archivos que salieron directo de OpenSSL, y poco confiable para archivos que pasaron por un humano, por una página de wiki o por un script que hace reemplazo de cadenas.

4. Qué acepta cada biblioteca: PKCS#1 vs PKCS#8 en tres ecosistemas

Esta es la matriz que explica la mayoría de las discusiones de formato entre equipos. Cada fila está medida sobre las versiones listadas al principio de este artículo.

BibliotecaPKCS#1PKCS#8OpenSSH¿El error se explica solo?
Node cryptoNoNo. Muchas causas, un único DECODER routines::unsupported
Go crypto/x509Sí, con función dedicadaSí, con función dedicadaNoSí. Nombra la función a la que debes cambiar
Biblioteca estándar de JavaNoNoNo. algid parse error, not a sequence es activamente engañoso

Lee las columnas y las discusiones se resuelven solas. Un servicio en Node y un servicio en Java compartiendo un mismo archivo de clave funcionan perfecto hasta que la clave es PKCS#1: en ese momento Node sigue firmando y Java lanza un mensaje sobre secuencias ASN.1. Nadie sospecha de la clave, porque está demostrado que funciona en producción en el otro servicio.

En Node no hay nada que configurar. Si el contenedor es PKCS#1 o PKCS#8, createPrivateKey() lo acepta. Cuando sí lanza un error, invierte tu tiempo en las siete causas de la Sección 2 y no en el 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);
}

Corre eso contra el archivo que carga tu aplicación, no contra una copia que hiciste a mano, y la rama catch imprime el par de código y mensaje que puedes buscar en la Sección 2.

Go tiene dos contenedores y dos funciones, y llamar a la equivocada es el fallo más común del ecosistema. El mensaje te dice cuál usar, así que la corrección es mecánica. Probar las dos en orden elimina la decisión:

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
}

Eso sí, la trampa de EC hay que manejarla antes. Contra un archivo salido de openssl ecparam -genkey, pem.Decode devuelve un bloque cuyo Type es EC PARAMETERS, y las tres funciones de análisis fallan sobre él con:

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

Ese mensaje nunca menciona bloques PEM, así que la reacción habitual es sospechar de la clave. Mejor sáltate el bloque de parámetros:

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

O evita generar el bloque extra desde el principio: agrega -noout al comando ecparam que escribe el archivo.

La biblioteca estándar de Java lee PKCS#8 y nada más. Dale a PKCS8EncodedKeySpec una clave PKCS#1 en Java 1.8.0_162 y obtienes:

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

«algid» es el identificador de algoritmo, el campo que PKCS#8 agrega y que PKCS#1 no tiene. El parser lo buscó, encontró el comienzo de un módulo RSA y se rindió. El mensaje es correcto e inútil en la misma medida. Convierte el archivo y el error desaparece:

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

La ruta de carga que sí funciona en Java 8, una vez que el archivo es PKCS#8, es lo bastante corta como para meterla dentro de un test mientras confirmas la corrección:

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

La alternativa a convertir es agregar BouncyCastle, que sí lee PKCS#1. Convertir es un solo comando y cero dependencias, así que convierte, salvo que algo más en tu stack ya necesite esa biblioteca.

5. Los caracteres que no puedes ver

Aquí el consejo popular está equivocado, y se puede demostrar.

La indentación: al revés de lo que te contaron

Una instrucción muy repetida dice que toda línea de un PEM, salvo los delimitadores, debe empezar en la columna cero. Probado en Node v25.8.2, es justo al revés:

Cambio en el archivoResultado
Todas las líneas indentadasFalla
Solo la línea -----BEGIN indentadaFalla
Solo la línea -----END indentadaFalla
Solo las líneas base64 del cuerpo indentadasSe acepta
Un espacio antes de todo el PEMFalla
Una línea vacía antes de todo el PEMSe acepta

Así que la regla es: las líneas -----BEGIN y -----END deben empezar en la columna cero, y la indentación de las líneas del cuerpo no importa. Justo las dos líneas que a la gente le dicen que puede indentar son las dos que rompen, y las líneas que le piden alinear son las que tienen margen.

Esto importa por cómo terminan indentándose las claves privadas. Nadie indenta un PEM a mano. Pasa cuando una clave se pega dentro de un bloque YAML, un archivo de values de Helm, un heredoc de Terraform o una cadena de triple comilla de Python dentro del cuerpo de una clase. Todos esos indentan el bloque entero de manera uniforme, delimitadores incluidos, que es la primera fila de esa tabla.

La secuencia \n literal de una variable de entorno de una sola línea

Un PEM tiene saltos de línea y una variable de entorno, en la práctica, no. Así que las claves aterrizan en los archivos .env como una sola línea con \n escrito como dos caracteres. Lo que sea que lea ese archivo le entrega a tu código una cadena con barras invertidas, y el parser ve un delimitador seguido de basura. En Node esta es la causa 6 de la Sección 2, con el mismo mensaje DECODER routines::unsupported que todo lo demás.

Deshazlo en el punto de uso:

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

Agrega dos resguardos alrededor de eso. Primero, aplica el reemplazo solo cuando la cadena contenga de verdad esa secuencia de dos caracteres; así un valor genuinamente multilínea que llegó por otro cargador queda intacto de todos modos. Segundo, prefiere base64 para todo el PEM si tu plataforma lo permite: guarda una sola línea de base64, decodifícala al arrancar y la pregunta del escapado desaparece por completo.

La BOM: inofensiva en Node, y sin probar en el resto

Una marca de orden de bytes son tres bytes, EF BB BF, que algunos editores de Windows escriben al inicio de un archivo UTF-8. El consejo de quitarla antes de cargar una clave es común. En Node v25.8.2 no hizo ninguna diferencia: un PEM con la BOM delante se analizó correctamente tanto en forma de cadena como en forma de Buffer que empieza con esos tres bytes.

Delimita bien ese resultado. Se midió únicamente en Node v25.8.2. Java, Python y otros parsers no se probaron aquí, y nada en este artículo dice cómo se comportan. Si estás depurando un servicio Java, la BOM sigue siendo una pregunta abierta y no un descarte.

La BOM sí rompe otras cosas, que es probablemente de donde salió por asociación el consejo sobre las claves. JSON.parse sobre una cadena con la BOM delante es un fallo real y bien documentado, cubierto en BOM UTF-8: corregir errores de JSON.parse y CSV. Un archivo de clave guardado dentro de una configuración JSON puede fallar, entonces, mucho antes de que algo mire la clave.

Finales de línea, salto de línea final y ancho de plegado

Tres sospechosos más que Node v25.8.2 absolvió:

  • Finales de línea CRLF, aceptados. Una clave que pasó por Windows no está rota de forma automática.
  • Un salto de línea final ausente, aceptado. Ten en cuenta que este depende del parser: se dice que algunos parsers rechazan un PEM sin su salto de línea final, y Node no es uno de ellos. Node lo acepta; otros parsers no se probaron aquí.
  • Un cuerpo sin plegar, aceptado. El base64 no necesita estar plegado a 64 caracteres.

Lo que sí rompe un cuerpo base64 es un carácter perdido, insertado o sustituido, que es un fallo distinto del plegado. Un cliente de chat que convierte un salto de línea en un espacio, o un campo de texto que se come el último carácter, produce un cuerpo que ya no decodifica. Copia con un botón de copiar, no arrastrando el mouse.

6. OpenSSL 3.x te cambió el valor por defecto sin avisar

Medido en OpenSSL 3.6.2 7 Apr 2026:

ComandoContenedor que escribe
openssl genrsa -out k.pem 2048PKCS#8, cabecera 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.pemConvierte PKCS#1 a PKCS#8
openssl rsa -in b.pem -traditional -out a.pemConvierte PKCS#8 a PKCS#1

Lee otra vez las dos primeras filas. En esta build, genrsa te da PKCS#8 por defecto, y -traditional es lo que produce el archivo BEGIN RSA PRIVATE KEY. Muchísimas guías siguen describiendo genrsa como el comando de PKCS#1 y genpkey como el de PKCS#8, y seguirlas te va a dejar convencido de que generaste un formato que no generaste.

La consecuencia práctica aparece en las migraciones. Un equipo en Java recibe una clave que funciona de parte de un colega con un OpenSSL más viejo, todo va bien, y seis meses después alguien regenera la clave en una máquina nueva. Mismo comando, misma documentación, contenedor distinto, y ahora el JDK lanza algid parse error, not a sequence contra una clave que «se generó exactamente igual». No fue así.

Nunca supongas, entonces. Verifica:

head -1 key.pem

Una línea de salida, y la tabla de la Sección 3 te dice qué tienes en la mano. Haz esto antes de correr cualquier comando de conversión, porque convertir un archivo PKCS#8 a PKCS#8 es una operación nula que parece una corrección y no corrige nada.

Si prefieres no pensar en los flags, el generador de claves RSA online emite ambos contenedores a partir del mismo par de claves con un interruptor, así que puedes producir una copia PKCS#1 y una PKCS#8 de una misma clave y probar cada una contra la biblioteca que te está rechazando.

7. El piso de 2048 bits que rechaza una clave perfectamente válida

Hay un fallo que parece un problema de formato y no lo es. En el código fuente de jsonwebtoken, sign.js lanza:

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

El código lo dispara cuando alg es un algoritmo RS o PS, la clave tiene menos de 2048 bits y no se activó allowInsecureKeySizes. Esa comprobación es propia de la biblioteca, no del runtime. Node v25.8.2 analiza una clave RSA de 1024 bits sin quejarse; modulusLength: 1024 produce un objeto de clave como cualquier otro. Así que la clave es estructuralmente válida, el contenedor es el correcto, OpenSSL la lee, y aun así la llamada de firma falla.

La señal delatora es que este mensaje nombra un número. Los errores de formato hablan de decodificadores, secuencias y material de clave; este habla de bits. Si ves un tamaño en el mensaje, deja de mirar el PEM.

De dónde salen las claves de 1024 bits suele ser historia: una clave generada hace años contra un valor por defecto que desde entonces cambió, o un fixture de tests que nadie volvió a revisar porque las claves chicas se generan más rápido. La corrección es generar un par nuevo de 2048 bits o más, y no echar mano de la escotilla de emergencia, porque esa escotilla apaga una comprobación que existe por algo.

Para confirmar que el tamaño es el único problema que queda, firma el mismo payload con una clave nueva del tamaño correcto en el codificador JWT. Si eso produce un token y tu propio código no, la diferencia está en tu clave, no en tus claims ni en tu configuración.

8. Un flujo de trabajo repetible para los fallos de clave RS256

Ejecuta esto en orden. Cada paso encuentra la causa o elimina una rama.

  1. Lee la línea de cabecera. head -1 key.pem, y compárala con la tabla de la Sección 3. Eso te dice el contenedor, si el archivo está cifrado y si es una clave OpenSSH que nunca va a funcionar.
  2. Pídele a OpenSSL que la analice. openssl rsa -in key.pem -noout -text | head -1 para RSA, o openssl pkey -in key.pem -noout para cualquier algoritmo. Que funcione significa que los bytes son una clave válida y el problema está del lado de la biblioteca. Que falle significa que el archivo está dañado y sigues al paso 4.
  3. Revisa la fila de tu biblioteca en la matriz. Sección 4. Si estás en Java con un archivo PKCS#1, o en Go llamando a la función de análisis equivocada, aquí terminaste.
  4. Mira los caracteres invisibles. head -c 32 key.pem | xxd muestra los primeros bytes, lo que atrapa una BOM, un espacio inicial y un delimitador indentado de un solo vistazo. Después confirma que las líneas -----BEGIN y -----END empiezan en la columna cero, según la Sección 5.
  5. Biseca con una clave de referencia que funcione. Genera un par nuevo en el generador de claves RSA online, apunta tu código a él y mira si el error sobrevive. Si sobrevive, el bug está en tu código de carga y no en el archivo de clave, y ninguna cantidad de reformateo del original va a ayudar. Si desaparece, el archivo original es el culpable y ahora tienes una clave funcional contra la cual comparar.
  6. Revisa el algoritmo y el tamaño al final. Confirma que la cabecera dice RS256, y confirma que la clave tiene al menos 2048 bits, según la Sección 7.

El paso 5 es el que la gente se salta y el que más tiempo ahorra. Una clave de referencia limpia convierte un vago «la clave no funciona» en una respuesta binaria sobre cuál de los dos lados está roto.

FAQ

¿Cuál es la diferencia entre BEGIN RSA PRIVATE KEY y BEGIN PRIVATE KEY?

Son dos contenedores alrededor de la misma clave RSA. BEGIN RSA PRIVATE KEY es PKCS#1 y guarda los números RSA directamente; BEGIN PRIVATE KEY es PKCS#8 y agrega un identificador de algoritmo, que es por lo que también puede transportar claves ECDSA y Ed25519. Cuál necesitas depende por completo de la biblioteca, y el generador de claves RSA online escribe cualquiera de los dos.

¿Por qué openssl genrsa produce un formato distinto al del tutorial?

Porque el valor por defecto cambió. En OpenSSL 3.6.2, openssl genrsa -out k.pem 2048 escribe PKCS#8 con una cabecera BEGIN PRIVATE KEY. Para obtener la disposición tradicional PKCS#1 que describen las guías viejas, agrega -traditional. Corre head -1 sobre la salida en lugar de confiar en cualquier tutorial acerca de lo que produce tu build.

¿Cómo corrijo algid parse error, not a sequence en Java?

Ese mensaje en Java 1.8.0_162 significa que le diste a PKCS8EncodedKeySpec una clave PKCS#1. La biblioteca estándar no lee PKCS#1 en absoluto. Convierte una vez con openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem, o agrega BouncyCastle si algo más en el proyecto ya lo necesita.

¿Cada línea de una clave privada tiene que empezar en la columna cero?

No, y el consejo habitual lo tiene al revés. Probado en Node v25.8.2, indentar solo las líneas base64 del cuerpo se analiza sin problema, mientras que indentar solo la línea -----BEGIN o solo la línea -----END falla. Una línea vacía antes del PEM se acepta; un espacio delante, no.

¿Cómo se debe guardar una clave privada en un archivo .env?

O como una sola línea entrecomillada con escapes \n que deshaces al cargarla con .replace(/\\n/g, '\n'), o como una sola línea de base64 que decodificas al arrancar. La segunda es más segura porque no hay ninguna convención de escapado que un cargador de configuración pueda interpretar mal.

¿Puedo usar una clave de 1024 bits con RS256?

Node v25.8.2 analiza una clave RSA de 1024 bits sin error, pero el código fuente de jsonwebtoken se niega a firmar con ella: secretOrPrivateKey has a minimum key size of 2048 bits, salvo que se active allowInsecureKeySizes. Genera mejor una clave de 2048 bits. El mensaje nombra una cantidad de bits, que es como lo distingues de un problema de formato.

¿Por qué obtengo un error de formato de clave privada RS256 que pide una clave asimétrica si le pasé un archivo de clave privada?

En el código fuente de jsonwebtoken, secretOrPrivateKey must be an asymmetric key when using ${header.alg} se dispara cuando alg es RS, PS o ES y la clave no es una clave privada. Normalmente el valor es una cadena de secreto al estilo HS256 que quedó de una configuración anterior. Una cadena aleatoria va con HS256 y con el generador de secreto JWT; RS256 necesita un par de claves, no un secreto.

Conclusión

El costo de este bug está en el diagnóstico. Una sola cadena de error cubre siete causas en Node, el mensaje de Java apunta a ASN.1 cuando la respuesta real es «contenedor equivocado», y el consejo de formato más repetido sobre el tema está invertido. No puedes leer hasta llegar a la respuesta, así que en su lugar eliminas: línea de cabecera, análisis con OpenSSL, matriz de bibliotecas, caracteres invisibles, clave de referencia que funciona.

Dos hábitos evitan la repetición. Anota qué contenedor requiere cada servicio, junto a la clave en tu almacén de secretos, porque la restricción vive en la biblioteca y no en la clave. Y mantén en tu entorno de desarrollo un par de claves que funcione, puramente como control, para que la primera pregunta sobre cualquier fallo de clave tenga una respuesta de sí o no en un minuto.

Para la pregunta más amplia de cómo se deben emitir, rotar y acotar estas claves una vez que cargan correctamente, mira Buenas prácticas de seguridad JWT: ataques y defensas.

Etiquetas: jwt rsa pem openssl debugging security

Artículos relacionados

Ver todos los artículos