mirror of
https://github.com/etesync/android
synced 2025-02-02 10:51:10 +00:00
TlsSniSocketFactory improvements
* make it work again with non-proxied connections * add tests for proxied and non-proxied connections * add tests for SNI, host name and certificate verification
This commit is contained in:
parent
897ede7582
commit
ca0ad612a7
@ -1,27 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
*/
|
||||||
|
|
||||||
package at.bitfire.davdroid.webdav;
|
package at.bitfire.davdroid.webdav;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.security.cert.CertPathValidatorException;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
import org.apache.commons.lang.ArrayUtils;
|
||||||
|
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
public void testCiphers() throws IOException {
|
TlsSniSocketFactory factory = TlsSniSocketFactory.INSTANCE;
|
||||||
SSLSocket socket = (SSLSocket)TlsSniSocketFactory.INSTANCE.createSocket(null);
|
|
||||||
|
|
||||||
Log.i(TAG, "Enabled:");
|
private InetSocketAddress sampleTlsEndpoint;
|
||||||
for (String cipher : socket.getEnabledCipherSuites())
|
|
||||||
Log.i(TAG, cipher);
|
|
||||||
|
|
||||||
Log.i(TAG, "Supported:");
|
@Override
|
||||||
for (String cipher : socket.getSupportedCipherSuites())
|
protected void setUp() {
|
||||||
Log.i(TAG, cipher);
|
// sni.velox.ch is used to test SNI (without SNI support, the certificate is invalid)
|
||||||
|
sampleTlsEndpoint = new InetSocketAddress("sni.velox.ch", 443);
|
||||||
|
}
|
||||||
|
|
||||||
assert(true);
|
public void testCreateSocket() {
|
||||||
|
try {
|
||||||
|
@Cleanup SSLSocket socket = factory.createSocket(null);
|
||||||
|
assertFalse(socket.isConnected());
|
||||||
|
} catch (IOException e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testConnectSocket() {
|
||||||
|
try {
|
||||||
|
@Cleanup SSLSocket socket = factory.createSocket(null);
|
||||||
|
|
||||||
|
factory.connectSocket(1000, socket, new HttpHost(sampleTlsEndpoint.getHostName()), sampleTlsEndpoint, null, null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "I/O exception", e);
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCreateLayeredSocket() {
|
||||||
|
try {
|
||||||
|
// connect plain socket first
|
||||||
|
@Cleanup Socket plain = new Socket();
|
||||||
|
plain.connect(sampleTlsEndpoint);
|
||||||
|
assertTrue(plain.isConnected());
|
||||||
|
|
||||||
|
// then create TLS socket on top of it and establish TLS Connection
|
||||||
|
@Cleanup SSLSocket socket = factory.createLayeredSocket(plain, sampleTlsEndpoint.getHostName(), sampleTlsEndpoint.getPort(), null);
|
||||||
|
assertTrue(socket.isConnected());
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "I/O exception", e);
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testSetTlsParameters() throws IOException {
|
||||||
|
@Cleanup SSLSocket socket = factory.createSocket(null);
|
||||||
|
factory.setTlsParameters(socket);
|
||||||
|
|
||||||
|
String enabledProtocols[] = socket.getEnabledProtocols();
|
||||||
|
// SSL (all versions) should be disabled
|
||||||
|
for (String protocol : enabledProtocols)
|
||||||
|
assertFalse(protocol.contains("SSL"));
|
||||||
|
// TLS v1+ should be enabled
|
||||||
|
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1"));
|
||||||
|
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1.1"));
|
||||||
|
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1.2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void testHostnameNotInCertificate() {
|
||||||
|
try {
|
||||||
|
// 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
|
||||||
|
InetSocketAddress host = new InetSocketAddress(sampleTlsEndpoint.getAddress().getHostAddress(), 443);
|
||||||
|
|
||||||
|
@Cleanup SSLSocket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null);
|
||||||
|
fail();
|
||||||
|
} catch (IOException e) {
|
||||||
|
assertFalse(ExceptionUtils.indexOfType(e, SSLPeerUnverifiedException.class) == -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testUntrustedCertificate() {
|
||||||
|
try {
|
||||||
|
// host with certificate that is not trusted by default
|
||||||
|
InetSocketAddress host = new InetSocketAddress("cacert.org", 443);
|
||||||
|
|
||||||
|
@Cleanup SSLSocket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null);
|
||||||
|
fail();
|
||||||
|
} catch (IOException e) {
|
||||||
|
assertFalse(ExceptionUtils.indexOfType(e, CertPathValidatorException.class) == -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/*******************************************************************************
|
/*
|
||||||
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
* Copyright (c) 2015 Ricki Hirner (bitfire web engineering).
|
||||||
* All rights reserved. This program and the accompanying materials
|
* All rights reserved. This program and the accompanying materials
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
* which accompanies this distribution, and is available at
|
* which accompanies this distribution, and is available at
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
******************************************************************************/
|
*/
|
||||||
package at.bitfire.davdroid.webdav;
|
package at.bitfire.davdroid.webdav;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -13,35 +13,25 @@ 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.ArrayUtils;
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.apache.http.HttpHost;
|
import org.apache.http.HttpHost;
|
||||||
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
|
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
|
||||||
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifierHC4;
|
|
||||||
import org.apache.http.protocol.HttpContext;
|
import org.apache.http.protocol.HttpContext;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.CertPathValidatorException;
|
|
||||||
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.HandshakeCompletedEvent;
|
|
||||||
import javax.net.ssl.HandshakeCompletedListener;
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
import javax.net.ssl.SSLSession;
|
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;
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
|
|
||||||
public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
||||||
private static final String TAG = "davdroid.SNISocketFactory";
|
private static final String TAG = "davdroid.SNISocketFactory";
|
||||||
@ -65,35 +55,48 @@ public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
|||||||
2) the plain socket is connected to http://proxy:8080
|
2) the plain socket is connected to http://proxy:8080
|
||||||
3) a CONNECT request is sent to the proxy and the response is parsed
|
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)
|
4) socket = createLayeredSocket(plain) is called to "upgrade" the plain connection to a TLS connection (but no handshake yet)
|
||||||
5) SNI is set up for socket
|
5) reasonable encryption settings are applied to socket
|
||||||
6) handshake and certificate/host name verification
|
6) SNI is set up for socket
|
||||||
|
7) handshake and certificate/host name verification
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Socket createSocket(HttpContext context) throws IOException {
|
public SSLSocket createSocket(HttpContext context) throws IOException {
|
||||||
return sslSocketFactory.createSocket();
|
return (SSLSocket)sslSocketFactory.createSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Socket connectSocket(int timeout, Socket sock, HttpHost host, InetSocketAddress remoteAddr, InetSocketAddress localAddr, HttpContext context) throws IOException {
|
public SSLSocket connectSocket(int timeout, Socket sock, HttpHost host, InetSocketAddress remoteAddr, InetSocketAddress localAddr, HttpContext context) throws IOException {
|
||||||
Log.d(TAG, "Preparing direct TLS connection to " + host);
|
Log.d(TAG, "Establishing direct TLS connection to " + host);
|
||||||
final SSLSocket socket = (SSLSocket)((sock != null) ? sock : createSocket(context));
|
final SSLSocket socket = (sock != null) ? (SSLSocket)sock : createSocket(context);
|
||||||
connectAndVerify(socket, host.getHostName());
|
|
||||||
|
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;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Socket createLayeredSocket(Socket plain, String host, int port, HttpContext context) throws IOException {
|
public SSLSocket createLayeredSocket(Socket plain, String host, int port, HttpContext context) throws IOException {
|
||||||
Log.d(TAG, "Preparing proxied TLS connection to " + host);
|
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);
|
final SSLSocket socket = (SSLSocket)sslSocketFactory.createSocket(plain, host, port, true);
|
||||||
connectAndVerify(socket, host);
|
|
||||||
|
// establish and verify TLS connection
|
||||||
|
establishAndVerify(socket, host);
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establishes a connection to an unconnected SSLSocket:
|
* Establishes and verifies a TLS connection to a (TCP-)connected SSLSocket:
|
||||||
* - prepare socket
|
* - set TLS parameters like allowed protocols and ciphers
|
||||||
* - set SNI host name
|
* - set SNI host name
|
||||||
* - verify host name
|
* - verify host name
|
||||||
* - verify certificate
|
* - verify certificate
|
||||||
@ -101,19 +104,17 @@ public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
|||||||
* @param host host name for SNI
|
* @param host host name for SNI
|
||||||
* @throws SSLPeerUnverifiedException
|
* @throws SSLPeerUnverifiedException
|
||||||
*/
|
*/
|
||||||
private void connectAndVerify(SSLSocket socket, String host) throws IOException, SSLPeerUnverifiedException {
|
private void establishAndVerify(SSLSocket socket, String host) throws IOException, SSLPeerUnverifiedException {
|
||||||
// prepare socket (set encryption etc.)
|
setTlsParameters(socket);
|
||||||
prepareSSLSocket(socket);
|
|
||||||
|
|
||||||
// set SNI hostname
|
|
||||||
setSniHostname(socket, host);
|
setSniHostname(socket, host);
|
||||||
|
|
||||||
// TLS handshake
|
// TLS handshake, throws an exception for untrusted certificates
|
||||||
socket.startHandshake();
|
socket.startHandshake();
|
||||||
|
|
||||||
// verify hostname and certificate
|
// verify hostname and certificate
|
||||||
SSLSession session = socket.getSession();
|
SSLSession session = socket.getSession();
|
||||||
if (!hostnameVerifier.verify(host, session))
|
if (!hostnameVerifier.verify(host, session))
|
||||||
|
// throw exception for inavlid host names
|
||||||
throw new SSLPeerUnverifiedException(host);
|
throw new SSLPeerUnverifiedException(host);
|
||||||
|
|
||||||
Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
|
Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
|
||||||
@ -122,14 +123,14 @@ public class TlsSniSocketFactory implements LayeredConnectionSocketFactory {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares a TLS/SSL connetion socket by:
|
* Prepares a TLS/SSL connection socket by:
|
||||||
* - setting the default TrustManager (as we have created an "insecure" connection to avoid handshake problems before)
|
* - setting the default TrustManager (as we have created an "insecure" connection to avoid handshake problems before)
|
||||||
* - setting reasonable TLS protocol versions
|
* - setting reasonable TLS protocol versions
|
||||||
* - setting reasonable cipher suites (if required)
|
* - setting reasonable cipher suites (if required)
|
||||||
* @param socket unconnected SSLSocket to prepare
|
* @param socket unconnected SSLSocket to prepare
|
||||||
*/
|
*/
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
private void prepareSSLSocket(SSLSocket socket) {
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user