5

I'm trying to pin my server's self-signed certificate. My OkHttpClient takes two parameters, the first one is the ssl Socket Factory :

final TrustManager[] trustAllCerts = new TrustManager[] {
        new X509TrustManager() {
            @SuppressLint("TrustAllX509TrustManager")
            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {}

            @SuppressLint("TrustAllX509TrustManager")
            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {}

            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                  return new X509Certificate[0];
            }
         }
     };

// Install the all-trusting trust manager
SSLContext sslContext;
try {
     sslContext = SSLContext.getInstance("SSL");
     sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
 } catch (NoSuchAlgorithmException | KeyManagementException e) {
     e.printStackTrace();
     FirebaseCrash.report(e);
     return null;
}

// Create an ssl socket factory with our all-trusting manager
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

Second is a certificate pinner :

new CertificatePinner.Builder()
    .add("bogus.com", "sha1/BOGUS")
    .build()

Note: if I don't add a certificatePinner, then everything works fine. The problem is that when the request gets executed, CertificatePinner.check() gets called:

if (pins.isEmpty()) return;

Obviously, if I do set a (non-empty) certificatePinner, the method won't stop there and will continue. It then goes on to check that "at least one of the certificates pinned for my hostname is a trusted certificate".

The problem is that I passed an empty array in getAcceptedIssuers for my TrustManager - meaning the self-signed cert will trigger an exception since it hasn't been explicitly trusted in "getAcceptedIssues". Seems like it's impossible to pin a certificate that isn't trusted explicitly in "getAcceptedIssuers".

Is there any way to get around that? Is it by design?

This is how I build my OkHttpClient :

OkHttpClient client = new OkHttpClient.Builder()
    .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0])
    .certificatePinner(certPinner)
    .readTimeout(10, TimeUnit.SECONDS)
    .connectTimeout(10, TimeUnit.SECONDS)
    .build();
Mike
  • 4,550
  • 4
  • 33
  • 47
geecko
  • 660
  • 1
  • 9
  • 22

1 Answers1

5

The TrustManager, CertificatePinner and Hostname verification all do different but important things. If you want to use self-signed certificates but still have security, as opposed to self-signed certificates purely for ease of local development then you probably want to create a valid TrustManager.

e.g. https://github.com/yschimke/oksocial/blob/3757196cde420b9d0fe37cf385b66f4cdafb1ae1/src/main/java/com/baulsupp/oksocial/security/CertificateUtils.java#L19

  public static X509TrustManager load(List<File> serverCerts)
      throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException {
    return trustManagerForKeyStore(keyStoreForCerts(serverCerts));
  }

  public static X509TrustManager trustManagerForKeyStore(KeyStore ks)
      throws NoSuchAlgorithmException, KeyStoreException {
    TrustManagerFactory tmf =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

    tmf.init(ks);

    return (X509TrustManager) tmf.getTrustManagers()[0];
  }

  public static KeyStore keyStoreForCerts(List<File> serverCerts)
      throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
    CertificateFactory cf = CertificateFactory.getInstance("X.509");

    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    ks.load(null);

    for (int i = 0; i < serverCerts.size(); i++) {
      try (InputStream is = new FileInputStream(serverCerts.get(i))) {
        X509Certificate caCert = (X509Certificate) cf.generateCertificate(is);
        ks.setCertificateEntry("cacrt." + i, caCert);
      }
    }
    return ks;
  }

This will start off with the system certificates loaded, so your client can still be used to load externally hosted images etc.

Then on top of that you can use CertificatePinner to require that only your trusted self-signed certificates are used for your domain.

Yuri Schimke
  • 12,435
  • 3
  • 35
  • 69
  • The code you provided definitely helped me, though it's not exactly answering my original question. I did end up creating a valid TrustManager thanks to your help. – geecko Feb 19 '17 at 13:53
  • Yeah, I was trying to answer what I thought you were trying to do, add a self-signed certificate so you had a secure connection but pin specifically against that. Accepting all certificates and then trying to pin doesn't seem like a typical use case. – Yuri Schimke Feb 20 '17 at 08:44