it-swarm.com.ru

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

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

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

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

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

200
tmbrggmn

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

Хранилище ключей client - это файл формата PKCS # 12, содержащий

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

Для его генерации я использовал, например, команду OpenSSL pkcs12;

openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever"

Совет: убедитесь, что у вас установлена ​​последняя версия OpenSSL, not, версия 0.9.8h, потому что она, похоже, страдает от ошибки, которая не позволяет правильно генерировать файлы PKCS # 12.

Этот файл PKCS # 12 будет использоваться клиентом Java для представления сертификата клиента серверу, когда сервер явно запросил у клиента аутентификацию. Смотрите статья в Википедии о TLS , чтобы узнать, как на самом деле работает протокол для аутентификации сертификата клиента (здесь также объясняется, зачем нам нужен закрытый ключ клиента).

Хранилище доверенных сертификатов client - это прямой файл формата JKS, содержащий root или промежуточные сертификаты 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) можно найти на этот веб-сайт .

Вопросы/Замечания/Советы

  1. Проверка подлинности сертификата клиента может выполняться только сервером.
  2. (Важно!) Когда сервер запрашивает сертификат клиента (как часть рукопожатия TLS), он также предоставляет список доверенных ЦС как часть запроса сертификата. Когда сертификат клиента, который вы хотите представить для аутентификации, not не подписан одним из этих CA, он вообще не будет представлен (на мой взгляд, это странное поведение, но я уверен, что есть причина для Это). Это было основной причиной моих проблем, поскольку другая сторона не настроила свой сервер должным образом для принятия моего самозаверяющего клиентского сертификата, и мы предположили, что проблема была в моем конце в том, что я не правильно предоставил клиентский сертификат в запросе.
  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
210
tmbrggmn

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

Их JKS-файл является просто контейнером для сертификатов и пар ключей В сценарии аутентификации на стороне клиента различные части ключей будут расположены здесь:

  • Хранилище client будет содержать пару private и public key клиента. Это называется хранилище ключей .
  • Хранилище server будет содержать ключ клиента public . Это называется truststore .

Разделение склада доверенных сертификатов и хранилища ключей не обязательно, но рекомендуется. Они могут быть одним и тем же физическим файлом.

Чтобы установить расположение файловой системы двух хранилищ, используйте следующие системные свойства:

-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
30
mhaller

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

}
9
wildloop

Для тех из вас, кто просто хочет настроить двустороннюю аутентификацию (серверные и клиентские сертификаты), комбинация этих двух ссылок поможет вам: 

Настройка двусторонней аутентификации:

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, который упоминается. 

Далее настройте сервер через: 

https://www.digitalocean.com/community/tutorials/how-to-create-a-ssl-certificate-on-Apache-for-ubuntu-14-04

Обратите внимание, что вы уже создали серверы .crt и .key, поэтому вам больше не нужно делать этот шаг. 

7
hans

Я думаю, что исправлением здесь был тип хранилища ключей, у pkcs12 (pfx) всегда есть закрытый ключ, а тип JKS может существовать без закрытого ключа. Если вы не укажете в своем коде или не выберете сертификат через браузер, сервер не сможет узнать, что он представляет клиента на другом конце.

0
ObiWanKenobi