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

如何将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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 16:03:08