给定这个随机生成的 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 测试用例没有结果。
有什么想法吗?
提前谢谢您。
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