Программно получить KeyStore из PEM
Как программно получить KeyStore из файла PEM, содержащего как сертификат, так и закрытый ключ? Я пытаюсь предоставить сертификат клиента серверу, подключенному по протоколу HTTPS. Я подтвердил, что сертификат клиента работает, если я использую openssl и keytool для получения файла jks, который я загружаю динамически. Я даже могу заставить его работать, динамически читая файл p12 (PKCS12).
Я изучаю использование класса PEMReader из BouncyCastle, но не могу обойти некоторые ошибки. Я запускаю Java-клиент с опцией -Djavax.net.debug=all и веб-сервер Apache с отладочным LogLevel. Я не уверен, что искать, хотя. Журнал ошибок 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();
2 ответа
Я понял. Проблема в том, что сертификат X509 сам по себе недостаточен. Мне нужно было также поместить закрытый ключ в динамически сгенерированное хранилище ключей. Не похоже, что BouncyCastle PEMReader может обрабатывать PEM-файл как с сертификатом, так и с закрытым ключом одновременно, но он может обрабатывать каждый фрагмент отдельно. Я могу сам прочитать PEM в память и разбить его на два отдельных потока, а затем передать каждый из них в отдельный PEMReader. Поскольку я знаю, что файлы PEM, с которыми я имею дело, будут иметь сначала сертификат, а второй - закрытый ключ, я могу упростить код за счет надежности. Я также знаю, что разделитель END CERTIFICATE всегда будет окружен пятью дефисами. Реализация, которая работает для меня:
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));
}
Хотя ответ Райана работает хорошо, я хочу предоставить альтернативу другим разработчикам, поскольку в прошлом я сталкивался с аналогичной проблемой, когда мне также нужно было обрабатывать зашифрованные закрытые ключи в формате 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. Он позаботится об этом под обложками и сопоставит его с экземпляром KeyManager.