57

I think I need to create a new SSL Socket Factory? Also, I don't want to use the global SSL Context (https://github.com/square/okhttp/issues/184) for obvious reasons.

thanks!

EDIT:

As of okhttp 2.1.0 you can pin certificates very easily.

See the source code here to get started

Michael Barany
  • 1,639
  • 1
  • 12
  • 15

5 Answers5

96

UPDATE FOR OKHTTP 3.0

OKHTTP 3.0 has built-in support for pinning certificates. Start off by pasting the following code:

 String hostname = "yourdomain.com";
 CertificatePinner certificatePinner = new CertificatePinner.Builder()
     .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
     .build();
 OkHttpClient client = OkHttpClient.Builder()
     .certificatePinner(certificatePinner)
     .build();

 Request request = new Request.Builder()
     .url("https://" + hostname)
     .build();
 client.newCall(request).execute();

This will fail because AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is not a valid hash of your certificate. The exception thrown will have the correct hashes of your certificate:

 javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
   Peer certificate chain:
     sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL
     sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA
     sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
     sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root
   Pinned certificates for publicobject.com:
     sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
   at okhttp3.CertificatePinner.check(CertificatePinner.java)
   at okhttp3.Connection.upgradeToTls(Connection.java)
   at okhttp3.Connection.connect(Connection.java)
   at okhttp3.Connection.connectAndSetOwner(Connection.java)

Make sure you add these to your CertificatePinner object, and you have successfully pinned your certificate:

 CertificatePinner certificatePinner = new CertificatePinner.Builder()
   .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
   .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
   .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
   .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
   .build();

EVERYTHING PAST HERE IS FOR OLDER (2.x) VERSIONS OF OKHTTP

After reading this blog post I was able to modify the concept for use with OkHttp. You should use at least version 2.0 if you want to avoid using a global SSL context.

This modification applies only to the current instance of OkHttp, and changes that instance so that it only accepts certificates from the certificate specified. If you want other certificates (such as one from Twitter) to be accepted, you simply need to create a new OkHttp instance without the modifications described below.

1. Creating a TrustStore

In order to pin a certificate, you first need to create a truststore containing this certificate. To create the truststore we will use this handy script from nelenkov slightly modified for our purposes:

#!/bin/bash

if [ "$#" -ne 3 ]; then
  echo "Usage: importcert.sh <CA cert PEM file> <bouncy castle jar> <keystore pass>"
  exit 1
fi

CACERT=$1
BCJAR=$2
SECRET=$3

TRUSTSTORE=mytruststore.bks
ALIAS=`openssl x509 -inform PEM -subject_hash -noout -in $CACERT`

if [ -f $TRUSTSTORE ]; then
    rm $TRUSTSTORE || exit 1
fi

echo "Adding certificate to $TRUSTSTORE..."
keytool -import -v -trustcacerts -alias $ALIAS \
      -file $CACERT \
      -keystore $TRUSTSTORE -storetype BKS \
      -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
      -providerpath $BCJAR \
      -storepass $SECRET

echo "" 
echo "Added '$CACERT' with alias '$ALIAS' to $TRUSTSTORE..."

To run this script you need 3 things:

  1. Make sure keytool (included in Android SDK) is on your $PATH.
  2. Make sure you have the latest BouncyCastle jar file download in the same dir as the script. (Download here)
  3. The certificate you want to pin.

Now run the script

./gentruststore.sh your_cert.pem bcprov-jdk15on-150.jar your_secret_pass

Type 'yes' to trust the certificate, and when complete mytruststore.bks will be generated in your current dir.

2. Apply your TrustStore to your Android project

Create a directory raw under your res folder. Copy mytruststore.bks here.

Now here's a very simple class that pins your cert to OkHttp

import android.content.Context;
import android.util.Log;

import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import java.io.InputStream;
import java.io.Reader;
import java.security.KeyStore;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;


/**
 * Created by martin on 02/06/14.
 */
public class Pinning {

    Context context;
    public static String TRUST_STORE_PASSWORD = "your_secret";
    private static final String ENDPOINT = "https://api.yourdomain.com/";

    public Pinning(Context c) {
        this.context = c;
    }

    private SSLSocketFactory getPinnedCertSslSocketFactory(Context context) {
        try {
            KeyStore trusted = KeyStore.getInstance("BKS");
            InputStream in = context.getResources().openRawResource(R.raw.mytruststore);
            trusted.load(in, TRUST_STORE_PASSWORD.toCharArray());
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(trusted);
            sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            Log.e("MyApp", e.getMessage(), e);
        }
        return null;
    }

    public void makeRequest() {
        try {
            OkHttpClient client = new OkHttpClient();
            client.setSslSocketFactory(getPinnedCertSslSocketFactory(context));

            Request request = new Request.Builder()
                    .url(ENDPOINT)
                    .build();

            Response response = client.newCall(request).execute();

            Log.d("MyApp", response.body().string());

        } catch (Exception e) {
            Log.e("MyApp", e.getMessage(), e);

        }
    }
}

