PKCS#1 vs PKCS#8 is the question behind many "unsupported key format" errors, usually met when a key generated years ago with openssl genrsa meets a modern library. A PKCS#1 file (-----BEGIN RSA PRIVATE KEY-----) contains only an RSA key structure; a PKCS#8 file (-----BEGIN PRIVATE KEY-----) wraps any algorithm's private key with an algorithm identifier, and its encrypted form (-----BEGIN ENCRYPTED PRIVATE KEY-----) adds standard password protection.
At a glance
PKCS#1 RSAPrivateKey | PKCS#8 PrivateKeyInfo / OneAsymmetricKey | |
|---|---|---|
| Specification | RFC 8017 Appendix A.1.2 | RFC 5208, RFC 5958 |
| PEM label | RSA PRIVATE KEY | PRIVATE KEY; encrypted: ENCRYPTED PRIVATE KEY |
| Algorithms | RSA only | Any: RSA, EC, Ed25519, X25519, ML-DSA, ML-KEM … |
| Algorithm identifier | None (implied by the label) | rsaEncryption, id-ecPublicKey, 1.3.101.112 … |
| Encryption | OpenSSL-specific PEM headers (Proc-Type, DEK-Info), weak KDF | Standard PBES2 (PBKDF2 plus AES) per RFC 8018 |
| OpenSSL 3 output | Only with -traditional | Default for genpkey, pkey, rsa |
Java PKCS8EncodedKeySpec | Not accepted | Accepted |
| Go | x509.ParsePKCS1PrivateKey | x509.ParsePKCS8PrivateKey |
Node.js createPrivateKey | Accepted (PEM label detected) | Accepted |
WebCrypto importKey | Not accepted | pkcs8 format |
The public-key counterparts are PKCS#1 RSAPublicKey (BEGIN RSA PUBLIC KEY) and SubjectPublicKeyInfo (BEGIN PUBLIC KEY), the latter being what everything else expects.
What is inside
A PKCS#1 RSA private key is a DER SEQUENCE of version, modulus n, public exponent e, private exponent d, the primes p and q, and the CRT values dP, dQ and qInv. Because the structure says nothing about the algorithm, the PEM label must; there is no PKCS#1 form for elliptic-curve keys (OpenSSL's analogous BEGIN EC PRIVATE KEY format comes from SEC 1, RFC 5915).
PKCS#8 wraps an algorithm-specific key in a PrivateKeyInfo: a version, an AlgorithmIdentifier and an OCTET STRING holding the inner key, which for RSA is exactly the PKCS#1 structure. RFC 5958 (version 2, OneAsymmetricKey) can also carry the public key. EncryptedPrivateKeyInfo adds an encryption algorithm and the ciphertext; OpenSSL 3.6 uses PBES2 with PBKDF2-HMAC-SHA256 and AES-256-CBC when you request encryption.
Converting
The script below, run with OpenSSL 3.6.5, shows the defaults and a lossless round trip.
#!/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
The same commands convert SEC 1 EC keys (BEGIN EC PRIVATE KEY) to PKCS#8. OpenSSH private keys (BEGIN OPENSSH PRIVATE KEY) are a third format; ssh-keygen -p -m PKCS8 -f key rewrites one in place as PKCS#8.
Errors you will see
Java is the strictest reader: PKCS8EncodedKeySpec accepts only PKCS#8, and the wording of its error changed between releases. The program below loads one PEM file of each kind, and the commands after it run it on Java 17, 21 and 25 in the official Eclipse Temurin images.
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
Older answers on the web quote the Java 17 text; on Java 21 and 25 the top-level message is Unable to decode key, and "algid parse error, not a sequence" survives only in the cause. Search logs for both.
The other runtimes, reproduced the same day with keys generated by OpenSSL 3.6.5:
- Go:
x509.ParsePKCS8PrivateKeyreturnsx509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)(Go 1.25), which tells you exactly what happened. - Node.js 26 (
createPrivateKey, OpenSSL 3.6.5): PKCS#1 DER declared astype: "pkcs8"fails witherror:068000A8:asn1 encoding routines::wrong tag(codeERR_OSSL_ASN1_WRONG_TAG); a PKCS#1 body under aBEGIN PRIVATE KEYlabel fails witherror:1E08010C:DECODER routines::unsupported; the same PKCS#1 bytes passed tocreatePublicKeyasspkigiveFailed to read asymmetric key. An encrypted key loaded without a passphrase fails witherror:07880109:common libcrypto routines::interrupted or cancelled. - Keys encrypted with legacy algorithms: a traditional PEM key encrypted with DES, or a PKCS#8 key under PBES1 with MD5 and DES, loads in OpenSSL 3 only with the legacy provider. Without it,
openssl pkeyprintserror:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupportedfollowed by the algorithm it could not fetch (DES-CBC, orPBKDF1for PBES1), and Node.js reportserror:0308010C:digital envelope routines::unsupported(ERR_OSSL_EVP_UNSUPPORTED). - Python (
cryptography46):load_pem_private_keyreads PKCS#1 and PKCS#8 alike and raisesTypeError: Password was not given but private key is encryptedfor an encrypted key without a password. - WebCrypto:
importKey("pkcs8", …)with PKCS#1 bytes rejects with aDataError; Node.js 26 words itInvalid keyData, browsers use their own text.
The error catalog has a page for each, linked below.
The same split for public keys
Public keys have the same two shapes. PKCS#1 RSAPublicKey (BEGIN RSA PUBLIC KEY) holds only the modulus and exponent; SubjectPublicKeyInfo (BEGIN PUBLIC KEY, RFC 5280) wraps the key with an algorithm identifier, works for every algorithm and is what certificates, JWK converters, Java's X509EncodedKeySpec, Go's ParsePKIXPublicKey and WebCrypto's spki import expect. openssl rsa -pubin -in key.pem -RSAPublicKey_out and openssl pkey -pubin -in key.pem -pubout convert between them. The confusion runs both ways: tools that write SPKI often name the file id_rsa.pub-style, and OpenSSH's own one-line public keys (ssh-rsa AAAA…) are yet another format, convertible with ssh-keygen -e -m PKCS8.
When to choose which
- New keys, any algorithm: PKCS#8 (encrypted on disk, unencrypted inside a secrets manager that already encrypts).
- Java, Go, .NET, WebCrypto, cloud KMS imports: PKCS#8 (DER or PEM).
- Old software that only reads
RSA PRIVATE KEY(some appliances, older Ruby and PHP code, legacy SSH tools): PKCS#1 via-traditional, ideally only at the point of delivery. - Key plus certificate for Windows, macOS or Java keystores: PKCS#12, which contains PKCS#8 keys internally.
- SSH keys: the OpenSSH format for
sshitself; PKCS#8 when another tool needs the same key.
Migration notes
- Find PKCS#1 and SEC 1 files:
grep -l "BEGIN RSA PRIVATE KEY\|BEGIN EC PRIVATE KEY"across configuration repositories and secret stores. - Convert once with
openssl pkcs8 -topk8 -nocrypt(or-v2 aes-256-cbcto encrypt) and update the consumers; the key material and the corresponding certificate do not change. - Re-encrypt old encrypted keys: traditional PEM encryption (
Proc-TypeandDEK-Infoheaders) derives its key with a single MD5 iteration, and keys encrypted with DES or RC2 in either format need OpenSSL's legacy provider; re-encrypt with PBES2 and AES-256. - Update code to the generic parsers (
ParsePKCS8PrivateKey,load_pem_private_key,createPrivateKey) so new algorithms, including Ed25519 and ML-DSA, work without code changes. - Verify by signing with the converted key and checking the signature against the existing certificate; the key generator and ASN.1 explorer show the structure without uploading anything.