irpas技术客

Android使用SSL自签名证书_Danny_姜

irpas 5093

一般情况下公司都是通过CA机构来购买SSL证书,但是这种证书费用普遍比较贵,所以在debug环境下可以考虑使用自签名证书。

这篇内容将介绍Android如何使用自签名证书,主要分为以下4个步骤:

创建服务端SSL自签名证书

下载并配置 Tomcat 服务器

Android端导入SSL证书

同时支持自签名证书和系统证书

1 创建服务端SSL自签名证书

通过工具Keytool,可以使用如下命令快速生成Java服务器能够识别的jks格式证书:

keytool?-genkey?-alias?my_server?-keyalg?RSA?-keystore?my_server.jks?-validity?3600?-storepass?123456

执行以上命令后,会弹出一些咨询信息,可以根据实际情况填写或者随意填写也OK。如下所示:

What?is?your?first?and?last?name? ??[Unknown]:??Danny What?is?the?name?of?your?organizational?unit? ??[Unknown]:??Null What?is?the?name?of?your?organization? ??[Unknown]:??Null What?is?the?name?of?your?City?or?Locality? ??[Unknown]:??SH What?is?the?name?of?your?State?or?Province? ??[Unknown]:??SH What?is?the?two-letter?country?code?for?this?unit? ??[Unknown]:??CN Is?CN=Danny,?OU=Null,?O=Null,?L=SH,?ST=SH,?C=CN?correct? ??[no]:??yes Enter?key?password?for?<my_server> ????(RETURN?if?same?as?keystore?password): Re-enter?new?password:

注意最后需要输入密码123456。执行成功之后,就可以在当前目录看到一个新生成的服务端SSL证书:my_server.jks?。

2 下载并配置 Tomcat 服务器

创建好服务端使用的SSL证书之后,接下来就需要将其配置到服务端的配置项里,这里我们使用Tomcat搭建本地服务用来演示。

2.1 下载 Tomcat (Mac电脑版)

浏览器中输入Tomcat官方下载链接:

https://tomcat.apache.org/download-90.cgi

选中 core zip 包进行下载,如下图:

解压下载之后的zip包,并重命名为Tomcat,然后在Terminal中将其移动/Library目录,接下来进入 /Library/Tomcat/bin 目录,使用如下命令启动Tomcat服务器:

sudo?sh?startup.sh

上述命令需求admin权限,执行成功之后,就可以验证Tomcat是否成功打开。在浏览器中输入 localhost:8080,如果出现如下截图内容,则说明Tomcat启动成功。

2.2 Tomcat配置SSL证书

下载好Tomcat并启动成功之后,接下来就需要配置SSL证书了。进入Tomcat/conf/目录,编辑?server.xml?配置文件,在 <Service>?标签中添加如下?<Connector> 标签:

<?xml?version="1.0"?encoding="UTF-8"?> <Server> ??<Service> ???... ????<Connector ????SSLEnabled="true" ????acceptCount="100" ????clientAuth="false" ????disableUploadTimeout="true" ????enableLookups="true" ????keystoreFile="/Users/Danny.Jiang/Desktop/certificate/android_cert/my_server.jks" ????keystorePass="123456" ????maxSpareThreads="75" ????maxThreads="200" ????minSpareThreads="5" ????port="8181" ????protocol="org.apache.coyote.http11.Http11NioProtocol" ????scheme="https" ????secure="true" ????sslProtocol="TLS"?/> ??</Service> </Server>

添加以上配置之后,重新在浏览器中输入?https://localhost:8181/ 就会看到如下warning信息:

看到上述warning信息,就说明服务端的SSL证书配置成功了。

3 Android端导入SSL证书

3.1 导出Android端SSL证书

使用如下命令,从上面创建的服务端证书server.jks中导出客户端证书:

keytool?-export?-alias?my_server?-file?my_client.cer?-keystore?my_server.jks?-storepass?123456