As you can see we instantiate a new instance of OkHttpClient and call setSslSocketFactory, passing in a SSLSocketFactory with our custom truststore. Make sure you set TRUST_STORE_PASSWORD to the password you passed into the shell script. Your OkHttp instance should now only accept the certificate you specified.

Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
  • 25
    Please do not create an `OkHttpClient` for every request. Create **a single instance** and re-use it for every request. – Jake Wharton Jun 03 '14 at 17:20
  • 4
    This is sample code, of course you wouldn't do this for every request - notice the url is hardcoded as well. – Martin Konecny Jun 03 '14 at 17:49
  • thanks. what's the benefit of the password? someone can just rip my APK and see the password anyways. Is there a way to create a store without requiring a password? – Michael Barany Jun 03 '14 at 18:33
  • 3
    As far as I understand the certificate is not something you're supposed to keep private. Your server sends it to the client for each SSL handshake. In this case a password is required because the `keytool` usually is made for storing private keys, not certs. Unfortunately there is no way to disable passwords so use anything you like. – Martin Konecny Jun 03 '14 at 19:56
  • @JakeWharton When we have a single instance of OkHttpClient and try to make parallel/concurrent requests from different background threads (also by using enqueue ), the requests are still synchronised and responses are also received one by one. Is there anyway to overcome this behaviour by using only a single instance of okhttpclient? – Gunhan Dec 01 '15 at 16:27
  • Wonderful answer! Except it's outdated now with okhttp3: ````OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(getPinnedCertSslSocketFactory(context)) .build();```` – xorgate Jun 17 '16 at 09:03
  • This does not actually **pin** a certificate. This adds your cert as root certificate. Which means that every certificate signed with the one you've added is also valid. – Agent_L Nov 18 '16 at 12:32
  • @agent_l that's incorrect. It will only accept this one certificate you've created. All other certificates will be invalid. – Martin Konecny Nov 18 '16 at 12:34
  • @MartinKonecny I've tested your way with BKS (created with portecle) and the result is same: root cert in BKS, derived cert on website and connection is established while it should not be. – Agent_L Nov 18 '16 at 14:19
  • 3
    java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. – Aderbal Nunes Dec 09 '16 at 17:09
  • How to get hashes for OKHTTP 3 via command line or other tools? – Sebastian Roth Jan 27 '17 at 04:44
  • 2
    @SebastianRoth openssl x509 -inform der -in -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha1 -binary | openssl enc -base64 outputs a sha1 in base64 of your public key if that is what your were looking for... – Greg Mar 08 '17 at 14:53
  • People with pem file can use openssl x509 -inform pem -in -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha1 -binary | openssl enc -base64 thanx to @estoke – silentsudo Mar 22 '17 at 13:05
  • @sajadabbasi that's one of the downfalls of certificate pinning in general – Martin Konecny Nov 30 '18 at 15:00
  • @MartinKonecny ins't there any way to fix this problem? – sajad abbasi Nov 30 '18 at 18:56
  • how to generate Hash key ..? – Atif AbbAsi May 22 '21 at 12:18
27

This is easier than I thought with OkHttp.

Follow these steps:

1. Get the public sha1 keys. The OkHttp documentation gives us a clear way to do this complete with sample code. In case it goes away, here it is pasted in below:

For example, to pin https://publicobject.com, start with a broken configuration:

String hostname = "publicobject.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
    .add(hostname, "sha1/BOGUSPIN")
    .build();
OkHttpClient client = new OkHttpClient();
client.setCertificatePinner(certificatePinner);

Request request = new Request.Builder()
    .url("https://" + hostname)
    .build();
client.newCall(request).execute();   

As expected, this fails with a certificate pinning exception:

javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain: sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root

Pinned certificates for publicobject.com:

sha1/BOGUSPIN
at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
at com.squareup.okhttp.Connection.connect(Connection.java)
at com.squareup.okhttp.Connection.connectAndSetOwner(Connection.java)

Follow up by pasting the public key hashes from the exception into the certificate pinner's configuration:

Side note: If you are doing this on Android you will get a separate exception if you are doing this on a UI thread, so make sure you do this on a background thread.

2. Configure your OkHttp Client:

OkHttpClient client = new OkHttpClient();
client.setCertificatePinner(new CertificatePinner.Builder()
       .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
       .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
       .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
       .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
       .build());

That's all there is to it!

