如何从Java的RSAPrivateKey生成SSH兼容私钥?已尝试代码未生效
我太懂你现在的困扰了——直接把RSAPrivateKey.getEncoded()的结果Base64编码后拼接前缀后缀,生成的私钥SSH根本不认对吧?这是因为Java默认生成的私钥是PKCS#8格式的DER编码,而SSH期望的是PKCS#1格式的PEM私钥(也就是OpenSSL传统的RSA私钥格式),两者的结构完全不同,直接拼接肯定不行。
下面给你两种可行的解决方案,一种用第三方库(简单省心),一种纯JDK实现(无依赖):
方案一:使用BouncyCastle库(推荐)
BouncyCastle已经封装好了格式转换的逻辑,不用自己折腾ASN.1结构解析,非常方便。
步骤1:引入依赖
如果用Maven,在pom.xml里添加:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency>
步骤2:生成SSH兼容私钥的代码
import org.bouncycastle.openssl.PEMWriter; import java.io.FileWriter; import java.io.Writer; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Security; public class SshRsaKeyGenerator { public static void main(String[] args) throws Exception { // 注册BouncyCastle加密提供者 Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); // 生成2048位RSA密钥对 KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair kp = kpg.genKeyPair(); // 用PEMWriter直接输出SSH兼容的私钥 try (Writer out = new PEMWriter(new FileWriter("private.key"))) { // 传入RSAPrivateKey,PEMWriter会自动处理成PKCS#1格式的PEM out.writeObject(kp.getPrivate()); } } }
这段代码生成的私钥就是SSH直接能识别的格式——PEMWriter会自动加上-----BEGIN RSA PRIVATE KEY-----和-----END RSA PRIVATE KEY-----前缀后缀,还会自动把Base64编码分割成每行64字符,完全符合SSH的要求。
方案二:纯JDK实现(无第三方依赖)
如果不能引入外部库,我们可以手动解析PKCS#8的DER编码,提取出PKCS#1的内容,再转换成PEM格式。
核心逻辑
Java的RSAPrivateKey.getEncoded()返回的是PKCS#8的DER字节流,结构大致是:
PKCS#8 PrivateKeyInfo ::= SEQUENCE { version Version, privateKeyAlgorithm AlgorithmIdentifier, privateKey OCTET STRING -- 这里面就是PKCS#1格式的RSA私钥 }
我们需要解析这个SEQUENCE,取出privateKey字段里的内容,再对其进行Base64编码,然后加上SSH要求的前缀后缀,分割成每行64字符。
完整代码
import java.io.FileWriter; import java.io.IOException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPrivateKey; import java.util.Base64; public class PureJdkSshRsaGenerator { public static void main(String[] args) throws NoSuchAlgorithmException, IOException { // 生成2048位RSA密钥对 KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair kp = kpg.genKeyPair(); RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate(); // 从PKCS#8格式中提取PKCS#1格式的私钥字节 byte[] pkcs8Bytes = privateKey.getEncoded(); byte[] pkcs1Bytes = extractPkcs1FromPkcs8(pkcs8Bytes); // 转换成Base64编码并分割成每行64字符 String base64Encoded = Base64.getEncoder().encodeToString(pkcs1Bytes); StringBuilder pemBuilder = new StringBuilder(); pemBuilder.append("-----BEGIN RSA PRIVATE KEY-----\n"); for (int i = 0; i < base64Encoded.length(); i += 64) { int end = Math.min(i + 64, base64Encoded.length()); pemBuilder.append(base64Encoded.substring(i, end)).append("\n"); } pemBuilder.append("-----END RSA PRIVATE KEY-----"); // 写入文件 try (FileWriter out = new FileWriter("private.key")) { out.write(pemBuilder.toString()); } } // 手动解析PKCS#8 DER格式,提取PKCS#1内容 private static byte[] extractPkcs1FromPkcs8(byte[] pkcs8Bytes) { int index = 0; // 跳过SEQUENCE标签和长度 if (pkcs8Bytes[index++] != 0x30) throw new IllegalArgumentException("不是合法的PKCS#8格式"); int length = readDerLength(pkcs8Bytes, index); index += getLengthOfLength(length); // 跳过version字段 if (pkcs8Bytes[index++] != 0x02) throw new IllegalArgumentException("解析version字段失败"); index += 2; // 长度1,值为0 // 跳过privateKeyAlgorithm字段 if (pkcs8Bytes[index++] != 0x30) throw new IllegalArgumentException("解析算法字段失败"); length = readDerLength(pkcs8Bytes, index); index += getLengthOfLength(length) + length; // 读取privateKey OCTET STRING内容 if (pkcs8Bytes[index++] != 0x04) throw new IllegalArgumentException("解析私钥内容失败"); length = readDerLength(pkcs8Bytes, index); index += getLengthOfLength(length); // 提取PKCS#1格式的私钥字节 byte[] pkcs1Bytes = new byte[length]; System.arraycopy(pkcs8Bytes, index, pkcs1Bytes, 0, length); return pkcs1Bytes; } // 读取DER编码的长度字段 private static int readDerLength(byte[] bytes, int index) { int firstByte = bytes[index] & 0xFF; if ((firstByte & 0x80) == 0) { return firstByte; } int lengthBytes = firstByte & 0x7F; int length = 0; for (int i = 0; i < lengthBytes; i++) { length = (length << 8) | (bytes[index + 1 + i] & 0xFF); } return length; } // 获取长度字段本身的字节数 private static int getLengthOfLength(int length) { if (length < 0x80) { return 1; } else if (length <= 0xFF) { return 2; } else if (length <= 0xFFFF) { return 3; } else if (length <= 0xFFFFFF) { return 4; } else { return 5; } } }
验证方法
生成私钥后,可以用SSH工具验证有效性:
# 从私钥生成对应的公钥 ssh-keygen -y -f private.key > public.key
对比这个公钥和Java生成的kp.getPublic()转换后的OpenSSH格式公钥,应该完全一致。
内容的提问来源于stack exchange,提问作者user3876764

