Autenticazione del certificato client HTTPS Java


222

Sono abbastanza nuovo HTTPS/SSL/TLSe sono un po 'confuso su ciò che i clienti devono presentare esattamente quando effettuano l'autenticazione con i certificati.

Sto scrivendo un client Java che deve fare un semplice POSTdato per un particolare URL. Quella parte funziona bene, l'unico problema è che dovrebbe essere finito HTTPS. La HTTPSparte è abbastanza facile da gestire (con HTTPcliento utilizzando il HTTPSsupporto integrato di Java ), ma sono bloccato sull'autenticazione con i certificati client. Ho notato che c'è già una domanda molto simile qui, che non ho ancora provato con il mio codice (lo farò abbastanza presto). Il mio problema attuale è che, qualunque cosa io faccia, il client Java non invia mai il certificato (posso verificarlo con i PCAPdump).

Vorrei sapere che cosa dovrebbe presentare esattamente il client al server quando esegue l'autenticazione con i certificati (in particolare per Java, se questo è importante)? È un JKSfile o PKCS#12? Cosa dovrebbe essere in loro; solo il certificato client o una chiave? In tal caso, quale chiave? C'è un po 'di confusione su tutti i diversi tipi di file, tipi di certificato e simili.

Come ho detto prima sono nuovo, HTTPS/SSL/TLSquindi apprezzerei anche alcune informazioni di base (non deve essere un saggio; mi accontenterò di collegamenti a buoni articoli).


ho ricevuto due certificati da parte del cliente su come identificare quale è necessario aggiungere nel keystore e nel truststore potresti aiutare a identificare questo problema poiché hai già affrontato un tipo simile di problema, questo problema che ho sollevato in realtà non ho idea di cosa fare stackoverflow .com / questions / 61374276 /…
henrycharles il

Risposte:


233

Finalmente sono riuscito a risolvere tutti i problemi, quindi risponderò alla mia domanda. Queste sono le impostazioni / i file che ho usato per gestire i miei particolari problemi;

Il keystore del client è un file in formato PKCS # 12 contenente

  1. Il certificato pubblico del cliente (in questo caso firmato da una CA autofirmata)
  2. La chiave privata del cliente

Per generarlo ho usato il pkcs12comando di OpenSSL , per esempio;

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

Suggerimento: assicurati di ottenere l'ultimo OpenSSL, non la versione 0.9.8h perché sembra che soffra di un bug che non ti consente di generare correttamente i file PKCS # 12.

Questo file PKCS # 12 verrà utilizzato dal client Java per presentare il certificato client al server quando il server ha esplicitamente richiesto al client di eseguire l'autenticazione. Vedi l' articolo di Wikipedia su TLS per una panoramica del funzionamento effettivo del protocollo per l'autenticazione del certificato client (spiega anche perché qui abbiamo bisogno della chiave privata del client).

Il truststore del client è un file in formato JKS semplice contenente i certificati CA principali o intermedi . Questi certificati CA determineranno con quali endpoint ti sarà consentito comunicare, in questo caso consentiranno al tuo client di connettersi a qualsiasi server che presenta un certificato che è stato firmato da una delle CA del truststore.

Per generarlo puoi usare il keytool Java standard, per esempio;

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

Utilizzando questo truststore, il client proverà a eseguire una stretta di mano SSL completa con tutti i server che presentano un certificato firmato dalla CA identificata da myca.crt.

I file sopra sono esclusivamente per il client. Quando si desidera impostare anche un server, il server necessita dei propri file di chiavi e truststore. In questo sito Web è disponibile una guida dettagliata per l'impostazione di un esempio pienamente funzionante sia per un client che per un server Java (utilizzando Tomcat) .

