在Android 4.1+中强制开启TLSv1.1和TLSv1.2

在做MQTT时遇到了android4.4手机没法连接服务端的情况,查阅发现是因为系统默认没有开启TLSv1.1和TLSv1.2的支持。抛出的错误如下:

1
2
3
 MqttException (0) – javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7861ada8: Failure in SSL library, usually a protocol error
 
error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version (external/openssl/ssl/s23_clnt.c:741 0x71e78cf8:0x00000000)

Android 4.1开始支持TLSv1.1和TLSv1.2,但是在4.1~4.4版本中默认都是关闭。在某些情况下可能会导致SSL链接建立失败的问题(服务端强制使用1.1和/或1.2,而android 4.1~4.4中没有开启。),这时需要开启TLSv1.1和TLSv1.2。

我们需要使用自定义的SSLSocketFactory去创建SSL链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class CustomSslSocketFactory extends SSLSocketFactory {
 
    private SSLSocketFactory sslSocketFactory;
 
    public CustomSslSocketFactory(TrustManager[] trustManager) throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, trustManager, null);
        sslSocketFactory = context.getSocketFactory();
    }
 
    @Override
    public String[] getDefaultCipherSuites() {
        return sslSocketFactory.getDefaultCipherSuites();
    }
 
    @Override
    public String[] getSupportedCipherSuites() {
        return sslSocketFactory.getSupportedCipherSuites();
    }
 
    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(s, host, port, autoClose));
    }
 
    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
    }
 
    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port, localHost, localPort));
    }
 
    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
    }
 
    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(address, port, localAddress, localPort));
    }
 
    @Override
    public Socket createSocket() throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket());
    }
 
    private Socket enableTLSOnSocket(Socket socket) {
        ((SSLSocket) socket).setEnabledProtocols(new String[] {"TLSv1.2", "TLSv1.1"});
        return socket;
    }
}

需要注意的是如果遇到如下错误,那需要override一个空参数的createSocket()方法。

1
2
3
4
5
6
7
Error:java.net.SocketException: Unconnected is not implemented 
 
//Override createSocket()
    @Override
    public Socket createSocket() throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket());
    }

使用这个自定义的SSLSocketFactory类就可以去做TLSv1.1和TLSv1.2的SSL链接就可以了。

Alamofire SSL Pining

Alamofire是一个Swift http network library。用Alamofire做SSL Pining非常方便。CA证书的提取请参考上一篇文章。(需要注意的是iOS中需要提取DER格式的CA证书,而不是PEM。在保存证书时可以选择格式)

 

1
2
3
4
5
6
7
8
9
10
11
12
    let pathToCert = NSBundle.mainBundle().pathForResource("certificate", ofType: "cer")
    let localCertificate: NSData = NSData(contentsOfFile: pathToCert!)!
 
    let serverTrustPolicy = ServerTrustPolicy.PinCertificates(certificates: [SecCertificateCreateWithData(nil, localCertificate)!], validateCertificateChain: true, validateHost: true)
 
    let policies: [String: ServerTrustPolicy] = [
        "google.com": serverTrustPolicy
    ]
 
    let manager = Alamofire.Manager(serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))
    // make your request here
    // manager.request(...)

Retrofit/OkHttp SSL Pining

这周在做竞品研究的时候发现,只要安装抓包工具的相关CA证书,就可以看到SSL加密的数据了。为了防止request结构被其他人看到,决定使用SSL Pinning,使得App只认绑定证书。这样即便安装了抓包工具的证书,也会因为证书的key不对而不会建立链接。

OkHttp提供了一个CertificatePinner类可以方便的设置SSL Pinning,但实验下来发现在Android中不行。使用抓包工具还是可以看到加密内容。或许是我的使用方法有问题。不过我还是找另外一种方法做SSL Pinning。

这里使用的方法是从CA证书中读取相关的信息,并创建一个只信任指定CA证书的SSLSocketFactory对象,注入到OkHttp中。这样OkHttp会使用注入的SSLSocketFactory去创建SSL Socket了。

第一步提取CA证书(以Google为例):

Get Google CA
Get Google CA
Click “View Certificate”
Click “Export”

导出后得到一个后缀名为.crt的文件,该文件既是Google的CA证书。

 

第二步读取CA证书并创建创建SSLSocketFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
SSLSocketFactory = sslSocketFactory = null;
try {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
 
    InputStream caInput = context.getResources().openRawResource(R.raw.ca);
    Certificate ca = null;
    try {
        ca = certificateFactory.generateCertificate(caInput);
    } catch (CertificateException e) {
        e.printStrackTrace();
    } finally {
        caInput.close();
    }
 
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    if (ca == null) {
        return null;
    }
    keyStore.setCertificateEntry("ca", ca);
 
    String algorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
    trustManagerFactory.init(keyStore);
 
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
 
    sslSocketFactory = sslContext.getSocketFactory();
} catch (CertificateException|IOException|KeyStoreException|NoSuchAlgorithmException|KeyManagementException e) {
    e.printStackTrace();
}

 

第三步在创建OkHttpClient时将SSLSocketFactory注入到Builder:

1
2
3
OkHttpClient client = new OkHttpClient.Builder()
        .sslSocketFactory(SslSocketFactoryHelper.getSslSocketFactory(context.get()))
        .build();

这样就大功告成了。

再测试一下会发现因为CA证书不对,链接会无法建立。也就达到了SSL Pinning的效果。