RSA 密钥格式认不出来:PKCS#1、PKCS#8、SPKI 到底差在哪

· 约 6 分钟 🗝️ RSA 加解密

把一个 RSA 私钥从一个系统搬到另一个系统,最常见的失败不是权限也不是编码,而是格式不对。而这些格式看起来都长一个样:一段 -----BEGIN 开头的 Base64

五种 PEM 头,五种不同的东西

BEGIN 行格式装的是带算法标识
BEGIN RSA PRIVATE KEYPKCS#1RSA 私钥
BEGIN PRIVATE KEYPKCS#8任意算法私钥✅ 是
BEGIN ENCRYPTED PRIVATE KEYPKCS#8(加密)口令保护的私钥✅ 是
BEGIN PUBLIC KEYSPKI(X.509)任意算法公钥✅ 是
BEGIN RSA PUBLIC KEYPKCS#1RSA 公钥

核心区别在最后一列。

PKCS#1 是 OpenSSL 的传统格式,里面只有 RSA 的那几个大数——模数 n、公开指数 e,私钥还有 d、p、q、dp、dq、qinv。不带算法标识,因为格式本身就写死了是 RSA。

PKCS#8 和 SPKI 在外面包了一层,先声明「这是一把 rsaEncryption 算法的密钥」,再放实际数据。多这一层的意义是通用:同一个容器格式可以装 RSA、EC、Ed25519,解析方读了算法标识才知道怎么往下解。

不看 BEGIN 行怎么认

有时候你拿到的是一段裸 Base64,没有头尾行。这时候看正文里有没有 BgkqhkiG9w0BAQE

这一段是 rsaEncryption 的算法 OID(1.2.840.113549.1.1.1)在 DER 里的编码,经 Base64 之后的片段。逻辑很直接:

  • PKCS#8 和 SPKI 带算法标识 → 一定包含它
  • PKCS#1 不带 → 一定不包含

这条判据和密钥长度无关,比背前缀可靠。

如果想快速目测,2048 位密钥的常见开头是:

PKCS#8 私钥   MIIEvAIBADANBgkqhkiG9w0BAQEFAASC
PKCS#1 私钥   MIIEpAIBAAKCAQEA
SPKI   公钥   MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
PKCS#1 公钥   MIIBCgKCAQEA

前缀会随密钥长度变——4096 位的 PKCS#1 私钥开头是 MIIJKAIBAAKCAg,1024 位是 MIICWwIBAAKBgQ。所以优先用 OID 判据。

OpenSSL 3.0 改了默认格式

这一条值得单独拎出来,因为它让大量老教程失效了。

老教程说 openssl genrsa 产出 PKCS#1,这在 OpenSSL 1.x 上是对的。从 3.0 开始默认输出变成了 PKCS#8

$ openssl version
OpenSSL 3.6.3

$ openssl genrsa -out k.pem 2048 && head -1 k.pem
-----BEGIN PRIVATE KEY-----          # PKCS#8

$ openssl genrsa -traditional -out t.pem 2048 && head -1 t.pem
-----BEGIN RSA PRIVATE KEY-----      # PKCS#1

要拿回 PKCS#1 得显式加 -traditional

为什么这个变化很容易咬人:本地开发机是新版 OpenSSL、服务器上是旧版(或者反过来),同一条命令产出的格式不同,而对接方的解析代码只认其中一种。CI 里跑得好好的脚本换台机器就崩。

建议:不要假设命令的输出格式,生成完直接 head -1 看一眼。

Java 的 InvalidKeySpecException

这是这一整类问题里最高频的一个报错。

Java 标准库只支持 PKCS#8 私钥和 SPKI 公钥,不认 PKCS#1。把 BEGIN RSA PRIVATE KEY 的内容塞进 PKCS8EncodedKeySpec 必然抛 InvalidKeySpecException。这是长期存在的限制,不是 bug。

两条路:

# ① 转格式(推荐)
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem

② 引入 BouncyCastle,它的 PEMParser 两种都认——但为了读一个密钥引入整个 BC 依赖,多数情况下不划算。

另外两个高频错因:

  • 没剥掉 PEM 的头尾行和换行就直接 Base64 解码。-----BEGIN... / -----END... 两行和中间的所有换行都要先去掉
  • 私钥是加密的BEGIN ENCRYPTED PRIVATE KEY),得先用口令解密。PKCS8EncodedKeySpec 不会替你做这件事

各语言的支持情况

语言 / 库PKCS#1 私钥PKCS#8 私钥备注
Java 标准库只有 PKCS8EncodedKeySpec
Node.js crypto两种都自动识别
Python cryptographyload_pem_private_key 通吃
Go 标准库两个不同函数,用错报错
PHP openssl 扩展底层就是 OpenSSL
.NET.NET Core 3.0 之后

