Java中Ed25519公钥转SSH格式编码异常问题(兼容Apache MINA SSHD与BouncyCastle)
Java中Ed25519公钥转SSH格式编码异常问题(兼容Apache MINA SSHD与BouncyCastle)
兄弟,我看你已经在Ed25519密钥兼容的坑里摸爬滚打了一阵——搞定了自定义ContentSigner解决签名器不兼容的问题,现在卡在公钥转SSH格式和ssh-keygen输出不一致的问题上对吧?这问题核心出在不同库的Ed25519公钥实现,它们的getEncoded()输出结构不一样,你之前的编码方法假设了统一的ASN.1结构,自然会拿到错误的公钥数据。
问题根源
SSH的ssh-ed25519公钥格式非常简单:只需要原始的32字节Ed25519公钥材料,加上固定的格式包装(先写算法名的长度+算法名,再写公钥长度+公钥字节,最后Base64编码)。但你遇到的三种公钥实现:
- net.i2p的
EdDSAPublicKey - BouncyCastle的
BCEdDSAPublicKey - Sun的
EdDSAPublicKeyImpl
它们的getEncoded()返回的X.509编码结构细节不同,直接用统一的ASN.1解析逻辑会提取错误的公钥字节,导致最终输出和ssh-keygen不一致。
解决方案:统一提取32字节原始公钥
不管哪种实现,我们的目标都是拿到那32字节的原始公钥数据,再按照SSH格式打包。下面是修改后的编码方法,专门处理不同的Ed25519公钥实现:
import net.i2p.crypto.eddsa.EdDSAPublicKey; import org.bouncycastle.asn1.ASN1BitString; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.jce.provider.BCEdDSAPublicKey; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.security.PublicKey; import java.util.Base64; import java.nio.charset.StandardCharsets; private static String encodeEdDSAPublicKey(PublicKey publicKey) throws IOException { byte[] rawPublicKey; // 针对不同的Ed25519公钥实现,提取原始32字节公钥 if (publicKey instanceof EdDSAPublicKey) { // 处理Apache MINA依赖的net.i2p实现 EdDSAPublicKey i2pKey = (EdDSAPublicKey) publicKey; rawPublicKey = i2pKey.getA(); // 直接获取原始32字节公钥 } else if (publicKey instanceof BCEdDSAPublicKey) { // 处理BouncyCastle的实现 BCEdDSAPublicKey bcKey = (BCEdDSAPublicKey) publicKey; // Ed25519公钥不需要压缩标识,getEncoded(false)直接返回32字节原始数据 rawPublicKey = bcKey.getPoint().getEncoded(false); } else if (publicKey.getClass().getName().equals("sun.security.ec.ed.EdDSAPublicKeyImpl")) { // 处理Sun的EdDSAPublicKeyImpl实现 try (ASN1InputStream asn1InputStream = new ASN1InputStream(publicKey.getEncoded())) { ASN1Primitive primitive = asn1InputStream.readObject(); if (primitive instanceof ASN1Sequence) { ASN1Sequence seq = (ASN1Sequence) primitive; // X.509结构:[0]算法标识,[1]公钥比特串 ASN1BitString keyBitString = (ASN1BitString) seq.getObjectAt(1); rawPublicKey = keyBitString.getOctets(); // Sun的实现中,这个比特串就是直接的32字节原始公钥 } else { throw new IOException("Invalid ASN.1 structure for Sun EdDSA public key"); } } } else { // 通用降级逻辑:尝试解析X.509编码提取原始公钥 try (ASN1InputStream asn1InputStream = new ASN1InputStream(publicKey.getEncoded())) { ASN1Primitive primitive = asn1InputStream.readObject(); if (primitive instanceof ASN1Sequence) { ASN1Sequence seq = (ASN1Sequence) primitive; ASN1BitString keyBitString = (ASN1BitString) seq.getObjectAt(1); byte[] keyBytes = keyBitString.getOctets(); if (keyBytes.length == 32) { rawPublicKey = keyBytes; } else { // 处理某些嵌套ASN.1的情况 try (ASN1InputStream nestedInputStream = new ASN1InputStream(keyBytes)) { ASN1Primitive nestedPrimitive = nestedInputStream.readObject(); if (nestedPrimitive instanceof ASN1OctetString) { rawPublicKey = ((ASN1OctetString) nestedPrimitive).getOctets(); } else { throw new IOException("Failed to extract raw Ed25519 public key from unknown implementation"); } } } } else { throw new IOException("Invalid ASN.1 structure for Ed25519 public key"); } } } // 按照SSH标准格式打包数据 ByteArrayOutputStream byteOs = new ByteArrayOutputStream(); try (DataOutputStream dos = new DataOutputStream(byteOs)) { byte[] sshAlgBytes = "ssh-ed25519".getBytes(StandardCharsets.UTF_8); dos.writeInt(sshAlgBytes.length); dos.write(sshAlgBytes); dos.writeInt(rawPublicKey.length); dos.write(rawPublicKey); } return Base64.getEncoder().encodeToString(byteOs.toByteArray()); }
代码说明
- 分类型处理:针对每种已知的Ed25519公钥实现,用最直接的方式提取原始公钥(比如net.i2p的
getA()方法直接返回32字节,不需要解析ASN.1) - 通用降级逻辑:对于未知的实现,通过解析X.509编码的公钥结构,提取核心的公钥字节,确保兼容性
- SSH格式打包:严格按照SSH的规范写入数据(注意用UTF-8编码算法名,避免字符集问题)
测试验证
修改后,你可以用同一个私钥生成公钥,对比你的方法输出和ssh-keygen -y -f private_key的输出——两者的Base64部分应该完全一致,这样Putty就能正常识别并使用这个公钥连接服务器了。
备注:内容来源于stack exchange,提问作者Atanas Donev
相关产品推荐
相关产品推荐