Problemi / Osservazioni / Consigli

  1. L'autenticazione del certificato client può essere applicata solo dal server.
  2. ( Importante! ) Quando il server richiede un certificato client (come parte dell'handshake TLS), fornirà anche un elenco di CA affidabili come parte della richiesta di certificato. Quando il certificato client che si desidera presentare per l'autenticazione non è firmato da una di queste autorità di certificazione , non verrà affatto presentato (a mio avviso, si tratta di un comportamento strano, ma sono sicuro che ci sia una ragione). Questa era la causa principale dei miei problemi, poiché l'altra parte non aveva configurato correttamente il proprio server per accettare il mio certificato client autofirmato e ritenevamo che il problema fosse alla mia fine per non aver fornito correttamente il certificato client nella richiesta.
  3. Ottieni Wireshark. Ha un'eccellente analisi dei pacchetti SSL / HTTPS e sarà di grande aiuto per il debug e la ricerca del problema. È simile -Djavax.net.debug=sslma è più strutturato e (probabilmente) più facile da interpretare se non ti senti a tuo agio con l'output di debug SSL Java.
  4. È perfettamente possibile utilizzare la libreria httpclient di Apache. Se si desidera utilizzare httpclient, è sufficiente sostituire l'URL di destinazione con l'equivalente HTTPS e aggiungere i seguenti argomenti JVM (che sono gli stessi per qualsiasi altro client, indipendentemente dalla libreria che si desidera utilizzare per inviare / ricevere dati su 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
"Quando il certificato client che si desidera presentare per l'autenticazione non è firmato da una di queste autorità di certificazione, non verrà affatto presentato". I certificati non vengono presentati perché il client sa che non saranno accettati dal server. Inoltre, il tuo certificato può essere firmato da una CA "ICA" intermedia e il server può presentare al tuo client la CA "RCA" principale e il tuo browser web ti consentirà comunque di scegliere il tuo certificato anche se è firmato da ICA e non da RCA.
KyleM,

2
Come esempio del commento precedente, considera una situazione in cui hai una CA principale (RCA1) e due CA intermedie (ICA1 e ICA2). Su Apache Tomcat se si importa RCA1 nel truststore, il browser Web presenterà TUTTI i certificati firmati da ICA1 e ICA2, anche se non sono presenti nel truststore. Questo perché è la catena che conta non i singoli certificati.
KyleM,

2
"secondo me, questo è un comportamento strano, ma sono sicuro che ci sia una ragione per questo". Il motivo è che è quello che dice RFC 2246. Niente di strano. Consentire ai clienti di presentare certificati che non saranno accettati dal server è ciò che sarebbe strano e una completa perdita di tempo e spazio.
Marchese di Lorne,

1
Consiglio vivamente di non utilizzare un singolo keystore (e truststore) per tutta la JVM, come nell'esempio sopra. La personalizzazione di una singola connessione è più sicura e flessibile, ma richiede di scrivere un po 'più di codice. Devi personalizzare SSLContextcome nella risposta di @ Magnus.
Christopher Schultz,

63

Altre risposte mostrano come configurare globalmente i certificati client. Tuttavia, se si desidera definire a livello di codice la chiave client per una determinata connessione, piuttosto che definirla globalmente su ogni applicazione in esecuzione sulla propria JVM, è possibile configurare il proprio SSLContext in questo modo:

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"));

Ho dovuto usare sslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();. Non riuscivo a farlo funzionare loadKeyMaterial(...).
Conor Svensson,

4
Il materiale @ConorSvensson Trust è destinato al client che si fida del certificato del server remoto, il materiale chiave è destinato al server che si fida del client.
Magnus,

1
Mi piace molto questa risposta concisa e puntuale. Nel caso in cui le persone fossero interessate, fornisco qui un esempio funzionante con le istruzioni di costruzione. stackoverflow.com/a/46821977/154527
Alain O'Dea,

3
Mi hai salvato la giornata! Solo un ulteriore cambiamento che ho dovuto fare, inviare anche la password nel loadKeyMaterial(keystore, keyPassphrase.toCharArray())codice!
AnandShanbhag il

1
@peterh Sì, è specifico per http di Apache. Ogni libreria HTTP avrà il proprio modo di essere configurata, ma la maggior parte di essi dovrebbe in qualche modo utilizzare un SSLContext.
Magnus,

30

Il file JKS è solo un contenitore per certificati e coppie di chiavi. In uno scenario di autenticazione lato client, le varie parti delle chiavi si trovano qui:

  • Il client negozio 's conterrà del cliente privato e pubblico coppia di chiavi. Si chiama keystore .
  • L' archivio del server conterrà la chiave pubblica del client . Si chiama truststore .

La separazione di truststore e keystore non è obbligatoria ma consigliata. Possono essere lo stesso file fisico.

Per impostare le posizioni dei filesystem dei due archivi, utilizzare le seguenti proprietà di sistema:

-Djavax.net.ssl.keyStore=clientsidestore.jks

e sul server:

-Djavax.net.ssl.trustStore=serversidestore.jks

Per esportare il certificato del client (chiave pubblica) in un file, in modo da poterlo copiare sul server, utilizzare

keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks

Per importare la chiave pubblica del client nel keystore del server, utilizzare (come indicato nel poster, questo è già stato fatto dagli amministratori del server)

keytool -import -file publicclientkey.cer -store serversidestore.jks

Dovrei probabilmente menzionare che non ho alcun controllo sul server. Il server ha importato il nostro certificato pubblico. Gli amministratori di quel sistema mi hanno detto che devo fornire esplicitamente il certificato in modo che possa essere inviato durante l'handshake (il loro server lo richiede esplicitamente).
tmbrggmn,

Per il tuo certificato pubblico (quello noto al server) avrai bisogno di chiavi pubbliche e private come file JKS.
sfussenegger,

Grazie per il codice di esempio. Nel codice sopra, che cos'è esattamente "mykey-public.cer"? È questo il certificato pubblico del cliente (utilizziamo certificati autofirmati)?
tmbrggmn,

Sì, ho rinominato i file negli snippet di codice di conseguenza. Spero che questo lo chiarisca.
Mhaller,

Grazie, grazie. Continuo a confondermi perché apparentemente "chiave" e "certificato" sono usati in modo intercambiabile.
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>

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

}

Se preferisci che il certificato sia disponibile per tutte le applicazioni che utilizzano una particolare installazione JVM, segui invece questa risposta .
ADTC,

è configureRequest()giusto il metodo per impostare il proxy del progetto client?
Shareef,

sì, in questo caso è la configurazione del client http ed è la configurazione proxy
kinjelom

forse ho una domanda stupida, ma come posso creare un file cacert.jks? Ho un'eccezione Eccezione nel thread "main" java.io.FileNotFoundException:. \ Cacert.jks (Il sistema non riesce a trovare il file specificato)
Patlatus

6

Per quelli di voi che vogliono semplicemente impostare un'autenticazione a due vie (certificati server e client), una combinazione di questi due collegamenti vi porterà lì:

Impostazione autenticazione bidirezionale:

https://linuxconfig.org/apache-web-server-ssl-authentication

Non è necessario utilizzare il file di configurazione openssl di cui parlano; basta usare

  • $ openssl genrsa -des3 -out ca.key 4096

  • $ openssl req -new -x509 -days 365 -key ca.key -out ca.crt

per generare il proprio certificato CA, quindi generare e firmare le chiavi del server e del client tramite:

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

e

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

Per il resto, segui i passaggi nel link. La gestione dei certificati per Chrome funziona come nell'esempio di Firefox menzionato.

Successivamente, imposta il server tramite:

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

Nota che hai già creato il server .crt e .key, quindi non devi più fare questo passo.


1
Pensa di avere un refuso nel passaggio di generazione CSR del server: server.keynon usareclient.key
the.Legend

0

Mi sono collegato alla banca con SSL bidirezionale (certificato client e server) con Spring Boot. Quindi descrivi qui tutti i miei passi, spero che aiuti qualcuno (la soluzione di lavoro più semplice, ho trovato):

  1. Genera richiesta di certificato:

    • Genera chiave privata:

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • Genera richiesta di certificato:

      openssl req -new -key user.key -out user.csr -passin pass:MY_PASSWORD

    Conserva user.key(e password) e invia la richiesta di certificato user.csralla banca per il mio certificato

  2. Ricevi il certificato 2: il mio certificato radice client clientId.crte il certificato radice banca:bank.crt

  3. Crea keystore Java (inserisci la password della chiave e imposta la password del keystore):

    openssl pkcs12 -export -in clientId.crt -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root

    Non prestare attenzione sull'uscita: unable to write 'random state'. Java PKCS12 keystore.p12creato.

  4. Aggiungi nel keystore bank.crt(per semplicità ho usato un keystore):

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

    Controlla i certificati del keystore tramite:

    keytool -list -keystore keystore.p12
  5. Pronto per il codice Java :) Ho usato Spring Boot RestTemplatecon aggiunta org.apache.httpcomponents.httpcoredipendenza:

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

Puoi fare tutto con il keytool. In questo non è necessario OpenSSL.
Marchese di Lorne il

0

Dato un file p12 sia con il certificato che con la chiave privata (generato ad esempio da openssl), il codice seguente lo utilizzerà per una specifica connessione HttpsURLC:

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

L' SSLContextinizializzazione richiede del tempo, quindi potrebbe essere necessario memorizzarlo nella cache.


-1

Penso che la correzione qui sia stata il tipo di archivio chiavi, pkcs12 (pfx) ha sempre la chiave privata e il tipo JKS può esistere senza chiave privata. A meno che non specifichi nel tuo codice o selezioni un certificato tramite browser, il server non ha modo di sapere che rappresenta un client all'altro capo.


1
Il formato PKCS12 era tradizionalmente usato per privatekey-AND-cert, ma Java dall'8 nel 2014 (più di un anno prima di questa risposta) ha supportato PKCS12 contenente cert (s) senza privatekey (s). Indipendentemente dal formato del keystore, l'autenticazione del client richiede privatekey-AND-cert. Non capisco la tua seconda frase, ma il client Java può selezionare automaticamente un cert-and-key client se è disponibile almeno una voce adatta o è possibile configurare un gestore delle chiavi per utilizzarne una specificata.
dave_thompson_085

La tua seconda frase è totalmente errata. Il server fornisce i propri firmatari attendibili e il client muore o non fornisce un certificato che soddisfa tale vincolo. Automaticamente, non tramite "nel tuo codice". Questo è il "modo in cui il server sa di rappresentare un client".
Marchese di Lorne il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.