jpganz18
jpganz18

Reputation: 5858

how to add SSL certificates to okHttp mutual TLS connection?

I have a .pem and a .key files already that I dont want to install/import it locally, just tell my client to use them, is it possible? its not a self signed certificate

Basically, in my curl Im doing something like this:

curl --key mykey.key --cert mycert.pem https://someurl.com/my-endpoint

I have checked this answer

How to make https request with ssl certificate in Retrofit

also this https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java (but might not make sense since I dont get an object of the type I need)

Basically I have my okHttpClient

val okHttpClient = OkHttpClient.Builder()
    .sslSocketFactory(?, ?) //here I tried to call sslSocketFactory, trustManager following the example from the CustomTrust.java
    .build()

Any ideas for a solution?

Have checked this documentation as well but again the ssl part is not completed nor at the samples

https://square.github.io/okhttp/https/#customizing-trusted-certificates-kt-java

So I tried doing this (base on the okhttp samples)

private fun trustedCertificatesInputStream(): InputStream {
        val comodoRsaCertificationAuthority = (""
            + "-----BEGIN CERTIFICATE-----\n" +
            "-----END CERTIFICATE-----")
        return Buffer()
            .writeUtf8(comodoRsaCertificationAuthority)
            .inputStream()
    }


    val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }


    fun createClient() : OkHttpClient {

        val trustManager: X509TrustManager
        val sslSocketFactory: SSLSocketFactory
        try {
            trustManager = trustManagerForCertificates(trustedCertificatesInputStream())
            val sslContext = SSLContext.getInstance("TLS")
            sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
            sslSocketFactory = sslContext.socketFactory



        } catch (e: GeneralSecurityException) {
            throw RuntimeException(e)
        }
        return OkHttpClient.Builder()
            .sslSocketFactory(sslSocketFactory, trustManager)
            .connectTimeout(45, TimeUnit.SECONDS)
            .readTimeout(45, TimeUnit.SECONDS)
            .protocols(listOf(Protocol.HTTP_1_1))
            .addInterceptor(loggingInterceptor)
            .build()
    }


    @Throws(GeneralSecurityException::class)
    private fun trustManagerForCertificates(input: InputStream): X509TrustManager {
        val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
        val certificates: Collection<Certificate?> = certificateFactory.generateCertificates(input)
        val password = "password".toCharArray() // Any password will work.
        val keyStore = newEmptyKeyStore(password)

        for ((index, certificate) in certificates.withIndex()) {
            val certificateAlias = index.toString()
            keyStore.setCertificateEntry(certificateAlias, certificate)
        }
        // Use it to build an X509 trust manager.
        val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
        keyManagerFactory.init(keyStore, password)

        val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
        trustManagerFactory.init(keyStore)

        val trustManagers: Array<TrustManager> = trustManagerFactory.getTrustManagers()
        return trustManagers[0]!! as X509TrustManager
    }

    @Throws(GeneralSecurityException::class)
    private fun newEmptyKeyStore(password: CharArray): KeyStore {
        return try {
            val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
            val inputStream: InputStream? = null // By convention, 'null' creates an empty key store.
            keyStore.load(inputStream, password)
            keyStore
        } catch (e: IOException) {
            throw AssertionError(e)
        }

    }

and I get an error of

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

searching about the error seems like I should install the SSL locally, which I should avoid to do it since I cannot install it that way in the server, is there any way I can make it work?

Upvotes: 7

Views: 26885

Answers (2)

Hakan54
Hakan54

Reputation: 3871

Looking at your curl command and java code I see that they are not configured the same. Let's first have a look at your curl command and analyse it:

your curl command:

curl --key mykey.key --cert mycert.pem https://someurl.com/my-endpoint

curl options explained:

  • key => your client private key
  • cert => your client certificate chain
  • cacert => trusted server certificates

Your cacert option is empty so if your curl passes it means it matched the server certificate based on the default trusted certificates which is available within curl. The default trusted certificate within curl may differ with the default trusted certificates within java and therefor it can result into different behaviour. I would recommend to add the server certificate to your curl command and java code snippet.

Based on your curl command we can translate the options to java:

Mapping from curl to java

  • key => KeyManager
  • cert => KeyManager
  • cacert => TrustManager

The default java classes have limited support for parsing pem formatted private keys. As far as I know it can only parse unencrypted private keys. I can recommend Bouncy Castle to easily parse encrypted pem formatted private keys. The example below assumes you have an unencrypted private key.

Option 1

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class App {

    public static void main(String[] args) throws Exception {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

        InputStream trustedCertificateAsInputStream = Files.newInputStream(Paths.get("/path/to/server-certificate.pem"), StandardOpenOption.READ);
        Certificate trustedCertificate = certificateFactory.generateCertificate(trustedCertificateAsInputStream);
        KeyStore trustStore = createEmptyKeyStore("secret".toCharArray());
        trustStore.setCertificateEntry("server-certificate", trustedCertificate);

        String privateKeyContent = new String(Files.readAllBytes(Paths.get("/path/to/mykey.key")), Charset.defaultCharset())
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replaceAll(System.lineSeparator(), "")
                .replace("-----END PRIVATE KEY-----", "");

        byte[] privateKeyAsBytes = Base64.getDecoder().decode(privateKeyContent);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyAsBytes);

        InputStream certificateChainAsInputStream = Files.newInputStream(Paths.get("/path/to/mycert.pem"), StandardOpenOption.READ);
        Certificate certificateChain = certificateFactory.generateCertificate(certificateChainAsInputStream);

        KeyStore identityStore = createEmptyKeyStore("secret".toCharArray());
        identityStore.setKeyEntry("client", keyFactory.generatePrivate(keySpec), "secret".toCharArray(), new Certificate[]{certificateChain});

        trustedCertificateAsInputStream.close();
        certificateChainAsInputStream.close();

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(identityStore, "secret".toCharArray());
        KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagers, trustManagers, null);

        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

        OkHttpClient okHttpClient = OkHttpClient.Builder()
                .sslSocketFactory(sslSocketFactory, trustManagers[0])
                .build();
    }

    public static KeyStore createEmptyKeyStore(char[] keyStorePassword) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, keyStorePassword);
        return keyStore;
    }
}

The code is a bit verbose, but it should do the trick for you.

Option 2

If you prefer a less verbose alternative you can give the following snippet a try:

X509ExtendedKeyManager keyManager = PemUtils.loadIdentityMaterial(Paths.get("/path/to/mycert.pem"), Paths.get("/path/to/mycert.pem"));
X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial(Paths.get("/path/to/server-certificate.pem"));

SSLFactory sslFactory = SSLFactory.builder()
          .withIdentityMaterial(keyManager)
          .withTrustMaterial(trustManager)
          .build();

SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();
X509ExtendedtrustManager trustManager = sslFactory.getTrustManager().orElseThrow();

OkHttpClient okHttpClient = OkHttpClient.Builder()
          .sslSocketFactory(sslSocketFactory, trustManager)
          .build();

The above library is maintained by me and you can find it here: GitHub - SSLContext Kickstart

Upvotes: 8

Jesse Wilson
Jesse Wilson

Reputation: 40593

I presume you want to configure TLS mutual auth, and that's what they key is for?

Take a look at okhttp-tls which has APIs for turning certificate and private keys into the corresponding Java objects.

Upvotes: 4

Related Questions