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