在Android应用中使用自定义证书的HTTPS连接(下)
因为这部分才是本文的重点,要说得详细一点,所以单独做成一篇来说。
安全地使用自定义证书的HTTPS连接方式
终极解决方案是:把证书编译到应用中去,由应用自己来验证证书。
生成KeyStore
要验证自定义证书,首先要把证书编译到应用中去,这需要JSSE提供的keytool工具来生成KeyStore文件。参考《Java 安全套接字编程以及 keytool 使用最佳实践》,我试过了用JKS格式,但是结果连接失败,报错:Wrong version of key store。后来看了SO的这个帖才知道必须使用BKS的1.46版。更详细的内容参考这篇《Using a Custom Certificate Trust Store on Android》。
这里所谓的证书,实际上就是公钥,你可以从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]