Android javax.net.ssl.SSLException: hostname in certificate didn't match...

最近在开发过程中,遇到一个bug,我们的App在下载一个文件的时候,服务器端返回ConnectionException:

javax.net.ssl.SSLException: hostname in certificate didn't match:  !=  OR  OR 
我的代码很简单:

request = new HttpPost("https://foo.net/api/v1/baz");
request.setHeader("Authorization", "user:pass");
response = httpClient.execute(request);

为什么会有这个错误呢?这主要跟server 的ssl 验证方式有关。

The most likely cause for this problem is that the server uses Server Name Indication to choose which certificate to send. If the client doesn't support SNI, the server cannot choose which certificate to send during the SSL/TLS handshake (before any HTTP traffic is sent). SNI is required when you want to use multiple certificates on the same IP address and port, but not all clients support it.

 Apache HTTP Client library support SNI is quite recent, and certainly hasn't made it into the Android release.

Apache HttpClient supports SNI on Oracle’s Java 1.7 since 4.3.2, but this is not useable with Android’s Java flavour and it doesn’t seem that Google is willing to update the library. So, HttpClient doesn’t support SNI on Android by default.

两种解决方法:
1. Server 修改:to use a distinct IP address for each host (so as not to need SNI), if you're in control of server
2. 客户端修改:to use another HTTP Client library (e.g. HttpsURLConnection).  HttpsURLConnection which supports SNI by default since Android 2.3.x (see Android’s HTTP Clients).

参考:[Android] Using SNI and TLSv1.2 with Apache HttpClient library

If you want to use SSL/TLS with SNI (Server Name Indication) on Android, you’re basically encouraged to use HttpsURLConnection which supports SNI by default since Android 2.3.x (see Android’s HTTP Clients).

However, if you want to use other HTTP verbs than OPTIONS, HEAD, TRACE, GET, POST, PUT or DELETE (look for “HTTP Methods” in the docs), HttpsURLConnection is not an option. So you will have to stick with the HttpClient library, i.e. the DefaultHttpClient / AndroidHttpClient classes.

Apache HttpClient 4.0-alpha is shipped with Android, and it doesn’t seem that Google is willing to update the library. So, if you need a newer HttpClient version (and there are good reasons why you might need it), the HttpClient package names (Java name spaces) have to be changed. httpclientandroidlib is such a repackaging of recent HttpClient libraries to Android. In the meanwhile, there’s an official Android port of HttpClient.

HttpClient supports SNI on Oracle’s Java 1.7 since 4.3.2, but this is not useable with Android’s Java flavour. So, HttpClient doesn’t support SNI on Android by default.

However, there are two ways to get SNI:

  1. Since Android 4.2+ (API level 17) they have officially added SNI support to a class called SSLCertificateSocketFactory.
  2. Since Android 2.3 (Gingerbread), SNI is available in the OpenSSL implementation used by Android’s Java flavour. Sockets created by SSLCertificateSocketFactory are instances of SSLSocketImpl, and this class has a method called setHostname(String) that enables SNI and sets the SNI hostname for this socket. However, this feature is not documented and can only be used by reflection. It works on all devices I have seen, but theoretically, there might be Android variants (for instance, by certain vendors) that don’t provide this method because it’s not documented.
Using these two methods, it’s possible to add SNI support for your HttpClient application, too. (See code example below).

TLS v1.1/v1.2

Android versions >= 4.1and < 5.0 support TLS 1.1 and TLS 1.2, but these (newer and more secure) TLS versions are disabled, while only SSLv3 and TLSv1 stay enabled by default. This is fixed in Android 5.0, but for the versions between you’ll have to enable TLSv1.1 and TLSv1.2 manually:

ssl.setEnabledProtocols(ssl.getSupportedProtocols());

This line is for demonstration only! Actually, you should exclude SSLv3, too. You should also care about the used algorithms (read the NIST recommendations and other related documented) and maybe set them manually. Be sure that you know what you do, otherwise you can’t expect much security.

Working example for stock Android HttpClient

This code is for the HttpClient version which is shipped with Android (HttpClient 4.0-alpha). Look at the links below for an example using a recent HttpClient version.
// create new HTTP client
client = new ApacheHttpClient();

// use our own, SNI-capable LayeredSocketFactory for https://
SchemeRegistry schemeRegistry = client.getConnectionManager().getSchemeRegistry();
schemeRegistry.register(new Scheme("https", new TlsSniSocketFactory(), 443));
Then define your TlsSniSocketFactory:

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public class TlsSniSocketFactory implements LayeredSocketFactory {
    private static final String TAG = "davdroid.SNISocketFactory";

    final static HostnameVerifier hostnameVerifier = new StrictHostnameVerifier();

    // Plain TCP/IP (layer below TLS)
    @Override
    public Socket connectSocket(Socket s, String host, int port, InetAddress localAddress, int localPort, HttpParams params) throws IOException {
            return null;
    }

    @Override
    public Socket createSocket() throws IOException {
            return null;
    }

    @Override
    public boolean isSecure(Socket s) throws IllegalArgumentException {
            if (s instanceof SSLSocket)
                    return ((SSLSocket)s).isConnected();
            return false;
    }

    // TLS layer
    @Override
    public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
            if (autoClose) {
                    // we don't need the plainSocket
                    plainSocket.close();
            }

            // create and connect SSL socket, but don't do hostname/certificate verification yet
            SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
            SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(InetAddress.getByName(host), port);

            // enable TLSv1.1/1.2 if available
            // (see https://github.com/rfc2822/davdroid/issues/229)
            ssl.setEnabledProtocols(ssl.getSupportedProtocols());

            // set up SNI before the handshake
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    Log.i(TAG, "Setting SNI hostname");
                    sslSocketFactory.setHostname(ssl, host);
            } else {
                    Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
                    try {
                         java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
                         setHostnameMethod.invoke(ssl, host.getHostName());
                    } catch (Exception e) {
                            Log.w(TAG, "SNI not useable", e);
                    }
            }

            // verify hostname and certificate
            SSLSession session = ssl.getSession();
            if (!hostnameVerifier.verify(host, session))
                    throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);

            Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
                            " using " + session.getCipherSuite());
            return ssl;
    }
}

你可能感兴趣的:(Android javax.net.ssl.SSLException: hostname in certificate didn't match...)