Проверка подлинности сертификата клиента Java HTTPS
Я довольно новичок в HTTPS/SSL/TLS
и я немного смущен тем, что именно клиенты должны представлять при аутентификации с помощью сертификатов.
Я пишу Java -клиент, который должен сделать простой POST
данных для конкретного URL
, Эта часть работает нормально, единственная проблема заключается в том, что это должно быть сделано за HTTPS
, HTTPS
часть довольно проста в обращении (либо с HTTPclient
или используя встроенный Java HTTPS
поддержка), но я застрял на аутентификации с помощью клиентских сертификатов. Я заметил, что здесь уже есть очень похожий вопрос, который я еще не пробовал с моим кодом (сделаю это достаточно скоро). Моя текущая проблема заключается в том, что - что бы я ни делал - клиент Java никогда не отправляет сертификат (я могу проверить это с PCAP
отвалы).
Я хотел бы знать, что именно клиент должен представить серверу при аутентификации с помощью сертификатов (особенно для Java - если это вообще имеет значение)? Это JKS
файл или PKCS#12
? Что должно быть в них; просто сертификат клиента или ключ? Если да, то какой ключ? Существует много путаницы по поводу всех видов файлов, типов сертификатов и тому подобного.
Как я уже говорил, я новичок в HTTPS/SSL/TLS
поэтому я был бы признателен за некоторую справочную информацию (это не должно быть эссе; я остановлюсь на ссылках на хорошие статьи).
6 ответов
Наконец-то удалось решить все вопросы, поэтому я отвечу на свой вопрос. Это настройки / файлы, которые я использовал для решения своих проблем;
Хранилище ключей клиента - это файл формата PKCS#12, содержащий
- Публичный сертификат клиента (в данном случае подписанный самозаверяющим центром сертификации)
- Закрытый ключ клиента
Для генерации я использовал OpenSSL pkcs12
команда, например;
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever"
Совет: убедитесь, что вы получили последнюю версию OpenSSL, а не версию 0.9.8h, потому что, похоже, она страдает от ошибки, которая не позволяет вам правильно генерировать файлы PKCS#12.
Этот файл PKCS#12 будет использоваться клиентом Java для представления сертификата клиента серверу, когда сервер явно запросил у клиента аутентификацию. См. Статью в Википедии о TLS для обзора того, как на самом деле работает протокол для аутентификации сертификата клиента (здесь также объясняется, почему нам нужен закрытый ключ клиента).
Склад доверенных сертификатов клиента - это прямой файл формата JKS, содержащий корневые или промежуточные сертификаты CA. Эти сертификаты CA будут определять, с какими конечными точками вам будет разрешено общаться, в этом случае ваш клиент сможет подключиться к тому серверу, на котором будет представлен сертификат, подписанный одним из CA доверенного хранилища.
Для его генерации вы можете использовать стандартный Java keytool, например;
keytool -genkey -dname "cn=CLIENT" -alias truststorekey -keyalg RSA -keystore ./client-truststore.jks -keypass whatever -storepass whatever
keytool -import -keystore ./client-truststore.jks -file myca.crt -alias myca
Используя это доверенное хранилище, ваш клиент попытается выполнить полное рукопожатие SSL со всеми серверами, которые предоставляют сертификат, подписанный ЦС, идентифицированным myca.crt
,
Приведенные выше файлы предназначены исключительно для клиента. Если вы также хотите настроить сервер, ему нужны собственные файлы хранилища ключей и доверенных сертификатов. На этом веб-сайте можно найти отличное руководство по настройке полностью работающего примера для клиента и сервера Java (с использованием Tomcat).
Проблемы / Замечания / Советы
- Аутентификация сертификата клиента может быть применена только сервером.
- (Важно!) Когда сервер запрашивает сертификат клиента (как часть рукопожатия TLS), он также предоставляет список доверенных ЦС как часть запроса сертификата. Когда сертификат клиента, который вы хотите представить для аутентификации, не подписан ни одним из этих ЦС, он вообще не будет представлен (на мой взгляд, это странное поведение, но я уверен, что для этого есть причина). Это было основной причиной моих проблем, поскольку другая сторона не настроила свой сервер должным образом для принятия моего самозаверяющего клиентского сертификата, и мы предположили, что проблема была в моем конце в том, что я не правильно предоставил клиентский сертификат в запросе.
- Получить Wireshark. Он имеет отличный анализ пакетов SSL/HTTPS и будет очень полезен при отладке и поиске проблемы. Это похоже на
-Djavax.net.debug=ssl
но он более структурирован и (возможно) легче интерпретируется, если вам неудобны результаты отладки Java SSL. Вполне возможно использовать библиотеку Apache httpclient. Если вы хотите использовать httpclient, просто замените целевой URL-адрес на HTTPS-эквивалент и добавьте следующие аргументы JVM (которые одинаковы для любого другого клиента, независимо от библиотеки, которую вы хотите использовать для отправки / получения данных по HTTP/HTTPS):
-Djavax.net.debug=ssl -Djavax.net.ssl.keyStoreType=pkcs12 -Djavax.net.ssl.keyStore=client.p12 -Djavax.net.ssl.keyStorePassword=whatever -Djavax.net.ssl.trustStoreType=jks -Djavax.net.ssl.trustStore=client-truststore.jks -Djavax.net.ssl.trustStorePassword=whatever
Другие ответы показывают, как глобально настроить клиентские сертификаты. Однако, если вы хотите программно определить клиентский ключ для одного конкретного соединения, а не глобально определять его для каждого приложения, работающего на вашей JVM, то вы можете настроить свой собственный SSLContext следующим образом:
String keyPassphrase = "";
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("cert-key-pair.pfx"), keyPassphrase.toCharArray());
SSLContext sslContext = SSLContexts.custom()
.loadKeyMaterial(keyStore, null)
.build();
HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
HttpResponse response = httpClient.execute(new HttpGet("https://example.com"));
Их JKS-файл является просто контейнером для сертификатов и пар ключей. В сценарии аутентификации на стороне клиента различные части ключей будут расположены здесь:
- Хранилище клиента будет содержать пару из личного и открытого ключей клиента. Это называется хранилище ключей.
- Хранилище сервера будет содержать открытый ключ клиента. Это называется доверенное хранилище.
Разделение склада доверенных сертификатов и хранилища ключей не обязательно, но рекомендуется. Они могут быть одним и тем же физическим файлом.
Чтобы задать расположение файловой системы двух хранилищ, используйте следующие системные свойства:
-Djavax.net.ssl.keyStore=clientsidestore.jks
и на сервере:
-Djavax.net.ssl.trustStore=serversidestore.jks
Чтобы экспортировать сертификат клиента (открытый ключ) в файл, чтобы вы могли скопировать его на сервер, используйте
keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks
Чтобы импортировать открытый ключ клиента в хранилище ключей сервера, используйте (как упоминалось выше, администраторы сервера уже сделали это)
keytool -import -file publicclientkey.cer -store serversidestore.jks
Maven pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>some.examples</groupId>
<artifactId>sslcliauth</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>sslcliauth</name>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.4</version>
</dependency>
</dependencies>
</project>
Java-код:
package some.examples;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.InputStreamEntity;
public class SSLCliAuthExample {
private static final Logger LOG = Logger.getLogger(SSLCliAuthExample.class.getName());
private static final String CA_KEYSTORE_TYPE = KeyStore.getDefaultType(); //"JKS";
private static final String CA_KEYSTORE_PATH = "./cacert.jks";
private static final String CA_KEYSTORE_PASS = "changeit";
private static final String CLIENT_KEYSTORE_TYPE = "PKCS12";
private static final String CLIENT_KEYSTORE_PATH = "./client.p12";
private static final String CLIENT_KEYSTORE_PASS = "changeit";
public static void main(String[] args) throws Exception {
requestTimestamp();
}
public final static void requestTimestamp() throws Exception {
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
createSslCustomContext(),
new String[]{"TLSv1"}, // Allow TLSv1 protocol only
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(csf).build()) {
HttpPost req = new HttpPost("https://changeit.com/changeit");
req.setConfig(configureRequest());
HttpEntity ent = new InputStreamEntity(new FileInputStream("./bytes.bin"));
req.setEntity(ent);
try (CloseableHttpResponse response = httpclient.execute(req)) {
HttpEntity entity = response.getEntity();
LOG.log(Level.INFO, "*** Reponse status: {0}", response.getStatusLine());
EntityUtils.consume(entity);
LOG.log(Level.INFO, "*** Response entity: {0}", entity.toString());
}
}
}
public static RequestConfig configureRequest() {
HttpHost proxy = new HttpHost("changeit.local", 8080, "http");
RequestConfig config = RequestConfig.custom()
.setProxy(proxy)
.build();
return config;
}
public static SSLContext createSslCustomContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException {
// Trusted CA keystore
KeyStore tks = KeyStore.getInstance(CA_KEYSTORE_TYPE);
tks.load(new FileInputStream(CA_KEYSTORE_PATH), CA_KEYSTORE_PASS.toCharArray());
// Client keystore
KeyStore cks = KeyStore.getInstance(CLIENT_KEYSTORE_TYPE);
cks.load(new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASS.toCharArray());
SSLContext sslcontext = SSLContexts.custom()
//.loadTrustMaterial(tks, new TrustSelfSignedStrategy()) // use it to customize
.loadKeyMaterial(cks, CLIENT_KEYSTORE_PASS.toCharArray()) // load client certificate
.build();
return sslcontext;
}
}
Учитывая файл p12 с сертификатом и закрытым ключом (например, сгенерированный openssl), следующий код будет использовать его для определенного HttpsURLConnection:
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(new FileInputStream(keyStorePath), keystorePassword.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, keystorePassword.toCharArray());
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), null, null);
SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslSocketFactory);
В SSLContext
для инициализации требуется некоторое время, поэтому вы можете захотеть кэшировать его.
Для тех из вас, кто просто хочет настроить двустороннюю аутентификацию (серверные и клиентские сертификаты), комбинация этих двух ссылок поможет вам:
Настройка двусторонней аутентификации:
https://linuxconfig.org/apache-web-server-ssl-authentication
Вам не нужно использовать конфигурационный файл openssl, который они упоминают; просто используйте
$ openssl genrsa -des3 -out ca.key 4096
$ openssl req -new -x509 -days 365 -key ca.key -out ca.crt
создать свой собственный сертификат CA, а затем сгенерировать и подписать ключи сервера и клиента с помощью:
$ openssl genrsa -des3 -out server.key 4096
$ openssl req -new -key server.key -out server.csr
$ openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt
а также
$ openssl genrsa -des3 -out client.key 4096
$ openssl req -new -key client.key -out client.csr
$ openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt
В остальном следуйте инструкциям по ссылке. Управление сертификатами для Chrome работает так же, как в примере для Firefox, который упоминается.
Далее настройте сервер через:
Обратите внимание, что вы уже создали серверы.crt и.key, поэтому вам больше не нужно делать этот шаг.
Есть лучший способ, чем вручную переходить по URL-адресу https://, зная, какую кнопку нажимать в каком браузере, зная, где и как сохранить файл "сертификата", и, наконец, зная волшебное заклинание для keytool, чтобы установить его локально..
Просто сделай это:
- Сохраните код ниже в InstallCert.java
- Откройте командную строку и выполните:
javac InstallCert.java
- Беги как:
java InstallCert <host>[:port] [passphrase]
(порт и кодовая фраза необязательны)
Вот код для InstallCert, обратите внимание на год в заголовке, потребуется изменить некоторые части для "более поздних" версий java:
/*
* Copyright 2006 Sun Microsystems, Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of Sun Microsystems nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;
public class InstallCert {
public static void main(String[] args) throws Exception {
String host;
int port;
char[] passphrase;
if ((args.length == 1) || (args.length == 2)) {
String[] c = args[0].split(":");
host = c[0];
port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
String p = (args.length == 1) ? "changeit" : args[1];
passphrase = p.toCharArray();
} else {
System.out.println("Usage: java InstallCert <host>[:port] [passphrase]");
return;
}
File file = new File("jssecacerts");
if (file.isFile() == false) {
char SEP = File.separatorChar;
File dir = new File(System.getProperty("java.home") + SEP
+ "lib" + SEP + "security");
file = new File(dir, "jssecacerts");
if (file.isFile() == false) {
file = new File(dir, "cacerts");
}
}
System.out.println("Loading KeyStore " + file + "...");
InputStream in = new FileInputStream(file);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, passphrase);
in.close();
SSLContext context = SSLContext.getInstance("TLS");
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
X509TrustManager defaultTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
context.init(null, new TrustManager[] {tm}, null);
SSLSocketFactory factory = context.getSocketFactory();
System.out.println("Opening connection to " + host + ":" + port + "...");
SSLSocket socket = (SSLSocket)factory.createSocket(host, port);
socket.setSoTimeout(10000);
try {
System.out.println("Starting SSL handshake...");
socket.startHandshake();
socket.close();
System.out.println();
System.out.println("No errors, certificate is already trusted");
} catch (SSLException e) {
System.out.println();
e.printStackTrace(System.out);
}
X509Certificate[] chain = tm.chain;
if (chain == null) {
System.out.println("Could not obtain server certificate chain");
return;
}
BufferedReader reader =
new BufferedReader(new InputStreamReader(System.in));
System.out.println();
System.out.println("Server sent " + chain.length + " certificate(s):");
System.out.println();
MessageDigest sha1 = MessageDigest.getInstance("SHA1");
MessageDigest md5 = MessageDigest.getInstance("MD5");
for (int i = 0; i < chain.length; i++) {
X509Certificate cert = chain[i];
System.out.println
(" " + (i + 1) + " Subject " + cert.getSubjectDN());
System.out.println(" Issuer " + cert.getIssuerDN());
sha1.update(cert.getEncoded());
System.out.println(" sha1 " + toHexString(sha1.digest()));
md5.update(cert.getEncoded());
System.out.println(" md5 " + toHexString(md5.digest()));
System.out.println();
}
System.out.println("Enter certificate to add to trusted keystore or 'q' to quit: [1]");
String line = reader.readLine().trim();
int k;
try {
k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;
} catch (NumberFormatException e) {
System.out.println("KeyStore not changed");
return;
}
X509Certificate cert = chain[k];
String alias = host + "-" + (k + 1);
ks.setCertificateEntry(alias, cert);
OutputStream out = new FileOutputStream("jssecacerts");
ks.store(out, passphrase);
out.close();
System.out.println();
System.out.println(cert);
System.out.println();
System.out.println
("Added certificate to keystore 'jssecacerts' using alias '"
+ alias + "'");
}
private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
private static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 3);
for (int b : bytes) {
b &= 0xff;
sb.append(HEXDIGITS[b >> 4]);
sb.append(HEXDIGITS[b & 15]);
sb.append(' ');
}
return sb.toString();
}
private static class SavingTrustManager implements X509TrustManager {
private final X509TrustManager tm;
private X509Certificate[] chain;
SavingTrustManager(X509TrustManager tm) {
this.tm = tm;
}
public X509Certificate[] getAcceptedIssuers() {
throw new UnsupportedOperationException();
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
throw new UnsupportedOperationException();
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
this.chain = chain;
tm.checkServerTrusted(chain, authType);
}
}
}
Я подключился к банку с двусторонним SSL (сертификат клиента и сервера) с помощью Spring Boot. Итак, опишите здесь все мои шаги, надеюсь, это кому-то поможет (самое простое рабочее решение, которое я нашел):
Сгенерируйте запрос на сертификат:
Сгенерировать закрытый ключ:
openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
Сгенерировать запрос на сертификат:
openssl req -new -key user.key -out user.csr -passin pass:MY_PASSWORD
Хранить
user.key
(и пароль) и отправить запрос на сертификатuser.csr
в банк для моего сертификатаПолучите 2 сертификата: корневой сертификат моего клиента
clientId.crt
и корневой сертификат банка:bank.crt
Создайте хранилище ключей Java (введите пароль ключа и установите пароль хранилища ключей):
openssl pkcs12 -export -in clientId.crt -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root
Не обращайте внимания на вывод:
unable to write 'random state'
. Java PKCS12keystore.p12
создан.Добавить в хранилище ключей
bank.crt
(для простоты я использовал одно хранилище ключей):keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops
Проверьте сертификаты хранилища ключей:
keytool -list -keystore keystore.p12
Готов к Java-коду:) Я использовал Spring Boot
RestTemplate
с добавлениемorg.apache.httpcomponents.httpcore
зависимость:@Bean("sslRestTemplate") public RestTemplate sslRestTemplate() throws Exception { char[] storePassword = appProperties.getSslStorePassword().toCharArray(); URL keyStore = new URL(appProperties.getSslStore()); SSLContext sslContext = new SSLContextBuilder() .loadTrustMaterial(keyStore, storePassword) // use storePassword twice (with key password do not work)!! .loadKeyMaterial(keyStore, storePassword, storePassword) .build(); // Solve "Certificate doesn't match any of the subject alternative names" SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(socketFactory).build(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client); RestTemplate restTemplate = new RestTemplate(factory); // restTemplate.setMessageConverters(List.of(new Jaxb2RootElementHttpMessageConverter())); return restTemplate; }
Я думаю, что исправлением здесь был тип хранилища ключей, у pkcs12(pfx) всегда есть закрытый ключ, а тип JKS может существовать без закрытого ключа. Если вы не укажете в своем коде или не выберете сертификат через браузер, сервер не сможет узнать, что он представляет клиента на другом конце.