PAYATHON 2026

Kotlin 中如何将 PEM 公钥还原为 AsymmetricKeyParameter?

数据小陈

结论

PemReader.readPemObject() 返回的是 PemObject,无法将其强制转换为 AsymmetricKeyParameter。对于 -----BEGIN PUBLIC KEY----- 格式的公钥,应使用 PublicKeyFactory.createKey(...) 解析 pemObject.content。

写入时,先用 SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(key) 将公钥参数转换为 SubjectPublicKeyInfo,再交给 JcaPEMWriter 写出。

示例使用的是 Ed25519KeyPairGenerator,实际处理的是 Ed25519 公钥,原标题中的 RSA 与代码不一致。

为什么强制转换失败

原代码中的这一行触发了 IDE 提示:

return pemObject as AsymmetricKeyParameter

PemReader 读取 PEM 文本后,得到的 PemObject 包含类型标签、可选头信息和 Base64 解码后的字节。这些字节还没有被解析成密码学密钥对象。

对于这里使用的公钥格式,转换过程是:

PEM 文本
  → PemObject
  → content 中的 SubjectPublicKeyInfo DER 字节
  → PublicKeyFactory.createKey(...)
  → AsymmetricKeyParameter

as 只能转换兼容的对象类型,不会执行 ASN.1 解码或创建公钥对象。因此,改用 as? 也不能解决问题。

正确的编码与解析代码

以下代码沿用原问题中的 Bouncy Castle 轻量级密钥 API,以 SubjectPublicKeyInfo 作为公钥的编码结构:

import java.io.StringReader
import java.io.StringWriter
import java.security.SecureRandom
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
import org.bouncycastle.crypto.params.AsymmetricKeyParameter
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters
import org.bouncycastle.crypto.util.PublicKeyFactory
import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemReader

fun generateKeyPair(): AsymmetricCipherKeyPair {
    val random = SecureRandom()
    val keyGen = Ed25519KeyPairGenerator()
    keyGen.init(Ed25519KeyGenerationParameters(random))
    return keyGen.generateKeyPair()
}

fun keyToString(key: AsymmetricKeyParameter): String {
    require(!key.isPrivate) { "Expected a public key" }

    val publicKeyInfo =
        SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(key)

    val stringWriter = StringWriter()
    JcaPEMWriter(stringWriter).use { writer ->
        writer.writeObject(publicKeyInfo)
    }
    return stringWriter.toString()
}

fun stringToKey(encodedKey: String): AsymmetricKeyParameter =
    PemReader(StringReader(encodedKey)).use { reader ->
        val pemObject = reader.readPemObject()
            ?: throw IllegalArgumentException("No PEM object found")

        require(pemObject.type == "PUBLIC KEY") {
            "Expected PUBLIC KEY (SubjectPublicKeyInfo)"
        }

        PublicKeyFactory.createKey(pemObject.content)
    }

写入和读取时需要注意:

  • 写出时向 JcaPEMWriter 传入 SubjectPublicKeyInfo。不要假设它能直接识别轻量级 API 的所有 AsymmetricKeyParameter 实现。
  • 读取时,PublicKeyFactory.createKey(...) 解析 SubjectPublicKeyInfo 中的算法标识和公钥数据,返回对应的密钥参数对象。

use 会关闭 reader 和 writer,也能在发生异常时释放资源。

如何检查公钥是否正确还原

对于题目中的 Ed25519 密钥,可以比较转换前后的原始公钥字节:

import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters

fun checkRoundTrip() {
    val original = generateKeyPair().public as Ed25519PublicKeyParameters
    val pem = keyToString(original)
    val restored = stringToKey(pem)

    check(restored is Ed25519PublicKeyParameters)
    check(original.encoded.contentEquals(restored.encoded))
}

类型检查发生在密钥解析之后,此时 restored 已经是实际的密钥参数对象。check 成功后,Kotlin 可以将其智能转换为 Ed25519PublicKeyParameters。

注意 PEM 标签与编码结构

上述读取函数针对以下格式:

-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----

其内容是 SubjectPublicKeyInfo 的 DER 编码,适用于相应算法受当前 Bouncy Castle 版本支持的公钥。

如果需要处理 RSA,应先确认输入格式。RSA 公钥可能使用 PUBLIC KEY,也可能使用 RSA PUBLIC KEY。后者通常承载 PKCS#1 RSAPublicKey 结构,不能直接套用上述针对 SubjectPublicKeyInfo 的读取函数。证书的 CERTIFICATE 和私钥的 PRIVATE KEY 也需要各自对应的解析流程。

题目没有提供 Bouncy Castle 版本。如果无法解析某个算法或找不到相关类,应检查所用版本的算法支持,并确保 bcprov、bcpkix 等依赖使用一致的版本系列。

备注:内容仅供参考。