PEM 编解码 API 已在 JDK 28 转正。inside.java(2026-10-06)宣布 JEP 542 targeted to JDK 28。JEP 页当前 Status 为 Completed、Release 为 28,History 写明相对 JDK 27 第三轮预览「without further change」定稿。JDK 28 Early-Access 的 API 文档里,PEMEncoder 已标 Since 28,不再带 preview 标注。

为什么平台终于收了 PEM

RFC 7468 定义的 Privacy-Enhanced Mail 文本格式,早就成了证书链、OpenSSL 工具链、OpenSSH 密钥、硬件认证设备(JEP 举例提到 YubiKey)导入导出的常见载体。邮件场景只是历史起点;运维与安全工程里更常把它当「可复制粘贴的密钥与证书运输格式」。一段 PEM 就是 Base64 包着的二进制,外面再套 BEGIN / END 和类型标签。平台侧其实早就能拿到密钥、证书、CRL 的二进制编码,也有 Base64;缺的是把「拆 header、认类型、选工厂、处理加密私钥」串成一条官方路径。

2022 年 Java Cryptographic Extensions Survey 把这件事标成痛点。JEP 动机里写得很直白:编码公钥还算机械;解码要自己解析文本、判断该用哪家 KeyFactory / CertificateFactory、再认出算法;加密私钥往往要写十几行。JEP 542 把这条路径收进 java.security,目标就是短 API、对齐 PKCS#8 / X.509 / PKCS#8 v2.0 这些已有二进制标准。

三轮预览到转正

完整路径是:JDK 25 JEP 470 首预览 → JDK 26 JEP 524 第二预览(有小改)→ JDK 27 JEP 538 第三预览(再有小改)→ JDK 28 JEP 542 定稿。JEP 542 的 History 明确写:在第三轮预览之后,finalize the API without further change。

第三轮相对第二轮的主要调整(见 JEP 538 History)包括:PEM 从 record 改成普通类,并增加接受 byte[] Base64 内容的构造;DEREncodable 改名为 BinaryEncodable;EncryptedPrivateKeyInfo 增加 getKeyPair,部分 getKey / getKeyPair 签名收成只接受 Key;PEMDecoder.withFactory 改名为 withFactoriesOf;新增运行时异常 CryptoException。这些改动已经落在 JDK 27 preview 上。JEP 542 相对 JEP 538,公开 Description 里的类型与方法签名对照一致:没有再改名,也没有再增减公开方法。可以把「零变化」理解成第三轮预览接口原样转正。

如何理解 Status「Completed」和 inside.java 的「targeted」:JEP 页记录的是特性交付状态(Completed,Release 28);短讯是发布流程上的 target 公告。两者不矛盾。JDK 28 EA release notes 已把 JEP 542 列进 Notable Features;EA API 文档可直接对照。

API 落在哪、能编什么

新类型都在 java.security:

  • BinaryEncodable:密封空接口,统一「能转成标准二进制编码」的对象,供 Encoder/Decoder 处理。apparent permitted 包括 AsymmetricKey、KeyPair、PKCS8EncodedKeySpec、X509EncodedKeySpec、EncryptedPrivateKeyInfo、X509Certificate、X509CRL、PEM。另有非公开 permitted 类型,因此对 BinaryEncodable 做 switch 时仍须保留 default 或 case BinaryEncodable,以免将来扩展时炸 MatchException。
  • PEMEncoder / PEMDecoder:不可变、线程安全、可复用。设计刻意靠近 Base64 的 Encoder/Decoder 与 HexFormat 那类「先拿实例再反复用」的风格。
  • PEM:平台没有对应 Java 类型时的兜底(例如 PKCS#10 证书请求)。也能保留 PEM header 之前的 leading data;需要那段前缀时,显式 decode(..., PEM.class)。

标准覆盖面按 JEP Goals:PKCS#8 私钥、X.509 公钥、证书、CRL,以及 PKCS#8 v2.0 加密私钥(含可同时携带公私钥的 OneAsymmetricKey)。EA 的 PEMEncoder 文档把类型到 PEM header 的对应写清楚了,例如 PrivateKey → PRIVATE KEY,配置了加密后变成 ENCRYPTED PRIVATE KEY;X509Certificate → CERTIFICATE;X509CRL → X509 CRL。KeyPair 按 OneAsymmetricKey 编成 PRIVATE KEY。

解码侧还有两个配置点:withDecryption(char[]) 绑定口令后,加密私钥会直接解成 PrivateKey(仍可解码未加密对象);不绑口令时,加密文本会落成 EncryptedPrivateKeyInfo,再自行 getKey / getKeyPair。withFactoriesOf(Provider) 则指定用哪家 Provider 的 KeyFactory / CertificateFactory 来造对象。

以前手工解析,现在一行解码

以前读一段 EC 公钥 PEM,大致要拆 header/footer、清空白、Base64 解码,再交给 KeyFactory:

String pem = """
    -----BEGIN PUBLIC KEY-----
    MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
    cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
    -----END PUBLIC KEY-----
    """;

String b64 = pem.replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");
byte[] der = Base64.getDecoder().decode(b64);
PublicKey key = KeyFactory.getInstance("EC")
    .generatePublic(new X509EncodedKeySpec(der));

JDK 28(以及接口相同的 JDK 27 preview)上,同样的公钥可以直接:

ECPublicKey key = PEMDecoder.of().decode(pem, ECPublicKey.class);

不知道确切子类型时,用模式匹配:

switch (PEMDecoder.of().decode(pem)) {
    case PublicKey pub -> { /* 验签、写入信任材料等 */ }
    case PrivateKey priv -> { /* 签名、握手本端身份等 */ }
    case X509Certificate cert -> { /* 校验证书链节点 */ }
    case X509CRL crl -> { /* 吊销列表 */ }
    default -> throw new IllegalArgumentException("unexpected PEM type");
}

编码对称:PEMEncoder.of().encodeToString(key) 得到字符串;encode(...) 得到 ISO-8859-1 字节数组。从 InputStream 解码也有对应重载。输入里 header 之前的杂散文本在解码成具体密钥/证书类型时会被忽略;若你要那段前缀,解码成 PEM 再读 leadingData()。

加密私钥读写

withEncryption / withDecryption 把口令绑到新的不可变编解码器上。默认 PBE 算法由安全属性 jdk.epkcs8.defaultAlgorithm 给出,当前默认是 PBEWithHmacSHA256AndAES_128。算法名与参数会写进 PEM 文本,以后默认值变更也不影响已生成密文的解密。

char[] password = "changeit".toCharArray();
try {
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
    kpg.initialize(256);
    PrivateKey privateKey = kpg.generateKeyPair().getPrivate();

    String encryptedPem = PEMEncoder.of()
            .withEncryption(password)
            .encodeToString(privateKey);

    PrivateKey restored = PEMDecoder.of()
            .withDecryption(password)
            .decode(encryptedPem, PrivateKey.class);

    // 不带口令解码时拿到 EncryptedPrivateKeyInfo,再自行 getKey
    EncryptedPrivateKeyInfo epki =
            PEMDecoder.of().decode(encryptedPem, EncryptedPrivateKeyInfo.class);
    PrivateKey also = epki.getKey(password);
} finally {
    Arrays.fill(password, '\0');
}

需要非默认算法、自定义 AlgorithmParameterSpec 或指定 Provider 时,先走 EncryptedPrivateKeyInfo.encrypt(...),再把结果交给 PEMEncoder。仅配置了加密的 Encoder 只能编码 PrivateKey、KeyPair、PKCS8EncodedKeySpec;其它 BinaryEncodable 会抛 IllegalArgumentException。解密失败抛 CryptoException;PEM 文本本身解析失败则是 IllegalArgumentException。从流读取时按 ISO-8859-1 解释字节。

示例怎么跑,以及和 Spring Boot 的关系

本文示例按 JDK 28 Early-Access 编写:API 已是正式特性,不需要 --enable-preview。可从 jdk.java.net/28 取 EA 构建;EA 文档(例如 build 18 的 PEMEncoder)与 release notes 均已收录 JEP 542。类在 java.base 的 java.security 包,无需额外模块。把口令放进 char[]、用完 Arrays.fill 清掉,和操作其它敏感材料时一样。

若环境暂时只有 JDK 27,应对照 JEP 538 开 preview:编译 javac --release 27 --enable-preview ...,运行 java --enable-preview ...。第三预览与定稿接口一致,示例可共用;差别只在是否要开 preview 开关。从 preview 升到 28 时,按 JEP 542「without further change」的口径,应用代码一般不用改 import 或方法名,只要拿掉 --enable-preview 并确认运行在 28+。

Spring Boot 一侧,spring.ssl.bundle.pem 本来就会加载 PEM 证书与私钥(配置项里也有 private-key-password 这类口令字段)。那是框架自己的 PEM 加载路径,和 JEP 542 没有文档上的直接对接关系;本文不把两者写成已经打通的集成。应用代码若自己解析 PEM 文件、拼 KeyStore 或做密钥轮换,JDK 28 起可以改用平台 API,少维护一套 header 拆分与 KeyFactory 分支。

什么时候仍要手写,什么时候交给 PEM 类

多数「文件里就是一段标准 PEM」的场景,优先走 PEMDecoder / PEMEncoder。证书链若是多段 CERTIFICATE 拼在同一文件里,需要按段循环解码(每次解码消费一段 header/footer 之间的内容;从流读时也是按 PEM 对象推进)。JEP 文本本身以单个 BinaryEncodable 为单元,没有单独的「证书链容器」类型;链的组装仍是应用层的事,只是每一环的解析变短了。

平台没有对应类型的 PEM(常见例子是 PKCS#10 证书请求)会落成 PEM 对象。这时可以读 type()、content(),或 decode() 拿到 Base64 解开后的原始字节,再交给第三方库或自有 ASN.1 解析。PEMEncoder 编码 PEM 时只按 type() 包 header/footer,不校验内容是否合法,适合「原样搬运」未知类型。

第三方 Provider 场景下,用 withFactoriesOf(provider) 固定工厂来源,避免默认 Provider 造不出某种算法密钥。加密路径若默认 PBE 不够用,不要硬拧 withEncryption;改走 EncryptedPrivateKeyInfo.encrypt(key, password, algorithm, params, provider)(或带 Key / SecureRandom 的重载),再 PEMEncoder.of().encode(epki)。

测试面(JEP Testing 一节)覆盖了:所有支持的 BinaryEncodable 往返、RSA / EC / ML-KEM / EdDSA 等对象、与第三方工具互相读写、以及坏 PEM 的负向用例。应用侧若要做互操作抽检,用 OpenSSL 生成一段再让 JDK 解码、或反过来,是成本很低的烟雾测试。

参考链接

— 感谢阅读 —

一起交流

分享你的思考,让讨论更进一步。