在 Android 7 org.apache.http (Apache HttpClient 4.0) 中启用已弃用的密码套件后主机名不匹配

Getting hostname mismatch after enabling deprecated cipher suite in Android 7 org.apache.http (Apache HttpClient 4.0)

我设法启用了正确的密码套件,但在此过程中我破坏了主机名比较。

这是我开始之前的代码:

HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, 10000);
HttpConnectionParams.setSoTimeout(params, 20000);

DefaultHttpClient client = new DefaultHttpClient(params);

HttpPost request = new HttpPost(url);
...
HttpResponse response = client.execute(request);
...

从 Android 7 开始,我收到以下错误:

javax.net.ssl.SSLHandshakeException: Handshake failed
    at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:429)
    at org.apache.http.conn.ssl.SSLSocketFactory.createSocket(SSLSocketFactory.java:406)
    at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:170)
    at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:169)
    at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:124)
    at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:366)
    at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:560)
    at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:492)
    at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:470)
    ...
Caused by: javax.net.ssl.SSLProtocolException: SSL handshake terminated: ssl=0x9f7a03c0: Failure in SSL library, usually a protocol error
error:10000410:SSL routines:OPENSSL_internal:SSLV3_ALERT_HANDSHAKE_FAILURE (external/boringssl/src/ssl/s3_pkt.c:610 0x9c571b00:0x00000001)
error:1000009a:SSL routines:OPENSSL_internal:HANDSHAKE_FAILURE_ON_CLIENT_HELLO (external/boringssl/src/ssl/s3_clnt.c:764 0x9c614196:0x00000000)
    at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
    at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)
        ... 19 more

这显然是由于网络服务器要求客户端支持 RC4, 似乎只支持 through Android 6 (api 23)。不幸的是,目前无法升级网络服务器,所以我必须在应用程序中启用所需的密码套件。

这是我尝试过的:

// HttpParams params is same as before

DefaultHttpClient client = new DefaultHttpClient(params);

ClientConnectionManager connManager = client.getConnectionManager();
SchemeRegistry schemeRegistry = connManager.getSchemeRegistry();
Scheme scheme = schemeRegistry.getScheme("https");
final SSLSocketFactory impl = (SSLSocketFactory) scheme.getSocketFactory();

SocketFactory sf = new SocketFactory() {

    @Override
    public Socket createSocket() throws IOException {
        SSLSocket socket = (SSLSocket) impl.createSocket();
        ArrayList<String> list = new ArrayList<>(Arrays.asList(socket.getEnabledCipherSuites()));
        list.add("SSL_RSA_WITH_RC4_128_SHA");
        socket.setEnabledCipherSuites(list.toArray(new String[list.size()])); // this is where I add the required cipher suites after the SSLSocket is created
        return socket;
    }

    // unchanged wrappers
    @Override
    public Socket connectSocket(Socket socket, String s, int i, InetAddress inetAddress, int i1, HttpParams httpParams) throws IOException { return impl.connectSocket(socket, s, i, inetAddress, i1, httpParams); }

    @Override
    public boolean isSecure(Socket socket) throws IllegalArgumentException { return impl.isSecure(socket); }
};

schemeRegistry.register(new Scheme("https", sf, 443));

// request & execute same as before

这成功添加了所需的密码套件,但似乎破坏了主机名比较,因为我得到以下信息:

javax.net.ssl.SSLException: hostname in certificate didn't match: <10.10.10.10> != <sub.example.com> OR <sub.example.com>

此主机的 IP 地址正确。大概我的所有 overrides/wrappers 都导致了使用 IP 地址而不是在 url.

中传递的原始 https://sub.example.com/ 主机名的情况

你能帮帮我吗?我不知道如何解决主机名不匹配的问题。

知道了!似乎 Apache HTTP 客户端库正在检查我的套接字工厂 sf 是否为 instanceof LayeredSocketFactory,在这种情况下,该库使用另一个 createSocket 方法来传递所需的主机名(sub.example.com 而不是 10.10.10.10) 以进行正确的 SSL/TLS 比较。

所以,sf需要这样写。请注意额外的 createSocket 覆盖:

LayeredSocketFactory sf = new LayeredSocketFactory() {

    javax.net.ssl.SSLSocketFactory defaultFactory = HttpsURLConnection.getDefaultSSLSocketFactory();

    @Override
    public Socket createSocket(Socket tcpSocket, String host, int port, boolean autoClose) throws IOException {
        // I'm not actually using impl.createSocket, because then the hostname verifier would have kicked in prior to addMyCipherSuite
        SSLSocket sslSocket = (SSLSocket) defaultFactory.createSocket(tcpSocket, host, port, autoClose);
        impl.getHostnameVerifier().verify(host, addMyCipherSuite(sslSocket));
        return sslSocket;
    }


    @Override
    public Socket createSocket() throws IOException {
        return addMyCipherSuite((SSLSocket) impl.createSocket());
    }

    private Socket addMyCipherSuite(SSLSocket socket) {
        ArrayList<String> list = new ArrayList<>(Arrays.asList(socket.getEnabledCipherSuites()));
        list.add("SSL_RSA_WITH_RC4_128_SHA");
        socket.setEnabledCipherSuites(list.toArray(new String[list.size()])); // this is where I add the required cipher suites after the SSLSocket is created
        return socket;
    }

    // unchanged wrappers
    @Override
    public Socket connectSocket(Socket socket, String s, int i, InetAddress inetAddress, int i1, HttpParams httpParams) throws IOException { return impl.connectSocket(socket, s, i, inetAddress, i1, httpParams); }

    @Override
    public boolean isSecure(Socket socket) throws IllegalArgumentException { return impl.isSecure(socket); }
};