在Android应用中使用自定义证书的HTTPS连接(下)

因为这部分才是本文的重点,要说得详细一点,所以单独做成一篇来说。

安全地使用自定义证书的HTTPS连接方式

终极解决方案是:把证书编译到应用中去,由应用自己来验证证书。

生成KeyStore

要验证自定义证书,首先要把证书编译到应用中去,这需要JSSE提供的keytool工具来生成KeyStore文件。参考《Java 安全套接字编程以及 keytool 使用最佳实践》,我试过了用JKS格式,但是结果连接失败,报错:Wrong version of key store。后来看了SO的这个帖才知道必须使用BKS的1.46版。更详细的内容参考这篇《》。

这里所谓的证书,实际上就是公钥,你可以从web服务器配置的.crt文件或.pem文件里获得。比如12306就直接提供了公钥证书下载,真是“服务周到”啊。

还 有一个比较简单的办法就是直接从浏览器里获得。比如用 FireFox 打开 https 链接,在地址栏顶部的小锁上点一下,然后点“更多信息……”-“查看证书”-“详细内容”-“导出”,即可将网站的X.509证书导出为一个文本文件。不 过需要注意的是,这种方法只对某些HTTPS服务器有效——通常是使用自签名证书或是使用类似StarCom免费证书服务器,但是对 12306 或 google 这种的就无效了,具体原因不明。

另外,不论是浏览器导出,还是服务器端获得,都是公钥证书,有两种格式:纯文本的.crt格式或是二进制的.cer格式。两种都可以用。

然后,你需要一个特定版本的JCE Provider,就是上面说过的那个SO帖里给的:http://www.bouncycastle.org/download/bcprov-jdk15on-146.jar 。注意,bouncycastle官网上目前发布的1.50版我试了一下不可用,不知道是不是我打开的方式不对,总之用这个1.46版的是没错的。

把这两个文件放在一起,然后在这个目录下运行以下命令:

keytool -importcert -v -trustcacerts -alias cert12306 -file srca.cer \
-keystore cert12306.bks -storetype BKS \
-providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath ./bcprov-jdk15on-146.jar -storepass pw12306

运行后将显示证书内容并提示你是否确认,按Y回车确认即可。

其 中cert12306是个随便取的别名,供keytool管理时方便而已。srca.cer就是从12306网站下载的证书文件。 cert12306.bks是生成的keyStore文件,注意,这个文件必须以JAVA变量名的方式命名,比如不能直接叫12306.bks,否则在加 载资源时会因为名字不是合格的JAVA变量名而出错。 ./bcprov-jdk15on-146.jar 就是刚才下载的那个JCE Provider。最后pw12306是一个密码,用于确保KeyStore文件本身的安全。

使用自定义keyStore实现连接

以 下就是这个方案的实现,基本上和TrustAll差不多,也是需要一个自定义的SSLSocketFactory,不过因为还是需要验证证书的,所以就不 需要再定义TrustManager了,用系统内置的即可。不过为了读取KeyStore资源,需要增加一个Context参数。

另外,前面生成的那个cert12306.bks文件要放到 res/raw/ 目录下。

public class SSLCustomSocketFactory extends SSLSocketFactory {
    private static final String TAG = "SSLCustomSocketFactory";
private static final String KEY_PASS = "pw12306";

public SSLCustomSocketFactory(KeyStore trustStore) throws Throwable {
    super(trustStore);
}

public static SSLSocketFactory getSocketFactory(Context context) {
    try {
        InputStream ins = context.getResources().openRawResource(R.raw.cert12306);

        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        try {
            trustStore.load(ins, KEY_PASS.toCharArray());
        }
        finally {
            ins.close();
        }
        SSLSocketFactory factory = new SSLCustomSocketFactory(trustStore);
        return factory;
    } catch (Throwable e) {
        Log.d(TAG, e.getMessage());
        e.printStackTrace();
    }
    return null;
}

}

其中cert12306就是bks资源文件名,pw12306就是前面设置的密码。

同样的,使用这个Factory注册到scheme:

schReg.register(new Scheme("https", SSLCustomSocketFactory.getSocketFactory(context), 443));

现在,也可以成功地连接12306了,而且如此实现,基本上就能达到与用商业证书一样的安全性了。

不过因为现在只使用了12306的证书,没有使用系统证书,所以只能连接12306,连GOOGLE都连接不了了。但这个问题好解决,只需要在连接不同网站时使用不同的HttpClient即可。或者自己实现一个混合验证的SSLSocketFactory。

更加安全的双向认证的HTTPS连接方式

当然,还有一种更安全的HTTPS通讯方式叫做双向认证。相对的,上面所说的全都是指服务端的单向认证:即只有服务器配置了证书,客户端只是使用服务器证书的公钥。而双向认证则是客户端也有一个由服务器签发的证书,这样服务器可以确认连接过来的客户端的身份,网络银行就使用了这种方式。

双向认证的实现方式与本篇的实现方式差不多,只是在trustStore基础之上再增加一个keyStore,其内容为客户端证书,当然这个证书就是同时包含公钥和私钥的。因为搭建一个双向认证的服务器比较麻烦,一般应用也没必要,所以这里不再详细说明。

最后再次放出本文例子程序的完整代码(Android Studio 0.4.2项目):https://bitbucket.org/raptorz/democert

推送到[go4pro.org]