旧密钥格式解析:PEM、DER、PKCS#12 的结构与读取

密钥格式这东西,说白了就是密钥的“身份证”。新 SDK 不认识老身份证,你就得帮它翻译。这一章我带你看看最常见的那几张“身份证”——PEM、DER 和 PKCS#12。

我个人习惯,不管接什么项目,第一件事就是先搞清楚密钥的包装格式。不然解析到一半发现是乱码,吭哧半天才发现格式选错了,你说冤不冤?

三种主要格式的底层结构

这三种格式,实际上是在不同“包装纸”里装同一块肉——公私钥对。区别在于包装纸的材质、有没有商标,以及能不能自带密码。

格式 存储方式 是否加密 常见后缀 场景
PEM Base64 文本 可选(可加口令) .pem, .crt, .key, .cer Web 服务器、开源项目
DER 二进制 否 .der, .cer Java 默认格式、嵌入式
PKCS#12 二进制容器 是 .p12, .pfx Windows、浏览器导出、Apple
小提示:判断一个文件是不是 PEM 格式,用文本编辑器打开,第一行必须是 -----BEGIN XXX-----。没有这个头,基本可以确定是 DER 或 P12。

PEM 格式:最常见的“明信片”

PEM 其实就是在 Base64 编码的密钥数据外面加了一圈“信封”——头部和尾部标记。结构超简单:

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDC1tP...
...(一堆 Base64 字符)...
wE4nL2jxK3iN1Q==
-----END PRIVATE KEY-----

嗯,这里要注意:PEM 头部的类型决定了密钥的内容。常见的类型有:

  • BEGIN RSA PRIVATE KEY —— 只含 RSA 私钥(PKCS#1 格式)
  • BEGIN PRIVATE KEY —— 通用私钥(PKCS#8 格式)
  • BEGIN CERTIFICATE —— X.509 证书
  • BEGIN ENCRYPTED PRIVATE KEY —— 加密后的私钥

避坑指南:我曾经遇到一个合作方给的是 BEGIN RSA PRIVATE KEY,但新 SDK 只认 BEGIN PRIVATE KEY(PKCS#8)。直接传过去就报错。怎么办呢?用 OpenSSL 转一下:openssl pkcs8 -topk8 -in old.key -out new.key。

DER 格式:二进制裸数据

DER 是 PEM 的“亲弟弟”——内容完全一样,只是去掉了 Base64 编码和头尾标记。它直接以 ASN.1 的二进制形式存储。

为什么要用 DER?因为有些系统(比如 Java 的 KeyStore)只认二进制,不认文本。DER 的好处是解析快,体积比 PEM 小 30% 左右。但坏处也很明显——你拿着 DER 文件,用文本编辑器打开就是一坨乱码。

注意:千万不要用 .pem 后缀给 DER 文件,反之亦然。否则新 SDK 读的时候会解析失败,我见过太多人在这上面浪费时间了。

Python 里如何区分两者?很简单:

def detect_format(file_path):
    with open(file_path, 'rb') as f:
        header = f.read(20)
    # PEM 文件以 ASCII 的 "-----BEGIN" 开头
    if b'-----BEGIN' in header:
        return 'PEM'
    # DER 文件前几字节是 ASN.1 的 SEQUENCE 标记 (0x30)
    if header[0] == 0x30:
        return 'DER'
    return 'unknown'

PKCS#12 格式:带密码的保险柜

PKCS#12(通常存为 .p12 或 .pfx)是一个容器格式。它可以把私钥、公钥、证书链甚至根证书全部打包成一个文件。最关键的是,它支持密码保护。

这玩意儿在 Windows 和浏览器里特别常见。你从 Chrome 导出证书,默认就是 PKCS#12 格式。

它的内部结构其实是一个层级化的数据包:

  • AuthSafe:最外层,包含所有受保护的数据
  • MacData:完整性校验用的 MAC 值
  • SafeContents:内部包含多个 SafeBag
  • SafeBag:每个包里可以放私钥、证书或其他数据

解析时你需要提供口令(passphrase),否则只能看到密文,拿不到密钥。我在项目中遇到过一些旧版本 OpenSSL 生成的 P12 文件,密码明明正确但就是打不开。后来发现是加密算法问题——默认用 RC2-40 或 3DES,而新 SDK 只支持 AES。解决方法:

# 旧 -> 新:把加密算法升级到 AES-256
openssl pkcs12 -in old.p12 -out temp.pem -nodes
openssl pkcs12 -export -in temp.pem -out new.p12 -passout pass:yourpass

Python 读取:三板斧

不管是哪种格式,Python 里用 cryptography 或 pyOpenSSL 都能搞定。我个人推荐 cryptography,接口更现代,坑也少。

读取 PEM 私钥

from cryptography.hazmat.primitives import serialization

with open("private_key.pem", "rb") as f:
    private_key = serialization.load_pem_private_key(
        f.read(),
        password=None,  # 如果没加密就是 None
    )

读取 DER 证书

from cryptography import x509

with open("cert.der", "rb") as f:
    cert = x509.load_der_x509_certificate(f.read())

读取 PKCS#12 容器

from cryptography.hazmat.primitives.serialization import pkcs12

with open("keystore.p12", "rb") as f:
    private_key, certificate, additional_certs = pkcs12.load_key_and_certificates(
        f.read(),
        b"your_password"
    )
一个小心得:不确定密钥文件格式的时候,先用 detect_format() 判断一下,再决定用哪个加载函数。这样你的适配代码就不会在文件格式上“翻车”了。

本章知识体系

下面这张图把三种格式的关系和转换路径画清楚了,你可以照着走一遍:

密钥格式解析与转换流程 密钥文件 (.pem / .der / .p12) 格式检测 detect_format() PEM 文本解析 serialization DER 二进制解析 x509 PKCS#12 容器解析 pkcs12 密码解密 Passphrase 内存密钥对象 private_key / cert 📌 秘诀: 第一步先 detect, 再选择对应加载器。

看懂这张图,密钥格式解析的核心流程就抓住了。从文件到内存对象,中间就三步:识别格式 -> 选择解析器 -> 解析(可能需要解密)。剩下的就是具体 API 调用了。