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 等依赖使用一致的版本系列。
备注:内容仅供参考。
