首页域名资讯 正文

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

2025-01-28 2 0条评论

前言

由于移动设备使用的网络环境各种各样,而且常常接入不安全的公共WIFI——如果你对公共WIFI环境的安全性没有警惕性的话,就难怪你开发出不安全的程序,把你的用户置于危险境地——这话一点都不夸张。

而要想在不安全的网络环境下安全地使用网络,最好的办法就是通过VPN连接到安全网络环境中去。但这并不总是能够保证的。所以需要应用开发者在开发的时候尽量减少用户的安全风险。

通过HTTPS连接网络是一种常用的方法。但是在实际使用中存在几个困难:

* 使用商业证书的成本
* 使用自定义证书不被系统承认
* 忽略证书验证则可能被“中间人攻击”

本文将针对这些问题讨论技术解决方案。

因为最近又开始试用Android Studio,所以这里的Demo是用Android Studio 0.4.2写的。

基本的HTTP连接方式

首先来看基本的HTTP连接方式实现,程序的项目框架是直接用向导生成后略作修改。主要就是增加一个异步网络调用的任务,任务内容大致为:

[java]

  1. HttpUriRequest request = newHttpGet(url);
  2. HttpClient client = DemoHttp.getClient();
  3. try{
  4. HttpResponse httpResponse = client.execute(request);
  5. int responseCode = httpResponse.getStatusLine().getStatusCode();
  6. String message = httpResponse.getStatusLine().getReasonPhrase();
  7. HttpEntity entity = httpResponse.getEntity();
  8. if (responseCode == 200 && entity != null)
  9. {
  10. return parseString(entity);
  11. }
  12. }
  13. finally{
  14. getConnectionManager().shutdown();
  15. }
  16. return“”;

上面这个函数功能就是创建一个HttpClient去GET url的内容,如果HTTP返回值为200(即无错误),则返回响应内容。

重点就在DemoHttp.getClient()里,对于基本的HTTP连接,以下是实现部分代码

[java]

  1. publicstatic HttpClient getClient() {
  2. BasicHttpParams params = new BasicHttpParams();
  3. setVersion(params, HttpVersion.HTTP_1_1);
  4. setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
  5. setUseExpectContinue(params, true);
  6. SchemeRegistry schReg = new SchemeRegistry();
  7. register(newScheme(“http”, PlainSocketFactory.getSocketFactory(), 80));
  8. ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
  9. return new DefaultHttpClient(connMgr, params);
  10. }

实际的实现代码当然比上面这两段多得多了,Java就是这么麻烦,一点小事都要写一大堆代码,为节约篇幅就不全部列出了,参见bitbucket上的完整代码吧。

顺便说一句,写网络通讯应用别忘记在Manifest.xml里加上相应的权限,否则会出一些很奇怪的错误。

HTTP连接的主要问题在于在传输过程中的内容都是明文,只要在同一网段内使用嗅探程序即可获得网内其它设备与服务器之间的通讯内容,完全没有安全性。

使用系统承认的商业证书的HTTPS连接方式

在上面的例子中,如果尝试用https连接的话,会报错称不支持https: Scheme ‘https’ not registered。最简单的解决办法就是参照HTTP的方式,加入对HTTPS的支持:

[java]

  1. register(newScheme(“https”, SSLSocketFactory.getSocketFactory(), 443));

关键代码就这么一句。

现在就可以像打开HTTP链接一样打开有效的HTTPS连接了,比如: https://www.trustauth.cn 。但可耻的 12306 的HTTPS却不行,因为它使用了不被系统承认的自定义证书:No peer certificate 。

这个方案使用了HTTPS连接,传输内容经过加密,嗅探程序已经无法获得通讯内容。而服务器的证书经过合法签名,被系统所承认,正常通讯也没有问题。

但是需要花钱买证书。

使用自定义证书并忽略验证的HTTPS连接方式

如果不想花钱,那么就只能用OPENSSL自己做一个证书,但问题在于,这个证书得不到系统的承认,后果同 12306 。为了解决这个问题,一个办法是跳过系统校验。

要跳过系统校验,就不能再使用系统标准的SSLSocketFactory了,需要自定义一个。然后为了在这个自定义SSLSocketFactory里跳过校验,还需要自定义一个TrustManager,在其中忽略所有校验,即TrustAll。

以下就是SSLTrustAllSocketFactory和SSLTrustAllManager的实现:

