diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java index 7833cca69..3b390e841 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java @@ -15,29 +15,49 @@ */ package com.squareup.okhttp; -import org.junit.Test; - import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public final class ConnectionSpecTest { + @Test public void noTlsVersions() throws Exception { + try { + new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(new TlsVersion[0]) + .build(); + fail(); + } catch (IllegalArgumentException expected) { + assertEquals("At least one TLS version is required", expected.getMessage()); + } + } - @Test - public void cleartextBuilder() throws Exception { + @Test public void noCipherSuites() throws Exception { + try { + new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .cipherSuites(new CipherSuite[0]) + .build(); + fail(); + } catch (IllegalArgumentException expected) { + assertEquals("At least one cipher suite is required", expected.getMessage()); + } + } + + @Test public void cleartextBuilder() throws Exception { ConnectionSpec cleartextSpec = new ConnectionSpec.Builder(false).build(); assertFalse(cleartextSpec.isTls()); } - @Test - public void tlsBuilder_explicitCiphers() throws Exception { + @Test public void tlsBuilder_explicitCiphers() throws Exception { ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true) .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) .tlsVersions(TlsVersion.TLS_1_2) @@ -48,8 +68,7 @@ public final class ConnectionSpecTest { assertTrue(tlsSpec.supportsTlsExtensions()); } - @Test - public void tlsBuilder_defaultCiphers() throws Exception { + @Test public void tlsBuilder_defaultCiphers() throws Exception { ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(true) @@ -59,8 +78,7 @@ public final class ConnectionSpecTest { assertTrue(tlsSpec.supportsTlsExtensions()); } - @Test - public void tls_defaultCiphers_noFallbackIndicator() throws Exception { + @Test public void tls_defaultCiphers_noFallbackIndicator() throws Exception { ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(false) @@ -79,17 +97,16 @@ public final class ConnectionSpecTest { assertTrue(tlsSpec.isCompatible(socket)); tlsSpec.apply(socket, false /* isFallback */); - assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols())); + assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols())); Set expectedCipherSet = - createSet( + set( CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName, CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName); assertEquals(expectedCipherSet, expectedCipherSet); } - @Test - public void tls_defaultCiphers_withFallbackIndicator() throws Exception { + @Test public void tls_defaultCiphers_withFallbackIndicator() throws Exception { ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(false) @@ -108,10 +125,10 @@ public final class ConnectionSpecTest { assertTrue(tlsSpec.isCompatible(socket)); tlsSpec.apply(socket, true /* isFallback */); - assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols())); + assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols())); Set expectedCipherSet = - createSet( + set( CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName, CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName); if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) { @@ -120,8 +137,7 @@ public final class ConnectionSpecTest { assertEquals(expectedCipherSet, expectedCipherSet); } - @Test - public void tls_explicitCiphers() throws Exception { + @Test public void tls_explicitCiphers() throws Exception { ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true) .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) .tlsVersions(TlsVersion.TLS_1_2) @@ -141,17 +157,16 @@ public final class ConnectionSpecTest { assertTrue(tlsSpec.isCompatible(socket)); tlsSpec.apply(socket, true /* isFallback */); - assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols())); + assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols())); - Set expectedCipherSet = createSet(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName); + Set expectedCipherSet = set(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName); if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) { expectedCipherSet.add("TLS_FALLBACK_SCSV"); } assertEquals(expectedCipherSet, expectedCipherSet); } - @Test - public void tls_stringCiphersAndVersions() throws Exception { + @Test public void tls_stringCiphersAndVersions() throws Exception { // Supporting arbitrary input strings allows users to enable suites and versions that are not // yet known to the library, but are supported by the platform. ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) @@ -160,7 +175,7 @@ public final class ConnectionSpecTest { .build(); } - public void tls_missingRequiredCipher() throws Exception { + @Test public void tls_missingRequiredCipher() throws Exception { ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true) .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) .tlsVersions(TlsVersion.TLS_1_2) @@ -185,8 +200,43 @@ public final class ConnectionSpecTest { assertFalse(tlsSpec.isCompatible(socket)); } - @Test - public void tls_missingTlsVersion() throws Exception { + @Test public void allEnabledCipherSuites() throws Exception { + ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledCipherSuites() + .build(); + assertNull(tlsSpec.cipherSuites()); + + SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket(); + sslSocket.setEnabledCipherSuites(new String[] { + CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName, + CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName, + }); + + tlsSpec.apply(sslSocket, false); + assertEquals(Arrays.asList( + CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName, + CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName), + Arrays.asList(sslSocket.getEnabledCipherSuites())); + } + + @Test public void allEnabledTlsVersions() throws Exception { + ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledTlsVersions() + .build(); + assertNull(tlsSpec.tlsVersions()); + + SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket(); + sslSocket.setEnabledProtocols(new String[] { + TlsVersion.SSL_3_0.javaName(), + TlsVersion.TLS_1_1.javaName() + }); + + tlsSpec.apply(sslSocket, false); + assertEquals(Arrays.asList(TlsVersion.SSL_3_0.javaName(), TlsVersion.TLS_1_1.javaName()), + Arrays.asList(sslSocket.getEnabledProtocols())); + } + + @Test public void tls_missingTlsVersion() throws Exception { ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true) .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) .tlsVersions(TlsVersion.TLS_1_2) @@ -206,7 +256,48 @@ public final class ConnectionSpecTest { assertFalse(tlsSpec.isCompatible(socket)); } - private static Set createSet(String... values) { - return new LinkedHashSet(Arrays.asList(values)); + @Test public void equalsAndHashCode() throws Exception { + ConnectionSpec allCipherSuites = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledCipherSuites() + .build(); + ConnectionSpec allTlsVersions = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledTlsVersions() + .build(); + + Set set = new CopyOnWriteArraySet<>(); + assertTrue(set.add(ConnectionSpec.MODERN_TLS)); + assertTrue(set.add(ConnectionSpec.COMPATIBLE_TLS)); + assertTrue(set.add(ConnectionSpec.CLEARTEXT)); + assertTrue(set.add(allTlsVersions)); + assertTrue(set.add(allCipherSuites)); + + assertTrue(set.remove(ConnectionSpec.MODERN_TLS)); + assertTrue(set.remove(ConnectionSpec.COMPATIBLE_TLS)); + assertTrue(set.remove(ConnectionSpec.CLEARTEXT)); + assertTrue(set.remove(allTlsVersions)); + assertTrue(set.remove(allCipherSuites)); + assertTrue(set.isEmpty()); + } + + @Test public void allEnabledToString() throws Exception { + ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledTlsVersions() + .allEnabledCipherSuites() + .build(); + assertEquals("ConnectionSpec(cipherSuites=[all enabled], tlsVersions=[all enabled], " + + "supportsTlsExtensions=true)", connectionSpec.toString()); + } + + @Test public void simpleToString() throws Exception { + ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) + .build(); + assertEquals("ConnectionSpec(cipherSuites=[TLS_RSA_WITH_RC4_128_MD5], tlsVersions=[TLS_1_2], " + + "supportsTlsExtensions=true)", connectionSpec.toString()); + } + + private static Set set(T... values) { + return new LinkedHashSet<>(Arrays.asList(values)); } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java index 5e0f7d868..9742638f9 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java +++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java @@ -20,10 +20,20 @@ import java.util.Arrays; import java.util.List; import javax.net.ssl.SSLSocket; +import static com.squareup.okhttp.internal.Util.concat; +import static com.squareup.okhttp.internal.Util.contains; + /** * Specifies configuration for the socket connection that HTTP traffic travels through. For {@code * https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure * connection. + * + *

The TLS versions configured in a connection spec are only be used if they are also enabled in + * the SSL socket. For example, if an SSL socket does not have TLS 1.2 enabled, it will not be used + * even if it is present on the connection spec. The same policy also applies to cipher suites. + * + *

Use {@link Builder#allEnabledTlsVersions()} and {@link Builder#allEnabledCipherSuites} to + * defer all feature selection to the underlying SSL socket. */ public final class ConnectionSpec { @@ -67,19 +77,11 @@ public final class ConnectionSpec { /** Unencrypted, unauthenticated connections for {@code http:} URLs. */ public static final ConnectionSpec CLEARTEXT = new Builder(false).build(); - final boolean tls; - - /** - * Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use - * default set". - */ + private final boolean tls; + private final boolean supportsTlsExtensions; private final String[] cipherSuites; - - /** Used if tls == true. The TLS protocol versions to use. */ private final String[] tlsVersions; - final boolean supportsTlsExtensions; - private ConnectionSpec(Builder builder) { this.tls = builder.tls; this.cipherSuites = builder.cipherSuites; @@ -92,13 +94,12 @@ public final class ConnectionSpec { } /** - * Returns the cipher suites to use for a connection. This method can return {@code null} if the - * cipher suites enabled by default should be used. + * Returns the cipher suites to use for a connection. Returns {@code null} if all of the SSL + * socket's enabled cipher suites should be used. */ public List cipherSuites() { - if (cipherSuites == null) { - return null; - } + if (cipherSuites == null) return null; + CipherSuite[] result = new CipherSuite[cipherSuites.length]; for (int i = 0; i < cipherSuites.length; i++) { result[i] = CipherSuite.forJavaName(cipherSuites[i]); @@ -106,7 +107,13 @@ public final class ConnectionSpec { return Util.immutableList(result); } + /** + * Returns the TLS versions to use when negotiating a connection. Returns {@code null} if all of + * the SSL socket's enabled TLS versions should be used. + */ public List tlsVersions() { + if (tlsVersions == null) return null; + TlsVersion[] result = new TlsVersion[tlsVersions.length]; for (int i = 0; i < tlsVersions.length; i++) { result[i] = TlsVersion.forJavaName(tlsVersions[i]); @@ -122,57 +129,40 @@ public final class ConnectionSpec { void apply(SSLSocket sslSocket, boolean isFallback) { ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback); - sslSocket.setEnabledProtocols(specToApply.tlsVersions); - - String[] cipherSuitesToEnable = specToApply.cipherSuites; - // null means "use default set". - if (cipherSuitesToEnable != null) { - sslSocket.setEnabledCipherSuites(cipherSuitesToEnable); + if (specToApply.tlsVersions != null) { + sslSocket.setEnabledProtocols(specToApply.tlsVersions); + } + if (specToApply.cipherSuites != null) { + sslSocket.setEnabledCipherSuites(specToApply.cipherSuites); } } /** - * Returns a copy of this that omits cipher suites and TLS versions not enabled by - * {@code sslSocket}. + * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code + * sslSocket}. */ private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) { - String[] cipherSuitesToEnable = null; - if (cipherSuites != null) { - String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites(); - cipherSuitesToEnable = - Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom); + String[] cipherSuitesIntersection = cipherSuites != null + ? Util.intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites()) + : sslSocket.getEnabledCipherSuites(); + String[] tlsVersionsIntersection = tlsVersions != null + ? Util.intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols()) + : sslSocket.getEnabledProtocols(); + + // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 + // the SCSV cipher is added to signal that a protocol fallback has taken place. + if (isFallback && contains(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV")) { + cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV"); } - if (isFallback) { - // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 - // the SCSV cipher is added to signal that a protocol fallback has taken place. - final String fallbackScsv = "TLS_FALLBACK_SCSV"; - boolean socketSupportsFallbackScsv = - Arrays.asList(sslSocket.getSupportedCipherSuites()).contains(fallbackScsv); - - if (socketSupportsFallbackScsv) { - // Add the SCSV cipher to the set of enabled cipher suites iff it is supported. - String[] oldEnabledCipherSuites = cipherSuitesToEnable != null - ? cipherSuitesToEnable - : sslSocket.getEnabledCipherSuites(); - String[] newEnabledCipherSuites = new String[oldEnabledCipherSuites.length + 1]; - System.arraycopy(oldEnabledCipherSuites, 0, - newEnabledCipherSuites, 0, oldEnabledCipherSuites.length); - newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv; - cipherSuitesToEnable = newEnabledCipherSuites; - } - } - - String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols(); - String[] protocolsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom); return new Builder(this) - .cipherSuites(cipherSuitesToEnable) - .tlsVersions(protocolsToEnable) + .cipherSuites(cipherSuitesIntersection) + .tlsVersions(tlsVersionsIntersection) .build(); } /** - * Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec. + * Returns {@code true} if the socket, as currently configured, supports this connection spec. * In order for a socket to be compatible the enabled cipher suites and protocols must intersect. * *

For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must @@ -187,20 +177,17 @@ public final class ConnectionSpec { return false; } - String[] enabledProtocols = socket.getEnabledProtocols(); - boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols); - if (!requiredProtocolsEnabled) { + if (tlsVersions != null + && !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) { return false; } - boolean requiredCiphersEnabled; - if (cipherSuites == null) { - requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0; - } else { - String[] enabledCipherSuites = socket.getEnabledCipherSuites(); - requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites); + if (cipherSuites != null + && !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) { + return false; } - return requiredCiphersEnabled; + + return true; } /** @@ -220,15 +207,6 @@ public final class ConnectionSpec { return false; } - private static boolean contains(T[] array, T value) { - for (T arrayValue : array) { - if (Util.equal(value, arrayValue)) { - return true; - } - } - return false; - } - @Override public boolean equals(Object other) { if (!(other instanceof ConnectionSpec)) return false; if (other == this) return true; @@ -256,16 +234,17 @@ public final class ConnectionSpec { } @Override public String toString() { - if (tls) { - List cipherSuites = cipherSuites(); - String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString(); - return "ConnectionSpec(cipherSuites=" + cipherSuitesString - + ", tlsVersions=" + tlsVersions() - + ", supportsTlsExtensions=" + supportsTlsExtensions - + ")"; - } else { + if (!tls) { return "ConnectionSpec()"; } + + String cipherSuitesString = cipherSuites != null ? cipherSuites().toString() : "[all enabled]"; + String tlsVersionsString = tlsVersions != null ? tlsVersions().toString() : "[all enabled]"; + return "ConnectionSpec(" + + "cipherSuites=" + cipherSuitesString + + ", tlsVersions=" + tlsVersionsString + + ", supportsTlsExtensions=" + supportsTlsExtensions + + ")"; } public static final class Builder { @@ -285,56 +264,58 @@ public final class ConnectionSpec { this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions; } + public Builder allEnabledCipherSuites() { + if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); + this.cipherSuites = null; + return this; + } + public Builder cipherSuites(CipherSuite... cipherSuites) { if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); - // Convert enums to the string names Java wants. This makes a defensive copy! String[] strings = new String[cipherSuites.length]; for (int i = 0; i < cipherSuites.length; i++) { strings[i] = cipherSuites[i].javaName; } - this.cipherSuites = strings; - return this; + return cipherSuites(strings); } public Builder cipherSuites(String... cipherSuites) { if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); - if (cipherSuites == null) { - this.cipherSuites = null; - } else { - // This makes a defensive copy! - this.cipherSuites = cipherSuites.clone(); + if (cipherSuites.length == 0) { + throw new IllegalArgumentException("At least one cipher suite is required"); } + this.cipherSuites = cipherSuites.clone(); // Defensive copy. + return this; + } + + public Builder allEnabledTlsVersions() { + if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); + this.tlsVersions = null; return this; } public Builder tlsVersions(TlsVersion... tlsVersions) { if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); - if (tlsVersions.length == 0) { - throw new IllegalArgumentException("At least one TlsVersion is required"); - } - // Convert enums to the string names Java wants. This makes a defensive copy! String[] strings = new String[tlsVersions.length]; for (int i = 0; i < tlsVersions.length; i++) { strings[i] = tlsVersions[i].javaName; } - this.tlsVersions = strings; - return this; + + return tlsVersions(strings); } public Builder tlsVersions(String... tlsVersions) { if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); - if (tlsVersions == null) { - this.tlsVersions = null; - } else { - // This makes a defensive copy! - this.tlsVersions = tlsVersions.clone(); + if (tlsVersions.length == 0) { + throw new IllegalArgumentException("At least one TLS version is required"); } + this.tlsVersions = tlsVersions.clone(); // Defensive copy. return this; } diff --git a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java index bfa95c410..512aa0d48 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java +++ b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java @@ -30,7 +30,7 @@ public enum TlsVersion { final String javaName; - private TlsVersion(String javaName) { + TlsVersion(String javaName) { this.javaName = javaName; } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java index efc26ec81..b05dc6df2 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java @@ -288,4 +288,15 @@ public final class Util { return e.getCause() != null && e.getMessage() != null && e.getMessage().contains("getsockname failed"); } + + public static boolean contains(String[] array, String value) { + return Arrays.asList(array).contains(value); + } + + public static String[] concat(String[] array, String value) { + String[] result = new String[array.length + 1]; + System.arraycopy(array, 0, result, 0, array.length); + result[result.length - 1] = value; + return result; + } }