我只是技术搬运工,如果搬运有误,请不吝指出,谢谢
安全套接字层(SSL)-现在技术上被认识为传输层安全(TLS)-是为客户端和服务器之间进行加密通讯的公共构建块。一个应用有可能错误使用SSL,导致恶意的实体在互联网上截取应用之间的数据。为了帮助你确定这种情况不会放生在您的APP上,这篇文章着重讲解了一些在使用安全网络协议时常见的陷阱,并且指出了一些关于公钥基础设施(PKI)的观念。
概念
在典型的SSL使用场景中,服务器被配置为与包含公钥的证书以及一个匹配的私钥。作为SSL客户端和服务端之间握手的一部分,服务端证明它具有签署证书的私钥。
但是,任何人都可以生成自己的证书和私钥,所以一个简单的握手并不能证明关于服务器的任何东西,除了服务器知道和证书公钥匹配的私钥。解决这个问题的一种方法是在客户端有一组它信任的一个或多个证书。如果证书不在集合中,服务器是不被信任。
这种简单的方法有几个缺点。服务器随着时间应该更新证书中的公钥。不幸的是现在的客户端APP必须随着服务端配置的改变而进行更新。这是非常有问题的,如果服务器不是应用开发者的控制下,例如,如果它是一个第三方web服务。如果应用APP必须和任意的服务器如Web浏览器或电子邮件引用交互,这种方法也会有问题。
为了解决这些缺点,服务器通常配置来自知名发行商称为证书认证中心(CA) 。主机平台中一般含有它信任的知名的CA列表。像Android4.2(Jelly Bean),Anadroid系统当前维护着超过100个CA,在明个版本中都有更新。类似于服务器,CA 具有证书和私钥。当发出了一个服务器证书,CA签名服务器证书使用它的私钥。然后,客户端可以验证通过已知CA平台发放证书的服务器。
然而,虽然解决了一些问题,但使用CA引入了另一个问题。由于CA为多个服务器颁发证书,你仍然需要一些方法来确保你所交互的是你想要的服务器。为了解决这个问题,CA颁发的证书通过特定的名字如:gmail.com或者主机通配符如*.google.com来识别服务器。
下面的例子将让这些概念更具体。下面通过命令行的片段中,openssl 工具的s_client命令着眼与维基百科的服务器证书信息。她指定端口443,因为那是默认的HTTPS。这个命令将openssl s_client的信息输出到openssl x509的命令(按照X.509 的标准格式化证书信息)。具体的说,该命令请求包含服务器名字信息的主题和表示CA的颁发者。
$ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA |
你可以看到证书是用RapidSSL CA颁发给匹配*.wikipedia.org服务器的。
HTTPS示例
假设你有一个由知名CA颁发证书的Web服务器,你可以使用简单的代码安全的请求访问:
URL url =new URL("https://wikipedia.org");URLConnection urlConnection = url.openConnection();InputStreamin= urlConnection.getInputStream(); copyInputStreamToOutputStream(in,System.out); |
是的,它真的可以这么简单。如果你想定制的HTTP请求,可以强制转换为HttpURLConnection类。Android关于HttpURLConnection的文旦有进一步关于如何处理请求、响应头,请求内容,管理cookies,使用代理,缓存响应等等的例子。但是在验证证书和主机名的细节方法,Android框架通过这些API来照顾它。这就是你需要注意的。接下来是一些需要考虑的地方。
常见问题验证服务器证书
假设通过getInputStream()来接受内容,它抛出了一个异常:
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
|
发生这种情况的原因很多,其中包括:
1、为服务器颁发证书的CA的未知的。
2、服务器证书不是由CA签名的,而是自签名。
3、服务器配置缺少中间CA
以下章节讨论当保持和服务器的安全连接时,如何解决这些问题。
未知的证书颁发机构
在这种情况下,出现SSLHandshakeException,是应为你有一个未被系统信任的CA。那有可能是你的证书是来至一个新的未被Android信任的CA,或者你的app跑在一个没有CA旧版本的系统上。通常是因为一个未知的CA,因为它不是公开的CA,是被组织机构私有发布的,如政府,公司或教育机构自己使用的。
幸运的是,你可以让HttpsURLConnection去信任指定的CAs。这个过程有一点复杂,接下来是一个例子:从一个InputStream中得到的指定CA,使用它创建一个用来创建和初始化TruastManager的KeyStore。一个TrustManager是系统用来验证来至服务器的证书。
一个新的TrustManager,该示例初始化一个新的SSLContext它提供了一个SSLSokectFactory,你可以使用它来替换默认的来至HttpsURLConnection的SSLSocketFactory.通过这种办法,连接将使用你的CAs来证书验证。
下面是使用来自华盛顿大学的组织CA的完整的例子:
// Load CAs from an InputStream// (could be from a resource or ByteArrayInputStream or ...)CertificateFactory cf =CertificateFactory.getInstance("X.509");// From https://www.washington.edu/itconnect/security/ca/load-der.crtInputStream caInput =newBufferedInputStream(newFileInputStream("load-der.crt"));Certificate ca;try{ ca = cf.generateCertificate(caInput); System.out.println("ca="+((X509Certificate) ca).getSubjectDN());}finally{ caInput.close();}// Create a KeyStore containing our trusted CAsString keyStoreType =KeyStore.getDefaultType();KeyStore keyStore =KeyStore.getInstance(keyStoreType); keyStore.load(null,null); keyStore.setCertificateEntry("ca", ca);// Create a TrustManager that trusts the CAs in our KeyStoreString tmfAlgorithm =TrustManagerFactory.getDefaultAlgorithm();TrustManagerFactory tmf =TrustManagerFactory.getInstance(tmfAlgorithm); tmf.init(keyStore);// Create an SSLContext that uses our TrustManagerSSLContext context =SSLContext.getInstance("TLS"); context.init(null, tmf.getTrustManagers(),null);// Tell the URLConnection to use a SocketFactory from our SSLContext URL url =new URL("https://certs.cac.washington.edu/CAtest/");HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); urlConnection.setSSLSocketFactory(context.getSocketFactory());InputStreamin= urlConnection.getInputStream(); copyInputStreamToOutputStream(in,System.out);
|
通过自定义的TrustManager,它知道你的CAs,系统可以验证你的服务器证书,来至可信任的颁发者。
注意:许多网站描述一个贫穷的替代解决方案是安装 的TrustManager,什么也不做。如果你这样做你还不如不加密您的连接,因为任何人都可以通过在公共Wi-Fi热点攻击你,通过伪装成服务器。然后攻击者可以记录密码和个人数据。千万别这样做!!!
自签名服务证书
解决办法可以使用上一节相同的方法解决。
缺少中间证书授权机构
第三中情况出现SSLHandshkeException是由于缺少中间CA. 大多数公共CA不直接签署服务器证书。相反,他们使用他们的主要CA证书,被称为根CA,去签名中间CA.他们这样做,所以根CA可以脱机存储,以减少风险。但是想Android的这样的系统只信任根CA,这样会留下一个在服务器证书(被中间CA签名)和根CA直接的裂缝。为了解决这个问题,服务器不会单单发送它自己的证书在SSL握手中,而是一个证书链从服务CA到中间必要的路径到达根CA。
在这个例子中看看是怎么样的,这里是mail.google.com的证书链,通过openssl s_client command:查看:
$ openssl s_client -connect mail.google.com:443
---
Certificate chain
0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mail.google.com
i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
---
|
这里可以看出,服务器的证书是通过Thawte SGC CA颁发的,它是一个中间CA,另外一个证书 Thawte SGC CA的证书是由Verisign CA颁发的,它是初始证书,被Android信任。
然而,这种服务器不配置中间CA的配置是不少见的。例如,这是一个会引起Android浏览器和Android应用错误的服务器:
$ openssl s_client -connect egov.uscis.gov:443
---
Certificate chain
0 s:/C=US/ST=District Of Columbia/L=Washington/O=U.S. Department of Homeland Security/OU=United States Citizenship and Immigration Services/OU=Terms of use at www.verisign.com/rpa (c)05/CN=egov.uscis.gov
i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)10/CN=VeriSign Class 3 International Server CA - G3
---
|
有趣的是当在大多数桌面浏览器访问服务器的时候不会引起像未知CA和自签名证书的错误。这是因为桌面浏览器随着时间的推移缓存了可信的中间CA。一旦浏览器已经访问过一个中间CA,它在下次访问时就不需要中间CA。
有些网站故意这样做为了web服务器资源利用。例如,他们可能有一个主页HTML是通过证书链来提供服务的,但是其他资源如images,CSS或者JavaScript不在CA范围,可能是为了节省宽带。不幸的是有些时候这些服务有可能提供一个web服务给你的Android app,但是不起作用。
有两种方法去解决这个问题:
<!--[if !supportLists]-->l <!--[endif]-->配置服务包含中间CA在服务链中。大多数CAs提供如何在常规Web服务中配置的文档。这是唯一的方法,如果你需要站点可以工作在Android 4.2以后的Android浏览器中。
<!--[if !supportLists]-->l <!--[endif]-->或者,对待中间CA像对待其他未知的CA,创建TrustManager,从而达到直接信赖,就像前两节所说的。
验证主机名的常见问题
正如本文开头提到的,验证SSL连接有两个关键部分。第一个是验证证书来至受信任的源,这就是上一节的重点。这一节的重点的第二部分:确保你通信的服务器提供正确的证书。如果不是,你可以看到如下错误:
java.io.IOException: Hostname 'example.com' was not verified at libcore.net.http.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223) at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446) at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290) at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240) at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282) at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177) at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
|
发生这样的原因之一是因为服务配置错误。服务器配置了一个证书,它没有主题(subject),或者供选择的主题名字字段和你访问的服务不匹配。一个证书被多个不同的服务使用是有可能的。例如,使用openssl
s_client -connect google.com:443 | openssl x509 –text
命令
看google.com的证书,你可以看到一个主题支持*.googl.com,但是同样支持备选的*.youtube.com,*.android.com和其他的。这个错误只可能在你连接的服务器不在证书列表接受的范围之内是发生。
不幸的是这有可能另外一个原因导致:virtual hoting(虚拟主机)。当多个Http主机名分享一个服务器,web服务器可以根据客户端HTTP/1.1请求找到目标主机名。不幸的是对于HTTPS来说这是复杂的,因为服务器必须知道哪个证书是需要返回的在HTTP请求之前。为了解决这个问题,新版本的SSL,特别是TLSv1.0以及更高版本支持 SNI(Server Name Indication),它允许SSL客户端指定主机名,因此真确的证书会返回。
幸运的,HttpsURLConnection 支持SNI 自从Android2.3. 如果你需要支持Android2.2.(或者更老),需要建立一个虚拟主机在一个唯一的端口上,因此服务器证书真确的返回。
更激烈的另一种方法是更换HostnameVerifier,使用一个在你虚拟主机没有使用的的主机名,但是是默认返回的。
注意:替换HostnameVerifier可能是非常危险的,如果另外一个虚拟主机不是在你的控制之下,因为一个中间人攻击可能直接传输到另外的服务,在你不知道的情况下。
如果你还是确定你要覆盖主机名认证,这里是一个例子,它为简单的URLConnnection替换了验证:至少仍然验证app预期的主机名:
// Create an HostnameVerifier that hardwires the expected hostname.
|
但是需要记住,如果你是因为虚拟主机而替换了主机名验证,它仍然是非常危险的,如果虚拟主机不再你的控制之下,你应该找到一个可选的主机来避免这个问题。
关于直接使用SSLSocket的警告
到目前为止,这些例子主要集中在使用HTTPS HttpsURLConnection的。有时,应用需要使用SSL独立于HTTP。例如,电子邮件应用程序可以使用SMTP,POP3或IMAP的SSL变种。在这些情况下,应用程序将要直接使用SSLSocket,和HttpsURLConnnection内部使用的情况大致相同。
到目前为止描述的解决证书验证的技术童谣适用于SSLSocket.实际上,当使用客户化的TrustManager时,传递给ThttpsURLConnection的是一个SSLSocketFactory.因此如果你需要使用客户化的TrustManager和一个SSLSocket,遵循相同的步骤,使用SSLSocketFactory创建SSLSocket。
注意:SSLSocket不执行主机名验证。它是又你的app通过调用getDefaultHostnameVerifier()来验证预期的主机名。另外需要注意的是 HostnameVerifier.verify()捕获抛出一个异常,但是会返回一个布尔值,那是你不许检查的。
这是一个例子,展示了在这种情况下你可以如何做。它展示了当连接到gmail.com端口443没有用SNI支持的时候,你将受到mail.google.com的证书。在这种情况下这是预期到的,因此确保证书是mail.google.com:
// Open SSLSocket directly to gmail.comSocketFactory sf =SSLSocketFactory.getDefault();SSLSocket socket =(SSLSocket) sf.createSocket("gmail.com",443);HostnameVerifier hv =HttpsURLConnection.getDefaultHostnameVerifier();SSLSession s = socket.getSession();// Verify that the certicate hostname is for mail.google.com// This is due to lack of SNI support in the current SSLSocket.if(!hv.verify("mail.google.com", s)){ thrownewSSLHandshakeException("Expected mail.google.com, " "found "+ s.getPeerPrincipal());}// At this point SSLSocket performed certificate verificaiton and// we have performed hostname verification, so it is safe to proceed.// ... use socket ... socket.close();
|
黑名单
SSL很大程度上依赖于CAs颁发证书给只有被验证过的服务器和域。在极少情况下,CAs也有可能别欺骗,一个主机名的证书被发放给不是主机的拥有者。
为了降低这种风险,Android有这个能力给出指定证书的黑名单,或者是整个CA。这个黑名单列表是从Android4.2开始内置到操作系统的,并且可以运程更新。
钉(Pinning)
应用程序可以通过被称为钉的技术进一步保护自己免受欺诈颁发的证书。这基本上是利用以上的未知CA例子,让APP信任受限集合的CAs.这样可以避免因为系统提只供100个CA带来的安全通道的威胁。
客户端证书
本文侧重于SSL的用户与服务器的安全通信。SSL还支持客户端证书,允许该服务器来验证客户端的身份的概念。虽然超出了本文的范围,涉及的技术类似于指定自定义的TrustManager。请参阅有关创建自定义的的KeyManager说明文档中的 HttpsURLConnection的。
Nogotofail:网络流量安全测试工具
Nogotofail是一个让你可以简单的确保你的TLS/SSL是否有缺陷的工具。它是一个自动的,强大的和稳定的网络安全测试工具。
Nogotofail主要有3个用处:
l 查出Bug和漏洞
l 验证修复和回归查看
l了解应用和设备产生的流量
Nogotofail适用于Android,iOS,Linux和Windows,Chrome操作系统,OSX,其实你用它来连接到互联网的任何设备。它由一个易于配置设置的客户端,并且可以都到Android和Linux的通知,并且它的攻击引擎可以部署为路由器,VPN服务器或者代理。