许多“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 RSAPrivateKey | PKCS#8 PrivateKeyInfo / OneAsymmetricKey | |
|---|---|---|
| 规范 | RFC 8017 附录 A.1.2 | RFC 5208、RFC 5958 |
| PEM 标签 | RSA PRIVATE KEY | PRIVATE 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 | 不接受 | 接受 |
| Go | x509.ParsePKCS1PrivateKey | x509.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 运行,展示默认格式以及一次无损的往返转换。
#!/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 运行它。
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
网上较早的回答引用的是 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(
cryptography46):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。
迁移说明
- 找出 PKCS#1 和 SEC 1 文件:在配置仓库和机密存储中执行
grep -l "BEGIN RSA PRIVATE KEY\|BEGIN EC PRIVATE KEY"。 - 一次性转换:使用
openssl pkcs8 -topk8 -nocrypt(要加密则改用-v2 aes-256-cbc),并更新各个使用方;密钥材料和对应的证书都不会改变。 - 重新加密旧的加密密钥:传统 PEM 加密(
Proc-Type和DEK-Info头部)只用一次 MD5 迭代来派生密钥,而用 DES 或 RC2 加密的密钥无论采用哪种格式,都需要 OpenSSL 的 legacy 提供程序;改用 PBES2 和 AES-256 重新加密。 - 把代码改为使用通用解析函数(
ParsePKCS8PrivateKey、load_pem_private_key、createPrivateKey),这样包括 Ed25519 和 ML-DSA 在内的新算法无需修改代码就能使用。 - 验证:用转换后的密钥签名,再用现有证书验证签名;密钥生成器和 ASN.1 查看器可以显示结构,不会上传任何内容。