如何以编程方式从同时包含证书和私钥的PEM文件中获取KeyStore?我正在尝试向HTTPS连接中的服务器提供客户端证书。我已经确认,如果我使用openssl和keytool来获得一个动态加载的jks文件,客户端证书就可以正常工作。我甚至可以通过动态读入一个p12 (PKCS12)文件来让它工作。
我正在考虑使用BouncyCastle中的PEMReader类,但我无法克服一些错误。我使用-Djavax.net.debug=all选项运行Java客户端,使用调试LogLevel运行Apache web服务器。不过,我不确定要找什么。Apache错误日志表明:
...
OpenSSL: Write: SSLv3 read client certificate B
OpenSSL: Exit: error in SSLv3 read client certificate B
Re-negotiation handshake failed: Not accepted by client!?Java客户端程序指示:
...
main, WRITE: TLSv1 Handshake, length = 48
main, waiting for close_notify or alert: state 3
main, Exception while waiting for close java.net.SocketException: Software caused connection abort: recv failed
main, handling exception: java.net.SocketException: Software caused connection abort: recv failed
%% Invalidated: [Session-3, TLS_RSA_WITH_AES_128_CBC_SHA]
main, SEND TLSv1 ALERT: fatal, description = unexpected_message
...客户端代码:
public void testClientCertPEM() throws Exception {
String requestURL = "https://mydomain/authtest";
String pemPath = "C:/Users/myusername/Desktop/client.pem";
HttpsURLConnection con;
URL url = new URL(requestURL);
con = (HttpsURLConnection) url.openConnection();
con.setSSLSocketFactory(getSocketFactoryFromPEM(pemPath));
con.setRequestMethod("GET");
con.setDoInput(true);
con.setDoOutput(false);
con.connect();
String line;
BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()));
while((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
con.disconnect();
}
public SSLSocketFactory getSocketFactoryFromPEM(String pemPath) throws Exception {
Security.addProvider(new BouncyCastleProvider());
SSLContext context = SSLContext.getInstance("TLS");
PEMReader reader = new PEMReader(new FileReader(pemPath));
X509Certificate cert = (X509Certificate) reader.readObject();
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setCertificateEntry("alias", cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keystore, null);
KeyManager[] km = kmf.getKeyManagers();
context.init(km, null, null);
return context.getSocketFactory();
} 我注意到服务器正在输出日志中的SSLv3,而客户端是TLSv1。如果我添加了系统属性-Dhttps.protocols=SSLv3,那么客户端也将使用SSLv3,但我得到了相同的错误消息。我还尝试在不改变结果的情况下添加-Dsun.security.ssl.allowUnsafeRenegotiation=true。
我已经用谷歌搜索过了,这个问题的通常答案是先使用openssl和keytool。在我的情况下,我需要在运行时直接读取PEM。实际上,我正在移植一个已经可以做到这一点的C++程序,坦率地说,我很惊讶在Java语言中做这件事有多么困难。C++代码:
curlpp::Easy request;
...
request.setOpt(new Options::Url(myurl));
request.setOpt(new Options::SslVerifyPeer(false));
request.setOpt(new Options::SslCertType("PEM"));
request.setOpt(new Options::SslCert(cert));
request.perform();发布于 2012-09-20 22:30:44
我想通了。问题是X509Certificate本身是不够的。我还需要将私钥放入动态生成的密钥库中。BouncyCastle PEMReader似乎不能同时处理包含证书和私钥的PEM文件,但它可以分别处理每个部分。我可以自己将PEM读入内存,并将其分成两个单独的流,然后将每个流提供给一个单独的PEMReader。因为我知道我正在处理的PEM文件将首先拥有证书,其次是私钥,所以我可以以健壮性为代价来简化代码。我还知道末尾证书分隔符总是用五个连字符括起来。对我有效的实现是:
protected static SSLSocketFactory getSocketFactoryPEM(String pemPath) throws Exception {
Security.addProvider(new BouncyCastleProvider());
SSLContext context = SSLContext.getInstance("TLS");
byte[] certAndKey = fileToBytes(new File(pemPath));
String delimiter = "-----END CERTIFICATE-----";
String[] tokens = new String(certAndKey).split(delimiter);
byte[] certBytes = tokens[0].concat(delimiter).getBytes();
byte[] keyBytes = tokens[1].getBytes();
PEMReader reader;
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(certBytes)));
X509Certificate cert = (X509Certificate)reader.readObject();
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(keyBytes)));
PrivateKey key = (PrivateKey)reader.readObject();
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setCertificateEntry("cert-alias", cert);
keystore.setKeyEntry("key-alias", key, "changeit".toCharArray(), new Certificate[] {cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keystore, "changeit".toCharArray());
KeyManager[] km = kmf.getKeyManagers();
context.init(km, null, null);
return context.getSocketFactory();
}更新:这似乎可以在没有BouncyCastle的情况下完成:
byte[] certAndKey = fileToBytes(new File(pemPath));
byte[] certBytes = parseDERFromPEM(certAndKey, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
byte[] keyBytes = parseDERFromPEM(certAndKey, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");
X509Certificate cert = generateCertificateFromDER(certBytes);
RSAPrivateKey key = generatePrivateKeyFromDER(keyBytes);..。
protected static byte[] parseDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) {
String data = new String(pem);
String[] tokens = data.split(beginDelimiter);
tokens = tokens[1].split(endDelimiter);
return DatatypeConverter.parseBase64Binary(tokens[0]);
}
protected static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey)factory.generatePrivate(spec);
}
protected static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
}发布于 2021-07-08 05:47:18
虽然Ryan的回答效果很好,但我想为其他开发人员提供一个替代方案,因为我在过去也遇到过类似的挑战,我还需要处理pem格式的加密私钥。我已经创建了一个库来简化加载pem文件和从中创建SSLSocketFactory或SSLContext的过程,请参阅此处:GitHub - SSLContext Kickstart希望您喜欢:)
可以使用以下代码片段加载pem文件:
var keyManager = PemUtils.loadIdentityMaterial("certificate-chain.pem", "private-key.pem");
var trustManager = PemUtils.loadTrustMaterial("some-trusted-certificate.pem");
var sslFactory = SSLFactory.builder()
.withIdentityMaterial(keyManager)
.withTrustMaterial(trustManager)
.build();
var sslContext = sslFactory.getSslContext();
var sslSocketFactory = sslFactory.getSslSocketFactory();回到您的主要问题,使用上面的代码片段,不需要从pem文件创建keystore对象。它将在幕后处理这一点,并将其映射到一个KeyManager实例。
https://stackoverflow.com/questions/12501117
复制相似问题