Authentification par certificat client HTTPS Java

222

Je suis assez nouveau HTTPS/SSL/TLSet je suis un peu confus sur ce que les clients sont censés présenter exactement lors de l'authentification avec des certificats.

J'écris un client Java qui doit faire un simple POSTde données à un particulier URL. Cette partie fonctionne bien, le seul problème est qu'elle est censée être terminée HTTPS. La HTTPSpartie est assez facile à manipuler (avec HTTPclientou en utilisant le HTTPSsupport intégré de Java ), mais je suis bloqué sur l'authentification avec les certificats clients. J'ai remarqué qu'il y a déjà une question très similaire ici, que je n'ai pas encore essayée avec mon code (le fera assez tôt). Mon problème actuel est que - quoi que je fasse - le client Java n'envoie jamais le certificat (je peux le vérifier avec des PCAPvidages).

Je voudrais savoir exactement ce que le client est censé présenter au serveur lors de l'authentification avec des certificats (spécifiquement pour Java - si cela est important)? Est-ce un JKSfichier, ou PKCS#12? Ce qui est censé être en eux; juste le certificat client ou une clé? Si oui, quelle clé? Il y a pas mal de confusion sur tous les différents types de fichiers, les types de certificats et autres.

Comme je l'ai déjà dit, je suis novice, HTTPS/SSL/TLSdonc j'apprécierais également quelques informations de base (ne doit pas être un essai; je me contenterai de liens vers de bons articles).

tmbrggmn
la source
J'ai donné deux certificats du client pour identifier celui qui doit ajouter dans le magasin de clés et le magasin de confiance. Pourriez-vous s'il vous plaît aider à identifier ce problème car vous avez déjà traversé un type de problème similaire, ce problème que j'ai soulevé n'a en fait aucune idée de ce qu'il faut faire stackoverflow .com / questions / 61374276 /…
henrycharles

Réponses:

233

Enfin réussi à résoudre tous les problèmes, je vais donc répondre à ma propre question. Ce sont les paramètres / fichiers que j'ai utilisés pour gérer la résolution de mes problèmes particuliers;

Le fichier de clés du client est un fichier au format PKCS # 12 contenant

  1. Le certificat public du client (dans ce cas, signé par une autorité de certification auto-signée)
  2. La clé privée du client

Pour le générer, j'ai utilisé la pkcs12commande d'OpenSSL , par exemple;

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

Astuce: assurez-vous d'obtenir la dernière version d' OpenSSL, pas la version 0.9.8h car cela semble souffrir d'un bogue qui ne vous permet pas de générer correctement les fichiers PKCS # 12.

Ce fichier PKCS # 12 sera utilisé par le client Java pour présenter le certificat client au serveur lorsque le serveur a explicitement demandé au client de s'authentifier. Voir l'article Wikipedia sur TLS pour un aperçu du fonctionnement du protocole d'authentification par certificat client (explique également pourquoi nous avons besoin de la clé privée du client ici).

Le fichier de clés certifiées du client est un fichier au format JKS simple contenant les certificats d'autorité de certification racine ou intermédiaire . Ces certificats d'autorité de certification détermineront les points de terminaison avec lesquels vous serez autorisé à communiquer, dans ce cas, ils permettront à votre client de se connecter au serveur qui présente un certificat signé par l'une des autorités de certification du magasin de clés de confiance.

Pour le générer, vous pouvez utiliser le keytool Java standard, par exemple;

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

À l'aide de ce magasin de clés de confiance, votre client essaiera d'effectuer une négociation SSL complète avec tous les serveurs qui présentent un certificat signé par l'autorité de certification identifiée par myca.crt.

