Skip to content
ecdsa.com

PKCS#1 vs PKCS#8: BEGIN RSA PRIVATE KEY or PRIVATE KEY

Both hold the same RSA numbers. PKCS#8 adds an algorithm identifier and optional encryption, which is why every modern library prefers it and some refuse anything else.

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 RSAPrivateKeyPKCS#8 PrivateKeyInfo / OneAsymmetricKey
SpecificationRFC 8017 Appendix A.1.2RFC 5208, RFC 5958
PEM labelRSA PRIVATE KEYPRIVATE KEY; encrypted: ENCRYPTED PRIVATE KEY
AlgorithmsRSA onlyAny: RSA, EC, Ed25519, X25519, ML-DSA, ML-KEM …
Algorithm identifierNone (implied by the label)rsaEncryption, id-ecPublicKey, 1.3.101.112 …
EncryptionOpenSSL-specific PEM headers (Proc-Type, DEK-Info), weak KDFStandard PBES2 (PBKDF2 plus AES) per RFC 8018
OpenSSL 3 outputOnly with -traditionalDefault for genpkey, pkey, rsa
Java PKCS8EncodedKeySpecNot acceptedAccepted
Gox509.ParsePKCS1PrivateKeyx509.ParsePKCS8PrivateKey
Node.js createPrivateKeyAccepted (PEM label detected)Accepted
WebCrypto importKeyNot acceptedpkcs8 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.

OpenSSL 3.6: PKCS#8, PKCS#1 and encrypted PKCS#8
#!/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"
Output
-----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.

LoadKey.java: load PEM private keys with PKCS8EncodedKeySpec
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);
      }
    }
  }
}
Run it on Java 17, 21 and 25 next to a PKCS#8 and a PKCS#1 key
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
Output (20 October 2026)
== 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.ParsePKCS8PrivateKey returns x509: 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 as type: "pkcs8" fails with error:068000A8:asn1 encoding routines::wrong tag (code ERR_OSSL_ASN1_WRONG_TAG); a PKCS#1 body under a BEGIN PRIVATE KEY label fails with error:1E08010C:DECODER routines::unsupported; the same PKCS#1 bytes passed to createPublicKey as spki give Failed to read asymmetric key. An encrypted key loaded without a passphrase fails with error: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 pkey prints error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported followed by the algorithm it could not fetch (DES-CBC, or PBKDF1 for PBES1), and Node.js reports error:0308010C:digital envelope routines::unsupported (ERR_OSSL_EVP_UNSUPPORTED).
  • Python (cryptography 46): load_pem_private_key reads PKCS#1 and PKCS#8 alike and raises TypeError: Password was not given but private key is encrypted for an encrypted key without a password.
  • WebCrypto: importKey("pkcs8", …) with PKCS#1 bytes rejects with a DataError; Node.js 26 words it Invalid 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 ssh itself; PKCS#8 when another tool needs the same key.

Migration notes

  1. Find PKCS#1 and SEC 1 files: grep -l "BEGIN RSA PRIVATE KEY\|BEGIN EC PRIVATE KEY" across configuration repositories and secret stores.
  2. Convert once with openssl pkcs8 -topk8 -nocrypt (or -v2 aes-256-cbc to encrypt) and update the consumers; the key material and the corresponding certificate do not change.
  3. Re-encrypt old encrypted keys: traditional PEM encryption (Proc-Type and DEK-Info headers) 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.
  4. Update code to the generic parsers (ParsePKCS8PrivateKey, load_pem_private_key, createPrivateKey) so new algorithms, including Ed25519 and ML-DSA, work without code changes.
  5. 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.

References

All comparisons