Вопрос-ответ

Java HTTPS client certificate authentication

Проверка подлинности клиентского сертификата Java HTTPS

Я довольно новичок в HTTPS/SSL/TLS и я немного запутался в том, что именно клиенты должны представлять при аутентификации с помощью сертификатов.

Я пишу Java-клиент, которому необходимо выполнить простую POST передачу данных конкретному URL. Эта часть работает нормально, единственная проблема в том, что это должно быть сделано заново HTTPS. С HTTPS частью довольно легко справиться (либо с помощью HTTPclientвстроенной HTTPS поддержки Java), но я застрял на аутентификации с помощью клиентских сертификатов. Я заметил, что здесь уже есть очень похожий вопрос, который я еще не опробовал в своем коде (сделаю это достаточно скоро). Моя текущая проблема заключается в том, что - что бы я ни делал - клиент Java никогда не отправляет сертификат (я могу проверить это с помощью PCAP дампов).

Я хотел бы знать, что именно клиент должен предоставлять серверу при аутентификации с помощью сертификатов (особенно для Java - если это вообще имеет значение)? Это JKS файл, или PKCS#12? Что в них должно быть; просто клиентский сертификат или ключ? Если да, то какой ключ? Существует довольно большая путаница во всех различных типах файлов, типах сертификатов и тому подобном.

Как я уже говорил ранее, я новичок в HTTPS/SSL/TLS поэтому я также был бы признателен за некоторую справочную информацию (это не обязательно должно быть эссе; я довольствуюсь ссылками на хорошие статьи).

Переведено автоматически
Ответ 1

Наконец-то удалось решить все проблемы, поэтому я отвечу на свой собственный вопрос. Это настройки / файлы, которые я использовал для решения моей конкретной проблемы (проблем);

Хранилище ключей клиента представляет собой файл формата PKCS # 12, содержащий


  1. Общедоступный сертификат клиента (в данном случае подписанный самозаверяющим центром сертификации)

  2. Закрытый ключ клиента

Для его генерации я использовал команду 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 truststore.

Для его генерации вы можете использовать стандартный 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) можно найти на этом веб-сайте.

Проблемы / Замечания / Советы


  1. Аутентификация по клиентскому сертификату может быть принудительно реализована только сервером.

  2. (Важно!) Когда сервер запрашивает клиентский сертификат (как часть подтверждения TLS), он также предоставляет список доверенных центров сертификации как часть запроса сертификата. Когда клиентский сертификат, который вы хотите представить для проверки подлинности, не подписан одним из этих центров сертификации, он вообще не будет представлен (на мой взгляд, это странное поведение, но я уверен, что для этого есть причина). Это было основной причиной моих проблем, поскольку другая сторона не настроила свой сервер должным образом для приема моего самозаверяющего клиентского сертификата, и мы предположили, что проблема была с моей стороны из-за неправильного предоставления клиентского сертификата в запросе.

  3. Получите Wireshark. Он отлично анализирует пакеты SSL / HTTPS и окажет огромную помощь в отладке и поиске проблемы. Это похоже на -Djavax.net.debug=ssl, но более структурировано и (возможно) проще интерпретировать, если вам неудобен вывод отладки Java SSL.

  4. Вполне возможно использовать библиотеку 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

Ответ 2

В других ответах показано, как глобально настроить клиентские сертификаты. Однако, если вы хотите программно определить клиентский ключ для одного конкретного соединения, а не глобально определять его для каждого приложения, запущенного на вашей 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"));
Ответ 3

Файл 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
Ответ 4

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;
}

}
java