PKCS#1 frente a PKCS#8 es la cuestión que hay detrás de muchos errores «unsupported key format», que suelen aparecer cuando una clave generada hace años con openssl genrsa llega a una biblioteca moderna. Un archivo PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) solo contiene una estructura de clave RSA; un archivo PKCS#8 (-----BEGIN PRIVATE KEY-----) envuelve la clave privada de cualquier algoritmo junto con un identificador de algoritmo, y su forma cifrada (-----BEGIN ENCRYPTED PRIVATE KEY-----) añade una protección estándar con contraseña.
De un vistazo
PKCS#1 RSAPrivateKey | PKCS#8 PrivateKeyInfo / OneAsymmetricKey | |
|---|---|---|
| Especificación | RFC 8017, apéndice A.1.2 | RFC 5208, RFC 5958 |
| Etiqueta PEM | RSA PRIVATE KEY | PRIVATE KEY; cifrada: ENCRYPTED PRIVATE KEY |
| Algoritmos | Solo RSA | Cualquiera: RSA, EC, Ed25519, X25519, ML-DSA, ML-KEM … |
| Identificador de algoritmo | Ninguno (lo implica la etiqueta) | rsaEncryption, id-ecPublicKey, 1.3.101.112 … |
| Cifrado | Encabezados PEM propios de OpenSSL (Proc-Type, DEK-Info), KDF débil | PBES2 estándar (PBKDF2 más AES) según el RFC 8018 |
| Salida de OpenSSL 3 | Solo con -traditional | Opción por defecto de genpkey, pkey, rsa |
PKCS8EncodedKeySpec de Java | No se acepta | Se acepta |
| Go | x509.ParsePKCS1PrivateKey | x509.ParsePKCS8PrivateKey |
createPrivateKey de Node.js | Se acepta (detecta la etiqueta PEM) | Se acepta |
importKey de WebCrypto | No se acepta | Formato pkcs8 |
Sus equivalentes para claves públicas son RSAPublicKey de PKCS#1 (BEGIN RSA PUBLIC KEY) y SubjectPublicKeyInfo (BEGIN PUBLIC KEY); este último es el que espera todo lo demás.
Qué contiene cada uno
Una clave privada RSA PKCS#1 es una SEQUENCE DER con la versión, el módulo n, el exponente público e, el exponente privado d, los primos p y q, y los valores CRT dP, dQ y qInv. Como la estructura no dice nada del algoritmo, tiene que decirlo la etiqueta PEM; no existe una forma PKCS#1 para las claves de curva elíptica (el formato análogo de OpenSSL, BEGIN EC PRIVATE KEY, procede de SEC 1, RFC 5915).
PKCS#8 envuelve una clave propia de un algoritmo en una PrivateKeyInfo: una versión, un AlgorithmIdentifier y un OCTET STRING que contiene la clave interior, que en RSA es exactamente la estructura PKCS#1. El RFC 5958 (versión 2, OneAsymmetricKey) también puede llevar la clave pública. EncryptedPrivateKeyInfo añade un algoritmo de cifrado y el texto cifrado; OpenSSL 3.6 usa PBES2 con PBKDF2-HMAC-SHA256 y AES-256-CBC cuando pides cifrado.
Conversión
El script siguiente, ejecutado con OpenSSL 3.6.5, muestra los valores por defecto y una conversión de ida y vuelta sin pérdidas.
#!/bin/sh
dir=$(mktemp -d)
cd "$dir" || exit 1
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pkcs8.pem 2>/dev/null
openssl pkey -in pkcs8.pem -traditional -out pkcs1.pem
openssl pkey -in pkcs8.pem -aes256 -passout pass:demo -out enc.pem
head -1 pkcs8.pem; head -1 pkcs1.pem; head -1 enc.pem
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out back.pem && head -1 back.pem
cmp -s pkcs8.pem back.pem && echo "round trip PKCS#1 -> PKCS#8: identical"
cd / && rm -rf "$dir"
-----BEGIN PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY-----
-----BEGIN ENCRYPTED PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
round trip PKCS#1 -> PKCS#8: identical
Los mismos comandos convierten a PKCS#8 las claves EC de SEC 1 (BEGIN EC PRIVATE KEY). Las claves privadas de OpenSSH (BEGIN OPENSSH PRIVATE KEY) son un tercer formato; ssh-keygen -p -m PKCS8 -f key reescribe una de ellas como PKCS#8 en el mismo archivo.
Errores que verás
Java es el lector más estricto: PKCS8EncodedKeySpec solo acepta PKCS#8, y el texto de su error cambió de una versión a otra. El programa siguiente carga un archivo PEM de cada tipo, y los comandos que vienen después lo ejecutan en Java 17, 21 y 25 con las imágenes oficiales de Eclipse Temurin.
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class LoadKey {
public static void main(String[] args) throws Exception {
System.out.println("== Java " + Runtime.version());
for (String file : args) {
String pem = Files.readString(Path.of(file));
byte[] der = Base64.getMimeDecoder().decode(pem.replaceAll("-----[A-Z ]+-----", ""));
try {
KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(der));
System.out.println(file + ": loaded");
} catch (Exception e) {
System.out.println(file + ": " + e);
for (Throwable t = e.getCause(); t != null; t = t.getCause()) System.out.println(" Caused by: " + t);
}
}
}
}
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pkcs8.pem 2>/dev/null
openssl pkey -in pkcs8.pem -traditional -out pkcs1.pem
for v in 17.0.20.1_1 21.0.12.1_1 25.0.4.1_1; do
docker run --rm -v "$PWD":/work:ro -w /work "eclipse-temurin:$v-jdk-noble" java LoadKey.java pkcs8.pem pkcs1.pem
done
== Java 17.0.20.1+1
pkcs8.pem: loaded
pkcs1.pem: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
Caused by: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
== Java 21.0.12.1+1-LTS
pkcs8.pem: loaded
pkcs1.pem: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: Unable to decode key
Caused by: java.security.InvalidKeyException: Unable to decode key
Caused by: java.io.IOException: algid parse error, not a sequence
== Java 25.0.4.1+1-LTS
pkcs8.pem: loaded
pkcs1.pem: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: Unable to decode key
Caused by: java.security.InvalidKeyException: Unable to decode key
Caused by: java.io.IOException: algid parse error, not a sequence
Las respuestas más antiguas de la web citan el texto de Java 17; en Java 21 y 25, el mensaje de primer nivel es Unable to decode key, y «algid parse error, not a sequence» solo se conserva en la causa. Busca ambos en los registros.
Los demás entornos de ejecución, reproducidos el mismo día con claves generadas por OpenSSL 3.6.5:
- Go:
x509.ParsePKCS8PrivateKeydevuelvex509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)(Go 1.25), que indica exactamente lo que ocurre. - Node.js 26 (
createPrivateKey, OpenSSL 3.6.5): un DER PKCS#1 declarado comotype: "pkcs8"falla conerror:068000A8:asn1 encoding routines::wrong tag(códigoERR_OSSL_ASN1_WRONG_TAG); un cuerpo PKCS#1 bajo una etiquetaBEGIN PRIVATE KEYfalla conerror:1E08010C:DECODER routines::unsupported; los mismos bytes PKCS#1 pasados acreatePublicKeycomospkidanFailed to read asymmetric key. Una clave cifrada que se carga sin frase de contraseña falla conerror:07880109:common libcrypto routines::interrupted or cancelled. - Claves cifradas con algoritmos heredados: una clave PEM tradicional cifrada con DES, o una clave PKCS#8 protegida con PBES1, MD5 y DES, solo se carga en OpenSSL 3 con el proveedor heredado (legacy). Sin él,
openssl pkeymuestraerror:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupportedseguido del algoritmo que no pudo obtener (DES-CBC, oPBKDF1en el caso de PBES1), y Node.js informa deerror:0308010C:digital envelope routines::unsupported(ERR_OSSL_EVP_UNSUPPORTED). - Python (
cryptography46):load_pem_private_keylee PKCS#1 y PKCS#8 por igual y lanzaTypeError: Password was not given but private key is encryptedcon una clave cifrada sin contraseña. - WebCrypto:
importKey("pkcs8", …)con bytes PKCS#1 devuelve una promesa rechazada con unDataError; Node.js 26 lo formula comoInvalid keyData, y los navegadores usan su propio texto.
El catálogo de errores tiene una página para cada uno, enlazada más abajo.
La misma división en las claves públicas
Las claves públicas tienen las mismas dos formas. RSAPublicKey de PKCS#1 (BEGIN RSA PUBLIC KEY) solo contiene el módulo y el exponente; SubjectPublicKeyInfo (BEGIN PUBLIC KEY, RFC 5280) envuelve la clave junto con un identificador de algoritmo, sirve para cualquier algoritmo y es lo que esperan los certificados, los conversores de JWK, X509EncodedKeySpec de Java, ParsePKIXPublicKey de Go y la importación spki de WebCrypto. openssl rsa -pubin -in key.pem -RSAPublicKey_out y openssl pkey -pubin -in key.pem -pubout convierten de una forma a la otra. La confusión va en ambos sentidos: las herramientas que escriben SPKI suelen nombrar el archivo al estilo de id_rsa.pub, y las claves públicas de una sola línea del propio OpenSSH (ssh-rsa AAAA…) son otro formato más, que se convierte con ssh-keygen -e -m PKCS8.
Cuándo elegir cada uno
- Claves nuevas, de cualquier algoritmo: PKCS#8 (cifrado en disco; sin cifrar dentro de un gestor de secretos que ya cifra).
- Java, Go, .NET, WebCrypto, importaciones en KMS en la nube: PKCS#8 (DER o PEM).
- Software antiguo que solo lee
RSA PRIVATE KEY(algunos dispositivos dedicados, código Ruby y PHP antiguo, herramientas SSH heredadas): PKCS#1 mediante-traditional, idealmente solo en el punto de entrega. - Clave junto con certificado para Windows, macOS o los almacenes de claves de Java: PKCS#12, que contiene internamente claves PKCS#8.
- Claves SSH: el formato de OpenSSH para el propio
ssh; PKCS#8 cuando otra herramienta necesita la misma clave.
Notas de migración
- Localiza los archivos PKCS#1 y SEC 1:
grep -l "BEGIN RSA PRIVATE KEY\|BEGIN EC PRIVATE KEY"en los repositorios de configuración y los almacenes de secretos. - Convierte una sola vez con
openssl pkcs8 -topk8 -nocrypt(o-v2 aes-256-cbcpara cifrar) y actualiza los consumidores; el material de la clave y el certificado correspondiente no cambian. - Vuelve a cifrar las claves cifradas antiguas: el cifrado PEM tradicional (encabezados
Proc-TypeyDEK-Info) deriva su clave con una sola iteración de MD5, y las claves cifradas con DES o RC2 en cualquiera de los dos formatos necesitan el proveedor heredado de OpenSSL; vuelve a cifrarlas con PBES2 y AES-256. - Pasa el código a los analizadores genéricos (
ParsePKCS8PrivateKey,load_pem_private_key,createPrivateKey) para que los algoritmos nuevos, incluidos Ed25519 y ML-DSA, funcionen sin cambiar el código. - Verifica firmando con la clave convertida y comprobando la firma con el certificado existente; el generador de claves y el explorador de ASN.1 muestran la estructura sin enviar nada a ningún servidor.