Mutual Authentication with NGinx and Java (Part 2)

Damien Metzler
Jul 28, 2021 · 5 min read

Context

In the first part of this series, we configured an NGinx server with mutual authentication and we were able to do some requests on it using cURL. In Java, the way certificates are managed can be sometimes cumbersome. In this post, we will see how to request a web resource in Java, using mutual authentication and different kind of key stores.

Java, Key, And Certificate Formats

In part1, we generated some keys in PEM format. This format encodes the certificate parts in base64 and encloses the result between markers. For instance:

-----BEGIN PRIVATE KEY----- 
MIIJQwIBADANBgkqhkiG9w0B... ...
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE----- MIIFCTCCAvGgAwIBAgIULqd76vqSzSWiFDbPe99gNfmZvjQwDQYJKoZIhvcNAQEL ... -----END CERTIFICATE-----
$ openssl pkcs8 -topk8 -inform PEM -in client.key -out client.pk8 -nocrypt
private KeySpec readKeyFromResource(URL resource) throws IOException {
try (var in = resource.openStream()) {
var text = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
var privateKeyPEM = text.replaceAll("-----BEGIN (.*)-----", "")
.replaceAll(System.lineSeparator(), "")
.replaceAll("-----END (.*)-----", "");
return new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyPEM));}
}
private List<Certificate> readCertificatesFromResource(URL certResource) throws IOException, CertificateException {
try (var in = certResource.openStream()) {
var certFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
return certificates.stream().map(Certificate.class::cast).collect(Collectors.toList());

}

}
var certs = readCertificatesFromResource(certResource);
var keySpec = readKeyFromResource(keyResource);
var kf = KeyFactory.getInstance("RSA");
var privateKey = kf.generatePrivate(keySpec);

keyStore.setKeyEntry("client", privateKey, DEFAULT_TRANSIENT_PASSWORD, certs.toArray(new Certificate[] {}));

Keystore and Keytool

To overcome this problem, Java offers a mechanism to store keys in a KeyStore backed by a file and protected by a password. The JDK ships with the utility keytool that can create or import keys in that keystore. Again, there are some format adjustments to run before being able to import our keys and certificates.

$ PASSWORD=changeit
$ openssl pkcs12 -export \
-in certs/client.crt \
-inkey certs/client.key \
-out certs/client.p12 \
-name client \
-CAfile certs/ca.crt \
-caname my-own-ca \
-passout pass:$PASSWORD
$ keytool -importkeystore \
-deststorepass $PASSWORD \
-destkeypass $PASSWORD \
-destkeystore certs/client.jks \
-srckeystore certs/client.p12 \
-srcstoretype PKCS12 \
-srcstorepass $PASSWORD \
-alias client

Importing keystore certs/client.p12 to certs/client.jks...
$ openssl x509 -outform der -in certs/ca.crt  -out certs/ca.der
$ keytool -import \
-noprompt \
-alias clientca \
-deststorepass $PASSWORD \
-keystore certs/client.jks \
-file certs/ca.der
Certificate was added to keystore

$ openssl x509 \
-outform der \
-in "$(mkcert -CAROOT)/rootCa.pem" \
-out certs/server.der
$ keytool \
-import \
-noprompt \
-alias serverca \
-deststorepass $PASSWORD \
-keystore certs/client.jks \
-file certs/server.der
Certificate was added to keystore
keytool -list -keystore certs/client.jks 
Enter keystore password:
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 3 entries
client, 28 Jul 2021, PrivateKeyEntry, Certificate fingerprint (SHA-256): D0:17:71:78:C7:C2:0A:10:2A:D2:6C:FF:F4:08:30:D1:48:AF:78:50:57:CD:DC:EB:DF:38:7D:7D:58:53:0F:B3
clientca, 28 Jul 2021, trustedCertEntry, Certificate fingerprint (SHA-256): CC:D1:28:89:72:C2:B5:5E:22:2B:D0:88:0A:05:B7:5E:53:E3:4F:0F:6B:28:C7:24:03:CA:F6:C7:24:8E:93:51
serverca, 28 Jul 2021, trustedCertEntry, Certificate fingerprint (SHA-256): DD:9B:E9:5A:8D:04:F7:AF:93:94:D7:C7:8F:B9:05:49:BD:C5:95:A5:11:C7:96:19:51:A7:09:6F:5C:9F:81:6C
try (var in = jksUrl.openStream()) {
var ks = KeyStore.getInstance("JKS");
ks.load(in, "changeit".toCharArray());
return ks;
}

TrustManager and SSL Context

In order to run some request on our endpoint, we will use the OkHttp library.

public OkHttpClient build() {

try {
var ks = buildKeyStore();

var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);
var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);

var sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());

var trustManager = getX509TrustManager(tmf);

return new OkHttpClient.Builder() //
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
.build();

} catch (IOException | KeyManagementException | NoSuchAlgorithmException | UnrecoverableKeyException
| KeyStoreException e) {
return null;
}

}

private X509TrustManager getX509TrustManager(TrustManagerFactory tmf) {
TrustManager[] trustManagers = tmf.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
}

Conclusion

In this second part, we have seen how to use the certificates that we created in the first part in a Java application. Java has its own way of dealing with certificates and it can be sometimes difficult to find its way between all the keys and certificate chains.

References

Nuxeo Open Kitchen

Get insights from our kitchen.

Nuxeo Open Kitchen

Get insights from our kitchen. Our philosophy is simple: no secrets, no hiding, just great code. This is the essence of the open kitchen. If you want to see the quality and care that goes into developing our software, simply take a look for yourself.

Damien Metzler

Written by

Nuxeo Open Kitchen

Get insights from our kitchen. Our philosophy is simple: no secrets, no hiding, just great code. This is the essence of the open kitchen. If you want to see the quality and care that goes into developing our software, simply take a look for yourself.