Go 的情况值得注意:x509.ParsePKCS1PrivateKeyx509.ParsePKCS8PrivateKey 是两个函数,格式和函数对不上就直接报错,不会自动回退。健壮的做法是先试一个,失败再试另一个。

常用转换命令

# 私钥 PKCS#1 → PKCS#8
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem

# 私钥 PKCS#8 → PKCS#1(3.0 之后必须加 -traditional)
openssl rsa -in pkcs8.pem -traditional -out pkcs1.pem

# 从私钥导出公钥(SPKI 格式)
openssl rsa -in private.pem -pubout -out public.pem

# 从私钥导出公钥(PKCS#1 格式)
openssl rsa -in private.pem -RSAPublicKey_out -out pub1.pem

# 解密加密的私钥
openssl pkcs8 -in encrypted.pem -out plain.pem

# 给私钥加口令保护
openssl pkcs8 -topk8 -in plain.pem -out encrypted.pem

# PEM ↔ DER
openssl rsa -in k.pem -outform DER -out k.der
openssl rsa -in k.der -inform DER -out k.pem

# 从证书里提取公钥
openssl x509 -in cert.pem -pubkey -noout > pub.pem

# 看密钥的实际内容(位数、模数、指数)
openssl rsa -in k.pem -text -noout

最后一条在排查时很有用——能被 -text 正常解析出来,才说明密钥内容是完好的,光看 BEGIN 行对不对不够。

PEM 和 DER:同一个东西的两种写法

DER 是 ASN.1 的确定性二进制编码,直接看是一堆不可读的字节。

PEM 把 DER 做 Base64,每 64 字符换行,前后加 BEGIN / END 标记。存在的理由很实际:二进制没法贴进邮件、YAML 配置和网页表单。

后缀完全不可靠。 .pem.key.crt.cer.der 没有强制约定——.cer 里可能是 PEM 也可能是 DER,.key 里可能是 PKCS#1 也可能是 PKCS#8。

判断只看内容:

  • 文本,以 -----BEGIN 开头 → PEM
  • 二进制,第一个字节是 0x30DER0x30 是 ASN.1 SEQUENCE 的标签)

私钥能导出公钥,反过来不行

PKCS#1 和 PKCS#8 的私钥结构里本来就完整包含模数 n 和公开指数 e。公钥就是 (n, e) 这一对,从私钥里直接读出来即可,不需要任何计算——所以导出是瞬时的。

反过来从公钥推私钥,需要把 n 分解成两个大素数。这件事做不到,正是 RSA 成立的前提。

如果你手上只有公钥却需要解密或签名,那说明这个密钥对不是给你用的,该去找持有私钥的一方。

用工具快速识别和验证

RSA 加解密的密钥输入框对上面几种格式都做了兼容:PKCS#1、PKCS#8、SPKI、X.509 证书,甚至没有 BEGIN 行的裸 Base64 都能识别。粘贴进去就能看到解析出的密钥位数——这是判断「密钥到底完不完好」最快的办法。

工具也能直接生成密钥对,加解密和签名验签全在浏览器本地完成,私钥不会离开你的设备。

转换完成后建议做一次往返验证:用新格式的密钥加密一段短文本再解密回来。格式看着对但内容在转换中损坏的情况是存在的,只有往返能验出来。

密钥格式弄对之后,下一个常撞上的限制是明文长度,见 RSA 到底能加密多长的数据

❓ 常见问题

BEGIN RSA PRIVATE KEY 和 BEGIN PRIVATE KEY 有什么区别?

多一个 RSA 就是另一种格式,不能互相直接替换BEGIN RSA PRIVATE KEYPKCS#1,也叫 OpenSSL 传统格式。它里面只有 RSA 的那几个数(模数、公私指数、两个素数等),不带算法标识——因为格式本身就写死了是 RSA。BEGIN PRIVATE KEYPKCS#8,它在外面包了一层,先声明「这是一把 rsaEncryption 算法的私钥」,再放实际的密钥数据。为什么要多这一层:PKCS#8 是通用容器,同一个格式可以装 RSA、EC、Ed25519 等各种密钥,解析方读了算法标识才知道该怎么解。实际影响:Java 的 PKCS8EncodedKeySpec 只吃 PKCS#8,塞一个 PKCS#1 进去就抛 InvalidKeySpecException;Node 的 crypto 两种都认;Python 的 cryptography 两种都认;Go 的标准库有两个不同函数 ParsePKCS1PrivateKeyParsePKCS8PrivateKey,用错就报错。

不看 BEGIN 那行,怎么判断一个 Base64 密钥是什么格式?

看 Base64 正文里有没有 BgkqhkiG9w0BAQE 这一段。它是 rsaEncryption 算法 OID(1.2.840.113549.1.1.1)在 DER 里的编码经 Base64 之后的片段。PKCS#8 和 SPKI 带算法标识,所以一定包含它;PKCS#1 不带,所以一定不包含。这条判据和密钥长度无关,比记具体前缀可靠。如果要更快地目测,2048 位密钥的常见开头是:PKCS#8 私钥 MIIEvAIBADANBgkqhkiG9w0BAQEFAASC,PKCS#1 私钥 MIIEpAIBAAKCAQEA,SPKI 公钥 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A,PKCS#1 公钥 MIIBCgKCAQEA注意前缀会随密钥长度变:4096 位的 PKCS#1 私钥开头是 MIIJKAIBAAKCAg,1024 位是 MIICWwIBAAKBgQ。所以优先用上面那条 OID 判据。

OpenSSL 生成的私钥为什么和教程里说的不一样?

因为 OpenSSL 3.0 改了 genrsa 的默认输出格式。老教程里说 openssl genrsa 产出 BEGIN RSA PRIVATE KEY(PKCS#1),这在 OpenSSL 1.x 上是对的;从 3.0 开始默认输出变成了 PKCS#8,也就是 BEGIN PRIVATE KEY。实测 OpenSSL 3.6 上 openssl genrsa -out k.pem 2048 出来的就是 PKCS#8。要拿回 PKCS#1 得显式加 -traditionalopenssl genrsa -traditional -out k.pem 2048这个变化很容易咬人:本地开发机是新版 OpenSSL、服务器上是旧版,或者反过来,同一条命令产出的格式不同,而对接方的解析代码只认其中一种。排查时的建议:不要假设命令的输出格式,生成完直接 head -1 看一眼 BEGIN 行。

Java 报 InvalidKeySpecException 怎么解决?

九成是把 PKCS#1 私钥塞进了 PKCS8EncodedKeySpec。Java 标准库只支持 PKCS#8 私钥和 SPKI 公钥,不认 PKCS#1——这是个长期存在的限制,不是 bug。两条解决路径:(1) 转格式(推荐)——用 openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem 把私钥转成 PKCS#8,Java 就能直接读;(2) 引入 BouncyCastle——它的 PEMParser 两种格式都认,但为了读一个密钥引入整个 BC 依赖,多数情况下不划算。另外两个高频错因:(1) 没去掉 PEM 的头尾行和换行就直接 Base64 解码——-----BEGIN... 那两行和中间的换行都要先剥掉;(2) 私钥是加密的BEGIN ENCRYPTED PRIVATE KEY),得先用口令解密,PKCS8EncodedKeySpec 不会替你做这件事。

PEM 和 DER 是什么关系?.key .pem .der .cer 这些后缀有区别吗?

DER 是二进制编码,PEM 是它的 Base64 文本包装,内容完全等价DER 是 ASN.1 的一种确定性二进制编码规则,直接看是一堆不可读的字节。PEM 把 DER 做 Base64,每 64 个字符换一行,前后加 -----BEGIN xxx----- / -----END xxx-----。之所以有 PEM,是因为二进制没法贴进邮件、配置文件和网页。后缀完全不可靠.pem.key.crt.cer.der 这些扩展名没有强制约定,.cer 里装的可能是 PEM 也可能是 DER,.key 里可能是 PKCS#1 也可能是 PKCS#8。唯一可靠的判断是看内容:文本、以 -----BEGIN 开头就是 PEM;二进制、以字节 0x30 开头就是 DER。转换命令openssl rsa -in k.pem -outform DER -out k.deropenssl rsa -in k.der -inform DER -out k.pem

我只有私钥,能推出公钥吗?反过来行吗?

私钥能推出公钥,公钥推不出私钥——后者要是能做到,RSA 就不成立了。为什么私钥能推公钥:PKCS#1 和 PKCS#8 的私钥结构里本来就完整包含模数 n 和公开指数 e(还包含 d、p、q、dp、dq、qinv 这些私有部分)。公钥就是 (n, e) 这一对,从私钥里直接读出来即可,不需要任何计算。命令openssl rsa -in private.pem -pubout -out public.pem,产出的是 SPKI 格式;要 PKCS#1 格式的公钥用 -RSAPublicKey_out实用场景:对接时经常出现「我只有一个 .key 文件,对方要公钥」,直接导出即可。反过来的诉求通常是场景搞错了——如果你只有公钥却需要解密或签名,那说明这个密钥对不是给你用的,该去找持有私钥的一方。

🗝️ 打开 RSA 加解密 公钥加密/私钥解密·PKCS1v1.5 与 OAEP·SHA 签名验签·PEM 密钥对生成·本地运行不上传

📖 同一工具的其他教程

🔗 相关阅读

全部教程 →