spierce7
  • 14,797
  • 13
  • 65
  • 106
  • +1 for the solution, however retrofit is not using the OkHttpClient I am setting with CertificatePinner, any idea? – dhaval Mar 31 '16 at 18:04
  • 1
    @dhaval You can use the retrofit builder, and supply an OkHttpClient to it. – spierce7 Jul 27 '16 at 18:24
  • cool! If my site certificate is going to expire in a month and I want both the old cert and the new one to work once its live can I add both certificate pins in my app? – Bootstrapper Nov 17 '16 at 15:18
  • @Bootstrapper I'm not an expert in this by any means. But I just went through this process. I worked with my IT team, and they swapped out the certificates on a test environment, and I was able to connect just fine to it. From my testing, I found that as long as at least one of the certificates matched, the request would still succeed. You should do your own testing though, as I only tested this very briefly, and we ended up taking a measure that didn't rely on this approach. – spierce7 Nov 17 '16 at 20:28
  • Well, this technically is pinning a **hash**, not a certificate itself. Why open yourself to a collision attack, even if it's purely hypothetical? – Agent_L Nov 18 '16 at 12:34
  • That seems ridiculous to me, but if you aren't comfortable with it, don't pin a hash. – spierce7 Nov 18 '16 at 20:20
  • 2
    i am getting this exception instead of the one with the sha256, any idea how to remedy? `javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.` – Fonix Jan 11 '17 at 05:09
  • ok seems like it may be server side, when using my UAT server it gives that error, but against my prod server its ok, so must be the way the certificates are setup on the server, not sure how that works though, maybe someone can clarify – Fonix Jan 11 '17 at 05:36
  • @Fonix Did you get any solution? – Palak Darji Jan 23 '21 at 06:30
17

If you don't have access to the domain (restricted access for example) and cant test bogus hash, but you have certificate file you can use openssl to retrieve it:

openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
  • 2
    If your certificate is already a der (eg. if taken from chrome), the command is `openssl x509 -inform der -in certificate.cer -fingerprint -sha256 -noout | openssl dgst -sha256 -binary | openssl enc -base64` – Tom Jan 09 '18 at 23:45
  • Be careful, and make sure to use -pubkey (-fingerprint will create a sha256 for the entire cert which is incorrect) – DummyData Jan 27 '21 at 17:27
6

To expand on the sample source code @Michael-barany shared, I have done some testing and it appears to be a misleading code sample. In the sample the code the exception noted 4 sha1 hashes from the certificate chain exception:

javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL
sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA
sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root

then subsequently added all 4 sha1 public key hashes to the CertificatePinner Builder.

CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
.add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
.add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
.add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
.build();

However, given tests I have performed and reviewing the code, only the first valid one would be interpreted, so you would be best suited to only include ONE of the hashes returned. You could use the most specific hash "DmxUShsZuNiqPQsX2Oi9uv2sCnw" for the precise site certificate... or you could use the most broad hash "T5x9IXmcrQ7YuQxXnxoCmeeQ84c" for the CA Root based on your desired security posture.

Dave Cobb
  • 151
  • 1
  • 3
  • 1
    You would have to post this to Square's github, but I think the author's intent was to have fallback hashes in case perhaps the certificate needed to be revoked and replaced it wouldn't cause havoc on existing client code. Again, that's just my thoughts. – Michael Barany May 12 '15 at 15:49
  • @MichaelBarany, FWIW [AFNetworking](https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFSecurityPolicy.m#L280) for iOS supports both validating that each pinned certificate is in the chain and validating that at least one pinned certificate is in the chain. I'm not an expert on which is more ideal, but it's nice to have that flexibility. – deRonbrown May 14 '15 at 02:18
3

I found the example mentioned in Unknown certificate authority section of this link developer.android.com/training/articles/security-ssl very useful.

The SSLSocketFactory returned in context.getSocketFactory() can be then used to set to OkHttpClient in setSslSocketFactory() method.

Note : The Unknown certificate authority section also mentions the link to download a cert file to use and check this code.

Here is the sample method I've written to obtain the SSLSocketFactory

private SSLSocketFactory getSslSocketFactory() {
    try {
        // Load CAs from an InputStream
        // (could be from a resource or ByteArrayInputStream or ...)
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        // From https://www.washington.edu/itconnect/security/ca/load-der.crt
        InputStream caInput = getApplicationContext().getResources().openRawResource(R.raw.loadder);
        Certificate ca = null;
        try {
            ca = cf.generateCertificate(caInput);
            System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
        } catch (CertificateException e) {
            e.printStackTrace();
        } finally {
            caInput.close();
        }

        // Create a KeyStore containing our trusted CAs
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);
        if (ca == null)
            return null;
        keyStore.setCertificateEntry("ca", ca);

        // Create a TrustManager that trusts the CAs in our KeyStore
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);

        // Create an SSLContext that uses our TrustManager
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, tmf.getTrustManagers(), null);

        return context.getSocketFactory();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

Later I'm just setting this to OkHttpClient like this

httpClient.setSslSocketFactory(sslSocketFactory);

and then make the https call

httpClient.newCall(requestBuilder.build()).enqueue(callback);
Vinayak
  • 523
  • 1
  • 9
  • 20