如何在Java中加载ssh-keygen生成的Ed25519公钥?
Ed25519公钥加载问题:OpenSSL生成的可正常转换,ssh-keygen生成的失败
我是密码学新手,目前遇到一个问题:将ed25519公钥字符串加载为java.security.PublicKey时,OpenSSL生成的密钥能正常转换,但ssh-keygen生成的密钥加载失败。我们基于Apache Mina搭建了SFTP服务器,需要支持客户端通过公钥登录。
以下是Scala编写的代码及运行输出:
def main(args: Array[String]): Unit = { // ssh key generated from openssl: // openssl genpkey -algorithm ed25519 -out private_key.pem // openssl pkey -in private_key.pem -pubout -out public_key.pem val sshKey = "MCowBQYDK2VwAyEA4LSmWoy4ZBYJuwRttwzSLu0KQAYBKGRMHqPNBAun0gA=" println(stringToKey(sshKey)) // ssh key generated using ssh-keygen (ssh-keygen -t ed25519 -C "martin") val sshKeyGen = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpjh408HMAa1uE60DFrUs0GcgRflP1Hc3iLJlesVfCb martin" val Array(_, keyData, _) = sshKeyGen.split(" ", 3) // Split the key println(stringToKey(keyData)) // Not working } def stringToKey(sshKey: String): PublicKey = { Security.addProvider(new BouncyCastleProvider()) val serializedKey: Array[Byte] = Base64.getDecoder.decode(sshKey) val kf: KeyFactory = KeyFactory.getInstance("Ed25519", "BC") val keySpec: X509EncodedKeySpec = new X509EncodedKeySpec(serializedKey) val key: PublicKey = kf.generatePublic(keySpec) key }
运行输出
**OpenSSL Key** Ed25519 Public Key [70:e6:65:14:c0:99:2d:13:58:87:e1:cc:95:9b:2c:93:39:03:39:9f] public data: e0b4a65a8cb8641609bb046db70cd22eed0a40060128644c1ea3cd040ba7d200 **SSH Keygen Key** Exception in thread "main" java.security.spec.InvalidKeySpecException: encoded key spec not recognized: failed to construct sequence from byte[]: unexpected end-of-contents marker at org.bouncycastle.jcajce.provider.asymmetric.util.BaseKeyFactorySpi.engineGeneratePublic(Unknown Source) at org.bouncycastle.jcajce.provider.asymmetric.edec.KeyFactorySpi.engineGeneratePublic(KeyFactorySpi.java:224) at java.base/java.security.KeyFactory.generatePublic(KeyFactory.java:345)
问题根源
OpenSSL生成的公钥是X.509 SubjectPublicKeyInfo格式,完全符合X509EncodedKeySpec的解析要求;而ssh-keygen生成的公钥是SSH专属格式——它的Base64解码后是一个ASN.1序列,包含两部分:ssh-ed25519字符串的字节数据,加上32字节的原始Ed25519公钥内容。直接用X509EncodedKeySpec解析这种结构,自然会因为格式不匹配报错。
解决方法
需要先解析SSH格式的公钥,提取出原始的32字节Ed25519公钥数据,再将其转换为符合X509EncodedKeySpec要求的结构,或者使用BouncyCastle的专门工具处理SSH公钥。
方法1:手动解析SSH公钥格式
SSH公钥的Base64解码后是TLV(类型-长度-值)序列,按以下步骤处理:
- 读取算法名称的长度和内容,验证是否为
ssh-ed25519 - 读取32字节的原始公钥数据
- 将原始数据包装为Ed25519公钥规范,再生成
PublicKey
修改后的代码示例:
import java.security.{PublicKey, Security} import java.security.spec.X509EncodedKeySpec import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters import org.bouncycastle.crypto.util.PublicKeyFactory import org.bouncycastle.jce.provider.BouncyCastleProvider import java.io.ByteArrayInputStream def main(args: Array[String]): Unit = { // OpenSSL生成的公钥 val opensslKey = "MCowBQYDK2VwAyEA4LSmWoy4ZBYJuwRttwzSLu0KQAYBKGRMHqPNBAun0gA=" println(stringToKey(opensslKey)) // ssh-keygen生成的公钥 val sshKeyGen = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpjh408HMAa1uE60DFrUs0GcgRflP1Hc3iLJlesVfCb martin" val Array(_, keyData, _) = sshKeyGen.split(" ", 3) println(sshStringToKey(keyData)) } // 处理OpenSSL的X.509格式公钥 def stringToKey(sshKey: String): PublicKey = { Security.addProvider(new BouncyCastleProvider()) val serializedKey = Base64.getDecoder.decode(sshKey) val kf = KeyFactory.getInstance("Ed25519", "BC") val keySpec = new X509EncodedKeySpec(serializedKey) kf.generatePublic(keySpec) } // 处理ssh-keygen的SSH格式公钥 def sshStringToKey(sshKeyBase64: String): PublicKey = { Security.addProvider(new BouncyCastleProvider()) val decoded = Base64.getDecoder.decode(sshKeyBase64) val in = new ByteArrayInputStream(decoded) // 读取算法名称的长度和内容 val algLength = readUint32(in) val algBytes = new Array[Byte](algLength) in.read(algBytes) if (new String(algBytes) != "ssh-ed25519") { throw new IllegalArgumentException("不是Ed25519类型的SSH公钥") } // 读取原始公钥数据 val keyLength = readUint32(in) val keyBytes = new Array[Byte](keyLength) in.read(keyBytes) // 转换为Java PublicKey val params = new Ed25519PublicKeyParameters(keyBytes, 0) PublicKeyFactory.createKey(SubjectPublicKeyInfo.getInstance(params.toASN1Primitive)).asInstanceOf[PublicKey] } // 读取SSH格式中的无符号32位整数(大端) private def readUint32(in: ByteArrayInputStream): Int = { val bytes = new Array[Byte](4) in.read(bytes) ((bytes(0) & 0xFF) << 24) | ((bytes(1) & 0xFF) << 16) | ((bytes(2) & 0xFF) << 8) | (bytes(3) & 0xFF) }
方法2:使用BouncyCastle的SSH公钥解析工具
BouncyCastle提供了org.bouncycastle.openssh.OpenSSHPublicKey类,可直接解析SSH格式公钥,代码更简洁:
import org.bouncycastle.openssh.OpenSSHPublicKey import java.security.{PublicKey, Security} import org.bouncycastle.jce.provider.BouncyCastleProvider def sshStringToKey(sshKeyBase64: String): PublicKey = { Security.addProvider(new BouncyCastleProvider()) val decoded = Base64.getDecoder.decode(sshKeyBase64) val sshPubKey = OpenSSHPublicKey.getInstance(decoded) sshPubKey.toPublicKey() }
注意:需要确保BouncyCastle依赖包含openssh模块,建议引入bcprov-jdk15on和bcpkix-jdk15on包。
内容的提问来源于stack exchange,提问作者dimuthu
相关产品推荐
相关产品推荐

