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 /** 24 * Specifies configuration for the socket connection that HTTP traffic travels through. For {@code 25 * https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure 26 * connection. 27 */ 28 public final class ConnectionSpec { 29 30 // This is a subset of the cipher suites supported in Chrome 37, current as of 2014-10-5. 31 // All of these suites are available on Android 5.0; earlier releases support a subset of 32 // these suites. https://github.com/square/okhttp/issues/330 33 private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] { 34 CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 35 CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 36 CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, 37 38 // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll 39 // continue to include them until better suites are commonly available. For example, none 40 // of the better cipher suites listed above shipped with Android 4.4 or Java 7. 41 CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 42 CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 43 CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 44 CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 45 CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, 46 CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA, 47 CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA, 48 CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, 49 CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, 50 CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, 51 CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, 52 }; 53 54 /** A modern TLS connection with extensions like SNI and ALPN available. */ 55 public static final ConnectionSpec MODERN_TLS = new Builder(true) 56 .cipherSuites(APPROVED_CIPHER_SUITES) 57 .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) 58 .supportsTlsExtensions(true) 59 .build(); 60 61 /** A backwards-compatible fallback connection for interop with obsolete servers. */ 62 public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS) 63 .tlsVersions(TlsVersion.TLS_1_0) 64 .supportsTlsExtensions(true) 65 .build(); 66 67 /** Unencrypted, unauthenticated connections for {@code http:} URLs. */ 68 public static final ConnectionSpec CLEARTEXT = new Builder(false).build(); 69 70 final boolean tls; 71 72 /** 73 * Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use 74 * default set". 75 */ 76 private final String[] cipherSuites; 77 78 /** Used if tls == true. The TLS protocol versions to use. */ 79 private final String[] tlsVersions; 80 81 final boolean supportsTlsExtensions; 82 ConnectionSpec(Builder builder)83 private ConnectionSpec(Builder builder) { 84 this.tls = builder.tls; 85 this.cipherSuites = builder.cipherSuites; 86 this.tlsVersions = builder.tlsVersions; 87 this.supportsTlsExtensions = builder.supportsTlsExtensions; 88 } 89 isTls()90 public boolean isTls() { 91 return tls; 92 } 93 94 /** 95 * Returns the cipher suites to use for a connection. This method can return {@code null} if the 96 * cipher suites enabled by default should be used. 97 */ cipherSuites()98 public List<CipherSuite> cipherSuites() { 99 if (cipherSuites == null) { 100 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 tlsVersions()109 public List<TlsVersion> tlsVersions() { 110 TlsVersion[] result = new TlsVersion[tlsVersions.length]; 111 for (int i = 0; i < tlsVersions.length; i++) { 112 result[i] = TlsVersion.forJavaName(tlsVersions[i]); 113 } 114 return Util.immutableList(result); 115 } 116 supportsTlsExtensions()117 public boolean supportsTlsExtensions() { 118 return supportsTlsExtensions; 119 } 120 121 /** Applies this spec to {@code sslSocket}. */ apply(SSLSocket sslSocket, boolean isFallback)122 void apply(SSLSocket sslSocket, boolean isFallback) { 123 ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback); 124 125 sslSocket.setEnabledProtocols(specToApply.tlsVersions); 126 127 String[] cipherSuitesToEnable = specToApply.cipherSuites; 128 // null means "use default set". 129 if (cipherSuitesToEnable != null) { 130 sslSocket.setEnabledCipherSuites(cipherSuitesToEnable); 131 } 132 } 133 134 /** 135 * Returns a copy of this that omits cipher suites and TLS versions not enabled by 136 * {@code sslSocket}. 137 */ supportedSpec(SSLSocket sslSocket, boolean isFallback)138 private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) { 139 String[] cipherSuitesToEnable = null; 140 if (cipherSuites != null) { 141 String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites(); 142 cipherSuitesToEnable = 143 Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom); 144 } 145 146 if (isFallback) { 147 // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 148 // the SCSV cipher is added to signal that a protocol fallback has taken place. 149 final String fallbackScsv = "TLS_FALLBACK_SCSV"; 150 boolean socketSupportsFallbackScsv = 151 Arrays.asList(sslSocket.getSupportedCipherSuites()).contains(fallbackScsv); 152 153 if (socketSupportsFallbackScsv) { 154 // Add the SCSV cipher to the set of enabled cipher suites iff it is supported. 155 String[] oldEnabledCipherSuites = cipherSuitesToEnable != null 156 ? cipherSuitesToEnable 157 : sslSocket.getEnabledCipherSuites(); 158 String[] newEnabledCipherSuites = new String[oldEnabledCipherSuites.length + 1]; 159 System.arraycopy(oldEnabledCipherSuites, 0, 160 newEnabledCipherSuites, 0, oldEnabledCipherSuites.length); 161 newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv; 162 cipherSuitesToEnable = newEnabledCipherSuites; 163 } 164 } 165 166 String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols(); 167 String[] protocolsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom); 168 return new Builder(this) 169 .cipherSuites(cipherSuitesToEnable) 170 .tlsVersions(protocolsToEnable) 171 .build(); 172 } 173 174 /** 175 * Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec. 176 * In order for a socket to be compatible the enabled cipher suites and protocols must intersect. 177 * 178 * <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must 179 * match the socket's enabled cipher suites. If there are no required cipher suites the socket 180 * must have at least one cipher suite enabled. 181 * 182 * <p>For protocols, at least one of the {@link #tlsVersions() required protocols} must match the 183 * socket's enabled protocols. 184 */ isCompatible(SSLSocket socket)185 public boolean isCompatible(SSLSocket socket) { 186 if (!tls) { 187 return false; 188 } 189 190 String[] enabledProtocols = socket.getEnabledProtocols(); 191 boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols); 192 if (!requiredProtocolsEnabled) { 193 return false; 194 } 195 196 boolean requiredCiphersEnabled; 197 if (cipherSuites == null) { 198 requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0; 199 } else { 200 String[] enabledCipherSuites = socket.getEnabledCipherSuites(); 201 requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites); 202 } 203 return requiredCiphersEnabled; 204 } 205 206 /** 207 * An N*M intersection that terminates if any intersection is found. The sizes of both 208 * arguments are assumed to be so small, and the likelihood of an intersection so great, that it 209 * is not worth the CPU cost of sorting or the memory cost of hashing. 210 */ nonEmptyIntersection(String[] a, String[] b)211 private static boolean nonEmptyIntersection(String[] a, String[] b) { 212 if (a == null || b == null || a.length == 0 || b.length == 0) { 213 return false; 214 } 215 for (String toFind : a) { 216 if (contains(b, toFind)) { 217 return true; 218 } 219 } 220 return false; 221 } 222 contains(T[] array, T value)223 private static <T> boolean contains(T[] array, T value) { 224 for (T arrayValue : array) { 225 if (Util.equal(value, arrayValue)) { 226 return true; 227 } 228 } 229 return false; 230 } 231 equals(Object other)232 @Override public boolean equals(Object other) { 233 if (!(other instanceof ConnectionSpec)) return false; 234 if (other == this) return true; 235 236 ConnectionSpec that = (ConnectionSpec) other; 237 if (this.tls != that.tls) return false; 238 239 if (tls) { 240 if (!Arrays.equals(this.cipherSuites, that.cipherSuites)) return false; 241 if (!Arrays.equals(this.tlsVersions, that.tlsVersions)) return false; 242 if (this.supportsTlsExtensions != that.supportsTlsExtensions) return false; 243 } 244 245 return true; 246 } 247 hashCode()248 @Override public int hashCode() { 249 int result = 17; 250 if (tls) { 251 result = 31 * result + Arrays.hashCode(cipherSuites); 252 result = 31 * result + Arrays.hashCode(tlsVersions); 253 result = 31 * result + (supportsTlsExtensions ? 0 : 1); 254 } 255 return result; 256 } 257 toString()258 @Override public String toString() { 259 if (tls) { 260 List<CipherSuite> cipherSuites = cipherSuites(); 261 String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString(); 262 return "ConnectionSpec(cipherSuites=" + cipherSuitesString 263 + ", tlsVersions=" + tlsVersions() 264 + ", supportsTlsExtensions=" + supportsTlsExtensions 265 + ")"; 266 } else { 267 return "ConnectionSpec()"; 268 } 269 } 270 271 public static final class Builder { 272 private boolean tls; 273 private String[] cipherSuites; 274 private String[] tlsVersions; 275 private boolean supportsTlsExtensions; 276 Builder(boolean tls)277 Builder(boolean tls) { 278 this.tls = tls; 279 } 280 Builder(ConnectionSpec connectionSpec)281 public Builder(ConnectionSpec connectionSpec) { 282 this.tls = connectionSpec.tls; 283 this.cipherSuites = connectionSpec.cipherSuites; 284 this.tlsVersions = connectionSpec.tlsVersions; 285 this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions; 286 } 287 cipherSuites(CipherSuite... cipherSuites)288 public Builder cipherSuites(CipherSuite... cipherSuites) { 289 if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); 290 291 // Convert enums to the string names Java wants. This makes a defensive copy! 292 String[] strings = new String[cipherSuites.length]; 293 for (int i = 0; i < cipherSuites.length; i++) { 294 strings[i] = cipherSuites[i].javaName; 295 } 296 this.cipherSuites = strings; 297 return this; 298 } 299 cipherSuites(String... cipherSuites)300 public Builder cipherSuites(String... cipherSuites) { 301 if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections"); 302 303 if (cipherSuites == null) { 304 this.cipherSuites = null; 305 } else { 306 // This makes a defensive copy! 307 this.cipherSuites = cipherSuites.clone(); 308 } 309 310 return this; 311 } 312 tlsVersions(TlsVersion... tlsVersions)313 public Builder tlsVersions(TlsVersion... tlsVersions) { 314 if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); 315 if (tlsVersions.length == 0) { 316 throw new IllegalArgumentException("At least one TlsVersion is required"); 317 } 318 319 // Convert enums to the string names Java wants. This makes a defensive copy! 320 String[] strings = new String[tlsVersions.length]; 321 for (int i = 0; i < tlsVersions.length; i++) { 322 strings[i] = tlsVersions[i].javaName; 323 } 324 this.tlsVersions = strings; 325 return this; 326 } 327 tlsVersions(String... tlsVersions)328 public Builder tlsVersions(String... tlsVersions) { 329 if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections"); 330 331 if (tlsVersions == null) { 332 this.tlsVersions = null; 333 } else { 334 // This makes a defensive copy! 335 this.tlsVersions = tlsVersions.clone(); 336 } 337 338 return this; 339 } 340 supportsTlsExtensions(boolean supportsTlsExtensions)341 public Builder supportsTlsExtensions(boolean supportsTlsExtensions) { 342 if (!tls) throw new IllegalStateException("no TLS extensions for cleartext connections"); 343 this.supportsTlsExtensions = supportsTlsExtensions; 344 return this; 345 } 346 build()347 public ConnectionSpec build() { 348 return new ConnectionSpec(this); 349 } 350 } 351 } 352