Аутентифікація клієнтського сертифікату Java HTTPS


222

Я досить новачок HTTPS/SSL/TLSі трохи розгублений щодо того, що саме повинні представити клієнти при автентифікації сертифікатів.

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

Мені хотілося б знати, що саме клієнт повинен представляти серверу при автентифікації сертифікатів (спеціально для Java - якщо це взагалі має значення)? Це JKSфайл чи PKCS#12? Що повинно бути в них; просто сертифікат клієнта чи ключ? Якщо так, то який ключ? Існує досить багато плутанини щодо всіх різних типів файлів, типів сертифікатів тощо.

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


я дав два сертифікати від клієнта, як визначити, що потрібно додати в сховище ключів та довіру, чи можете ви допомогти визначити цю проблему, оскільки ви вже пройшли подібний випуск, це питання я підняв насправді, не розуміючи, що робити stackoverflow .com / questions / 61374276 /…
henrycharles

Відповіді:


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

6
"Якщо клієнтський сертифікат, який ви хочете представити для аутентифікації, не підписується одним із цих ЦА, він взагалі не буде представлений". Сертифікати не представлені, оскільки клієнт знає, що сервер їх не прийняв. Крім того, ваш сертифікат може бути підписаний проміжним CA "ICA", і сервер може представити вашому клієнту кореневий CA "RCA", і ваш веб-браузер все одно дозволить вам вибрати ваш сертифікат, навіть якщо він підписаний ICA, а не RCA.
KyleM

2
Як приклад вищезазначеного коментаря, розгляньте ситуацію, коли у вас є один кореневий CA (RCA1) та два проміжних CA (ICA1 та ICA2). Якщо ви імпортуєте RCA1 у магазин довіри Apache Tomcat, ваш веб-браузер представить ВСІ сертифікати, підписані 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 призначений для того, щоб клієнт довіряв сертифікату віддаленого сервера, ключовий матеріал - для того, щоб сервер довіряв клієнту.
Магнус

1
Мені дуже подобається ця стисла та точна відповідь. У разі, якщо люди зацікавлені, я надаю тут відпрацьований приклад із інструкціями щодо побудови. stackoverflow.com/a/46821977/154527
Ален О'Деа

3
Ти врятував мій день! Ще одну додаткову зміну, яку мені довелося зробити, також надіслати пароль у loadKeyMaterial(keystore, keyPassphrase.toCharArray())коді!
AnandShanbhag

1
@peterh Так, це специфічно для http Apache. Кожна бібліотека 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

Я, мабуть, повинен зазначити, що я не маю контролю над сервером. Сервер імпортував наш загальнодоступний сертифікат. Адміни цієї системи мені сказали, що мені потрібно явно надати сертифікат, щоб його можна було надсилати під час рукостискання (їх сервер прямо вимагає цього).
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;
}

}

Якщо ви хочете мати сертифікат доступний для всіх програм, які використовують певну установку JVM, замість цього виконайте цю відповідь .
ADTC

configureRequest()правильний спосіб встановлення проксі клієнтського проекту?
shareef

так, це конфігурація клієнта 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залежністю:

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

Ви можете зробити все це за допомогою кнопок. У цьому взагалі немає необхідності для 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 (и) без privatekey (s). Незалежно від формату зберігання ключів, для автентифікації клієнта потрібен privatekey-AND-cert. Я не розумію ваше друге речення, але клієнт Java може автоматично вибрати клієнтський ключ і ключ, якщо принаймні одна відповідна запис доступна або керування ключами може бути налаштовано для використання вказаного.
dave_thompson_085

Ваше друге речення абсолютно невірне. Сервер надає своїх довірених підписників, а клієнт або помирає, або не надає сертифікат, який задовольняє це обмеження. Автоматично, не через "у своєму коді". Те є сервера «спосіб дізнатися , що являє інтереси клієнта».
Маркіз Лорн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.