Skip to content

Commit 84eca6d

Browse files
author
Kannan Goundan
committed
Java SDK: Stricter SSL: Hard-code the ciphersuites and trusted root certificates.
1 parent b668b15 commit 84eca6d

5 files changed

Lines changed: 1750 additions & 0 deletions

File tree

ChangeLog.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
- Stricter SSL: Hard-code the ciphersuites and trusted root
2+
certificates (instead of using the system defaults).
3+
14
---------------------------------------------
25
1.7.5 (2013-09-16)
36

pom.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868

6969
<build>
7070
<sourceDirectory>src</sourceDirectory>
71+
<resources>
72+
<resource>
73+
<directory>${project.build.directory}/generated-resources/pem-to-jks</directory>
74+
</resource>
75+
</resources>
7176
<testSourceDirectory>test</testSourceDirectory>
7277
<testResources>
7378
<testResource>
@@ -94,6 +99,22 @@
9499
<skipTests>${skipTests}</skipTests>
95100
</configuration>
96101
</plugin>
102+
<plugin>
103+
<groupId>com.dropbox.maven</groupId>
104+
<artifactId>pem-to-jks-maven-plugin</artifactId>
105+
<version>1.0</version>
106+
<executions>
107+
<execution>
108+
<configuration>
109+
<inputPem>src/com/dropbox/core/http/trusted-certs.crt</inputPem>
110+
<outputJks>${project.build.directory}/generated-resources/pem-to-jks/com/dropbox/core/http/trusted-certs.jks</outputJks>
111+
</configuration>
112+
<goals>
113+
<goal>convert</goal>
114+
</goals>
115+
</execution>
116+
</executions>
117+
</plugin>
97118
</plugins>
98119
</build>
99120

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
package com.dropbox.core.http;
2+
3+
import com.dropbox.core.util.IOUtil;
4+
import static com.dropbox.core.util.LangUtil.mkAssert;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.net.InetAddress;
9+
import java.net.Socket;
10+
import java.security.KeyManagementException;
11+
import java.security.KeyStore;
12+
import java.security.KeyStoreException;
13+
import java.security.NoSuchAlgorithmException;
14+
import java.security.cert.CertificateException;
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
17+
import java.util.HashSet;
18+
19+
import javax.net.ssl.HttpsURLConnection;
20+
import javax.net.ssl.SSLContext;
21+
import javax.net.ssl.SSLException;
22+
import javax.net.ssl.SSLSocket;
23+
import javax.net.ssl.SSLSocketFactory;
24+
import javax.net.ssl.TrustManager;
25+
import javax.net.ssl.TrustManagerFactory;
26+
27+
/**
28+
* The proper SSL configuration that should be used when connecting to Dropbox
29+
* API servers. This includes:
30+
* <li>A custom set of trusted root SSL certificates.</li>
31+
* <li>Requiring TLS v1.0 and above (TLS v1.2 if supported)</li>
32+
* <li>A limited set of allowed SSL ciphersuites.</li>
33+
*
34+
* <p>
35+
* {@link #getSSLSocketFactory} returns a properly configured object that you can use
36+
* to create sockets.
37+
* </p>
38+
*
39+
* <p>
40+
* If you have an {@link HttpsURLConnection}, call the convenience method
41+
* {@link #apply(HttpsURLConnection)} (before calling {@link HttpsURLConnection#connect})
42+
* to apply the appropriate security settings.
43+
* </p>
44+
*
45+
*/
46+
public class SSLConfig
47+
{
48+
/**
49+
* Apply security settings to an {@link HttpsURLConnection}. Make sure you
50+
* haven't called {@link HttpsURLConnection#connect} yet.
51+
*/
52+
public static void apply(HttpsURLConnection conn) throws SSLException
53+
{
54+
conn.setSSLSocketFactory(sslSocketFactory);
55+
}
56+
57+
public static SSLSocketFactory getSSLSocketFactory()
58+
{
59+
return sslSocketFactory;
60+
}
61+
62+
private static final SSLSocketFactory sslSocketFactory = createSSLSocketFactory();
63+
64+
private static final String[] protocolListTLS_v1_2 = {"TLSv1.2"};
65+
private static final String[] protocolListTLS_v1_0 = {"TLSv1.0"};
66+
private static final String[] protocolListTLS_v1 = {"TLSv1"};
67+
68+
// All client ciphersuites allowed by Dropbox.
69+
//
70+
// Including both RFC and OpenSSL ciphersuite naming conventions to support
71+
// all Android API levels:
72+
// - API Level >= 10 uses the RFC naming convention
73+
// - API Level < 10 uses the OpenSSL naming convention
74+
private static HashSet<String> allowedCipherSuites = new HashSet<String>(Arrays.asList(new String[] {
75+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
76+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
77+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
78+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
79+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
80+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
81+
"TLS_ECDHE_RSA_WITH_RC4_128_SHA",
82+
"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
83+
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
84+
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
85+
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
86+
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
87+
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
88+
"TLS_RSA_WITH_AES_256_GCM_SHA384",
89+
"TLS_RSA_WITH_AES_256_CBC_SHA256",
90+
"TLS_RSA_WITH_AES_256_CBC_SHA",
91+
"TLS_RSA_WITH_AES_128_GCM_SHA256",
92+
"TLS_RSA_WITH_AES_128_CBC_SHA256",
93+
"TLS_RSA_WITH_AES_128_CBC_SHA",
94+
"ECDHE-RSA-AES256-GCM-SHA384",
95+
"ECDHE-RSA-AES256-SHA384",
96+
"ECDHE-RSA-AES256-SHA",
97+
"ECDHE-RSA-AES128-GCM-SHA256",
98+
"ECDHE-RSA-AES128-SHA256",
99+
"ECDHE-RSA-AES128-SHA",
100+
"ECDHE-RSA-RC4-SHA",
101+
"DHE-RSA-AES256-GCM-SHA384",
102+
"DHE-RSA-AES256-SHA256",
103+
"DHE-RSA-AES256-SHA",
104+
"DHE-RSA-AES128-GCM-SHA256",
105+
"DHE-RSA-AES128-SHA256",
106+
"DHE-RSA-AES128-SHA",
107+
"AES256-GCM-SHA384",
108+
"AES256-SHA256",
109+
"AES256-SHA",
110+
"AES128-GCM-SHA256",
111+
"AES128-SHA256",
112+
"AES128-SHA",
113+
}));
114+
115+
private static void limitProtocolsAndCiphers(SSLSocket socket) throws SSLException
116+
{
117+
// Set TLS protocol version
118+
outer: {
119+
for (String protocol : socket.getSupportedProtocols()) {
120+
if (protocol.equals("TLSv1.2")) {
121+
socket.setEnabledProtocols(protocolListTLS_v1_2);
122+
break outer;
123+
}
124+
if (protocol.equals("TLSv1.0")) {
125+
socket.setEnabledProtocols(protocolListTLS_v1_0);
126+
break outer;
127+
}
128+
if (protocol.equals("TLSv1")) {
129+
socket.setEnabledProtocols(protocolListTLS_v1);
130+
break outer;
131+
}
132+
}
133+
throw new SSLException("Socket doesn't support protocols \"TLSv1.2\", \"TLSv1.0\" or \"TLSv1\".");
134+
}
135+
136+
socket.setEnabledCipherSuites(getFilteredCipherSuites(socket.getSupportedCipherSuites()));
137+
}
138+
139+
private static String[] getFilteredCipherSuites(String[] supportedCipherSuites)
140+
{
141+
// Since the supported cipher suites probably won't change, try to reuse the
142+
// result of the last filteration.
143+
CipherSuiteFilterationResults cached = cachedCipherSuiteFilterationResults;
144+
if (cached != null) {
145+
if (Arrays.equals(cached.supported, supportedCipherSuites)) {
146+
return cached.enabled;
147+
}
148+
}
149+
150+
// Filter the 'supported' list to yield the 'enabled' list.
151+
ArrayList<String> enabled = new ArrayList<String>(allowedCipherSuites.size());
152+
for (String supported : supportedCipherSuites) {
153+
if (allowedCipherSuites.contains(supported)) {
154+
enabled.add(supported);
155+
}
156+
}
157+
158+
String[] filteredArray = enabled.toArray(new String[enabled.size()]);
159+
cachedCipherSuiteFilterationResults = new CipherSuiteFilterationResults(supportedCipherSuites, filteredArray);
160+
return filteredArray;
161+
}
162+
163+
private static CipherSuiteFilterationResults cachedCipherSuiteFilterationResults = null;
164+
165+
private static final class CipherSuiteFilterationResults
166+
{
167+
// The ciphersuites supported by the underlying library.
168+
public final String[] supported;
169+
// The subset of 'supported' that we allow to be used.
170+
public final String[] enabled;
171+
172+
private CipherSuiteFilterationResults(String[] supported, String[] enabled)
173+
{
174+
this.supported = supported;
175+
this.enabled = enabled;
176+
}
177+
}
178+
179+
private static final String RootCertsResourceName = "trusted-certs.jks";
180+
181+
private static SSLSocketFactory createSSLSocketFactory()
182+
{
183+
KeyStore trustedCertKeyStore = loadKeyStore(RootCertsResourceName);
184+
TrustManager[] trustManagers = createTrustManagers(trustedCertKeyStore);
185+
SSLContext sslContext = createSSLContext(trustManagers);
186+
return new SSLSocketFactoryWrapper(sslContext.getSocketFactory());
187+
}
188+
189+
private static final class SSLSocketFactoryWrapper extends SSLSocketFactory
190+
{
191+
private final SSLSocketFactory mBase;
192+
193+
public SSLSocketFactoryWrapper(SSLSocketFactory base)
194+
{
195+
mBase = base;
196+
}
197+
198+
@Override
199+
public String[] getDefaultCipherSuites()
200+
{
201+
return mBase.getDefaultCipherSuites();
202+
}
203+
204+
@Override
205+
public String[] getSupportedCipherSuites()
206+
{
207+
return mBase.getSupportedCipherSuites();
208+
}
209+
210+
@Override
211+
public Socket createSocket(String host, int port) throws IOException
212+
{
213+
Socket socket = mBase.createSocket(host, port);
214+
limitProtocolsAndCiphers((SSLSocket) socket);
215+
return socket;
216+
}
217+
218+
@Override
219+
public Socket createSocket(InetAddress host, int port)
220+
throws IOException
221+
{
222+
Socket socket = mBase.createSocket(host, port);
223+
limitProtocolsAndCiphers((SSLSocket) socket);
224+
return socket;
225+
}
226+
227+
@Override
228+
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
229+
throws IOException
230+
{
231+
Socket socket = mBase.createSocket(host, port, localHost,
232+
localPort);
233+
limitProtocolsAndCiphers((SSLSocket) socket);
234+
return socket;
235+
}
236+
237+
@Override
238+
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
239+
throws IOException
240+
{
241+
Socket socket = mBase.createSocket(address, port,
242+
localAddress, localPort);
243+
limitProtocolsAndCiphers((SSLSocket) socket);
244+
return socket;
245+
}
246+
247+
@Override
248+
public Socket createSocket(Socket s, String host, int port, boolean autoClose)
249+
throws IOException
250+
{
251+
Socket socket = mBase.createSocket(s, host, port, autoClose);
252+
limitProtocolsAndCiphers((SSLSocket) socket);
253+
return socket;
254+
}
255+
}
256+
257+
private static SSLContext createSSLContext(TrustManager[] trustManagers)
258+
{
259+
SSLContext sslContext;
260+
try {
261+
sslContext = SSLContext.getInstance("TLS");
262+
}
263+
catch (NoSuchAlgorithmException ex) {
264+
throw mkAssert("Couldn't create SSLContext", ex);
265+
}
266+
267+
try {
268+
sslContext.init(null, trustManagers, null);
269+
}
270+
catch (KeyManagementException ex) {
271+
throw mkAssert("Couldn't initialize SSLContext", ex);
272+
}
273+
274+
return sslContext;
275+
}
276+
277+
private static TrustManager[] createTrustManagers(KeyStore trustedCertKeyStore)
278+
{
279+
TrustManagerFactory tmf;
280+
try {
281+
tmf = TrustManagerFactory.getInstance("X509");
282+
}
283+
catch (NoSuchAlgorithmException ex) {
284+
throw mkAssert("Unable to create TrustManagerFactory", ex);
285+
}
286+
287+
try {
288+
tmf.init(trustedCertKeyStore);
289+
}
290+
catch (KeyStoreException ex) {
291+
throw mkAssert("Unable to initialize TrustManagerFactory with key store", ex);
292+
}
293+
294+
return tmf.getTrustManagers();
295+
}
296+
297+
private static KeyStore loadKeyStore(String jksFileResourceName)
298+
{
299+
KeyStore keyStore;
300+
try {
301+
keyStore = KeyStore.getInstance("JKS");
302+
}
303+
catch (KeyStoreException ex) {
304+
throw mkAssert("Couldn't initialize JKS key store", ex);
305+
}
306+
307+
InputStream in = SSLConfig.class.getResourceAsStream(jksFileResourceName);
308+
if (in == null) {
309+
throw new AssertionError("Couldn't find resource \"" + jksFileResourceName + "\"");
310+
}
311+
try {
312+
keyStore.load(in, null);
313+
}
314+
catch (CertificateException ex) {
315+
throw mkAssert("Error loading from \"" + jksFileResourceName + "\"", ex);
316+
}
317+
catch (NoSuchAlgorithmException ex) {
318+
throw mkAssert("Error loading from \"" + jksFileResourceName + "\"", ex);
319+
}
320+
catch (IOException ex) {
321+
throw mkAssert("Error loading from \"" + jksFileResourceName + "\"", ex);
322+
}
323+
finally {
324+
IOUtil.closeInput(in);
325+
}
326+
327+
return keyStore;
328+
}
329+
}

src/com/dropbox/core/http/StandardHttpRequestor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ private HttpsURLConnection prepRequest(String url, Iterable<Header> headers) thr
159159
URL urlObject = new URL(url);
160160
HttpsURLConnection conn = (HttpsURLConnection) urlObject.openConnection(this.proxy);
161161

162+
SSLConfig.apply(conn);
162163
conn.setConnectTimeout(DefaultConnectTimeoutMillis);
163164
conn.setReadTimeout(DefaultReadTimeoutMillis);
164165
conn.setUseCaches(false);

0 commit comments

Comments
 (0)