From 85f74e2004eaf0d4ff339e8644df3d8e716361e5 Mon Sep 17 00:00:00 2001 From: jwilson Date: Mon, 28 Mar 2016 19:02:25 -0400 Subject: [PATCH] Accept user-provided trust managers in OkHttpClient.Builder Closes: https://github.com/square/okhttp/issues/2427 --- .../test/java/okhttp3/OkUrlFactoryTest.java | 2 +- .../main/java/okhttp3/CertificatePinner.java | 29 ++--- .../src/main/java/okhttp3/OkHttpClient.java | 110 ++++++++++++++---- .../java/okhttp3/recipes/CustomTrust.java | 76 +++++++----- 4 files changed, 141 insertions(+), 76 deletions(-) diff --git a/okhttp-urlconnection/src/test/java/okhttp3/OkUrlFactoryTest.java b/okhttp-urlconnection/src/test/java/okhttp3/OkUrlFactoryTest.java index d357807ef3f9..39f6066805dd 100644 --- a/okhttp-urlconnection/src/test/java/okhttp3/OkUrlFactoryTest.java +++ b/okhttp-urlconnection/src/test/java/okhttp3/OkUrlFactoryTest.java @@ -12,8 +12,8 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import okhttp3.internal.URLFilter; import okhttp3.internal.SslContextBuilder; +import okhttp3.internal.URLFilter; import okhttp3.internal.http.OkHeaders; import okhttp3.internal.io.InMemoryFileSystem; import okhttp3.mockwebserver.MockResponse; diff --git a/okhttp/src/main/java/okhttp3/CertificatePinner.java b/okhttp/src/main/java/okhttp3/CertificatePinner.java index 964959470678..626b91e774c2 100644 --- a/okhttp/src/main/java/okhttp3/CertificatePinner.java +++ b/okhttp/src/main/java/okhttp3/CertificatePinner.java @@ -126,9 +126,9 @@ public final class CertificatePinner { private final List pins; private final CertificateChainCleaner certificateChainCleaner; - private CertificatePinner(Builder builder) { - this.pins = Util.immutableList(builder.pins); - this.certificateChainCleaner = builder.certificateChainCleaner; + private CertificatePinner(List pins, CertificateChainCleaner certificateChainCleaner) { + this.pins = pins; + this.certificateChainCleaner = certificateChainCleaner; } /** @@ -207,8 +207,11 @@ List findMatchingPins(String hostname) { return result; } - Builder newBuilder() { - return new Builder(this); + /** Returns a certificate pinner that uses {@code certificateChainCleaner}. */ + CertificatePinner withCertificateChainCleaner(CertificateChainCleaner certificateChainCleaner) { + return this.certificateChainCleaner != certificateChainCleaner + ? new CertificatePinner(pins, certificateChainCleaner) + : this; } /** @@ -288,20 +291,6 @@ boolean matches(String hostname) { /** Builds a configured certificate pinner. */ public static final class Builder { private final List pins = new ArrayList<>(); - private CertificateChainCleaner certificateChainCleaner; - - public Builder() { - } - - Builder(CertificatePinner certificatePinner) { - this.pins.addAll(certificatePinner.pins); - this.certificateChainCleaner = certificatePinner.certificateChainCleaner; - } - - public Builder certificateChainCleaner(CertificateChainCleaner certificateChainCleaner) { - this.certificateChainCleaner = certificateChainCleaner; - return this; - } /** * Pins certificates for {@code pattern}. @@ -321,7 +310,7 @@ public Builder add(String pattern, String... pins) { } public CertificatePinner build() { - return new CertificatePinner(this); + return new CertificatePinner(Util.immutableList(pins), null); } } } diff --git a/okhttp/src/main/java/okhttp3/OkHttpClient.java b/okhttp/src/main/java/okhttp3/OkHttpClient.java index 194aaa6c3260..8df00c54a4b3 100644 --- a/okhttp/src/main/java/okhttp3/OkHttpClient.java +++ b/okhttp/src/main/java/okhttp3/OkHttpClient.java @@ -20,7 +20,9 @@ import java.net.ProxySelector; import java.net.UnknownHostException; import java.security.GeneralSecurityException; +import java.security.KeyStore; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import javax.net.SocketFactory; @@ -28,6 +30,8 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import okhttp3.internal.Internal; import okhttp3.internal.InternalCache; @@ -171,30 +175,16 @@ private OkHttpClient(Builder builder) { if (builder.sslSocketFactory != null || !isTLS) { this.sslSocketFactory = builder.sslSocketFactory; + this.certificateChainCleaner = builder.certificateChainCleaner; } else { - try { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); - this.sslSocketFactory = sslContext.getSocketFactory(); - } catch (GeneralSecurityException e) { - throw new AssertionError(); // The system has no TLS. Just give up. - } - } - if (sslSocketFactory != null && builder.certificateChainCleaner == null) { - X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory); - if (trustManager == null) { - throw new IllegalStateException("Unable to extract the trust manager on " + Platform.get() - + ", sslSocketFactory is " + sslSocketFactory.getClass()); - } + X509TrustManager trustManager = systemDefaultTrustManager(); + this.sslSocketFactory = systemDefaultSslSocketFactory(trustManager); this.certificateChainCleaner = CertificateChainCleaner.get(trustManager); - this.certificatePinner = builder.certificatePinner.newBuilder() - .certificateChainCleaner(certificateChainCleaner) - .build(); - } else { - this.certificateChainCleaner = builder.certificateChainCleaner; - this.certificatePinner = builder.certificatePinner; } + this.hostnameVerifier = builder.hostnameVerifier; + this.certificatePinner = builder.certificatePinner.withCertificateChainCleaner( + certificateChainCleaner); this.proxyAuthenticator = builder.proxyAuthenticator; this.authenticator = builder.authenticator; this.connectionPool = builder.connectionPool; @@ -207,6 +197,32 @@ private OkHttpClient(Builder builder) { this.writeTimeout = builder.writeTimeout; } + private X509TrustManager systemDefaultTrustManager() { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + return (X509TrustManager) trustManagers[0]; + } catch (GeneralSecurityException e) { + throw new AssertionError(); // The system has no TLS. Just give up. + } + } + + private SSLSocketFactory systemDefaultSslSocketFactory(X509TrustManager trustManager) { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { trustManager }, null); + return sslContext.getSocketFactory(); + } catch (GeneralSecurityException e) { + throw new AssertionError(); // The system has no TLS. Just give up. + } + } + /** Default connect timeout (in milliseconds). */ public int connectTimeoutMillis() { return connectTimeout; @@ -519,14 +535,62 @@ public Builder socketFactory(SocketFactory socketFactory) { } /** - * Sets the socket factory used to secure HTTPS connections. + * Sets the socket factory used to secure HTTPS connections. If unset, the system default will + * be used. * - *

