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

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());
}

代码说明

  1. 分类型处理:针对每种已知的Ed25519公钥实现,用最直接的方式提取原始公钥(比如net.i2p的getA()方法直接返回32字节,不需要解析ASN.1)
  2. 通用降级逻辑:对于未知的实现,通过解析X.509编码的公钥结构,提取核心的公钥字节,确保兼容性
  3. SSH格式打包:严格按照SSH的规范写入数据(注意用UTF-8编码算法名,避免字符集问题)

测试验证

修改后,你可以用同一个私钥生成公钥,对比你的方法输出和ssh-keygen -y -f private_key的输出——两者的Base64部分应该完全一致,这样Putty就能正常识别并使用这个公钥连接服务器了。

备注:内容来源于stack exchange,提问作者Atanas Donev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.21 09:14:52