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: <BPC-INSTALL-DIR>/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 <BPC-INSTALL-DIR>/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

Damit der Karaf den zuvor angelegten Truststore verwendet, muss die Konfigurationsdatei <BPC-INSTALL-DIR>/karaf/etc/org.ops4j.pax.web.cfg um folgende Eigenschaften erweitert werden:

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

Mindestens einer der Parameter org.ops4j.pax.web.ssl.clientauthwanted oder org.ops4j.pax.web.ssl.clientauthneeded muss auf true stehen. Sind beide auf false gesetzt (Default), wird kein Client-Zertifikat an den Endpunkt übermittelt.

Falls das Zertifikat nicht am Endpunkt ankommt: 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

Nach der BPC-Installation steht standardmäßig ein vordefinierter Truststore zur Verfügung. Dieser befindet sich im Verzeichnis <BPC-INSTALL-DIR>/karaf/etc/virtimo/ssl/virtimo_truststore.jks (Default-Passwort: virtimo). Die entsprechenden Pfad- und Passworteinstellungen sind in der Karaf-Konfig bereits hinterlegt. Administratoren können bei Bedarf weitere Zertifikate in diesen Truststore importieren oder durch Anpassung von Pfad und Passwort einen eigenen Truststore einbinden.

Java Client Code

Die folgenden Beispiele zeigen, wie ein Java-Client BPC-Endpunkte unter Verwendung der erstellten Client-Key- und Truststores aufruft.

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-INSTALL-DIR>/karaf/etc/virtimo/ssl/clientkey.jks";
    private final static String KEYSTORE_PASSWORD = "passw0rd";
    private final static String TRUSTSTORE_FILE_PATH = "/home/<user>/<BPC-INSTALL-DIR>/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-INSTALL-DIR>/karaf/etc/virtimo/ssl/clientkey.jks";
    private final static String KEYSTORE_PASSWORD = "passw0rd";
    private final static String TRUSTSTORE_FILE_PATH = "/home/<user>/<BPC-INSTALL-DIR>/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
            // This should not be used in a production environment!!
            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: