mirror of
https://github.com/etesync/android
synced 2024-11-26 09:58:11 +00:00
Use Apache HttpClient-Android SSLConnectionSocketFactory
* SSLConnectionSocketFactory is now SNI-capable, so there's no need to manage SNI in DAVdroid * keep secure protocol/cipher suite definitions * use a patched jar until https://issues.apache.org/jira/browse/HTTPCLIENT-1591 is fixed
This commit is contained in:
parent
a7115ad39c
commit
9e082d930b
@ -35,7 +35,7 @@ configurations.all {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// Apache Commons
|
// Apache Commons
|
||||||
compile 'commons-lang:commons-lang:2.6'
|
compile 'commons-lang:commons-lang:2.6'
|
||||||
compile 'org.apache.commons:commons-io:1.3.2'
|
compile 'commons-io:commons-io:2.4'
|
||||||
|
|
||||||
// Lombok for useful @helpers
|
// Lombok for useful @helpers
|
||||||
provided 'org.projectlombok:lombok:1.14.8'
|
provided 'org.projectlombok:lombok:1.14.8'
|
||||||
@ -56,9 +56,10 @@ dependencies {
|
|||||||
compile 'dnsjava:dnsjava:2.1.6'
|
compile 'dnsjava:dnsjava:2.1.6'
|
||||||
|
|
||||||
// HttpClient 4.3, Android flavour for WebDAV operations
|
// HttpClient 4.3, Android flavour for WebDAV operations
|
||||||
// we have to use our patched version of 4.3.5 to avoid https://issues.apache.org/jira/browse/HTTPCLIENT-1566
|
// we have to use our own patched build of 4.3.5.2-SNAPSHOT to avoid
|
||||||
compile files('lib/httpclient-android-4.3.5.davdroid1.jar')
|
// https://issues.apache.org/jira/browse/HTTPCLIENT-1591
|
||||||
// compile 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
compile files('lib/httpclient-android-4.3.5.2-davdroid1.jar')
|
||||||
|
// compile 'org.apache.httpcomponents:httpclient-android:4.3.5.2-SNAPSHOT'
|
||||||
|
|
||||||
// SimpleXML for parsing and generating WebDAV messages
|
// SimpleXML for parsing and generating WebDAV messages
|
||||||
compile('org.simpleframework:simple-xml:2.7.1') {
|
compile('org.simpleframework:simple-xml:2.7.1') {
|
||||||
|
Binary file not shown.
@ -14,6 +14,8 @@ import java.net.Socket;
|
|||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.security.cert.CertPathValidatorException;
|
import java.security.cert.CertPathValidatorException;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
@ -29,7 +31,7 @@ import lombok.Cleanup;
|
|||||||
public class TlsSniSocketFactoryTest extends TestCase {
|
public class TlsSniSocketFactoryTest extends TestCase {
|
||||||
private static final String TAG = "davdroid.TlsSniSocketFactoryTest";
|
private static final String TAG = "davdroid.TlsSniSocketFactoryTest";
|
||||||
|
|
||||||
TlsSniSocketFactory factory = TlsSniSocketFactory.INSTANCE;
|
TlsSniSocketFactory factory = TlsSniSocketFactory.getSocketFactory();
|
||||||
|
|
||||||
private InetSocketAddress sampleTlsEndpoint;
|
private InetSocketAddress sampleTlsEndpoint;
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ public class TlsSniSocketFactoryTest extends TestCase {
|
|||||||
|
|
||||||
public void testCreateSocket() {
|
public void testCreateSocket() {
|
||||||
try {
|
try {
|
||||||
@Cleanup SSLSocket socket = factory.createSocket(null);
|
@Cleanup Socket socket = factory.createSocket(null);
|
||||||
assertFalse(socket.isConnected());
|
assertFalse(socket.isConnected());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
fail();
|
fail();
|
||||||
@ -50,9 +52,7 @@ public class TlsSniSocketFactoryTest extends TestCase {
|
|||||||
|
|
||||||
public void testConnectSocket() {
|
public void testConnectSocket() {
|
||||||
try {
|
try {
|
||||||
@Cleanup SSLSocket socket = factory.createSocket(null);
|
factory.connectSocket(1000, null, new HttpHost(sampleTlsEndpoint.getHostName()), sampleTlsEndpoint, null, null);
|
||||||
|
|
||||||
factory.connectSocket(1000, socket, new HttpHost(sampleTlsEndpoint.getHostName()), sampleTlsEndpoint, null, null);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "I/O exception", e);
|
Log.e(TAG, "I/O exception", e);
|
||||||
fail();
|
fail();
|
||||||
@ -67,7 +67,7 @@ public class TlsSniSocketFactoryTest extends TestCase {
|
|||||||
assertTrue(plain.isConnected());
|
assertTrue(plain.isConnected());
|
||||||
|
|
||||||
// then create TLS socket on top of it and establish TLS Connection
|
// then create TLS socket on top of it and establish TLS Connection
|
||||||
@Cleanup SSLSocket socket = factory.createLayeredSocket(plain, sampleTlsEndpoint.getHostName(), sampleTlsEndpoint.getPort(), null);
|
@Cleanup Socket socket = factory.createLayeredSocket(plain, sampleTlsEndpoint.getHostName(), sampleTlsEndpoint.getPort(), null);
|
||||||
assertTrue(socket.isConnected());
|
assertTrue(socket.isConnected());
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -76,11 +76,8 @@ public class TlsSniSocketFactoryTest extends TestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSetTlsParameters() throws IOException {
|
public void testProtocolVersions() throws IOException {
|
||||||
@Cleanup SSLSocket socket = factory.createSocket(null);
|
String enabledProtocols[] = factory.protocols;
|
||||||
factory.setTlsParameters(socket);
|
|
||||||
|
|
||||||
String enabledProtocols[] = socket.getEnabledProtocols();
|
|
||||||
// SSL (all versions) should be disabled
|
// SSL (all versions) should be disabled
|
||||||
for (String protocol : enabledProtocols)
|
for (String protocol : enabledProtocols)
|
||||||
assertFalse(protocol.contains("SSL"));
|
assertFalse(protocol.contains("SSL"));
|
||||||
@ -91,27 +88,29 @@ public class TlsSniSocketFactoryTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void testHostnameNotInCertificate() {
|
public void testHostnameNotInCertificate() throws IOException {
|
||||||
try {
|
try {
|
||||||
// host with certificate that doesn't match host name
|
// host with certificate that doesn't match host name
|
||||||
// use the IP address as host name because IP addresses are usually not in the certificate subject
|
// use the IP address as host name because IP addresses are usually not in the certificate subject
|
||||||
InetSocketAddress host = new InetSocketAddress(sampleTlsEndpoint.getAddress().getHostAddress(), 443);
|
final String ipHostname = sampleTlsEndpoint.getAddress().getHostAddress();
|
||||||
|
InetSocketAddress host = new InetSocketAddress(ipHostname, 443);
|
||||||
@Cleanup SSLSocket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null);
|
@Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(ipHostname), host, null, null);
|
||||||
fail();
|
fail();
|
||||||
} catch (IOException e) {
|
} catch (SSLException e) {
|
||||||
assertFalse(ExceptionUtils.indexOfType(e, SSLPeerUnverifiedException.class) == -1);
|
Log.i(TAG, "Expected exception", e);
|
||||||
|
assertFalse(ExceptionUtils.indexOfType(e, SSLException.class) == -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testUntrustedCertificate() {
|
public void testUntrustedCertificate() throws IOException {
|
||||||
try {
|
try {
|
||||||
// host with certificate that is not trusted by default
|
// host with certificate that is not trusted by default
|
||||||
InetSocketAddress host = new InetSocketAddress("cacert.org", 443);
|
InetSocketAddress host = new InetSocketAddress("cacert.org", 443);
|
||||||
|
|
||||||
@Cleanup SSLSocket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null);
|
@Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null);
|
||||||
fail();
|
fail();
|
||||||
} catch (IOException e) {
|
} catch (SSLHandshakeException e) {
|
||||||
|
Log.i(TAG, "Expected exception", e);
|
||||||
assertFalse(ExceptionUtils.indexOfType(e, CertPathValidatorException.class) == -1);
|
assertFalse(ExceptionUtils.indexOfType(e, CertPathValidatorException.class) == -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ public class DavHttpClient {
|
|||||||
static {
|
static {
|
||||||
socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
|
socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
|
||||||
.register("http", PlainConnectionSocketFactory.getSocketFactory())
|
.register("http", PlainConnectionSocketFactory.getSocketFactory())
|
||||||
.register("https", TlsSniSocketFactory.INSTANCE)
|
.register("https", TlsSniSocketFactory.getSocketFactory())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// use request defaults from AndroidHttpClient
|
// use request defaults from AndroidHttpClient
|
||||||
|
@ -7,136 +7,42 @@
|
|||||||
*/
|
*/
|
||||||
package at.bitfire.davdroid.webdav;
|
package at.bitfire.davdroid.webdav;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.net.SSLCertificateSocketFactory;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.apache.http.HttpHost;
|
|
||||||
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
|
|
||||||
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifierHC4;
|
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifierHC4;
|
||||||
import org.apache.http.protocol.HttpContext;
|
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||||
|
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
|
||||||
import javax.net.ssl.SSLSession;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
import lombok.Cleanup;
|
||||||
|
|
||||||
|
public class TlsSniSocketFactory extends SSLConnectionSocketFactory {
|
||||||
private static final String TAG = "davdroid.TlsSniSocketFactory";
|
private static final String TAG = "davdroid.TlsSniSocketFactory";
|
||||||
|
|
||||||
public final static TlsSniSocketFactory INSTANCE = new TlsSniSocketFactory();
|
public static TlsSniSocketFactory getSocketFactory() {
|
||||||
|
return new TlsSniSocketFactory(
|
||||||
private final static SSLSocketFactory sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
|
(SSLSocketFactory) SSLSocketFactory.getDefault(),
|
||||||
|
new BrowserCompatHostnameVerifierHC4() // use BrowserCompatHostnameVerifier to allow IP addresses in the Common Name
|
||||||
// use BrowserCompatHostnameVerifier to allow IP addresses in the Common Name
|
);
|
||||||
private final static HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifierHC4();
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
For TLS connections without HTTPS (CONNECT) proxy:
|
|
||||||
1) socket = createSocket() is called
|
|
||||||
2) connectSocket(socket) is called which creates a new TLS connection (but no handshake yet)
|
|
||||||
3) reasonable encryption settings are applied to socket
|
|
||||||
4) SNI is set up for socket
|
|
||||||
5) handshake and certificate/host name verification
|
|
||||||
|
|
||||||
Layered sockets are used with HTTPS (CONNECT) proxies:
|
|
||||||
1) plain = createSocket() is called
|
|
||||||
2) the plain socket is connected to http://proxy:8080
|
|
||||||
3) a CONNECT request is sent to the proxy and the response is parsed
|
|
||||||
4) socket = createLayeredSocket(plain) is called to "upgrade" the plain connection to a TLS connection (but no handshake yet)
|
|
||||||
5) reasonable encryption settings are applied to socket
|
|
||||||
6) SNI is set up for socket
|
|
||||||
7) handshake and certificate/host name verification
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLSocket createSocket(HttpContext context) throws IOException {
|
|
||||||
return (SSLSocket)sslSocketFactory.createSocket();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLSocket connectSocket(int timeout, Socket sock, HttpHost host, InetSocketAddress remoteAddr, InetSocketAddress localAddr, HttpContext context) throws IOException {
|
|
||||||
Log.d(TAG, "Establishing direct TLS connection to " + host);
|
|
||||||
final SSLSocket socket = (sock != null) ? (SSLSocket)sock : createSocket(context);
|
|
||||||
|
|
||||||
if (localAddr != null)
|
|
||||||
socket.bind(localAddr);
|
|
||||||
|
|
||||||
// connect the socket on TCP level
|
|
||||||
socket.connect(remoteAddr, timeout);
|
|
||||||
|
|
||||||
// establish and verify TLS connection
|
|
||||||
establishAndVerify(socket, host.getHostName());
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SSLSocket createLayeredSocket(Socket plain, String host, int port, HttpContext context) throws IOException {
|
|
||||||
Log.d(TAG, "Establishing layered TLS connection to " + host);
|
|
||||||
|
|
||||||
// create new socket for TLS connection on top of existing socket
|
|
||||||
final SSLSocket socket = (SSLSocket)sslSocketFactory.createSocket(plain, host, port, true);
|
|
||||||
|
|
||||||
// establish and verify TLS connection
|
|
||||||
establishAndVerify(socket, host);
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Establishes and verifies a TLS connection to a (TCP-)connected SSLSocket:
|
|
||||||
* - set TLS parameters like allowed protocols and ciphers
|
|
||||||
* - set SNI host name
|
|
||||||
* - verify host name
|
|
||||||
* - verify certificate
|
|
||||||
* @param socket unconnected SSLSocket
|
|
||||||
* @param host host name for SNI
|
|
||||||
* @throws SSLPeerUnverifiedException
|
|
||||||
*/
|
|
||||||
private void establishAndVerify(SSLSocket socket, String host) throws IOException, SSLPeerUnverifiedException {
|
|
||||||
setTlsParameters(socket);
|
|
||||||
setSniHostname(socket, host);
|
|
||||||
|
|
||||||
// TLS handshake, throws an exception for untrusted certificates
|
|
||||||
socket.startHandshake();
|
|
||||||
|
|
||||||
// verify hostname and certificate
|
|
||||||
SSLSession session = socket.getSession();
|
|
||||||
if (!hostnameVerifier.verify(host, session))
|
|
||||||
// throw exception for inavlid host names
|
|
||||||
throw new SSLPeerUnverifiedException(host);
|
|
||||||
|
|
||||||
Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
|
|
||||||
" using " + session.getCipherSuite());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares a TLS/SSL connection socket by:
|
|
||||||
* - setting the default TrustManager (as we have created an "insecure" connection to avoid handshake problems before)
|
|
||||||
* - setting reasonable TLS protocol versions
|
|
||||||
* - setting reasonable cipher suites (if required)
|
|
||||||
* @param socket unconnected SSLSocket to prepare
|
|
||||||
*/
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
void setTlsParameters(SSLSocket socket) {
|
|
||||||
// Android 5.0+ (API level21) provides reasonable default settings
|
// Android 5.0+ (API level21) provides reasonable default settings
|
||||||
// but it still allows SSLv3
|
// but it still allows SSLv3
|
||||||
// https://developer.android.com/about/versions/android-5.0-changes.html#ssl
|
// https://developer.android.com/about/versions/android-5.0-changes.html#ssl
|
||||||
|
static String protocols[] = null, cipherSuites[] = null;
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
@Cleanup SSLSocket socket = (SSLSocket)SSLSocketFactory.getDefault().createSocket();
|
||||||
|
|
||||||
/* set reasonable protocol versions */
|
/* set reasonable protocol versions */
|
||||||
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
|
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
|
||||||
@ -146,12 +52,12 @@ public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
|||||||
if (!protocol.toUpperCase().contains("SSL"))
|
if (!protocol.toUpperCase().contains("SSL"))
|
||||||
protocols.add(protocol);
|
protocols.add(protocol);
|
||||||
Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", "));
|
Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", "));
|
||||||
socket.setEnabledProtocols(protocols.toArray(new String[0]));
|
TlsSniSocketFactory.protocols = protocols.toArray(new String[0]);
|
||||||
|
|
||||||
/* set reasonable cipher suites */
|
/* set reasonable cipher suites */
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// choose secure cipher suites
|
// choose secure cipher suites
|
||||||
List<String> allowedCiphers = Arrays.asList(new String[] {
|
List<String> allowedCiphers = Arrays.asList(new String[]{
|
||||||
// allowed secure ciphers according to NIST.SP.800-52r1.pdf Section 3.3.1 (see docs directory)
|
// allowed secure ciphers according to NIST.SP.800-52r1.pdf Section 3.3.1 (see docs directory)
|
||||||
// TLS 1.2
|
// TLS 1.2
|
||||||
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||||
@ -186,25 +92,14 @@ public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
|||||||
enabledCiphers.addAll(new HashSet<String>(Arrays.asList(socket.getEnabledCipherSuites())));
|
enabledCiphers.addAll(new HashSet<String>(Arrays.asList(socket.getEnabledCipherSuites())));
|
||||||
|
|
||||||
Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", "));
|
Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", "));
|
||||||
socket.setEnabledCipherSuites(enabledCiphers.toArray(new String[0]));
|
TlsSniSocketFactory.cipherSuites = enabledCiphers.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
public TlsSniSocketFactory(SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) {
|
||||||
private void setSniHostname(SSLSocket socket, String hostName) {
|
super(socketfactory, protocols, cipherSuites, hostnameVerifier);
|
||||||
// set SNI host name
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && sslSocketFactory instanceof SSLCertificateSocketFactory) {
|
|
||||||
Log.d(TAG, "Using documented SNI with host name " + hostName);
|
|
||||||
((SSLCertificateSocketFactory)sslSocketFactory).setHostname(socket, hostName);
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "No documented SNI support on Android <4.2, trying reflection method with host name " + hostName);
|
|
||||||
try {
|
|
||||||
java.lang.reflect.Method setHostnameMethod = socket.getClass().getMethod("setHostname", String.class);
|
|
||||||
setHostnameMethod.invoke(socket, hostName);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w(TAG, "SNI not useable", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user