如何将ECPrivateKey转换为PEM编码的OpenSSH格式私钥?
问题:ECDSA私钥解析与OpenSSH格式转换失败
原始ECDSA私钥
-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR1n1SFvy7Di392GmMy8JsWEjbffTCu nGKwZrIgq/yIy1C33ud4bxN3W4vbXCtZfyPeVbWNpW1eXSZ/3uWmcJ3SAAAAmCxLaSMsS2 kjAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHWfVIW/LsOLf3Ya YzLwmxYSNt99MK6cYrBmsiCr/IjLULfe53hvE3dbi9tcK1l/I95VtY2lbV5dJn/e5aZwnd IAAAAgUgu0f1JX6BTUL3UU3Xq3C8erF/W2cIgzuCHciLp55HYAAAAA -----END OPENSSH PRIVATE KEY-----
解析私钥为PrivateKey的代码
public PrivateKey readPrivateKeyAsOpenSsh(Reader reader) throws IOException { try (PemReader pemReader = new PemReader(reader)) { PemObject pemObject = pemReader.readPemObject(); byte[] content = pemObject.getContent(); AsymmetricKeyParameter keyParameter = OpenSSHPrivateKeyUtil.parsePrivateKeyBlob(content); if (!(keyParameter instanceof ECPrivateKeyParameters ecPrivateKeyParameters)) throw new UnsupportedOperationException("Unsupported format: " + pemObject.getType()); BigInteger d = ecPrivateKeyParameters.getD(); ECNamedCurveSpec ecNamedCurveSpec = getEcNamedCurveSpec(ecPrivateKeyParameters); ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecNamedCurveSpec); KeyFactory keyFactory; try { keyFactory = KeyFactory.getInstance("EC"); return keyFactory.generatePrivate(ecPrivateKeySpec); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { // Deployment-time decision throw new AssertionError(e); } } } /** * Returns the name of a key's algorithm. * * @param keyParameter a key's parameters * @return the name of the key's algorithm * @throws IOException if the algorithm is unsupported */ private static String getAlgorithm(AsymmetricKeyParameter keyParameter) throws IOException { return switch (keyParameter) { case RSAKeyParameters _ -> "RSA"; case DSAPublicKeyParameters _ -> "DSA"; case ECPublicKeyParameters _ -> "EC"; default -> throw new IOException("Unsupported key parameter: " + keyParameter.getClass().getName()); }; } /** * @param keyParameters the private key's parameters * @return the set of domain parameters used with elliptic curve cryptography (ECC) * @throws IOException if the elliptical curve used is unknown */ private ECNamedCurveSpec getEcNamedCurveSpec(ECPrivateKeyParameters keyParameters) throws IOException { ECDomainParameters domainParameters = keyParameters.getParameters(); X9ECParameters x9ECParameters = getX9EcParameters(domainParameters); if (x9ECParameters == null) throw new IOException("Failed to convert domain parameters to X9ECParameters"); EllipticCurve ellipticCurve = new EllipticCurve( new ECFieldFp(x9ECParameters.getCurve().getField().getCharacteristic()), x9ECParameters.getCurve().getA().toBigInteger(), x9ECParameters.getCurve().getB().toBigInteger()); ECPoint g = x9ECParameters.getG(); String curveName = getCurveName(domainParameters); if (curveName == null) throw new IOException("Failed to find the curve name"); return new ECNamedCurveSpec(curveName, ellipticCurve, new java.security.spec.ECPoint( g.getAffineXCoord().toBigInteger(), g.getAffineYCoord().toBigInteger()), x9ECParameters.getN(), x9ECParameters.getH()); } /** * Returns the X9ECParameters of the curve with the specified domain parameters. * * @param domainParameters domain parameters * @return null if no match is found */ private X9ECParameters getX9EcParameters(ECDomainParameters domainParameters) { Entry<String, X9ECParameters> details = getCurveDetails(domainParameters); if (details == null) return null; return details.getValue(); } /** * Returns the name of the curve with the specified domain parameters. * * @param domainParameters domain parameters * @return null if no match is found */ private String getCurveName(ECDomainParameters domainParameters) { Entry<String, X9ECParameters> details = getCurveDetails(domainParameters); if (details == null) return null; return details.getKey(); } /** * Returns the name and X9ECParameters of the curve with the specified domain parameters. * * @param domainParameters domain parameters * @return null if no match is found */ private Entry<String, X9ECParameters> getCurveDetails(ECDomainParameters domainParameters) { for (Enumeration<?> e = ECNamedCurveTable.getNames(); e.hasMoreElements(); ) { String name = (String) e.nextElement(); X9ECParameters x9EcParams = ECNamedCurveTable.getByName(name); if (x9EcParams.getCurve().equals(domainParameters.getCurve())) return new SimpleImmutableEntry<>(name, x9EcParams); } return null; }
转换回OpenSSH格式的失败代码
/** * Writes a {@code PrivateKey} as a PEM-encoded OpenSSH format stream. * * @param privateKey the key * @param writer the stream to write into * @throws NullPointerException if any of the arguments are null * @throws IOException if an error occurs while writing into the stream */ public void writePrivateKeyAsOpenSsh(ECPrivateKey privateKey, Writer writer) throws IOException { AsymmetricKeyParameter param = getAsymmetricKeyParameter(privateKey); try (JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) { byte[] encodedPrivateKey = OpenSSHPrivateKeyUtil.encodePrivateKey(param); PemObject pemObject = new PemObject("OPENSSH PRIVATE KEY", encodedPrivateKey); pemWriter.writeObject(pemObject); pemWriter.flush(); } }
生成的无效输出
-----BEGIN OPENSSH PRIVATE KEY----- MIIBaAIBAQQgUgu0f1JX6BTUL3UU3Xq3C8erF/W2cIgzuCHciLp55HaggfowgfcC AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 YyVRAgEBoUQDQgAEdZ9Uhb8uw4t/dhpjMvCbFhI2330wrpxisGayIKv8iMtQt97n eG8Td1uL21wrWX8j3lW1jaVtXl0mf97lpnCd0g== -----END OPENSSH PRIVATE KEY-----
需求
- 提供将
ECPrivateKey转换为有效PEM编码OpenSSH格式的方案; - 若有更简便的原私钥解析方式,请告知;
- 优先使用JCA或BouncyCastle库,非必要不使用其他库。
解决方案
1. 修复ECPrivateKey转OpenSSH格式的代码
问题核心是密钥参数转换不完整,以及JcaPEMWriter会自动修改格式。以下是修正后的实现:
第一步:完善密钥参数转换方法
确保JCA的ECPrivateKey能完整转换为BouncyCastle的ECPrivateKeyParameters(包含完整域参数):
private AsymmetricKeyParameter getAsymmetricKeyParameter(ECPrivateKey privateKey) throws IOException { try { KeyFactory keyFactory = KeyFactory.getInstance("EC"); ECPrivateKeySpec keySpec = keyFactory.getKeySpec(privateKey, ECPrivateKeySpec.class); // 从JCA参数中获取曲线名称,匹配BouncyCastle的曲线参数 ECNamedCurveParameterSpec curveSpec = (ECNamedCurveParameterSpec) keySpec.getParams(); X9ECParameters x9Params = ECNamedCurveTable.getByName(curveSpec.getName()); ECDomainParameters domainParams = new ECDomainParameters( x9Params.getCurve(), x9Params.getG(), x9Params.getN(), x9Params.getH()); return new ECPrivateKeyParameters(keySpec.getS(), domainParams); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new IOException("转换ECPrivateKey失败", e); } }
第二步:修正写入方法
使用普通PemWriter而非JcaPEMWriter,避免自动格式转换:
public void writePrivateKeyAsOpenSsh(ECPrivateKey privateKey, Writer writer) throws IOException { AsymmetricKeyParameter param = getAsymmetricKeyParameter(privateKey); // 生成符合OpenSSH标准的私钥Blob byte[] encodedPrivateKey = OpenSSHPrivateKeyUtil.encodePrivateKey(param); PemObject pemObject = new PemObject("OPENSSH PRIVATE KEY", encodedPrivateKey); // 使用普通PemWriter保证格式纯净 try (PemWriter pemWriter = new PemWriter(writer)) { pemWriter.writeObject(pemObject); pemWriter.flush(); } }
2. 更简便的私钥解析方式
直接使用BouncyCastle的PEMParser和JcaPEMKeyConverter,无需手动处理参数转换:
import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; public PrivateKey readPrivateKeyAsOpenSshSimplified(Reader reader) throws IOException { try (PEMParser pemParser = new PEMParser(reader)) { Object obj = pemParser.readObject(); JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); return converter.getPrivateKey((PrivateKeyInfo) obj); } catch (Exception e) { throw new IOException("解析OpenSSH私钥失败", e); } }
注意:需提前注册BouncyCastle提供者,在代码启动时添加:
Security.addProvider(new BouncyCastleProvider());
关键说明
- OpenSSH私钥格式包含特定版本标识和元数据,
OpenSSHPrivateKeyUtil.encodePrivateKey需要完整的域参数才能生成正确Blob; JcaPEMWriter会自动将密钥转换为PKCS#8格式,导致输出不符合OpenSSH标准,因此改用普通PemWriter;- 确保BouncyCastle库版本兼容,建议使用最新稳定版。
内容的提问来源于stack exchange,提问作者Gili
相关产品推荐
相关产品推荐

