Client Certificate Authentication

With "Client Certificate Authentication", the client and server perform a handshake in which both parties must know each other. In addition to the normal keystores, a truststore with the certificates of the other party must also be available.
In brief: The client knows the server and the server knows the client.

Client Certificate Authentication is currently only used for the Log Service endpoints.

Certificates, keystores and truststores

The steps for creating the certificates, keystores and truststores as well as the configuration in Karaf are shown here as an example. The steps are carried out in the following directory: [karaf]/etc/virtimo/ssl

1. Virtimo keystore for the Karaf

Generation of a pair of self-signed public and private keys for the Karaf. This step can be skipped as the keystore already exists. It is referenced in the [karaf]/etc/org.ops4j.pax.web.cfg.

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

To understand the content of our keystore:

keytool -list -keystore virtimo_keystore.jks

2. Export of the Virtimo certificate

This is exported from the Virtimo keystore.

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

To understand the content of the Virtimo certificate:

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 the Virtimo certificate into the client truststore

This is created if it does not already exist.

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

4. Creation of the client keystore

Generation of a pair of self-signed public and private keys for the client, which is the caller of our endpoints. Here is an example with two aliases.

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

To understand the content of the keystore:

keytool -list -keystore clientkey.jks

5. Client certificate export and Virtimo Truststore import

Export the client certificates via the aliases set above and import them into the Virtimo Truststore.

Export client certificates
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

Customization Karaf

The Karaf must know the truststore created above and know that it should use it. To do this, extend the [karaf]/etc/org.ops4j.pax.web.cfg.

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

If org.ops4j.pax.web.ssl.clientauthwanted and org.ops4j.pax.web.ssl.clientauthneeded are set to false (default), then no client certificate arrives at the endpoint for verification!

If there are problems in the future (certificate does not arrive at the endpoint): In the current (26.11.2021) PAX web source code contains other property names and these do not work with Karaf 4.3.3.

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

A trust store is always present by default after the installation of BPC. This trust store is located in [karaf]/etc/virtimo/ssl/virtimo_truststore.jks.
Default password is virtimo.
The entries are set by default in the Karaf config. Administrator can import further certificates in this store. However, you can also use your own store by specifying the path to the trust store and password.

Java Client Code

Here are two more examples (simple and complex) of how a Java client (caller of a BPC endpoint) can then call the BPC endpoints prepared for this with the client keystore and client truststore created above.

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: