如何将
java.security.interfaces.ECPublicKey
转换为 OpenSSH 字符串表示形式以便与 ssh 一起使用?
例如,给定
ECPublicKey
我想返回这样的字符串:ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEAFuExXweUtKN3KYzoV+6eEyVfN9CLyM48FO2B9bZQ51bLtQvVo1MNVCXuW73dD2CgHXPryEwsTMyUR74GHN50= [email protected]
回答我自己的问题...
您只需要下面定义的
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);
};
}
}