Les fichiers ci-dessus sont strictement réservés au client. Lorsque vous souhaitez également configurer un serveur, le serveur a besoin de ses propres fichiers de clés et de fichiers de clés certifiées. Une grande procédure pas à pas pour configurer un exemple pleinement fonctionnel pour un client et un serveur Java (à l'aide de Tomcat) peut être trouvée sur ce site Web .

Problèmes / remarques / conseils

  1. L'authentification du certificat client ne peut être appliquée que par le serveur.
  2. ( Important! ) Lorsque le serveur demande un certificat client (dans le cadre de la négociation TLS), il fournit également une liste des autorités de certification approuvées dans le cadre de la demande de certificat. Lorsque le certificat client que vous souhaitez présenter pour l'authentification n'est pas signé par l'une de ces autorités de certification, il ne sera pas présenté du tout (à mon avis, c'est un comportement étrange, mais je suis sûr qu'il y a une raison à cela). C'était la principale cause de mes problèmes, car l'autre partie n'avait pas configuré correctement son serveur pour accepter mon certificat client auto-signé et nous avons supposé que le problème était de mon côté pour ne pas fournir correctement le certificat client dans la demande.
  3. Obtenez Wireshark. Il a une excellente analyse des paquets SSL / HTTPS et sera une aide considérable pour le débogage et la recherche du problème. Il est similaire -Djavax.net.debug=sslmais plus structuré et (sans doute) plus facile à interpréter si vous n'êtes pas à l'aise avec la sortie de débogage SSL SSL.
  4. Il est parfaitement possible d'utiliser la bibliothèque Apache httpclient. Si vous souhaitez utiliser httpclient, remplacez simplement l'URL de destination par l'équivalent HTTPS et ajoutez les arguments JVM suivants (qui sont les mêmes pour tout autre client, quelle que soit la bibliothèque que vous souhaitez utiliser pour envoyer / recevoir des données via 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
la source
6
"Lorsque le certificat client que vous souhaitez présenter pour l'authentification n'est pas signé par l'une de ces autorités de certification, il ne sera pas présenté du tout". Les certificats ne sont pas présentés car le client sait qu'ils ne seront pas acceptés par le serveur. De plus, votre certificat peut être signé par une autorité de certification intermédiaire "ICA", et le serveur peut présenter à votre client l'autorité de certification racine "RCA", et votre navigateur Web vous permettra toujours de choisir votre certificat même s'il est signé par ICA et non RCA.
KyleM
2
Comme exemple du commentaire ci-dessus, considérons une situation où vous avez une autorité de certification racine (RCA1) et deux autorités de certification intermédiaires (ICA1 et ICA2). Sur Apache Tomcat, si vous importez RCA1 dans le magasin de confiance, votre navigateur Web présentera TOUS les certificats signés par ICA1 et ICA2, même s'ils ne sont pas dans votre magasin de confiance. C'est parce que c'est la chaîne qui compte, pas les certificats individuels.
KyleM
2
"à mon avis, c'est un comportement bizarre, mais je suis sûr qu'il y a une raison à cela". La raison en est que c'est ce qu'il dit dans la RFC 2246. Rien de bizarre à ce sujet. Permettre aux clients de présenter des certificats qui ne seront pas acceptés par le serveur est ce qui serait étrange et une perte de temps et d'espace complète.
Marquis de Lorne
1
Je déconseille fortement d'utiliser un seul magasin de clés à l'échelle de la JVM (et un magasin de confiance!) Comme vous l'avez fait dans votre exemple ci-dessus. La personnalisation d'une connexion unique est plus sûre et plus flexible, mais elle nécessite que vous écriviez un peu plus de code. Vous devez personnaliser le SSLContextcomme dans la réponse de @ Magnus.
Christopher Schultz
63

D'autres réponses montrent comment configurer globalement les certificats clients. Cependant, si vous souhaitez définir par programme la clé client pour une connexion particulière, plutôt que de la définir globalement dans chaque application exécutée sur votre machine virtuelle Java, vous pouvez configurer votre propre SSLContext comme suit:

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"));
Magnus
la source
Je devais utiliser sslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();. Je ne pouvais pas le faire fonctionner loadKeyMaterial(...).
Conor Svensson
4
Le matériel @ConorSvensson Trust est destiné au client qui fait confiance au certificat de serveur distant, le matériel clé est destiné au serveur qui fait confiance au client.
Magnus
1
J'aime vraiment cette réponse concise et précise. Au cas où les gens seraient intéressés, je fournis ici un exemple pratique avec des instructions de construction. stackoverflow.com/a/46821977/154527
Alain O'Dea
3
Tu m'as sauvé la journée! Juste un changement supplémentaire que je devais faire, envoyez également le mot de passe en loadKeyMaterial(keystore, keyPassphrase.toCharArray())code!
AnandShanbhag
1
@peterh Oui, il est spécifique à Apache http. Chaque bibliothèque HTTP aura sa propre façon d'être configurée, mais la plupart d'entre elles devraient en quelque sorte utiliser un SSLContext.
Magnus
30

Le fichier JKS n'est qu'un conteneur de certificats et de paires de clés. Dans un scénario d'authentification côté client, les différentes parties des clés seront situées ici:

  • Le client « magasin de contiendra du client public et privé paire de clés. Il s'agit d'un magasin de clés .
  • Le magasin du serveur contiendra la clé publique du client . Il s'agit d'un magasin de confiance .

La séparation de truststore et keystore n'est pas obligatoire mais recommandée. Il peut s'agir du même fichier physique.

Pour définir les emplacements du système de fichiers des deux magasins, utilisez les propriétés système suivantes:

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

et sur le serveur:

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

Pour exporter le certificat du client (clé publique) dans un fichier, afin de pouvoir le copier sur le serveur, utilisez

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

Pour importer la clé publique du client dans le magasin de clés du serveur, utilisez (comme l'affiche l'a mentionné, cela a déjà été fait par les administrateurs du serveur)

keytool -import -file publicclientkey.cer -store serversidestore.jks
mhaller
la source
Je devrais probablement mentionner que je n'ai aucun contrôle sur le serveur. Le serveur a importé notre certificat public. Les administrateurs de ce système m'ont dit que je devais fournir explicitement le certificat afin qu'il puisse être envoyé pendant la prise de contact (leur serveur le demande explicitement).
tmbrggmn
Vous aurez besoin de clés publiques et privées pour votre certificat public (celui qui est connu du serveur) en tant que fichier JKS.
sfussenegger
Merci pour l'exemple de code. Dans le code ci-dessus, qu'est-ce que "mykey-public.cer" exactement? S'agit-il du certificat public du client (nous utilisons des certificats auto-signés)?
tmbrggmn
Oui, j'ai renommé les fichiers dans les extraits de code en conséquence. J'espère que cela est clair.
mhaller
Oui, merci. Je continue à être confus car apparemment, "clé" et "certificat" sont utilisés de manière interchangeable.
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>

Code 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
la source
Si vous préférez que le certificat soit disponible pour toutes les applications qui utilisent une installation JVM particulière, suivez plutôt cette réponse .
ADTC
la méthode configureRequest()pour définir le proxy du projet client est-elle correcte?
shareef
oui, c'est la configuration du client http et c'est la configuration proxy dans ce cas
kinjelom
J'ai peut-être une question stupide, mais comment créer un fichier cacert.jks? J'ai une exception Exception dans le thread "main" java.io.FileNotFoundException:. \ Cacert.jks (Le système ne peut pas trouver le fichier spécifié)
Patlatus
6

Pour ceux d'entre vous qui souhaitent simplement configurer une authentification bidirectionnelle (certificats serveur et client), une combinaison de ces deux liens vous y mènera:

Configuration d'authentification bidirectionnelle:

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

Vous n'avez pas besoin d'utiliser le fichier de configuration openssl qu'ils mentionnent; il suffit d'utiliser

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

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

pour générer votre propre certificat CA, puis générer et signer les clés de serveur et de client via:

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

et

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

Pour le reste, suivez les étapes du lien. La gestion des certificats pour Chrome fonctionne de la même manière que dans l'exemple de Firefox qui est mentionné.

Ensuite, configurez le serveur via:

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

Notez que vous avez déjà créé le serveur .crt et .key afin que vous n'ayez plus à faire cette étape.

Hans
la source
1
Pensez que vous avez une faute de frappe dans l'étape de génération de la RSE du serveur: ne server.keypas utiliserclient.key
the.Legend
0

Je me suis connecté à une banque avec SSL bidirectionnel (certificat client et serveur) avec Spring Boot. Décrivez donc ici toutes mes étapes, j'espère que cela aide quelqu'un (solution de travail la plus simple, j'ai trouvé):

  1. Générer une requête sertificate:

    • Générer une clé privée:

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • Générer une demande de certificat:

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

    Conserver user.key(et mot de passe) et envoyer la demande de certificat user.csrà la banque pour mon sertificate

  2. Recevez 2 certificats: mon certificat racine client et mon certificat clientId.crtracine banque:bank.crt

  3. Créer un magasin de clés Java (entrez le mot de passe de la clé et définissez le mot de passe du magasin de clés):

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

    Ne prêtez pas attention à la sortie: unable to write 'random state'. Java PKCS12 keystore.p12créé.

  4. Ajouter dans le magasin de clés bank.crt(pour plus de simplicité, j'ai utilisé un magasin de clés):

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

    Vérifiez les certificats de magasin de clés en:

    keytool -list -keystore keystore.p12
  5. Prêt pour le code Java :) J'ai utilisé Spring Boot RestTemplateavec une org.apache.httpcomponents.httpcoredépendance supplémentaire :

    @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;
    }
Grigory Kislin
la source
Vous pouvez tout faire avec l'outil clé. Il n'y a aucun besoin d'OpenSSL dans tout cela.
Marquis de Lorne
0

Étant donné un fichier p12 avec à la fois le certificat et la clé privée (généré par openssl, par exemple), le code suivant l'utilisera pour une HttpsURLConnection spécifique:

    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' SSLContextinitialisation prend un certain temps, vous pouvez donc le mettre en cache.

Johannes Brodwall
la source
-1

Je pense que le correctif était le type de magasin de clés, pkcs12 (pfx) a toujours une clé privée et le type JKS peut exister sans clé privée. Sauf si vous spécifiez dans votre code ou sélectionnez un certificat via un navigateur, le serveur n'a aucun moyen de savoir qu'il représente un client à l'autre bout.

Obi Wan Kenobi
la source
1
Le format PKCS12 était traditionnellement utilisé pour privatekey-AND-cert, mais Java depuis 8 en 2014 (plus d'un an avant cette réponse) a pris en charge PKCS12 contenant des cert sans clé privée. Quel que soit le format du fichier de clés, l'authentification du client nécessite une clé privée et un certificat. Je ne comprends pas votre deuxième phrase, mais le client Java peut sélectionner automatiquement un certificat et une clé client si au moins une entrée appropriée est disponible ou si un gestionnaire de clés peut être configuré pour utiliser une entrée spécifiée.
dave_thompson_085
Votre deuxième phrase est totalement incorrecte. Le serveur fournit ses signataires approuvés et le client meurt ou ne fournit pas de certificat satisfaisant à cette contrainte. Automatiquement, pas via «dans votre code». C'est « la façon de le savoir représente un client » du serveur.
Marquis de Lorne