[java]

  1. publicclass SSLTrustAllSocketFactory extends SSLSocketFactory {
  2. private static final String TAG = “SSLTrustAllSocketFactory”;
  3. private SSLContext mCtx;
  4. public class SSLTrustAllManager implements X509TrustManager {
  5. @Override
  6. public void checkClientTrusted(X509Certificate[] arg0, String arg1)
  7. throws CertificateException {
  8. }
  9. @Override
  10. public void checkServerTrusted(X509Certificate[] arg0, String arg1)
  11. throws CertificateException {
  12. }
  13. @Override
  14. public X509Certificate[] getAcceptedIssuers() {
  15. return null;
  16. }
  17. }
  18. public SSLTrustAllSocketFactory(KeyStore truststore)
  19. throws Throwable {
  20. super(truststore);
  21. try {
  22. mCtx = SSLContext.getInstance(“TLS”);
  23. init(nullnewTrustManager[] { new SSLTrustAllManager() },
  24. null);
  25. setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
  26. catch (Exception ex) {
  27. }
  28. }
  29. @Override
  30. public Socket createSocket(Socket socket, String host,
  31. int port, boolean autoClose)
  32. throws IOException, UnknownHostException {
  33. returngetSocketFactory().createSocket(socket, host, port, autoClose);
  34. }
  35. @Override
  36. public Socket createSocket() throws IOException {
  37. returngetSocketFactory().createSocket();
  38. }
  39. public static SSLSocketFactory getSocketFactory() {
  40. try {
  41. KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
  42. load(nullnull);
  43. SSLSocketFactory factory = new SSLTrustAllSocketFactory(trustStore);
  44. return factory;
  45. catch (Throwable e) {
  46. d(TAG, e.getMessage());
  47. printStackTrace();
  48. }
  49. return null;
  50. }
  51. }

最后在注册scheme时使用这个Factory:

[java]

  1. register(newScheme(“https”, SSLTrustAllSocketFactory.getSocketFactory(), 443));

这样就可以成功打开 12306 的内容了。

不过这个方案虽然用了HTTPS,通讯的内容也经过了加密,嗅探程序也无法知道内容。但是通过更麻烦一些的“中间人攻击”,还是可以窃取通讯内容的:

在网内配置一个DNS,把目标服务器域名解析到本地的一个地址,然后在这个地址上用一个中间服务器作代理,它使用一个假的证书与客户端通讯,然后再由这个代理作为客户端连到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容就会经过这个代理。而因为客户端不校验证书,所以它用来加密的证书实际上是代理提供的假证书,那么代理就可以完全知道通讯内容。这个代理就是所谓的“中间人”。

但是不幸的是,网上搜到的大部分关于自定义证书的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帖里给的。注意,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文件,注意,这个文件必须以变量名的方式命名,比如不能直接叫12306.bks,否则在加载资源时会因为名字不是合格的JAVA变量名而出错。 ./bcprov-jdk15on-146.jar 就是刚才下载的那个JCE Provider。最后pw12306是一个密码,用于确保KeyStore文件本身的安全。

使用自定义keyStore实现连接

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

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

[java]

  1. publicclass SSLCustomSocketFactory extends SSLSocketFactory {
  2. private static final String TAG = “SSLCustomSocketFactory”;
  3. private static final String KEY_PASS = “pw12306”;
  4. public SSLCustomSocketFactory(KeyStore trustStore) throws Throwable {
  5. super(trustStore);
  6. }
  7. public static SSLSocketFactory getSocketFactory(Context context) {
  8. try {
  9. InputStream ins = context.getResources().openRawResource(R.raw.cert12306);
  10. KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
  11. try {
  12. load(ins, KEY_PASS.toCharArray());
  13. }
  14. finally {
  15. close();
  16. }
  17. SSLSocketFactory factory = new SSLCustomSocketFactory(trustStore);
  18. return factory;
  19. catch (Throwable e) {
  20. d(TAG, e.getMessage());
  21. printStackTrace();
  22. }
  23. return null;
  24. }
  25. }

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

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

[java]

  1. register(newScheme(“https”, SSLCustomSocketFactory.getSocketFactory(context), 443));

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

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

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

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

双向认证的实现方式与本篇的实现方式差不多,只是在trustStore基础之上再增加一个keyStore,其内容为客户端证书,当然这个证书就是同时包含公钥和私钥的。

文章来源于互联网

文章版权及转载声明

本文作者:亿网 网址:https://edns.com/ask/post/150569.html 发布于 2025-01-28
文章转载或复制请以超链接形式并注明出处。