如何将 ECPrivateKey 转换为 PEM 编码的 OpenSSH 格式?

问题描述 投票:0回答:1

给定这个随机生成的 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;
}

这似乎可行,但我不知道如何将其转换为 PEM 编码的 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-----

这似乎不是有效的密钥。我搜索讨论论坛和 BouncyCastle 测试用例没有结果。

有什么想法吗?

  • 如果有更简单的方法来解析原始输入,请告诉我。
  • 请不要建议使用 JCA 或 BouncyCastle 以外的库,除非没有其他办法。

提前谢谢您。

java bouncycastle openssh ecdsa
1个回答
0
投票

OpenSSH 最初使用 SEC1 格式作为 ECDSA 私钥,并于 2014 年引入了新的自定义格式。这种新格式的记录很少,例如这里,更多详细信息可以在OpenSSH私钥二进制格式OpenSSH私钥格式中找到。

BouncyCastle 能够使用

OpenSSHPrivateKeyUtil.parsePrivateKeyBlob()
导入新格式,但似乎不支持导出,特别是
OpenSSHPrivateKeyUtil.encodePrivateKey()
会生成 SEC1 格式的密钥。

以下实现以 OpenSSH 格式导出

ECPrivateKey
密钥,其中支持算法
ecdsa-sha2-nistp256
ecdsa-sha2-nistp384
ecdsa-sha2-nistp521
此处:

import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Random;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Base64;

...

class Converter {
    
    // supports ECDSA for nistp256, nistp384 and nistp521
    public String convertToOpenSshPrivateKey(ECPrivateKey ecPrivateKey) throws Exception {
        
        // get keytpe, pub0
        byte[] keyType = getKeyType(ecPrivateKey).getBytes(StandardCharsets.UTF_8);
        byte[] pub0 = Arrays.copyOfRange(keyType, keyType.length - 8, keyType.length);
        
        // get raw private key and calculate raw public key
        byte[] rawPrivateKey =  ecPrivateKey.getS().toByteArray(); // signed byte array
        ECPoint g = ((ECPrivateKeyParameters)ECUtil.generatePrivateKeyParameter(ecPrivateKey)).getParameters().getG();
        byte[] uncompressedPublicKey = g.multiply(ecPrivateKey.getS()).getEncoded(false);
        
        // public data
        int publicDataSize = 
                4 + keyType.length +                // 32-bit length, keytype
                4 + pub0.length +                   // 32-bit length, pub0
                4 + uncompressedPublicKey.length;   // 32-bit length, pub1
        ByteBuffer publicData = ByteBuffer.allocate(publicDataSize);  
        publicData.putInt(keyType.length); publicData.put(keyType);
        publicData.putInt(pub0.length); publicData.put(pub0);
        publicData.putInt(uncompressedPublicKey.length); publicData.put(uncompressedPublicKey);
        
        // private data
        int privateDataSizeUnpadded =               
                8 +                                 // 64-bit dummy checksum
                publicData.position() +             // public key parts 
                4 + rawPrivateKey.length +          // 64-bit dummy checksum
                4;                                  // 32-bit length, comment
        int paddingSize = (8 - privateDataSizeUnpadded % 8) % 8;
        int privateDataSize =               
                privateDataSizeUnpadded +           // private data, unpadded    
                paddingSize;                        // padding
        ByteBuffer privateData = ByteBuffer.allocate(privateDataSize);
        int chcksm = new Random().nextInt();
        privateData.putInt(chcksm); privateData.putInt(chcksm);
        privateData.put(publicData.array(), 0, publicData.position());
        privateData.putInt(rawPrivateKey.length); privateData.put(rawPrivateKey);
        privateData.putInt(0);
        byte[] paddingBlock = new byte[] {1,2,3,4,5,6,7}; // no cipher: blocksize: 8 
        byte[] padding = Arrays.copyOfRange(paddingBlock, 0, paddingSize);
        privateData.put(padding);
        
        // all data
        byte[] prefix = "openssh-key-v1\0".getBytes(StandardCharsets.UTF_8);
        byte[] none = "none".getBytes(StandardCharsets.UTF_8);
        int allDataSize = 
                prefix.length +                     // "openssh-key-v1"0x00
                4 + none.length +                   // 32-bit length, "none" 
                4 + none.length +                   // 32-bit length, "none"
                4 +                                 // 32-bit length, nil
                4 +                                 // 32-bit 0x01
                4 + publicData.position() +         // 32-bit length, public key parts
                4 + privateData.position();         // 32-bit length, private key parts
        ByteBuffer allData = ByteBuffer.allocate(allDataSize);
        allData.put(prefix); 
        allData.putInt(none.length); allData.put(none); 
        allData.putInt(none.length); allData.put(none); 
        allData.putInt(0); 
        allData.putInt(1);
        allData.putInt(publicData.position()); allData.put(publicData.array(), 0, publicData.position());
        allData.putInt(privateData.position()); allData.put(privateData.array(), 0, privateData.position());

        return format(allData.array(), 70, "\r\n");
    }
    
