如何将 ECDSA 公钥 (ECPublicKey) 转换为 OpenSSH 格式? [已关闭]

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

如何将

java.security.interfaces.ECPublicKey
转换为 OpenSSH 字符串表示形式以便与 ssh 一起使用?

例如,给定

ECPublicKey
我想返回这样的字符串:
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEAFuExXweUtKN3KYzoV+6eEyVfN9CLyM48FO2B9bZQ51bLtQvVo1MNVCXuW73dD2CgHXPryEwsTMyUR74GHN50= [email protected]

java bouncycastle openssh
1个回答
0
投票

回答我自己的问题...

您只需要下面定义的

toOpenSshPublicKey()
函数。不过,我还提供了一些您可能有用的相关功能:

  • 从文件加载私钥。
  • 将私钥转换为公钥。
  • 生成公钥的指纹。

代码

import com.github.cowwoc.pouch.core.WrappedCheckedException;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;

import java.io.BufferedReader;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.StringJoiner;

import static com.github.cowwoc.requirements10.java.DefaultJavaValidators.requireThat;
import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;

/**
 * SSH helper functions.
 */
public class SshKeys
{
    private final JcaPEMKeyConverter pemToJca = new JcaPEMKeyConverter();

    /**
     * Creates a new instance.
     */
    public SshKeys()
    {
    }

    /**
     * Loads a {@code PrivateKey} from a file.
     *
     * @param path the path of the private key file
     * @return the {@code PrivateKey}
     * @throws IOException if the file does not contain a valid {@code PrivateKey}
     */
    public PrivateKey loadPrivateKey(Path path) throws IOException
    {
        try (BufferedReader reader = Files.newBufferedReader(path);
             PEMParser parser = new PEMParser(reader))
        {
            Object keyPair = parser.readObject();
            return switch (keyPair)
            {
                // Based on https://github.com/bcgit/bc-java/blob/b048c864157376fdaa1a889588ce1dea08629d7a/mail/src/test/java/org/bouncycastle/mail/smime/test/MailGeneralTest.java#L281
                case PEMKeyPair pem -> pemToJca.getPrivateKey(pem.getPrivateKeyInfo());
                // Based on https://github.com/bcgit/bc-java/blob/b048c864157376fdaa1a889588ce1dea08629d7a/mail/src/test/java/org/bouncycastle/mail/smime/test/MailGeneralTest.java#L407
                case PrivateKeyInfo pki -> pemToJca.getPrivateKey(pki);
                default -> throw new ClassCastException(keyPair.getClass().getName());
            };
        }
    }

