如何使用Bouncy Castle创建兼容OpenSSH的ED25519密钥?
现有实现失效的核心问题
- 算法选型错误:代码中调用的
Curve25519是BouncyCastle实现的Montgomery曲线,对应X25519密钥交换场景,和SSH标准使用的ED25519(基于Edwards25519的Ed25519签名算法)属于不同的曲线体系,哪怕底层域参数同源,密钥结构和编码逻辑完全不兼容。 - 格式构造错误:代码将ASN.1结构的EC私钥直接套上
OPENSSH PRIVATE KEY的PEM标识,没有按照OpenSSH自定义的二进制序列化规则构造私钥内容,结构不符合规范时SSH客户端会直接判定文件无效。
符合OpenSSH兼容要求的实现逻辑
- 密钥生成阶段直接使用BC提供的原生
Ed25519算法生成器,不通过通用EC接口拼接X25519曲线参数。 - 严格按照OpenSSH私钥序列化规范构造二进制内容:
- 开头写入固定魔术字符串
openssh-key-v1\0 - 依次写入序列化后的无加密配置:算法标识
none、空的密钥派生参数、空认证块、密钥对数量1 - 序列化公钥部分:写入算法标识
ssh-ed25519、32字节原始公钥值 - 序列化私钥部分:写入和公钥匹配的校验随机数、算法标识
ssh-ed25519、32字节原始公钥值、64字节完整私钥值(32字节种子+32字节公钥拼接)、空注释字符串 - 所有长度字段均使用SSH协议规定的无符号32位大端整数格式编码
- 开头写入固定魔术字符串
- 最终将构造完成的二进制内容按64字符分行做Base64编码,包装为带
-----BEGIN OPENSSH PRIVATE KEY-----头尾的PEM文件,和ssh-keygen -t ed25519生成的id_ed25519格式完全一致。
可直接运行的参考实现
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Base64; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Security; public class OpenSshEd25519KeyGenerator { static { Security.insertProviderAt(new BouncyCastleProvider(), 1); } // 按SSH序列化规则写入带长度前缀的字节块 private static void writeSshEncodedBlock(DataOutputStream out, byte[] data) throws Exception { out.writeInt(data.length); out.write(data); } public static String generateCompatiblePrivateKey() throws Exception { // 生成原生Ed25519密钥对 KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519", "BC"); KeyPair keyPair = keyGen.generateKeyPair(); Ed25519PrivateKeyParameters privateParams = new Ed25519PrivateKeyParameters(keyPair.getPrivate().getEncoded(), 0); Ed25519PublicKeyParameters publicParams = privateParams.generatePublicKey(); byte[] privateSeed = privateParams.getEncoded(); byte[] publicKeyRaw = publicParams.getEncoded(); // 构造OpenSSH私钥二进制结构 ByteArrayOutputStream keyBuffer = new ByteArrayOutputStream(); DataOutputStream keyStream = new DataOutputStream(keyBuffer); // 写入格式魔术头 keyStream.write("openssh-key-v1\0".getBytes()); // 写入无加密配置 writeSshEncodedBlock(keyStream, "none".getBytes()); writeSshEncodedBlock(keyStream, new byte[0]); writeSshEncodedBlock(keyStream, new byte[0]); keyStream.writeInt(1); // 单密钥对 // 序列化公钥段 ByteArrayOutputStream pubBuffer = new ByteArrayOutputStream(); DataOutputStream pubStream = new DataOutputStream(pubBuffer); writeSshEncodedBlock(pubStream, "ssh-ed25519".getBytes()); writeSshEncodedBlock(pubStream, publicKeyRaw); pubStream.close(); writeSshEncodedBlock(keyStream, pubBuffer.toByteArray()); // 序列化私钥段 int checkValue = (int) (Math.random() * Integer.MAX_VALUE); keyStream.writeInt(checkValue); keyStream.writeInt(checkValue); ByteArrayOutputStream privBuffer = new ByteArrayOutputStream(); DataOutputStream privStream = new DataOutputStream(privBuffer); writeSshEncodedBlock(privStream, "ssh-ed25519".getBytes()); writeSshEncodedBlock(privStream, publicKeyRaw); // 拼接私钥+公钥的完整64字节字段 byte[] fullPrivate = new byte[64]; System.arraycopy(privateSeed, 0, fullPrivate, 0, 32); System.arraycopy(publicKeyRaw, 0, fullPrivate, 32, 32); writeSshEncodedBlock(privStream, fullPrivate); writeSshEncodedBlock(privStream, new byte[0]); // 空注释 privStream.close(); writeSshEncodedBlock(keyStream, privBuffer.toByteArray()); keyStream.close(); byte[] keyBinary = keyBuffer.toByteArray(); // 包装为标准PEM格式 String base64Content = Base64.toBase64String(keyBinary); StringBuilder pemBuilder = new StringBuilder("-----BEGIN OPENSSH PRIVATE KEY-----\n"); for (int pos = 0; pos < base64Content.length(); pos += 64) { int end = Math.min(pos + 64, base64Content.length()); pemBuilder.append(base64Content, pos, end).append("\n"); } pemBuilder.append("-----END OPENSSH PRIVATE KEY-----"); return pemBuilder.toString(); } }
生成的密钥文件可直接被OpenSSH客户端识别使用,无需格式转换。
内容的提问来源于stack exchange,提问作者mwarning
相关产品推荐
相关产品推荐