    private static String getKeyType(ECPrivateKey ecPrivateKey) {
        PrivateKeyInfo pki = PrivateKeyInfo.getInstance(ASN1Sequence.getInstance(ecPrivateKey.getEncoded()));
        ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(pki.getPrivateKeyAlgorithm().getParameters());
        if (oid.equals(new ASN1ObjectIdentifier("1.2.840.10045.3.1.7"))) {
            return "ecdsa-sha2-nistp256";
        } else if (oid.equals(new ASN1ObjectIdentifier("1.3.132.0.34"))) {
            return "ecdsa-sha2-nistp384";
        } else if (oid.equals(new ASN1ObjectIdentifier("1.3.132.0.35"))) {
            return "ecdsa-sha2-nistp521";
        } else {
            throw new RuntimeException("key type not defined");
        }
    }
    
    private static String format(byte[] data, int length, String sep) {
        String dataB64 = Base64.toBase64String(data);
        String[] dataB64Splitted = dataB64.split("(?<=\\G.{" + length + "})");
        String body = String.join(sep, dataB64Splitted);
        return String.format("%s%s%s%s%s", "-----BEGIN OPENSSH PRIVATE KEY-----", sep, body, sep, "-----END OPENSSH PRIVATE KEY-----");
    }
    
    public PrivateKey readPrivateKeyAsOpenSsh(Reader reader) throws IOException
    {
        ...
}

以下测试导入 OpenSSH 密钥并再次导出。由此创建原始密钥(除了随机

checksum
字段):

String key = "-----BEGIN OPENSSH PRIVATE KEY-----\r\n"
           + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS\r\n"
           + "1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR1n1SFvy7Di392GmMy8JsWEjbffTCu\r\n"
           + "nGKwZrIgq/yIy1C33ud4bxN3W4vbXCtZfyPeVbWNpW1eXSZ/3uWmcJ3SAAAAmCxLaSMsS2\r\n"
           + "kjAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHWfVIW/LsOLf3Ya\r\n"
           + "YzLwmxYSNt99MK6cYrBmsiCr/IjLULfe53hvE3dbi9tcK1l/I95VtY2lbV5dJn/e5aZwnd\r\n"
           + "IAAAAgUgu0f1JX6BTUL3UU3Xq3C8erF/W2cIgzuCHciLp55HYAAAAA\r\n"
           + "-----END OPENSSH PRIVATE KEY-----";

Converter converter = new Converter();
ECPrivateKey ecPrivateKey = (ECPrivateKey)converter.readPrivateKeyAsOpenSsh(new StringReader(key));     
String opensshKey = converter.convertToOpenSshPrivateKey(ecPrivateKey);
System.out.println(opensshKey); // outputs the above OpenSSH key
© www.soinside.com 2019 - 2024. All rights reserved.