javabouncycastleopensshecdsa

How to convert a ECPrivateKey to a PEM-encoded OpenSSH format?


Given this randomly generated ECDSA private key:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR1n1SFvy7Di392GmMy8JsWEjbffTCu
nGKwZrIgq/yIy1C33ud4bxN3W4vbXCtZfyPeVbWNpW1eXSZ/3uWmcJ3SAAAAmCxLaSMsS2
kjAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHWfVIW/LsOLf3Ya
YzLwmxYSNt99MK6cYrBmsiCr/IjLULfe53hvE3dbi9tcK1l/I95VtY2lbV5dJn/e5aZwnd
IAAAAgUgu0f1JX6BTUL3UU3Xq3C8erF/W2cIgzuCHciLp55HYAAAAA
-----END OPENSSH PRIVATE KEY-----

I am using this code to parse it into a 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;
}

This seems to work, but I can't figure out how to convert it bck to a PEM-encoded OPENSSH PRIVATE KEY. Here is the code that isn't working:

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

It returns this output:

-----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-----

which doesn't seem to be a valid key. I search discussion forums and the BouncyCastle testcases to no avail.

Any ideas?

Thank you in advance.


Solution

  • I got the following working using the MINA sshd library (https://github.com/apache/mina-sshd):

    /**
     * Writes a {@code KeyPair} as a PEM-encoded OpenSSH format stream.
     *
     * @param keys a key pair
     * @param out  the stream to write into
     * @throws NullPointerException     if any of the arguments are null
     * @throws IOException              if an error occurs while reading the stream
     * @throws GeneralSecurityException if the key pair is unsupported or invalid
     */
    public void writeKeyPairAsOpenSsh(Collection<KeyPair> keys, OutputStream out)
        throws IOException, GeneralSecurityException
    {
        OpenSSHKeyPairResourceWriter openSshWriter = OpenSSHKeyPairResourceWriter.INSTANCE;
        for (KeyPair key : keys)
            openSshWriter.writePrivateKey(key, null, null, out);
    }
    

    Short and to the point.