上述命令执行成功之后,将生成my_client.cer证书文件,这个就是Android端使用的自签名SSL证书。

3.2?将证书导入Android项目

创建Android项目SelfSignedCertificateDemo,如下

为了增加对比效果,我们创建2个Button控件,分别用来获取baidu和本地Tomcat服务器的数据,如下:

<?xml?version="1.0"?encoding="utf-8"?> <LinearLayout?xmlns:android="http://schemas.android.com/apk/res/android" ????android:layout_width="match_parent" ????android:layout_height="match_parent" ????android:orientation="vertical"> ????<Button ????????android:layout_width="match_parent" ????????android:layout_height="wrap_content" ????????android:onClick="getBaidu" ????????android:text="获取baidu首页数据"?/> ????<Button ????????android:layout_width="match_parent" ????????android:layout_height="wrap_content" ????????android:onClick="getTomcat" ????????android:text="获取Tomcat服务器数据"?/> </LinearLayout>

getBaidu和getTomcat这2个方法具体如下:

private?String?BAIDU_URL?=?"https://·/"; private?String?TOMCAT_URL?=?"https://192.168.1.105:8181/"; private?static?OkHttpClient?mOkHttpClient; public?void?getBaidu(View?view)?{ ????Request?request?=?new?Request.Builder() ????????????.url(BAIDU_URL) ????????????.build(); ????mOkHttpClient.newCall(request).enqueue(new?Callback()?{ ????????@Override ????????public?void?onFailure(Call?call,?IOException?e)?{ ????????????Log.i("TAG",?"getBaidu?onFailure:?"?+?e.getMessage()); ????????} ????????@Override ????????public?void?onResponse(Call?call,?Response?response)?throws?IOException?{ ????????????Log.i("TAG",?"getBaidu?response:?"?+?response.body().string()); ????????} ????}); } public?void?getTomcat(View?view)?{ ????Request?request?=?new?Request.Builder() ????????????.url(TOMCAT_URL) ????????????.build(); ????mOkHttpClient.newCall(request).enqueue(new?Callback()?{ ????????@Override ????????public?void?onFailure(Call?call,?IOException?e)?{ ????????????Log.i("TAG",?"getTomcat?onFailure:?"?+?e.getMessage()); ????????} ????????@Override ????????public?void?onResponse(Call?call,?Response?response)?throws?IOException?{ ????????????Log.i("TAG",?"getTomcat?response:?"?+?response.body().string()); ????????} ????}); }

上述代码中的TOMCAT_URL需要改为自己电脑的IP地址。默认情况下上述2个方法的执行结果如下:

