You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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(类型-长度-值)序列,按以下步骤处理:

  1. 读取算法名称的长度和内容,验证是否为ssh-ed25519
  2. 读取32字节的原始公钥数据
  3. 将原始数据包装为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 13:04:57