Client Certificate Authentication

Bei der "Client Certificate Authentication" machen Client und Server einen Handshake, bei dem sich beide Parteien kennen müssen. Dazu muss neben den normalen Keystores auch jeweils ein Truststore mit den Zertifikaten des jeweils anderen zur Verfügung stehen.
In Kürze: Der Client kennt den Server und der Server kennt den Client.

Client Certificate Authentication kommt aktuell nur bei den Log Service-Endpunkten zum Einsatz.

Zertifikate, Keystores und Truststores

Hier werden exemplarisch die Schritte zur Erstellung der Zertifikate, Keystores und Truststores sowie die Konfiguration im Karaf gezeigt. Die Schritte werden in folgendem Verzeichnis ausgeführt: [karaf]/etc/virtimo/ssl

1. Virtimo Keystore für den Karaf

Generierung eines Paars selbst-signierter Public und Private Keys für den Karaf. Dieser Schritt kann übersprungen werden, da der Keystore bereits existiert. Er wird in der [karaf]/etc/org.ops4j.pax.web.cfg referenziert.

virtimo_keystore.jks
keytool -genkey \
  -alias virtimo \
  -keystore virtimo_keystore.jks \
  -dname "EMAILADDRESS=support@virtimo.de, CN=Virtimo AG, OU=Lab, O=Virtimo AG, L=Berlin, ST=Germany, C=DE" \
  -storepass virtimo \
  -keypass virtimo \
  -storetype jks \
  -validity 1000 \
  -keyalg RSA

Zum Verständnis den Inhalt unseres Keystores anzeigen lassen:

keytool -list -keystore virtimo_keystore.jks

2. Export des Virtimo-Zertifikats

Dieses wird aus dem Virtimo Keystore exportiert.

virtimo_keystore.jks → virtimo.cer
keytool -export \
  -alias virtimo \
  -file virtimo.cer \
  -keystore virtimo_keystore.jks \
  -storepass virtimo \
  -storetype jks

Zum Verständnis den Inhalt des Virtimo Zertifikats anzeigen lassen:

keytool -printcert -file virtimo.cer
...
> EMAILADDRESS=support@virtimo.de, CN=Virtimo AG, OU=Lab, O=Virtimo AG, L=Berlin, ST=Germany, C=DE

3. Import des Virtimo-Zertifikats in den Client Truststore

Dieser wird angelegt, wenn noch nicht vorhanden.

virtimo.cer → clienttrust.jks
keytool -import \
  -file virtimo.cer \
  -alias default \
  -keystore clienttrust.jks \
  -storepass passw0rd \
  -keypass passw0rd \
  -storetype jks

4. Erstellung des Client Keystores

Generierung eines Paars selbst-signierter Public und Private Keys für den Client, welcher der Aufrufer unserer Endpunkte ist. Hier exemplarisch am Beispiel mit zwei Aliasen.

clientkey.jks
keytool -genkey \
  -alias user0 \
  -keystore clientkey.jks \
  -dname "CN=employee0, OU=Lab, O=Virtimo AG, L=Berlin, ST=Germany, C=DE" \
  -storepass passw0rd \
  -keypass passw0rd \
  -storetype jks \
  -validity 1000 \
  -keyalg RSA

keytool -genkey \
  -alias admin0 \
  -keystore clientkey.jks \
  -dname "CN=manager0, OU=Lab, O=Virtimo AG, L=Berlin, ST=Germany, C=DE" \
  -storepass passw0rd \
  -keypass passw0rd \
  -storetype jks \
  -validity 1000 \
  -keyalg RSA

Zum Verständnis den Inhalt des Keystores anzeigen lassen:

keytool -list -keystore clientkey.jks

5. Client Zertifikat Export und Virtimo Truststore Import

Export der Client Zertifikate per oben gesetzten Aliases und Import dieser in den Virtimo Truststore.

Export Client Zertifikate
keytool -export \
  -alias user0 \
  -file user0.cer \
  -keystore clientkey.jks \
  -storepass passw0rd \
  -storetype jks

keytool -export \
  -alias admin0 \
  -file admin0.cer \
  -keystore clientkey.jks \
  -storepass passw0rd \
  -storetype jks
Virtimo Truststore Import
keytool -import \
  -file user0.cer \
  -alias user0 \
  -keystore virtimo_truststore.jks \
  -storepass virtimo \
  -keypass virtimo \
  -storetype jks

keytool -import \
  -file admin0.cer \
  -alias admin0 \
  -keystore virtimo_truststore.jks \
  -storepass virtimo \
  -keypass virtimo \
  -storetype jks

Anpassung Karaf

Der Karaf muss den oben angelegten Truststore kennen und wissen, dass er ihn benutzen soll. Dazu die [karaf]/etc/org.ops4j.pax.web.cfg erweitern.

org.ops4j.pax.web.ssl.truststore = ${karaf.etc}/virtimo/ssl/virtimo_truststore.jks
org.ops4j.pax.web.ssl.truststore.password = virtimo

org.ops4j.pax.web.ssl.clientauthwanted = true
org.ops4j.pax.web.ssl.clientauthneeded = false

Sind org.ops4j.pax.web.ssl.clientauthwanted und org.ops4j.pax.web.ssl.clientauthneeded auf false gesetzt (Default), dann kommt am Endpunkt kein Client Zertifikat zur Überprüfung an!

Wenn es in Zukunft mal Probleme gibt (Zertifikat kommt nicht am Endpunkt an): Im aktuellen (26.11.2021) PAX Web Source Code stehen andere property-Namen und die funktionieren nicht mit dem Karaf 4.3.3.

org.ops4j.pax.web.ssl.clientauth.wanted = true
org.ops4j.pax.web.ssl.clientauth.needed = false

Als Default ist ein Truststore nach der Installation von BPC immer vorhanden. Dieser Trust-Store befindet sich im [karaf]/etc/virtimo/ssl/virtimo_truststore.jks.
Default-Passwort ist virtimo.
Die Einträge sind in der Karaf-Konfig standardmäßig gesetzt. Administrator kann weitere Zertifikate in diesem Store importieren. Man kann allerdings auch seinen eigenen Store verwenden, indem man den Pfad zu dem Trust-Store und Passwort angibt.

Java Client Code

Hier noch zwei Beispiele (einfach und komplex) wie ein Java Client (Aufrufer eines BPC Endpunktes) dann die dafür vorbereiteten BPC Endpunkte mit den oben erstellen Client Keystore und Client Truststore aufrufen kann.

SSLMutualAuthTestSimple.java
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.methods.GetMethod;

public class SSLMutualAuthTestSimple {
    private final static String KEYSTORE_FILE_PATH = "/home/<user>/bpc/karaf/etc/virtimo/ssl/clientkey.jks";
    private final static String KEYSTORE_PASSWORD = "passw0rd";
    private final static String TRUSTSTORE_FILE_PATH = "/home/<user>/bpc/karaf/etc/virtimo/ssl/clienttrust.jks";
    private final static String TRUSTSTORE_PASSWORD = "passw0rd";

    public static void main(String[] args) {
        try {
            // System.setProperty("javax.net.debug", "ssl,handshake"); // for debugging ssl and handshake stuff

            System.setProperty("javax.net.ssl.keyStore", KEYSTORE_FILE_PATH);
            System.setProperty("javax.net.ssl.keyStorePassword", KEYSTORE_PASSWORD);

            System.setProperty("javax.net.ssl.trustStore", TRUSTSTORE_FILE_PATH);
            System.setProperty("javax.net.ssl.trustStorePassword", TRUSTSTORE_PASSWORD);

            GetMethod method = new GetMethod();
            method.setURI(new URI("https://localhost:8282/cxf/bpc-logservice/log/1234567890?config=true", false));

            HttpClient client = new HttpClient();
            client.executeMethod(method);

            System.out.println(method.getResponseBodyAsString());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
SSLMutualAuthTestComplex.java
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.PrivateKeyDetails;
import org.apache.http.ssl.PrivateKeyStrategy;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.security.KeyStore;
import java.util.Map;

public class SSLMutualAuthTestComplex {
    private final static String KEYSTORE_FILE_PATH = "/home/<user>/bpc/karaf/etc/virtimo/ssl/clientkey.jks";
    private final static String KEYSTORE_PASSWORD = "passw0rd";
    private final static String TRUSTSTORE_FILE_PATH = "/home/<user>/bpc/karaf/etc/virtimo/ssl/clienttrust.jks";
    private final static String TRUSTSTORE_PASSWORD = "passw0rd";

    public static void main(String[] args) {
        try {
            // System.setProperty("javax.net.debug", "all"); // to debug all "net" related stuff

            String CERT_ALIAS = "admin0";

            KeyStore keyStore = KeyStore.getInstance("JKS");
            FileInputStream keyStoreFile = new FileInputStream(new File(KEYSTORE_FILE_PATH));
            keyStore.load(keyStoreFile, KEYSTORE_PASSWORD.toCharArray());

            KeyStore trustStore = KeyStore.getInstance("JKS");
            FileInputStream trustStoreFile = new FileInputStream(new File(TRUSTSTORE_FILE_PATH));
            trustStore.load(trustStoreFile, TRUSTSTORE_PASSWORD.toCharArray());

            // Create all-trusting host name verifier
            // Das sollte produktiv nicht verwendet werden!!!
            HostnameVerifier allHostsValid = new HostnameVerifier() {
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };

            SSLContext sslContext = SSLContexts.custom()
                    .loadKeyMaterial(keyStore, KEYSTORE_PASSWORD.toCharArray(), new PrivateKeyStrategy() {
                        @Override
                        public String chooseAlias(Map<String, PrivateKeyDetails> aliases, Socket socket) {
                            return CERT_ALIAS;
                        }
                    })
                    .loadTrustMaterial(trustStore, null)
                    .build();

            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
                    sslContext,
                    new String[]{"TLSv1.2", "TLSv1.1", "TLSv1"},
                    null,
                    allHostsValid /* SSLConnectionSocketFactory.getDefaultHostnameVerifier() */
            );

            try (CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build()) {
                HttpGet request = new HttpGet("https://localhost:8282/cxf/bpc-logservice/log/1234567890?config=true");
                request.setHeader("Accept", "application/json");

                HttpResponse response = client.execute(request);
                System.out.println("Response code: " + response.getStatusLine().getStatusCode());
                System.out.println("Response content:");

                BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
                String line;
                while ((line = rd.readLine()) != null) {
                    System.out.println(line);

                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Keywords: