1 /* 2 * Copyright (C) 2014 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.okhttp; 17 18 import com.squareup.okhttp.internal.Util; 19 import java.util.Arrays; 20 import java.util.List; 21 import javax.net.ssl.SSLSocket; 22 23 import static com.squareup.okhttp.internal.Util.concat; 24 import static com.squareup.okhttp.internal.Util.contains; 25 26 /** 27 * Specifies configuration for the socket connection that HTTP traffic travels through. For {@code 28 * https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure 29 * connection. 30 * 31 * <p>The TLS versions configured in a connection spec are only be used if they are also enabled in 32 * the SSL socket. For example, if an SSL socket does not have TLS 1.2 enabled, it will not be used 33 * even if it is present on the connection spec. The same policy also applies to cipher suites. 34 * 35 * <p>Use {@link Builder#allEnabledTlsVersions()} and {@link Builder#allEnabledCipherSuites} to 36 * defer all feature selection to the underlying SSL socket. 37 */ 38 public final class ConnectionSpec { 39 40 // This is a subset of the cipher suites supported in Chrome 46, current as of 2015-11-05. 41 // All of these suites are available on Android 5.0; earlier releases support a subset of 42 // these suites. https://github.com/square/okhttp/issues/330 43 private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] { 44 CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 45 CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 46 CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, 47 48 // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll 49 // continue to include them until better suites are commonly available. For example, none 50 // of the better cipher suites listed above shipped with Android 4.4 or Java 7. 51 CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 52 CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 53 CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 54 CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 55 CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, 56 CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA, 57 CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, 58 CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, 59 CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, 60 CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, 61 }; 62 63 /** A modern TLS connection with extensions like SNI and ALPN available. */ 64 public static final ConnectionSpec MODERN_TLS = new Builder(true) 65 .cipherSuites(APPROVED_CIPHER_SUITES) 66 .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) 67 .supportsTlsExtensions(true) 68 .build(); 69 70 /** A backwards-compatible fallback connection for interop with obsolete servers. */ 71 public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS) 72 .tlsVersions(TlsVersion.TLS_1_0) 73 .supportsTlsExtensions(true) 74 .build(); 75 76 /** Unencrypted, unauthenticated connections for {@code http:} URLs. */ 77 public static final ConnectionSpec CLEARTEXT = new Builder(false).build(); 78 79 private final boolean tls; 80 private final boolean supportsTlsExtensions; 81 private final String[] cipherSuites; 82 private final String[] tlsVersions; 83 ConnectionSpec(Builder builder)84 private ConnectionSpec(Builder builder) { 85 this.tls = builder.tls; 86 this.cipherSuites = builder.cipherSuites; 87 this.tlsVersions = builder.tlsVersions; 88 this.supportsTlsExtensions = builder.supportsTlsExtensions; 89 } 90 isTls()91 public boolean isTls() { 92 return tls; 93 } 94 95 /** 96 * Returns the cipher suites to use for a connection. Returns {@code null} if all of the SSL 97 * socket's enabled cipher suites should be used. 98 */ cipherSuites()99 public List<CipherSuite> cipherSuites() { 100 if (cipherSuites == null) return null; 101 102 CipherSuite[] result = new CipherSuite[cipherSuites.length]; 103 for (int i = 0; i < cipherSuites.length; i++) { 104 result[i] = CipherSuite.forJavaName(cipherSuites[i]); 105 } 106 return Util.immutableList(result); 107 } 108 109 /** 110 * Returns the TLS versions to use when negotiating a connection. Returns {@code null} if all of 111 * the SSL socket's enabled TLS versions should be used. 112 */ tlsVersions()113 public List<TlsVersion> tlsVersions() { 114 if (tlsVersions == null) return null; 115 116 TlsVersion[] result = new TlsVersion[tlsVersions.length]; 117 for (int i = 0; i < tlsVersions.length; i++) { 118 result[i] = TlsVersion.forJavaName(tlsVersions[i]); 119 } 120 return Util.immutableList(result); 121 } 122 supportsTlsExtensions()123 public boolean supportsTlsExtensions() { 124 return supportsTlsExtensions; 125 } 126 127 /** Applies this spec to {@code sslSocket}. */ apply(SSLSocket sslSocket, boolean isFallback)128 void apply(SSLSocket sslSocket, boolean isFallback) { 129 ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback); 130 131 if (specToApply.tlsVersions != null) { 132 sslSocket.setEnabledProtocols(specToApply.tlsVersions); 133 } 134 if (specToApply.cipherSuites != null) { 135 sslSocket.setEnabledCipherSuites(specToApply.cipherSuites); 136 } 137 } 138 139 /** 140 * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code 141 * sslSocket}. 142 */ supportedSpec(SSLSocket sslSocket, boolean isFallback)143 private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) { 144 String[] cipherSuitesIntersection = cipherSuites != null 145 ? Util.intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites()) 146 : sslSocket.getEnabledCipherSuites(); 147 String[] tlsVersionsIntersection = tlsVersions != null 148 ? Util.intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols()) 149 : sslSocket.getEnabledProtocols(); 150 151 // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 152 // the SCSV cipher is added to signal that a protocol fallback has taken place. 153 if (isFallback && contains(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV")) { 154 cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV"); 155 } 156 157 return new Builder(this) 158 .cipherSuites(cipherSuitesIntersection) 159 .tlsVersions(tlsVersionsIntersection) 160 .build(); 161 } 162 163 /** 164 * Returns {@code true} if the socket, as currently configured, supports this connection spec. 165 * In order for a socket to be compatible the enabled cipher suites and protocols must intersect. 166 * 167 * <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must 168 * match the socket's enabled cipher suites. If there are no required cipher suites the socket 169 * must have at least one cipher suite enabled. 170 * 171 * <p>For protocols, at least one of the {@link #tlsVersions() required protocols} must match the 172 * socket's enabled protocols. 173 */ isCompatible(SSLSocket socket)174 public boolean isCompatible(SSLSocket socket) { 175 if (!tls) { 176 return false; 177 } 178 179 if (tlsVersions != null 180 && !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) { 181 return false; 182 } 183 184 if (cipherSuites != null 185 && !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) { 186 return false; 187 } 188 189 return true; 190 } 191 192 /** 193 * An N*M intersection that terminates if any intersection is found. The sizes of both 194 * arguments are assumed to be so small, and the likelihood of an intersection so great, that it 195 * is not worth the CPU cost of sorting or the memory cost of hashing. 196 */ nonEmptyIntersection(String[] a, String[] b)197 private static boolean nonEmptyIntersection(String[] a, String[] b) { 198 if (a == null || b == null || a.length == 0 || b.length == 0) { 199 return false; 200 } 201 for (String toFind : a) { 202 if (contains(b, toFind)) { 203 return true; 204 } 205 } 206 return false; 207 } 208 equals(Object other)209 @Override public boolean equals(Object other) { 210 if (!(other instanceof ConnectionSpec)) return false; 211 if (other == this) return true; 212 213 ConnectionSpec that = (ConnectionSpec) other; 214 if (this.tls != that.tls) return false; 215 216 if (tls) { 217 if (!Arrays.equals(this.cipherSuites, that.cipherSuites)) return false; 218 if (!Arrays.equals(this.tlsVersions, that.tlsVersions)) return false; 219 if (this.supportsTlsExtensions != that.supportsTlsExtensions) return false; 220 } 221 222 return true; 223 } 224 hashCode()225 @Override public int hashCode() { 226 int result = 17; 227 if (tls) { 228 result = 31 * result + Arrays.hashCode(cipherSuites); 229 result = 31 * result + Arrays.hashCode(tlsVersions); 230 result = 31 * result + (supportsTlsExtensions ? 0 : 1); 231 } 232 return result; 233 } 234 toString()235 @Override public String toString() { 236 if (!tls) { 237 return "ConnectionSpec()"; 238 } 239 240 String cipherSuitesString = cipherSuites != null ? cipherSuites().toString() : "[all enabled]"; 241 String tlsVersionsString = tlsVersions != null ? tlsVersions().toString() : "[all enabled]"; 242 return "ConnectionSpec(" 243 + "cipherSuites=" + cipherSuitesString 244 + ", tlsVersions=" + tlsVersionsString 245 + ", supportsTlsExtensions=" + supportsTlsExtensions 246 + ")"; 247 } 248 249 public static final class Builder { 250 private boolean tls; 251 private String[] cipherSuites; 252 private String[] tlsVersions; 253 private boolean supportsTlsExtensions; 254 Builder(boolean tls)255 Builder(boolean tls) { 256 this.tls = tls; 257 } 258 Builder(ConnectionSpec connectionSpec)259 public Builder(ConnectionSpec connectionSpec) { 260 this.tls = connectionSpec.tls; 261 this.cipherSuites = connectionSpec.cipherSuites; 262 this.tlsVersions = connectionSpec.tlsVersions; 263 this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions; 264 } 265 allEnabledCipherSuites()266 public Builder allEnabledCipherSuites() { 267 if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); 268 this.cipherSuites = null; 269 return this; 270 } 271 cipherSuites(CipherSuite... cipherSuites)272 public Builder cipherSuites(CipherSuite... cipherSuites) { 273 if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); 274 275 String[] strings = new String[cipherSuites.length]; 276 for (int i = 0; i < cipherSuites.length; i++) { 277 strings[i] = cipherSuites[i].javaName; 278 } 279 return cipherSuites(strings); 280 } 281 cipherSuites(String... cipherSuites)282 public Builder cipherSuites(String... cipherSuites) { 283 if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); 284 285 if (cipherSuites.length == 0) { 286 throw new IllegalArgumentException("At least one cipher suite is required"); 287 } 288 289 this.cipherSuites = cipherSuites.clone(); // Defensive copy. 290 return this; 291 } 292 allEnabledTlsVersions()293 public Builder allEnabledTlsVersions() { 294 if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); 295 this.tlsVersions = null; 296 return this; 297 } 298 tlsVersions(TlsVersion... tlsVersions)299 public Builder tlsVersions(TlsVersion... tlsVersions) { 300 if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); 301 302 String[] strings = new String[tlsVersions.length]; 303 for (int i = 0; i < tlsVersions.length; i++) { 304 strings[i] = tlsVersions[i].javaName; 305 } 306 307 return tlsVersions(strings); 308 } 309 tlsVersions(String... tlsVersions)310 public Builder tlsVersions(String... tlsVersions) { 311 if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); 312 313 if (tlsVersions.length == 0) { 314 throw new IllegalArgumentException("At least one TLS version is required"); 315 } 316 317 this.tlsVersions = tlsVersions.clone(); // Defensive copy. 318 return this; 319 } 320 supportsTlsExtensions(boolean supportsTlsExtensions)321 public Builder supportsTlsExtensions(boolean supportsTlsExtensions) { 322 if (!tls) throw new IllegalStateException("no TLS extensions for cleartext connections"); 323 this.supportsTlsExtensions = supportsTlsExtensions; 324 return this; 325 } 326 build()327 public ConnectionSpec build() { 328 return new ConnectionSpec(this); 329 } 330 } 331 } 332