If unset, a lazily created SSL socket factory will be used. + * @deprecated {@code SSLSocketFactory} does not expose its {@link X509TrustManager}, which is + * a field that OkHttp needs to build a clean certificate chain. This method instead must + * use reflection to extract the trust manager. Applications should prefer to call {@link + * #sslSocketFactory(SSLSocketFactory, X509TrustManager)}, which avoids such reflection. */ public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory) { if (sslSocketFactory == null) throw new NullPointerException("sslSocketFactory == null"); + X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory); + if (trustManager == null) { + throw new IllegalStateException("Unable to extract the trust manager on " + Platform.get() + + ", sslSocketFactory is " + sslSocketFactory.getClass()); + } this.sslSocketFactory = sslSocketFactory; - this.certificateChainCleaner = null; + this.certificateChainCleaner = CertificateChainCleaner.get(trustManager); + return this; + } + + /** + * Sets the socket factory and trust manager used to secure HTTPS connections. If unset, the + * system defaults will be used. + * + *

Most applications should not call this method, and instead use the system defaults. Those + * classes include special optimizations that can be lost if the implementations are decorated. + * + *

If necessary, you can create and configure the defaults yourself with the following code: + * + *

   {@code
+     *
+     *   TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
+     *       TrustManagerFactory.getDefaultAlgorithm());
+     *   trustManagerFactory.init((KeyStore) null);
+     *   TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+     *   if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+     *     throw new IllegalStateException("Unexpected default trust managers:"
+     *         + Arrays.toString(trustManagers));
+     *   }
+     *   X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
+     *
+     *   SSLContext sslContext = SSLContext.getInstance("TLS");
+     *   sslContext.init(null, new TrustManager[] { trustManager }, null);
+     *   SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+     *
+     *   OkHttpClient client = new OkHttpClient.Builder()
+     *       .sslSocketFactory(sslSocketFactory, trustManager);
+     *       .build();
+     * }
+ */ + public Builder sslSocketFactory( + SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) { + if (sslSocketFactory == null) throw new NullPointerException("sslSocketFactory == null"); + if (trustManager == null) throw new NullPointerException("trustManager == null"); + this.sslSocketFactory = sslSocketFactory; + this.certificateChainCleaner = CertificateChainCleaner.get(trustManager); return this; } diff --git a/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java b/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java index 6fe27a3933c4..0ee606cbf00e 100644 --- a/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java +++ b/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java @@ -19,13 +19,16 @@ import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; +import java.util.Arrays; import java.util.Collection; 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 javax.net.ssl.X509TrustManager; import okhttp3.CertificatePinner; import okhttp3.Headers; import okhttp3.OkHttpClient; @@ -37,9 +40,19 @@ public final class CustomTrust { private final OkHttpClient client; public CustomTrust() { - SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream()); + X509TrustManager trustManager; + SSLSocketFactory sslSocketFactory; + try { + trustManager = trustManagerForCertificates(trustedCertificatesInputStream()); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { trustManager }, null); + sslSocketFactory = sslContext.getSocketFactory(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + client = new OkHttpClient.Builder() - .sslSocketFactory(sslContext.getSocketFactory()) + .sslSocketFactory(sslSocketFactory, trustManager) .build(); } @@ -139,7 +152,7 @@ private InputStream trustedCertificatesInputStream() { } /** - * Returns a SSL context that trusts {@code certificates} and none other. HTTPS services whose + * Returns a trust manager that trusts {@code certificates} and none other. HTTPS services whose * certificates have not been signed by these certificates will fail with a {@code * SSLHandshakeException}. * @@ -158,37 +171,36 @@ private InputStream trustedCertificatesInputStream() { * not use custom trusted certificates in production without the blessing of your server's TLS * administrator. */ - public SSLContext sslContextForTrustedCertificates(InputStream in) { - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - Collection certificates = certificateFactory.generateCertificates(in); - if (certificates.isEmpty()) { - throw new IllegalArgumentException("expected non-empty set of trusted certificates"); - } + private X509TrustManager trustManagerForCertificates(InputStream in) + throws GeneralSecurityException { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = certificateFactory.generateCertificates(in); + if (certificates.isEmpty()) { + throw new IllegalArgumentException("expected non-empty set of trusted certificates"); + } - // Put the certificates a key store. - char[] password = "password".toCharArray(); // Any password will work. - KeyStore keyStore = newEmptyKeyStore(password); - int index = 0; - for (Certificate certificate : certificates) { - String certificateAlias = Integer.toString(index++); - keyStore.setCertificateEntry(certificateAlias, certificate); - } + // Put the certificates a key store. + char[] password = "password".toCharArray(); // Any password will work. + KeyStore keyStore = newEmptyKeyStore(password); + int index = 0; + for (Certificate certificate : certificates) { + String certificateAlias = Integer.toString(index++); + keyStore.setCertificateEntry(certificateAlias, certificate); + } - // Wrap it up in an SSL context. - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( - KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, password); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(keyStore); - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), - new SecureRandom()); - return sslContext; - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); + // Use it to build an X509 trust manager. + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); } + return (X509TrustManager) trustManagers[0]; } private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {