6

I'm looking to use the new HttpClient provided in java 11. It's not clear how to do mutual TLS (2 way auth, where both client and server present a certificate.)

Could someone provide an example of mutual TLS with HttpClient?

fafrd
  • 1,017
  • 13
  • 17
  • You will need to create a SSLContext with your certificates and setup SSLParameters like param = new SSLParameters(); param.setNeedClientAuth​(true); HttpClient.newBuilder(). sslContext​(context).sslParameters(param) – JEY Jul 21 '20 at 20:33
  • 1
    @JEY That's a good start, but I also need to set the certificate+key. It's not clear how to do this with SSLParametrs or SSLContext – fafrd Jul 22 '20 at 18:53

1 Answers1

16

Figured it out. Create an HttpClient, then pass in SSLContext and SSLParameters objects.

Load cert/key into SSLContext:

 // cert+key data. assuming X509 pem format
final byte[] publicData = your_cert_data; // -----BEGIN CERTIFICATE----- ...
final byte[] privateData = your_key_data; // -----BEGIN PRIVATE KEY----- ...

// parse certificate
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
final Collection<? extends Certificate> chain = certificateFactory.generateCertificates(
        new ByteArrayInputStream(publicData));

LOG.info("Successfully loaded the client cert certificate chain {}", String.join(" -> ", chain
        .stream()
        .map(certificate -> {
            if (certificate instanceof X509Certificate) {
                final X509Certificate x509Cert = (X509Certificate) certificate;
                return x509Cert.getSubjectDN().toString();
            } else {
                return certificate.getType();
            }
        }).collect(Collectors.toList())));

// parse key
final Key key = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateData));

// place cert+key into KeyStore
KeyStore clientKeyStore = KeyStore.getInstance("jks");
final char[] pwdChars = KEYSTORE_PASSWORD.toCharArray(); // use a random string, like from java.security.SecureRandom
clientKeyStore.load(null, null);
clientKeyStore.setKeyEntry(YOUR_SERVICE_NAME, key, pwdChars, chain.toArray(new Certificate[0]));

// initialize KeyManagerFactory
KeyManagerFactory keyMgrFactory = KeyManagerFactory.getInstance("SunX509");
keyMgrFactory.init(clientKeyStore, pwdChars);

// populate SSLContext with key manager
SSLContext sslCtx = SSLContext.getInstance("TLSv1.2");
sslCtx.init(keyMgrFactory.getKeyManagers(), null, null);

Create ssl parameters, set needClientAuth to true:

SSLParameters sslParam = new SSLParameters();
sslParam.setNeedClientAuth(true);

finally, create the HttpClient:

HttpClient client = HttpClient.newBuilder()
    .sslContext(sslCtx)
    .sslParameters(sslParam)
    .build();
fafrd
  • 1,017
  • 13
  • 17
  • crazy! I had to do that like 1/2 a day ago... `sslCtx.init(keyMgrFactory.getKeyManagers(), null, null);` - you are passing a null trust manager. is that really what you want? – Eugene Jul 24 '20 at 20:12
  • 1
    @Eugene: SSLContext.init(,trustmgr=null,) uses the _default_ trustmanager whcih in turn uses the default truststore which unless overridden is the file JRE/lib/security/cacerts, which is prepopulated with the 'usual suspects' like Digicert GoDaddy LetsEncrypt. So usually yes you want that. – dave_thompson_085 Jul 25 '20 at 00:50
  • @dave_thompson_085 I am so glad you came up with this comment, I saw you answers and comments on this subject. The reasons I asked was because I had to create and empty `TrustManager` ( as opposed to a `null/default` one), otherwise I would always get a handshake failure. My knowledge is somehow limited in this area, so was wondering if you would bring some light... I received a private key and a client certificate in a pem file, and the only way for to connect was via an empty trust manager instead of a null, which looks like Ive done something fishy, at best. – Eugene Jul 25 '20 at 01:03
  • @Eugene: if you mean empty as in no trust anchors, that would work only with 'anonymous' suites which are _very_ rare (and gone in TLS1.3). If you mean the oft-posted 'check-nothing/accept-everything' stub code, that is insecure except in an environment where active attacks are fully prevented by other methods, which may well count as piscine. In any case, that's (1) a completely different issue than this Q, and A, and Stack comments not related to the Q&A can be and often are deleted and (2) likely too complicated to handle in comments anyway. You may want to ask a Question. – dave_thompson_085 Jul 28 '20 at 02:20
  • 2
    Amount of code that is needed to make such a basic request is staggering. I think there should be a factory method that would allow a way to simply provide a two files (one with certificate and one with private key). – Krzysztof Krasoń Oct 03 '20 at 08:04
  • 1
    The line `final Key key = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateData));` does not work for X.509 encoded key, which is assumed in first lines of this fragment. – Krzysztof Krasoń Oct 03 '20 at 08:15