    /**
     * Converts a private key to a public key, if possible.
     *
     * @param privateKey the private key
     * @return the public key
     */
    public PublicKey convertToPublicKey(PrivateKey privateKey)
    {
        // Get the key factory based on the private key algorithm
        try
        {
            KeyFactory keyFactory = KeyFactory.getInstance(privateKey.getAlgorithm(), PROVIDER_NAME);
            return switch (privateKey)
            {
                case ECPrivateKey ecPrivateKey ->
                {
                    // Based on https://github.com/aergoio/heraj/blob/0bcea46c46429c320da711632624605a6225d20f/core/util/src/main/java/hera/util/pki/ECDSAKeyGenerator.java#L173
                    // and https://github.com/bcgit/bc-java/blob/efe1f511d8c58978af38e45215a7b7bf6477a10c/pkix/src/main/java/org/bouncycastle/eac/jcajce/JcaPublicKeyConverter.java#L83
                    ECParameterSpec params = ecPrivateKey.getParams();

                    // Find the curve name from the private key parameters
                    String curveName = ((ECNamedCurveSpec) params).getName();
                    X9ECParameters x9Params = ECNamedCurveTable.getByName(curveName);
                    ECDomainParameters domainParams = new ECDomainParameters(
                        x9Params.getCurve(),
                        x9Params.getG(),
                        x9Params.getN(),
                        x9Params.getH(),
                        x9Params.getSeed()
                    );

                    BigInteger d = ecPrivateKey.getS();
                    ECPoint Q = new FixedPointCombMultiplier().multiply(domainParams.getG(), d).normalize();

                    // Create ECPublicKeySpec
                    ECPublicKeySpec spec = new ECPublicKeySpec(
                        new java.security.spec.ECPoint(Q.getAffineXCoord().toBigInteger(),
                            Q.getAffineYCoord().toBigInteger()), params
                    );

                    yield keyFactory.generatePublic(spec);
                }
                default -> throw new ClassCastException(privateKey.getClass().getName());
            };
        }
        catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e)
        {
            throw WrappedCheckedException.wrap(e);
        }
    }

    /**
     * Returns the OpenSSH representation of a public key.
     *
     * @param publicKey the public key
     * @param comment   the comment to append to the end of the key
     * @return the PEM representation of the public key
     */
    public String toOpenSshPublicKey(PublicKey publicKey, String comment)
    {
        return switch (publicKey)
        {
            case ECPublicKey ecPublicKey -> toOpenSshPublicKey(ecPublicKey, comment);
            default -> throw new ClassCastException(publicKey.getClass().getName());
        };
    }

    /**
     * @param publicKey a public key
     * @return the OpenSSH fingerprint of the key
     * @throws NullPointerException if {@code publicKey} is null
     */
    public String toOpenSshFingerprint(PublicKey publicKey)
    {
        return switch (publicKey)
        {
            case ECPublicKey ecPublicKey -> toOpenSshFingerprint(ecPublicKey);
            default -> throw new ClassCastException(publicKey.getClass().getName());
        };
    }

    /**
     * @param publicKey a public key
     * @return the OpenSSH fingerprint of the key
     * @throws NullPointerException if {@code publicKey} is null
     */
    public String toOpenSshFingerprint(ECPublicKey publicKey)
    {
        String opensshPublicKey = toOpenSshPublicKey(publicKey, "");

        // Extract the base64 part of the OpenSSH public key
        String base64Key = opensshPublicKey.split(" ")[1];
        byte[] keyBytes = Base64.getDecoder().decode(base64Key);

        // Compute the SHA-256 fingerprint
        MessageDigest md;
        try
        {
            md = MessageDigest.getInstance("MD5");
        }
        catch (NoSuchAlgorithmException e)
        {
            // This is a deployment-time decision. Either the JVM supports this digest type or it doesn't.
            throw new AssertionError(e);
        }
        byte[] fingerprint = md.digest(keyBytes);

        StringJoiner hexFingerprint = new StringJoiner(":");
        for (byte b : fingerprint)
            hexFingerprint.add(String.format("%02x", b));
        return hexFingerprint.toString();
    }

    /**
     * @param publicKey the public key
     * @param comment   the comment to append to the end of the key
     * @return the OpenSSH representation of the key
     * @throws NullPointerException if any of the arguments are null
     */
    public String toOpenSshPublicKey(ECPublicKey publicKey, String comment)
    {
        requireThat(comment, "comment").isNotNull();
        AsymmetricKeyParameter param = getAsymmetricKeyParameter(publicKey);

        // Encode the public key in OpenSSH format
        byte[] encodedPublicKey;
        try
        {
            encodedPublicKey = OpenSSHPublicKeyUtil.encodePublicKey(param);
        }
        catch (IOException e)
        {
            // The exception is declared but never actually thrown by the method
            throw new AssertionError(e);
        }

        // Determine the SSH key type based on the curve name
        String sshKeyType = getSshKeyType(publicKey);
        return sshKeyType + " " + Base64.getEncoder().encodeToString(encodedPublicKey) + " " + comment;
    }

    /**
     * @param publicKey a public key
     * @return the asymmetric key parameters of the key
     * @throws NullPointerException if {@code publicKey} is null
     */
    private AsymmetricKeyParameter getAsymmetricKeyParameter(ECPublicKey publicKey)
    {
        // Retrieve the curve parameters from the named curve
        X9ECParameters ecParams = ECNamedCurveTable.getByName(getCurveName(publicKey));
        ECCurve curve = ecParams.getCurve();
        ECPoint g = ecParams.getG();
        BigInteger n = ecParams.getN();
        BigInteger h = ecParams.getH();

        // Convert java.security.spec.ECPoint to BouncyCastle ECPoint
        java.security.spec.ECPoint w = publicKey.getW();
        ECPoint q = curve.createPoint(w.getAffineX(), w.getAffineY());

        ECDomainParameters domainParams = new ECDomainParameters(curve, g, n, h);
        return new ECPublicKeyParameters(q, domainParams);
    }

    /**
     * @param publicKey a public key
     * @return the name of the elliptic curve used in the public key
     * @throws NullPointerException if {@code publicKey} is null
     */
    private String getCurveName(ECPublicKey publicKey)
    {
        ECNamedCurveSpec params = (ECNamedCurveSpec) publicKey.getParams();
        return params.getName();
    }

    /**
     * @param publicKey a public key
     * @return the SSH type of the public key
     * @throws NullPointerException if {@code publicKey} is null
     */
    private String getSshKeyType(ECPublicKey publicKey)
    {
        String curveName = getCurveName(publicKey);
        // SSH key prefix: https://datatracker.ietf.org/doc/html/rfc5656#section-6.2
        return "ecdsa-sha2-" + switch (curveName)
        {
            // Mapping from curve type to SSH key type: https://datatracker.ietf.org/doc/html/rfc5656#section-10.1
            // Equivalent curves: https://www.rfc-editor.org/rfc/rfc4492.html#page-32
            case "secp256r1", "prime256v1" -> "nistp256";
            case "secp384r1" -> "nistp384";
            case "secp521r1" -> "nistp521";
            default -> throw new IllegalArgumentException("Invalid curve type: " + publicKey);
        };
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.