getBaidu?response:?<!DOCTYPE?html> ????<!--STATUS?OK--><html>?<head><meta?http-equiv=content-type?content=text/html;charset=utf-8><meta?http-equiv=X-UA-Compatible?content=IE=Edge><meta?content=always?name=referrer><link?rel=stylesheet?type=text/css?href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head>?<body?link=#0000cc>?<div?id=wrapper>?<div?id=head>?<div?class=head_wrapper>?<div?class=s_form>?<div?class=s_form_wrapper>?<div?id=lg>?<img?hidefocus=true?src=//·/img/bd_logo1.png?width=270?height=129>?</div>?<form?id=form?name=f?action=//·/s?class=fm>?<input?type=hidden?name=bdorz_come?value=1>?<input?type=hidden?name=ie?value=utf-8>?<input?type=hidden?name=f?value=8>?<input?type=hidden?name=rsv_bp?value=1>?<input?type=hidden?name=rsv_idx?value=1>?<input?type=hidden?name=tn?value=baidu><span?class="bg?s_ipt_wr"><input?id=kw?name=wd?class=s_ipt?value?maxlength=255?autocomplete=off?autofocus=autofocus></span><span?class="bg?s_btn_wr"><input?type=submit?id=su?value=百度一下?class="bg?s_btn"?autofocus></span>?</form>?</div>?</div>?<div?id=u1>?<a?href=http://news.baidu.com?name=tj_trnews?class=mnav>新闻</a>?<a?href=https://·?name=tj_trhao123?class=mnav>hao123</a>?<a?href=http://map.baidu.com?name=tj_trmap?class=mnav>地图</a>?<a?href=http://v.baidu.com?name=tj_trvideo?class=mnav>视频</a>?<a?href=http://tieba.baidu.com?name=tj_trtieba?class=mnav>贴吧</a>?<noscript>?<a?href=http://·/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2F·%2f%3fbdorz_come%3d1?name=tj_login?class=lb>登录</a>?</noscript>?<script>document.write('<a?href="http://·/bdorz/login.gif?login&tpl=mn&u='+?encodeURIComponent(window.location.href+?(window.location.search?===?""???"?"?:?"&")+?"bdorz_come=1")+?'"?name="tj_login"?class="lb">登录</a>'); ????????????????????</script>?<a?href=//·/more/?name=tj_briicon?class=bri?style="display:?block;">更多产品</a>?</div>?</div>?</div>?<div?id=ftCon>?<div?id=ftConw>?<p?id=lh>?<a?href=http://home.baidu.com>关于百度</a>?<a?href=http://ir.baidu.com>About?Baidu</a>?</p>?<p?id=cp>&copy;2017&nbsp;Baidu&nbsp;<a?href=http://·/duty/>使用百度前必读</a>&nbsp;?<a?href=http://jianyi.baidu.com/?class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp;?<img?src=//·/img/gs.gif>?</p>?</div>?</div>?</div>?</body>?</html> getTomcat?onFailure:?java.security.cert.CertPathValidatorException:?Trust?anchor?for?certification?path?not?found.

可以看出getTomcat请求报错,原因是客户端验证服务端SSL证书失败。最简单的办法就是强制客户端让不检查所有的SSL证书,做法如下:

1 创建自定义X509TrustManager和HostnameVerifier

/** ?*?创建信任所有证书的TrustManager ?*?@return ?*/ private?static?X509TrustManager?createTrustAllTrustManager()?{ ????return?new?X509TrustManager()?{ ????????@Override ????????public?void?checkClientTrusted(X509Certificate[]?chain,?String?authType)?throws?CertificateException?{ ????????} ????????@Override ????????public?void?checkServerTrusted(X509Certificate[]?chain,?String?authType)?throws?CertificateException?{ ????????} ????????@Override ????????public?X509Certificate[]?getAcceptedIssuers()?{ ????????????return?new?X509Certificate[0]; ????????} ????}; } //实现信任所有域名的HostnameVerifier接口 private?static?class?TrustAllHostnameVerifier?implements?HostnameVerifier?{ ????@Override ????public?boolean?verify(String?hostname,?SSLSession?session)?{ ????????//域名校验,默认都通过 ????????return?true; ????} }

上面自定义X509TrustManager中的checkClientTreusted和checkServerTrusted都是空实现,也就是不检查客户端和服务端的SSL证书信息。另外在自定义HostnameVerifier中的verify方法返回true,默认信任所有域名,否则在请求时会报如下错误:

Hostname?XXX?not?verified:

2 根据自定义X509TrustManager创建OkHttpClient

将之前创建的空实现X509TrustManager传入createSSLClient方法,如下

private?static?OkHttpClient?createSSLClient(X509TrustManager?x509TrustManager){ ????OkHttpClient.Builder?builder?=?new?OkHttpClient.Builder() ????????????.connectTimeout(60,?TimeUnit.SECONDS) ????????????.sslSocketFactory(createSSLSocketFactory(x509TrustManager),x509TrustManager) ????????????.hostnameVerifier(new?TrustAllHostnameVerifier()); ????return?builder.build(); } private?static?SSLSocketFactory?createSSLSocketFactory(TrustManager?trustManager)?{ ????SSLSocketFactory?ssfFactory?=?null; ????try?{ ????????SSLContext?sc?=?SSLContext.getInstance("TLS"); ????????sc.init(null,?new?TrustManager[]{trustManager},?new?SecureRandom()); ????????ssfFactory?=?sc.getSocketFactory(); ????}?catch?(Exception?e)?{ ????????e.printStackTrace(); ????} ????return?ssfFactory; }

通过上述方法,即可创建出不检查所有SSL证书的OkHttpClient对象。再次执行getBaidu和getTomcat方法,执行结果如下:

getBaidu?response:? ????<!DOCTYPE?html> ????<!--STATUS?OK--><html>?<head><meta?http-equiv=content-type?content=text/html;charset=utf-8><meta?http-equiv=X-UA-Compatible?content=IE=Edge><meta?content=always?name=referrer><link?rel=stylesheet?type=text/css?href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head>?<body?link=#0000cc>?<div?id=wrapper>?<div?id=head>?<div?class=head_wrapper>?<div?class=s_form>?<div?class=s_form_wrapper>?<div?id=lg>?<img?hidefocus=true?src=//·/img/bd_logo1.png?width=270?height=129>?</div>?<form?id=form?name=f?action=//·/s?class=fm>?<input?type=hidden?name=bdorz_come?value=1>?<input?type=hidden?name=ie?value=utf-8>?<input?type=hidden?name=f?value=8>?<input?type=hidden?name=rsv_bp?value=1>?<input?type=hidden?name=rsv_idx?value=1>?<input?type=hidden?name=tn?value=baidu><span?class="bg?s_ipt_wr"><input?id=kw?name=wd?class=s_ipt?value?maxlength=255?autocomplete=off?autofocus=autofocus></span><span?class="bg?s_btn_wr"><input?type=submit?id=su?value=百度一下?class="bg?s_btn"?autofocus></span>?</form>?</div>?</div>?<div?id=u1>?<a?href=http://news.baidu.com?name=tj_trnews?class=mnav>新闻</a>?<a?href=https://·?name=tj_trhao123?class=mnav>hao123</a>?<a?href=http://map.baidu.com?name=tj_trmap?class=mnav>地图</a>?<a?href=http://v.baidu.com?name=tj_trvideo?class=mnav>视频</a>?<a?href=http://tieba.baidu.com?name=tj_trtieba?class=mnav>贴吧</a>?<noscript>?<a?href=http://·/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2F·%2f%3fbdorz_come%3d1?name=tj_login?class=lb>登录</a>?</noscript>?<script>document.write('<a?href="http://·/bdorz/login.gif?login&tpl=mn&u='+?encodeURIComponent(window.location.href+?(window.location.search?===?""???"?"?:?"&")+?"bdorz_come=1")+?'"?name="tj_login"?class="lb">登录</a>'); ????????????????????</script>?<a?href=//·/more/?name=tj_briicon?class=bri?style="display:?block;">更多产品</a>?</div>?</div>?</div>?<div?id=ftCon>?<div?id=ftConw>?<p?id=lh>?<a?href=http://home.baidu.com>关于百度</a>?<a?href=http://ir.baidu.com>About?Baidu</a>?</p>?<p?id=cp>&copy;2017&nbsp;Baidu&nbsp;<a?href=http://·/duty/>使用百度前必读</a>&nbsp;?<a?href=http://jianyi.baidu.com/?class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp;?<img?src=//·/img/gs.gif>?</p>?</div>?</div>?</div>?</body>?</html> getTomcat?response:? ????<!DOCTYPE?html> ????<html?lang="en"> ????????<head> ????????????<meta?charset="UTF-8"?/> ????????????<title>Apache?Tomcat/8.5.72</title> ????????????<link?href="favicon.ico"?rel="icon"?type="image/x-icon"?/> ????????????<link?href="tomcat.css"?rel="stylesheet"?type="text/css"?/> ????????</head> ????????<body> ????????????<div?id="wrapper"> ????????????????<div?id="navigation"?class="curved?container"> ????????????????????<span?id="nav-home"><a?href="https://tomcat.apache.org/">Home</a></span> ????????????????????<span?id="nav-hosts"><a?href="/docs/">Documentation</a></span> ????????????????????<span?id="nav-config"><a?href="/docs/config/">Configuration</a></span> ????????????????????<span?id="nav-examples"><a?href="/examples/">Examples</a></span> ????????????????????<span?id="nav-wiki"><a?href="https://wiki.apache.org/tomcat/FrontPage">Wiki</a></span> ????????????????????<span?id="nav-lists"><a?href="https://tomcat.apache.org/lists.html">Mailing?Lists</a></span> ????????????????????<span?id="nav-help"><a?href="https://tomcat.apache.org/findhelp.html">Find?Help</a></span> ????????????????????<br?class="separator"?/> ????????????????</div> ????。。。 ????</html>

可以看出,百度和Tomcat服务器的数据都能成功获取。但是这种方式存在极大的安全漏洞。因为并没有做任何SSL证书的校验,很容易被MITM(Man In The Middle)攻击。

比较好的优化方式当然是在客户端使用自签名SSL证书,验证服务器的身份合法之后,再进行后续的数据传输操作。通过以下步骤将客户端证书my_client.cer导入到项目中:

1 将my_client.cer保存在assets文件夹中

创建assets目录,并将my_client.cer保存到此目录下,如下:

保存好后,通过如下方式将证书转换为InputStream格式:

private?InputStream?getInputStreamFromAsset(){ ????InputStream?inputStream?=?null; ????try?{ ????????inputStream?=?getAssets().open("my_clent.cer"); ????}?catch?(IOException?e)?{ ????????e.printStackTrace(); ????} ????return?inputStream; }

2 创建只信任自签名证书的X509TrustManager

将转换后的InputStream传入以下方法,创建自定义X509TrustManager

/** ?????*?创建只信任指定证书的TrustManager ?????*?@param?inputStream:证书输入流 ?????*?@return ?????*/ ????@Nullable ????private?static?X509TrustManager?createTrustCustomTrustManager(InputStream?inputStream)?{ ????????try?{ ????????????CertificateFactory?certificateFactory?=?CertificateFactory.getInstance("X.509"); ????????????KeyStore?keyStore?=?KeyStore.getInstance(KeyStore.getDefaultType()); ????????????keyStore.load(null); ????????????Certificate?certificate?=?certificateFactory.generateCertificate(inputStream); ????????????//将证书放入keystore中 ????????????String?certificateAlias?=?"ca"; ????????????keyStore.setCertificateEntry(certificateAlias,?certificate); ????????????if?(inputStream?!=?null)?{ ????????????????inputStream.close(); ????????????} ????????????TrustManagerFactory?trustManagerFactory?=?TrustManagerFactory. ????????????????????getInstance(TrustManagerFactory.getDefaultAlgorithm()); ????????????trustManagerFactory.init(keyStore); ????????????TrustManager[]?trustManagers?=?trustManagerFactory.getTrustManagers(); ????????????if?(trustManagers.length?!=?1?||?!(trustManagers[0]?instanceof?X509TrustManager))?{ ????????????????throw?new?IllegalStateException("Unexpected?default?trust?managers:" ????????????????????????+?Arrays.toString(trustManagers)); ????????????} ????????????return?(X509TrustManager)?trustManagers[0]; ????????}?catch?(Exception?e)?{ ????????????e.printStackTrace(); ????????} ????????return?null; ????}

以上方法将自签名证书保存到Java对象KeyStore中,并最终创建只信任自签名证书的X509TrustManager对象。重新将此对象传给上文中的createSSLClient方法后,就是一个加载自签名SSL证书的OkHttpClient对象了。

再次执行getBaidu和getTomcat方法,执行结果如下:

getBaidu?onFailure:?java.security.cert.CertPathValidatorException:?Trust?anchor?for?certification?path?not?found. getTomcat?response:? ????<!DOCTYPE?html> ????<html?lang="en"> ????????<head> ????????????<meta?charset="UTF-8"?/> ????????????<title>Apache?Tomcat/8.5.72</title> ????????????<link?href="favicon.ico"?rel="icon"?type="image/x-icon"?/> ????????????<link?href="tomcat.css"?rel="stylesheet"?type="text/css"?/> ????????</head> ????????<body> ????????... ????????</body> ????</html>

以上结果显示获取baidu数据失败,而获取Tomcat数据成功。

log 显示结果正好跟最初的默认结果相反,这是因为当前所有的https请求都使用自签名证书去校验服务器身份。因为Tomcat配置了本地证书所以能够成功验明正身;但是baidu并没有配置我们的自签名证书,也就无法正确验明身份了。

支持自签名证书和系统证书

为了能让getBaidu正常获取数据,并且getTomcat也在安全的环境下获取数据,我们需要在这个X509TrustManager中再添加对系统自带SSL证书的信任。具体如下:

/** ?*?创建既信任自签名证书又信任系统自带证书的TrustManager ?*/ private?static?X509TrustManager?createTrustCustomAndDefaultTrustManager(InputStream?inputStream)?{ ????try?{ ????????//?获取信任系统自带证书的TrustManager ????????final?X509TrustManager?systemTrustManager?=?getSystemTrustManager(); ????????//?获取信任自签名证书的TrustManager ????????final?X509TrustManager?selfTrustManager?=?createTrustCustomTrustManager(inputStream); ????????return?new?X509TrustManager()?{ ????????????@Override ????????????public?void?checkClientTrusted(X509Certificate[]?chain,?String?authType)?throws?CertificateException?{ ????????????????systemTrustManager.checkClientTrusted(chain,?authType); ????????????} ????????????@Override ????????????public?void?checkServerTrusted(X509Certificate[]?chain,?String?authType)?throws?CertificateException?{ ????????????????try?{ ????????????????????//?默认使用信任自签名证书的TrustManager验证服务端身份 ????????????????????selfTrustManager.checkServerTrusted(chain,?authType); ????????????????}?catch?(CertificateException?e)?{ ????????????????????//?此处使用系统自带SSL证书验证服务端身份 ????????????????????systemTrustManager.checkServerTrusted(chain,?authType); ????????????????} ????????????} ????????????@Override ????????????public?X509Certificate[]?getAcceptedIssuers()?{ ????????????????return?systemTrustManager.getAcceptedIssuers(); ????????????} ????????}; ????}?catch?(Exception?e)?{ ????????e.printStackTrace(); ????} ????return?null; } /** ?*?创建信任系统自带证书的TrustManager ?*/ private?static?X509TrustManager?getSystemTrustManager()?throws?NoSuchAlgorithmException,?KeyStoreException?{ ????TrustManagerFactory?tmf?=?TrustManagerFactory ????????????????.getInstance(TrustManagerFactory.getDefaultAlgorithm()); ????tmf.init((KeyStore)?null); ????for?(TrustManager?tm?:?tmf.getTrustManagers())?{ ????????if?(tm?instanceof?X509TrustManager)?{ ????????????return?(X509TrustManager)?tm; ????????} ????} ????return?null; }

可以看出在自定义X509TrustManager的checkServerTrusted方法中,先使用信任自签名证书的TrustManager验证服务端,如果没有验证成功,则继续使用系统默认TrustManager来继续验证。

通过以上设置之后,getBaidu?和?getTomcat这2个方法都能正确获取数据了。对源码有需求,或者想一起探讨共同进步的,欢迎关注公众号发私信或者加微信。

如果你喜欢本文

长按二维码关注


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #Android #自签名证书