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

222

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

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

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

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

tmbrggmn
источник
Я дал два сертификата от клиента, как определить, какой из них нужно добавить в хранилище ключей и хранилище доверенных сертификатов. Не могли бы вы помочь выявить эту проблему, поскольку вы уже сталкивались с подобным видом проблемы, которую я поднял, фактически не зная, что делать stackoverflow .com / questions / 61374276 /…
Генрихарль

Ответы:

233

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

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

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

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

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

Для его генерации вы можете использовать стандартный 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), он также предоставляет список доверенных 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
tmbrggmn
источник
6
«Когда сертификат клиента, который вы хотите представить для аутентификации, не подписан ни одним из этих ЦС, он вообще не будет представлен». Сертификаты не представлены, потому что клиент знает, что они не будут приняты сервером. Кроме того, ваш сертификат может быть подписан промежуточным центром сертификации «ICA», и сервер может предоставить вашему клиенту корневой центр сертификации «RCA», и ваш веб-браузер все равно позволит вам выбрать сертификат, даже если он подписан ICA, а не RCA.
KyleM
2
В качестве примера приведенного выше комментария рассмотрим ситуацию, когда у вас есть один корневой CA (RCA1) и два промежуточных CA (ICA1 и ICA2). В Apache Tomcat, если вы импортируете RCA1 в хранилище доверенных сертификатов, ваш веб-браузер представит ВСЕ сертификаты, подписанные ICA1 и ICA2, даже если их нет в вашем хранилище доверенных сертификатов. Это потому, что цепочка имеет значение, а не отдельные сертификаты.
KyleM
2
«На мой взгляд, это странное поведение, но я уверен, что есть причина для этого». Причина в том, что это то, что говорится в RFC 2246. Ничего странного в этом нет. Разрешение клиентам представлять сертификаты, которые не будут приняты сервером, - это то, что было бы странно, и полная трата времени и пространства.
Маркиз Лорн
1
Я бы настоятельно рекомендовал не использовать единственное хранилище ключей JVM (и хранилище доверенных сертификатов!), Как в примере выше. Настройка одного соединения более безопасна и более гибка, но требует, чтобы вы написали немного больше кода. Вы должны настроить SSLContextкак в ответе @ Магнуса.
Кристофер Шульц
63

Другие ответы показывают, как глобально настроить клиентские сертификаты. Однако если вы хотите программно определить ключ клиента для одного конкретного соединения, а не глобально определять его для каждого приложения, работающего на вашей 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"));
Магнус
источник
Я должен был использовать sslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();. Я не мог заставить это работать loadKeyMaterial(...).
Конор Свенссон
4
Материал @ConorSvensson Trust предназначен для клиента, доверяющего сертификату удаленного сервера, материал ключа - для сервера, доверяющего клиенту.
Магнус
1
Мне очень нравится этот краткий и точный ответ. На случай, если люди заинтересуются, я приведу здесь рабочий пример с инструкциями по сборке. stackoverflow.com/a/46821977/154527
Ален О'Ди
3
Ты спас мой день! Только одно дополнительное изменение, которое я должен был сделать, отправьте пароль в loadKeyMaterial(keystore, keyPassphrase.toCharArray())коде, а также!
AnandShanbhag
1
@peterh Да, это специфично для Apache http. Каждая HTTP-библиотека будет иметь свой собственный способ настройки, но большинство из них должны каким-то образом использовать SSLContext.
Магнус
30

Их 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
mhaller
источник
Я должен, вероятно, упомянуть, что я не имею никакого контроля над сервером. Сервер импортировал наш публичный сертификат. Администраторы этой системы сказали мне, что мне нужно явно предоставить сертификат, чтобы его можно было отправить во время рукопожатия (их сервер явно запрашивает это).
tmbrggmn
Вам понадобятся открытый и закрытый ключи для вашего открытого сертификата (того, который известен серверу) в виде файла JKS.
sfussenegger
Спасибо за пример кода. В приведенном выше коде, что такое «mykey-public.cer»? Это публичный сертификат клиента (мы используем самозаверяющие сертификаты)?
tmbrggmn
Да, я переименовал файлы в фрагментах кода соответственно. Я надеюсь, что это ясно дает понять.
Малхлер
Правильно, спасибо. Я все время путаюсь, потому что, очевидно, «ключ» и «сертификат» используются взаимозаменяемо.
tmbrggmn
10

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

}
kinjelom
источник
Если вы предпочитаете, чтобы сертификат был доступен для всех приложений, использующих конкретную установку JVM, вместо этого следуйте этому ответу .
ADTC
метод configureRequest()для установки прокси клиентского проекта правильно?
Шариф
да, это конфигурация клиента http и в этом случае это конфигурация прокси
kinjelom
может у меня тупой вопрос, но как мне создать файл cacert.jks? У меня есть исключение Исключение в потоке "main" java.io.FileNotFoundException:. \ Cacert.jks (система не может найти указанный файл)
Patlatus
6

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

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

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, поэтому вам больше не нужно делать этот шаг.

ганс
источник
1
Подумайте , у вас есть опечатка в сервере этапе генерации CSR: следует использовать server.keyнеclient.key
the.Legend
0

Я подключился к банку по двустороннему протоколу SSL (сертификат клиента и сервера) с помощью Spring Boot. Итак, опишите здесь все мои шаги, надеюсь, это кому-нибудь поможет (самое простое рабочее решение, которое я нашел):

  1. Генерация запроса сертификата:

    • Создать закрытый ключ:

      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. Получите 2 сертификата: мой клиентский корневой сертификат clientId.crtи банковский корневой сертификат:bank.crt

  3. Создать хранилище ключей 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 PKCS12 keystore.p12создан.

  4. Добавьте в хранилище ключей bank.crt(для простоты я использовал одно хранилище ключей):

    keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops

    Проверьте сертификаты хранилища ключей по:

    keytool -list -keystore keystore.p12
  5. Готов к Java-коду :) Я использовал Spring Boot RestTemplateс org.apache.httpcomponents.httpcoreзависимостью add :

    @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;
    }
Григорий Кислин
источник
Вы можете сделать все это с помощью keytool. В этом нет необходимости для OpenSSL.
Маркиз Лорн
0

Учитывая файл 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инициализации занимает некоторое время, поэтому вы можете его кэшировать.

Йоханнес Бродуолл
источник
-1

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

Оби-Ван Кеноби
источник
1
Формат PKCS12 традиционно использовался для privatekey-AND-cert, но Java с 8 в 2014 году (более чем за год до этого ответа) поддерживала PKCS12, содержащую cert (s) без privatekey (s). Независимо от формата хранилища ключей для аутентификации клиента требуется privatekey-AND-cert. Я не понимаю ваше второе предложение, но Java-клиент может автоматически выбирать клиентские сертификаты и ключи, если хотя бы одна подходящая запись доступна или менеджер ключей может быть настроен для использования указанной.
dave_thompson_085
Ваше второе предложение совершенно неверно. Сервер предоставляет доверенных подписчиков, а клиент либо умирает, либо не предоставляет сертификат, который удовлетворяет этому ограничению. Автоматически, а не через «в вашем коде». То есть сервера «способ узнать , что представляет интересы клиента».
Маркиз Лорн