Java Https Client Certificate Authentication

Java HTTPS client certificate authentication

Finally managed to solve all the issues, so I'll answer my own question. These are the settings/files I've used to manage to get my particular problem(s) solved;

The client's keystore is a PKCS#12 format file containing

  1. The client's public certificate (in this instance signed by a self-signed CA)
  2. The client's private key

To generate it I used OpenSSL's pkcs12 command, for example;

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

Tip: make sure you get the latest OpenSSL, not version 0.9.8h because that seems to suffer from a bug which doesn't allow you to properly generate PKCS#12 files.

This PKCS#12 file will be used by the Java client to present the client certificate to the server when the server has explicitly requested the client to authenticate. See the Wikipedia article on TLS for an overview of how the protocol for client certificate authentication actually works (also explains why we need the client's private key here).

The client's truststore is a straight forward JKS format file containing the root or intermediate CA certificates. These CA certificates will determine which endpoints you will be allowed to communicate with, in this case it will allow your client to connect to whichever server presents a certificate which was signed by one of the truststore's CA's.

To generate it you can use the standard Java keytool, for example;

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

Using this truststore, your client will try to do a complete SSL handshake with all servers who present a certificate signed by the CA identified by myca.crt.

The files above are strictly for the client only. When you want to set-up a server as well, the server needs its own key- and truststore files. A great walk-through for setting up a fully working example for both a Java client and server (using Tomcat) can be found on this website.

Issues/Remarks/Tips

  1. Client certificate authentication can only be enforced by the server.
  2. (Important!) When the server requests a client certificate (as part of the TLS handshake), it will also provide a list of trusted CA's as part of the certificate request. When the client certificate you wish to present for authentication is not signed by one of these CA's, it won't be presented at all (in my opinion, this is weird behaviour, but I'm sure there's a reason for it). This was the main cause of my issues, as the other party had not configured their server properly to accept my self-signed client certificate and we assumed that the problem was at my end for not properly providing the client certificate in the request.
  3. Get Wireshark. It has great SSL/HTTPS packet analysis and will be a tremendous help debugging and finding the problem. It's similar to -Djavax.net.debug=ssl but is more structured and (arguably) easier to interpret if you're uncomfortable with the Java SSL debug output.
  4. It's perfectly possible to use the Apache httpclient library. If you want to use httpclient, just replace the destination URL with the HTTPS equivalent and add the following JVM arguments (which are the same for any other client, regardless of the library you want to use to send/receive data over 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

How do I use client certificates in a client java application?

In order to load your certificates into your application your will need to package them into a truststore.

Creating a truststore

given the 3 files:

  • client-cert.pem
  • client-key.pem
  • root.pem

Run the following commands in your terminal. Replace PASSWORD with your desired password.

  1. Package your client key and certificate into a keystore. This will create a PKCS12 keystore file.

    openssl pkcs12 -export \
    -inkey client-key.pem -in client-cert.pem \
    -out client.pfx -passout pass:PASSWORD \
    -name qlikClient
  2. Add the keystore to your truststore. It will create a truststore if the destination doesn't exit. This will create a PKCS12 truststore file. By default it creates a JKS file which is a proprietary format. By specifying -deststoretype PKCS12 you will create a file which is in an industry standard format.

    keytool -importkeystore \
    -destkeystore truststore.pfx -deststoretype PKCS12 -deststorepass PASSWORD \
    -srckeystore client.pfx -srcstorepass PASSWORD -srcstoretype PKCS12 \
    -alias qlikClient
  3. Add your root CA to the truststore

    keytool -importcert \
    -keystore truststore.pfx -storepass PASSWORD \
    -file root.pem -noprompt \
    -alias qlikServerCACert

Note that in the above commands we use the same PASSWORD for both the keystore and the truststore. You could alternatively use different passwords. Also note that you have to specify an alias for each item you add to the truststore.

If you want your truststore to trust all cacerts available in your system add -trustcacerts option to step 2 or 3.

You can use the following command to list the contents of your truststore

keytool -list -keystore truststore.pfx -storepass PASSWORD

Using the truststore in you application

Once you have your truststore you need to load it into your application. Assuming you have a constant KEYSTORE_PATH holding the path to your truststore and keyStorePass holding the password, read the truststore file into a KeyStore

private KeyStore readStore() {
try (InputStream keyStoreStream = new FileInputStream(KEYSTORE_PATH)) {
KeyStore keyStore = KeyStore.getInstance("PKCS12"); // or "JKS"
keyStore.load(keyStoreStream, keyStorePass.toCharArray());
return keyStore;
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

Create a custom SSLContext and a custom HttpClient,

final KeyStore truststore = readStore();

final SSLContext sslContext;
try {
sslContext = SSLContexts.custom()
.loadTrustMaterial(truststore, new TrustAllStrategy())
.loadKeyMaterial(truststore, keyStorePass.toCharArray(), (aliases, socket) -> "qlikClient")
.build();
} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | UnrecoverableKeyException e) {
throw new RuntimeException("Failed to read keystore", e);
}
final CloseableHttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();

You can now use this HttpClient to make requests to your API.

HttpResponse response = httpClient.execute(new HttpGet("https://sense-gcp-central1eu.net:4242/qrs/app/full"));

Or, if you are using the OpenUnirest/unirest-java library, you can configure Unirest to use your custom HttpClient

Unirest.config().httpClient(httpClient);
HttpResponse<JsonNode> response = Unirest.get("https://sense-gcp-central1eu.net:4242/qrs/app/full").asJson();

References

  • https://alvinalexander.com/java/java-keytool-keystore-certificate-tutorials
  • How do I use an SSL client certificate with Apache HttpClient?

Client Certificate authentication in Java

Thanks for posting the log, I'll make this an answer since it's too much for a comment.

Short answer: the server is asking for a client certificate and you don't have one or are not configured to provide one that's acceptable.

Search in your log for this:

main, READ: TLSv1 Handshake, length = 11296
*** CertificateRequest
Cert Types: RSA, DSS
Cert Authorities:

This is the server asking you for a client certificate. What follows is a list of acceptable CAs. You must have a certificate in your keystore issued by one of those CAs and client certificates must be enabled for the session on the client side.

The next part of the log then says this:

*** ServerHelloDone
Warning: no suitable certificate found - continuing without client
authentication

Unfortunately it seems you don't have a certificate acceptable to the server, or are not configured for client certificate mode and that's why it cuts you off and aborts the handshake.

How to enforce the use of client authentication certificates from the client side

No. It cannot be enforced from the client side, it is only enforced by the server.

The plain https (one-way) is basically checking if the server is trusted by the client, it the client trusts the server, the communication happens. But the client authentication is a step further, on top of the client trusting the server, the server also tries to authenticate the user. Only if both parties are trusted by each other, the communication happens.

All of this chain of trust verification happens at protocol level. You don't have to worry about how it will happen. All you have to do is setup the chain-of-trust right (certificate chain in truststore).

Your concept of double encryption is not a feasible solution. The client and the server first talk to each other in plain text to see if they can communicate in a secured way (https) here on. Once they come to terms, all of the traffic will be encrypted from there on. Including your payload.

There are few security challenges when you are encrypting the payload on the server and decrypting on the client side using your own key, like, how do you transport the key to decrypt to the client side?

Http call in java not sending client certificate

So, I don't know if this is a bug or intended behavior (if so, why?), but apparently the PFX file must be password-protected, then it gets sent correctly. I could not make this work with a non-protected PFX file and passing null as the password like I was doing in the code I posted in the question.

So the problem is solved, but I would be curious if anyone could comment on WHY this happens.



Related Topics



Leave a reply



Submit