跳到主要内容
ecdsa.com

PKCS#1 vs PKCS#8:BEGIN RSA PRIVATE KEY 还是 PRIVATE KEY

两者存放的是相同的 RSA 数值。PKCS#8 多了算法标识符和可选的加密,所以现代库都优先使用它,有些库甚至只接受它。

许多“unsupported key format”错误,背后都是 PKCS#1 与 PKCS#8 的问题,通常是多年前用 openssl genrsa 生成的密钥遇上现代库时出现。PKCS#1 文件(-----BEGIN RSA PRIVATE KEY-----)只包含 RSA 密钥结构;PKCS#8 文件(-----BEGIN PRIVATE KEY-----)用算法标识符包裹任意算法的私钥,其加密形式(-----BEGIN ENCRYPTED PRIVATE KEY-----)还加上了标准的密码保护。

概览

PKCS#1 RSAPrivateKeyPKCS#8 PrivateKeyInfo / OneAsymmetricKey
规范RFC 8017 附录 A.1.2RFC 5208、RFC 5958
PEM 标签RSA PRIVATE KEYPRIVATE KEY;加密形式:ENCRYPTED PRIVATE KEY
算法仅 RSA任意:RSA、EC、Ed25519、X25519、ML-DSA、ML-KEM 等
算法标识符无(由标签隐含)rsaEncryption、id-ecPublicKey、1.3.101.112 等
加密OpenSSL 专有的 PEM 头部(Proc-Type、DEK-Info),KDF 强度弱按 RFC 8018 的标准 PBES2(PBKDF2 加 AES)
OpenSSL 3 的输出仅在使用 -traditional 时genpkey、pkey、rsa 的默认格式
Java PKCS8EncodedKeySpec不接受接受
Gox509.ParsePKCS1PrivateKeyx509.ParsePKCS8PrivateKey
Node.js createPrivateKey接受(可识别 PEM 标签)接受
WebCrypto importKey不接受pkcs8 格式

与之对应的公钥格式是 PKCS#1 RSAPublicKey(BEGIN RSA PUBLIC KEY)和 SubjectPublicKeyInfo(BEGIN PUBLIC KEY),其他所有软件需要的都是后者。

内部结构

PKCS#1 RSA 私钥是一个 DER SEQUENCE,依次包含版本、模数 n、公钥指数 e、私钥指数 d、素数 p 和 q,以及 CRT 值 dP、dQ 和 qInv。这个结构本身不说明算法,所以必须由 PEM 标签来说明;椭圆曲线密钥没有 PKCS#1 形式(OpenSSL 中与之类似的 BEGIN EC PRIVATE KEY 格式来自 SEC 1,即 RFC 5915)。

PKCS#8 把特定算法的密钥包裹在 PrivateKeyInfo 中:版本号、AlgorithmIdentifier,以及存放内层密钥的 OCTET STRING;对 RSA 来说,内层密钥正是 PKCS#1 结构。RFC 5958(版本 2,OneAsymmetricKey)还可以携带公钥。EncryptedPrivateKeyInfo 增加了加密算法和密文;在你要求加密时,OpenSSL 3.6 使用 PBES2,配合 PBKDF2-HMAC-SHA256 和 AES-256-CBC。

转换

下面的脚本用 OpenSSL 3.6.5 运行,展示默认格式以及一次无损的往返转换。

OpenSSL 3.6:PKCS#8、PKCS#1 和加密的 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"
输出
-----BEGIN PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY-----
-----BEGIN ENCRYPTED PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
round trip PKCS#1 -> PKCS#8: identical

同样的命令也能把 SEC 1 EC 密钥(BEGIN EC PRIVATE KEY)转换为 PKCS#8。OpenSSH 私钥(BEGIN OPENSSH PRIVATE KEY)是第三种格式;ssh-keygen -p -m PKCS8 -f key 会把这种密钥就地改写为 PKCS#8。

你会遇到的错误

Java 的解析最严格:PKCS8EncodedKeySpec 只接受 PKCS#8,而且错误的措辞在不同版本之间有变化。下面的程序对每种格式各加载一个 PEM 文件,后面的命令在官方 Eclipse Temurin 镜像中用 Java 17、21 和 25 运行它。

LoadKey.java:用 PKCS8EncodedKeySpec 加载 PEM 私钥
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);
      }
    }
  }
}
在 Java 17、21 和 25 上运行,同目录放一个 PKCS#8 密钥和一个 PKCS#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
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
输出(2026年10月20日)
== 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

网上较早的回答引用的是 Java 17 的文本;在 Java 21 和 25 上,顶层消息是 Unable to decode key,“algid parse error, not a sequence”只保留在异常原因(cause)中。搜索日志时两者都要查。

其他运行时的情况如下,同一天用 OpenSSL 3.6.5 生成的密钥复现:

  • Go:x509.ParsePKCS8PrivateKey 返回 x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)(Go 1.25),准确说明了问题所在。
  • Node.js 26(createPrivateKey,OpenSSL 3.6.5):把 PKCS#1 DER 声明为 type: "pkcs8" 时,会报错 error:068000A8:asn1 encoding routines::wrong tag(错误码 ERR_OSSL_ASN1_WRONG_TAG);BEGIN PRIVATE KEY 标签下放的是 PKCS#1 内容时,会报错 error:1E08010C:DECODER routines::unsupported;把同样的 PKCS#1 字节作为 spki 传给 createPublicKey,得到的是 Failed to read asymmetric key。不提供口令就加载加密的密钥,会报错 error:07880109:common libcrypto routines::interrupted or cancelled。
  • 用旧算法加密的密钥:用 DES 加密的传统 PEM 密钥,或用 PBES1(MD5 加 DES)保护的 PKCS#8 密钥,在 OpenSSL 3 中只有启用 legacy 提供程序才能加载。不启用时,openssl pkey 会输出 error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported,后面跟着它无法获取的算法(DES-CBC,PBES1 则是 PBKDF1),Node.js 则报告 error:0308010C:digital envelope routines::unsupported(ERR_OSSL_EVP_UNSUPPORTED)。
  • Python(cryptography 46):load_pem_private_key 对 PKCS#1 和 PKCS#8 都能读取;对于没有提供密码的加密密钥,会抛出 TypeError: Password was not given but private key is encrypted。
  • WebCrypto:用 PKCS#1 字节调用 importKey("pkcs8", …) 时,Promise 会以 DataError 被拒绝;Node.js 26 的措辞是 Invalid keyData,各浏览器则使用各自的文本。

错误目录中每种错误都有单独的页面,链接见下文。

公钥的两种形式

公钥同样有两种形态。PKCS#1 RSAPublicKey(BEGIN RSA PUBLIC KEY)只包含模数和指数;SubjectPublicKeyInfo(BEGIN PUBLIC KEY,RFC 5280)用算法标识符包裹密钥,适用于所有算法,证书、JWK 转换器、Java 的 X509EncodedKeySpec、Go 的 ParsePKIXPublicKey 以及 WebCrypto 的 spki 导入需要的都是这种格式。openssl rsa -pubin -in key.pem -RSAPublicKey_out 和 openssl pkey -pubin -in key.pem -pubout 可以在两者之间转换。混淆是双向的:写出 SPKI 的工具常常把文件命名成 id_rsa.pub 的样子,而 OpenSSH 自己的单行公钥(ssh-rsa AAAA…)又是另一种格式,可以用 ssh-keygen -e -m PKCS8 转换。

如何选择

  • 新密钥,任意算法:PKCS#8(在磁盘上加密;放在本身已加密的机密管理器中则不加密)。
  • Java、Go、.NET、WebCrypto、云 KMS 导入:PKCS#8(DER 或 PEM)。
  • 只能读取 RSA PRIVATE KEY 的旧软件(某些专用设备、较旧的 Ruby 和 PHP 代码、旧版 SSH 工具):用 -traditional 生成 PKCS#1,最好只在交付环节这样做。
  • 用于 Windows、macOS 或 Java 密钥库的密钥加证书:PKCS#12,其内部包含 PKCS#8 密钥。
  • SSH 密钥:ssh 本身用 OpenSSH 格式;其他工具需要同一个密钥时用 PKCS#8。

迁移说明

  1. 找出 PKCS#1 和 SEC 1 文件:在配置仓库和机密存储中执行 grep -l "BEGIN RSA PRIVATE KEY\|BEGIN EC PRIVATE KEY"。
  2. 一次性转换:使用 openssl pkcs8 -topk8 -nocrypt(要加密则改用 -v2 aes-256-cbc),并更新各个使用方;密钥材料和对应的证书都不会改变。
  3. 重新加密旧的加密密钥:传统 PEM 加密(Proc-Type 和 DEK-Info 头部)只用一次 MD5 迭代来派生密钥,而用 DES 或 RC2 加密的密钥无论采用哪种格式,都需要 OpenSSL 的 legacy 提供程序;改用 PBES2 和 AES-256 重新加密。
  4. 把代码改为使用通用解析函数(ParsePKCS8PrivateKey、load_pem_private_key、createPrivateKey),这样包括 Ed25519 和 ML-DSA 在内的新算法无需修改代码就能使用。
  5. 验证:用转换后的密钥签名,再用现有证书验证签名;密钥生成器和 ASN.1 查看器可以显示结构,不会上传任何内容。

参考资料

全部对比