做者:曹丰斌html
本文首先分析HTTP协议在安全性上的不足,进而阐述HTTPS实现安全通讯的关键技术点和原理。而后经过抓包分析HTTPS协议的握手以及通讯过程。最后总结一下本身在开发过程当中遇到的HTTPS相关的问题,并给出当前项目中对HTTPS问题的系统解决方案,以供总结和分享。若有不当之处,欢迎批评和指正。java
HTTP1.x在传输数据时,全部传输的内容都是明文,客户端和服务器端都没法验证对方的身份,存在的问题以下:android
- 通讯使用明文(不加密),内容可能会被窃听;
- 不验证通讯方的身份,有可能遭遇假装;
- 没法证实报文的完整性,因此有可能已遭篡改;
其实这些问题不只在HTTP上出现,其余未加密的协议中也会存在这类问题。git
按TCP/IP协议族的工做机制,互联网上的任何角落都存在通讯内容被窃听的风险。而HTTP协议自己不具有加密的功能,所传输的都是明文。即便已经通过过加密处理的通讯,也会被窥视到通讯内容,这点和未加密的通讯是相同的。只是说若是通讯通过加密,就有可能让人没法破解报文信息的含义,但加密处理后的报文信息自己仍是会被看到的。github
在HTTP协议通讯时,因为不存在确认通讯方的处理步骤,所以任何人均可以发起请求。另外,服务器只要接收到请求,无论对方是谁都会返回一个响应。所以不确认通讯方,存在如下隐患:算法
- 没法肯定请求发送至目标的Web服务器是不是按真实意图返回响应的那台服务器。有多是已假装的 Web 服务器;
- 没法肯定响应返回到的客户端是不是按真实意图接收响应的那个客户端。有多是已假装的客户端;
- 没法肯定正在通讯的对方是否具有访问权限。由于某些Web服务器上保存着重要的信息,只想发给特定用户通讯的权限;
- 没法断定请求是来自何方、出自谁手;
- 即便是无心义的请求也会照单全收,没法阻止海量请求下的DoS攻击;
所谓完整性是指信息的准确度。若没法证实其完整性,一般也就意味着没法判断信息是否准确。HTTP协议没法证实通讯的报文完整性,在请求或响应送出以后直到对方接收以前的这段时间内,即便请求或响应的内容遭到篡改,也没有办法获悉。浏览器
好比,从某个Web网站下载内容,是没法肯定客户端下载的文件和服务器上存放的文件是否先后一致的。文件内容在传输途中可能已经被篡改成其余的内容。即便内容真的已改变,做为接收方的客户端也是觉察不到的。像这样,请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击称为中间人攻击(Man-in-the-Middle attack,MITM)。安全
因为上述的几个问题,须要一种可以提供以下功能的HTTP安全技术:服务器
- (1) 服务器认证(客户端知道它们是在与真正的而不是伪造的服务器通话);
- (2) 客户端认证(服务器知道它们是在与真正的而不是伪造的客户端通话);
- (3) 完整性(客户端和服务器的数据不会被修改);
- (4) 加密(客户端和服务器的对话是私密的,无需担忧被窃听);
- (5) 效率(一个运行的足够快的算法,以便低端的客户端和服务器使用);
- (6) 普适性(基本上全部的客户端和服务器都支持这些协议);
在这样的需求背景下,HTTPS技术诞生了。HTTPS协议的主要功能基本都依赖于TLS/SSL协议,提供了身份验证、信息加密和完整性校验的功能,能够解决HTTP存在的安全问题。本节就重点探讨一下HTTPS协议的几个关键技术点。微信
加密算法通常分为两种:
- 对称加密:加密与解密的密钥相同。以DES算法为表明;
- 非对称加密:加密与解密的密钥不相同。以RSA算法为表明;
对称加密强度很是高,通常破解不了,但存在一个很大的问题就是没法安全地生成和保管密钥,假如客户端和服务器之间每次会话都使用固定的、相同的密钥加密和解密,确定存在很大的安全隐患。
在非对称密钥交换算法出现之前,对称加密一个很大的问题就是不知道如何安全生成和保管密钥。非对称密钥交换过程主要就是为了解决这个问题,使密钥的生成和使用更加安全。但同时也是HTTPS性能和速度严重下降的“罪魁祸首”。
HTTPS采用对称加密和非对称加密二者并用的混合加密机制,在交换密钥环节使用非对称加密方式,以后的创建通讯交换报文阶段则使用对称加密方式。
非对称加密最大的一个问题,就是没法证实公钥自己就是货真价实的公钥。好比,正准备和某台服务器创建公开密钥加密方式下的通讯时,如何证实收到的公开密钥就是本来预想的那台服务器发行的公开密钥。或许在公开密钥传输途中,真正的公开密钥已经被攻击者替换掉了。
若是不验证公钥的可靠性,至少会存在以下的两个问题:中间人攻击和信息抵赖。
为了解决上述问题,可使用由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书。
CA使用具体的流程以下:
- (1) 服务器的运营人员向数字证书认证机构(CA)提出公开密钥的申请;
- (2) CA经过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的全部权等;
- (3) 若是信息审核经过,CA会对已申请的公开密钥作数字签名,而后分配这个已签名的公开密钥,并将该公开密钥放入公钥证书后绑定在一块儿。
证书包含如下信息:申请者公钥、申请者的组织信息和我的信息、签发机构CA的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;
签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,而后,采用CA的私钥对信息摘要进行加密,密文即签名;- (4) 客户端在HTTPS握手阶段向服务器发出请求,要求服务器返回证书文件;
- (5) 客户端读取证书中的相关的明文信息,采用相同的散列函数计算获得信息摘要,而后,利用对应CA的公钥解密签名数据,对比证书的信息摘要,若是一致,则能够确认证书的合法性,即公钥合法;
- (6) 客户端而后验证证书相关的域名信息、有效时间等信息;
- (7) 客户端会内置信任CA的证书信息(包含公钥),若是CA不被信任,则找不到对应CA的证书,证书也会被断定非法。
在这个过程注意几点:
- (1) 申请证书不须要提供私钥,确保私钥永远只能被服务器掌握;
- (2) 证书的合法性仍然依赖于非对称加密算法,证书主要是增长了服务器信息以及签名;
- (3) 内置CA对应的证书称为根证书;颁发者和使用者相同,本身为本身签名,叫自签名证书;
- (4) 证书=公钥+申请者与颁发者信息+签名;
HTTPS协议历史简介:
- (1) SSL协议的第一个版本由Netscape公司开发,但这个版本从未发布过;
- (2) SSL协议第二版于1994年11月发布。第一次部署是在Netscape Navigator1.1浏览器上,发行于1995年3月;
- (3) SSL 3于1995年年末发布,虽然名称与早先的协议版本相同,但SSL3是彻底从新设计的协议,该设计一直沿用到今天。
- (4) TLS 1.0于1999年1月问世,与SSL 3相比,版本修改并不大;
- (5) 2006年4月,下一个版本TLS 1.1才问世,仅仅修复了一些关键的安全问题;
- (6) 2008年8月,TLS1.2发布。该版本添加了对已验证加密的支持,而且基本上删除了协议说明中全部硬编码的安全基元,使协议彻底弹性化;
宏观上,TLS以记录协议(record protocol)实现。记录协议负责在传输链接上交换全部的底层消息,并能够配置加密。每一条TLS记录以一个短标头起始。标头包含记录内容的类型(或子协议)、协议版本和长度。消息数据紧跟在标头以后,以下图所示:
TLS的主规格说明书定义了四个核心子协议:
- 握手协议(handshake protocol);
- 密钥规格变动协议(change cipher spec protocol);
- 应用数据协议(application data protocol);
- 警报协议(alert protocol);
握手是TLS协议中最精密复杂的部分。在这个过程当中,通讯双方协商链接参数,而且完成身份验证。根据使用的功能的不一样,整个过程一般须要交换6~10条消息。根据配置和支持的协议扩展的不一样,交换过程可能有许多变种。在使用中常常能够观察到如下三种流程:
- (1) 完整的握手,对服务器进行身份验证(单向验证,最多见);
- (2) 对客户端和服务器都进行身份验证的握手(双向验证);
- (3) 恢复以前的会话采用的简短握手;
本节以QQ邮箱的登陆过程为例,经过抓包来对单向验证的握手流程进行分析。单向验证的一次完整的握手流程以下所示:
主要分为四个步骤:
- (1) 交换各自支持的功能,对须要的链接参数达成一致;
- (2) 验证出示的证书,或使用其余方式进行身份验证;
- (3) 对将用于保护会话的共享主密钥达成一致;
- (4) 验证握手消息是否被第三方团体修改;
下面对这一过程进行详细的分析。
在握手流程中,ClientHello是第一条消息。这条消息将客户端的功能和首选项传送给服务器。包含客户端支持的SSL的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
ServerHello消息将服务器选择的链接参数传送回客户端。这个消息的结构与ClientHello相似,只是每一个字段只包含一个选项。服务器的加密组件内容以及压缩方法等都是从接收到的客户端加密组件内筛选出来的。
以后服务器发送Certificate报文,报文中包含公开密钥证书,服务器必须保证它发送的证书与选择的算法套件一致。不过Certificate消息是可选的,由于并不是全部套件都使用身份验证,也并不是全部身份验证方法都须要证书。
ServerKeyExchange消息的目的是携带密钥交换的额外数据。消息内容对于不一样的协商算法套件都会存在差别。在某些场景中,服务器不须要发送任何内容,在这些场景中就不须要发送ServerKeyExchange消息。
ServerHelloDone消息代表服务器已经将全部预计的握手消息发送完毕。在此以后,服务器会等待客户端发送消息。
ClientKeyExchange消息携带客户端为密钥交换提供的全部信息。这个消息受协商的密码套件的影响,内容随着不一样的协商密码套件而不一样。
ChangeCipherSpec消息代表发送端已取得用以生成链接参数的足够信息,已生成加密密钥,而且将切换到加密模式。客户端和服务器在条件成熟时都会发送这个消息。注意:ChangeCipherSpec不属于握手消息,它是另外一种协议,只有一条消息,做为它的子协议进行实现。
Finished消息意味着握手已经完成。消息内容将加密,以便双方能够安全地交换验证整个握手完整性所需的数据。客户端和服务器在条件成熟时都会发送这个消息。
在一些对安全性要求更高的场景下,可能会出现双向验证的需求。完整的双向验证流程以下:
能够看到,同单向验证流程相比,双向验证多了以下两条消息:CertificateRequest与CertificateVerify,其他流程大体相同。
Certificate Request是TLS规定的一个可选功能,用于服务器认证客户端的身份。经过服务器要求客户端发送一个证书实现,服务器应该在ServerKeyExchange以后当即发送CertificateRequest消息。
消息结构以下:
enum { rsa_sign(1), dss_sign(2), rsa_fixed_dh(3),dss_fixed_dh(4), rsa_ephemeral_dh_RESERVED(5),dss_ephemeral_dh_RESERVED(6), fortezza_dms_RESERVED(20), ecdsa_sign(64), rsa_fixed_ecdh(65), ecdsa_fixed_ecdh(66), (255) } ClientCertificateType; opaque DistinguishedName<1..2^16-1>;struct { ClientCertificateType certificate_types<1..2^8-1>; SignatureAndHashAlgorithm supported_signature_algorithms<2^16-1>; DistinguishedName certificate_authorities<0..2^16-1>; } CertificateRequest;
能够选择发送一份本身接受的证书颁发机构列表,这些机构都用其可分辨名称来表示.
当须要作客户端认证时,客户端发送CertificateVerify消息,来证实本身确实拥有客户端证书的私钥。这条消息仅仅在客户端证书有签名能力的状况下发送。CertificateVerify必须紧跟在ClientKeyExchange以后。消息结构以下:
struct { Signature handshake_messages_signature; } CertificateVerify;
应用数据协议携带着应用消息,只以TLS的角度考虑的话,这些就是数据缓冲区。记录层使用当前链接安全参数对这些消息进行打包、碎片整理和加密。以下图所示,能够看到传输的数据已是通过加密以后的了。
警报的目的是以简单的通知机制告知对端通讯出现异常情况。它一般会携带close_notify异常,在链接关闭时使用,报告错误。警报很是简单,只有两个字段:
struct { AlertLevel level; AlertDescription description; } Alert;
- AlertLevel字段:表示警报的严重程度;
- AlertDescription:直接表示警报代码;
这是最多见的一种问题,一般会抛出以下类型的异常:
出现此类错误一般可能由如下的三种缘由致使:
- (1) 颁发服务器证书的CA未知;
- (2) 服务器证书不是由CA签署的,而是自签署(比较常见);
- (3) 服务器配置缺乏中间 CA;
当服务器的CA不被系统信任时,就会发生 SSLHandshakeException。多是购买的CA证书比较新,Android系统还未信任,也多是服务器使用的是自签名证书(这个在测试阶段常常遇到)。
解决此类问题常见的作法是:指定HttpsURLConnection信任特定的CA集合。在本文的第5部分代码实现模块,会详细的讲解如何让Android应用信任自签名证书集合或者跳过证书校验的环节。
SSL链接有两个关键环节。首先是验证证书是否来自值得信任的来源,其次确保正在通讯的服务器提供正确的证书。若是没有提供,一般会看到相似于下面的错误:
出现此类问题的缘由一般是因为服务器证书中配置的域名和客户端请求的域名不一致所致使的。
有两种解决方案:
- (1) 从新生成服务器的证书,用真实的域名信息;
- (2) 自定义HostnameVerifier,在握手期间,若是URL的主机名和服务器的标识主机名不匹配,则验证机制能够回调此接口的实现程序来肯定是否应该容许此链接。能够经过自定义HostnameVerifier实现一个白名单的功能。
代码以下:
HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { // 设置接受的域名集合 if (hostname.equals(...)) { return true; } } }; HttpsURLConnection.setDefaultHostnameVerifier(DO_NOT_VERIFY);
SSL支持服务端经过验证客户端的证书来确认客户端的身份。这种技术与TrustManager的特性类似。本文将在第5部分代码实现模块,讲解如何让Android应用支持客户端证书验证的方式。
以前在接口联调的过程当中,测试那边反馈过一个问题是在Android 4.4如下的系统出现HTTPS请求不成功而在4.4以上的系统上却正常的问题。相应的错误以下:
03-09 09:21:38.427: W/System.err(2496): javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xb7fa0620: Failure in SSL library, usually a protocol error 03-09 09:21:38.427: W/System.err(2496): error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0xa90e6990:0x00000000)
按照官方文档的描述,Android系统对SSL协议的版本支持以下:
Name | Supported (API Levels) |
---|---|
Default | 10+ |
SSL | 10–TBD |
SSLv3 | 10–TBD |
TLS | 1+ |
TLSv1 | 10+ |
TLSv1.1 | 16+ |
TLSv1.2 | 16+ |
也就是说,按官方的文档显示,在API 16+以上,TLS1.1和TLS1.2是默认开启的。可是实际上在API 20+以上才默认开启,4.4如下的版本是没法使用TLS1.1和TLS 1.2的,这也是Android系统的一个bug。
参照stackoverflow上的一些方式,比较好的一种解决方案以下:
SSLSocketFactory noSSLv3Factory; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory()); } else { noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory(); }
对于4.4如下的系统,使用自定义的TLSSocketFactory,开启对TLS1.1和TLS1.2的支持,核心代码:
public class TLSSocketFactory extends SSLSocketFactory { private SSLSocketFactory internalSSLSocketFactory; public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, null, null); internalSSLSocketFactory = context.getSocketFactory(); } public TLSSocketFactory(SSLSocketFactory delegate) throws KeyManagementException, NoSuchAlgorithmException { internalSSLSocketFactory = delegate; } ...... @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); } // 开启对TLS1.1和TLS1.2的支持 private Socket enableTLSOnSocket(Socket socket) { if(socket != null && (socket instanceof SSLSocket)) { ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); } return socket; } }
本部分主要基于第四部分提出的Android应用中使用HTTPS遇到的一些常见的问题,给出一个比较系统的解决方案。
无论是使用自签名证书,仍是采起客户端身份验证,核心都是建立一个本身的KeyStore,而后使用这个KeyStore建立一个自定义的SSLContext。总体类图以下:
类图中的MySSLContext能够应用在HTTPUrlConnection的方式与服务端链接的过程当中:
if (JarConfig.__self_signed_https) { SSLContextByTrustAll mSSLContextByTrustAll = new SSLContextByTrustAll(); MySSLContext mSSLContext = new MySSLContext(mSSLContextByTrustAll); SSLSocketFactory noSSLv3Factory; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory()); } else { noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory(); } httpsURLConnection.setSSLSocketFactory(noSSLv3Factory); httpsURLConnection.setHostnameVerifier(MY_DOMAIN_VERIFY); }else { httpsURLConnection.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault()); httpsURLConnection.setHostnameVerifier(DO_NOT_VERIFY); }
核心是经过httpsURLConnection.setSSLSocketFactory使用自定义的校验逻辑。总体设计上使用策略模式决定采用哪一种验证机制:
- makeContextWithCilentAndServer 单向验证方式(自定义信任的证书集合)
- makeContextWithServer 双向验证方式(自定义信任的证书集合,并使用客户端证书)
- makeContextToTrustAll (信任全部的CA证书,不安全,仅供测试阶段使用)
在App中,把服务端证书放到资源文件下(一般是asset目录下,由于证书对于每个用户来讲都是相同的,而且也不会常常发生改变),可是也能够放在设备的外部存储上。
public class SSLContextWithServer implements GetSSLSocket { // 在这里进行服务器正式的名称的配置 private String[] serverCertificateNames = {"serverCertificateNames1" ,"serverCertificateNames2"}; @Override public SSLContext getSSLSocket() { String[] caCertString = new String[serverCertificateNames.length]; for(int i = 0 ; i < serverCertificateNames.length ; i++) { try { caCertString[i] = readCaCert(serverCertificateNames[i]); } catch(Exception e) { } } SSLContext mSSLContext = null; try { mSSLContext = SSLContextFactory.getInstance().makeContextWithServer(caCertString); } catch(Exception e) { } return mSSLContext; }
serverCertificateNames中定义了App所信任的证书名称(这些证书文件必需要放在指定的文件路径下,并其要保证名称相同),然后就能够加载服务端证书链到keystore,经过获取到的可信任并带有服务端证书的keystore,就能够用它来初始化自定义的SSLContext了:
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { originalX509TrustManager.checkServerTrusted(chain, authType); } catch(CertificateException originalException) { try { X509Certificate[] reorderedChain = reorderCertificateChain(chain); CertPathValidator validator = CertPathValidator.getInstance("PKIX"); CertificateFactory factory = CertificateFactory.getInstance("X509"); CertPath certPath = factory.generateCertPath(Arrays.asList(reorderedChain)); PKIXParameters params = new PKIXParameters(trustStore); params.setRevocationEnabled(false); validator.validate(certPath, params); } catch(Exception ex) { throw originalException; } } }
和上面的过程相似,只不过这里提供的TrustManager不须要提供信任的证书集合,默认接受任意客户端证书便可:
public class AcceptAllTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { //do nothing,接受任意客户端证书 } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { //do nothing,接受任意服务端证书 } @Override public X509Certificate[] getAcceptedIssuers() { return null; }
然后构造相应的SSLContext:
public SSLContext makeContextToTrustAll() throws Exception { AcceptAllTrustManager tm = new AcceptAllTrustManager(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { tm }, null); return sslContext; }
1. TLS协议分析(5) handshake协议 证书与密钥交换
2. HTTPS深刻理解
3. HTTPS原理介绍以内容加密
4. How to disable SSLv3 in android for HttpsUrlConnection?
5. Android 4.1+ enable TLS 1.1 and TLS 1.2
6. Google关于https的官方文档
7. 使用HTTPS与SSL来保证安全性
8. Android App 安全的HTTPS 通讯
9. 详解https是如何确保安全的?
10. 图解HTTP--第七章
11. HTTP: The Definitive Guide
12. Bulletproof SSL and TLS:
Understanding and Deploying SSL/TLS and PKI to Secure Servers and Web Applications
更多精彩内容欢迎关注腾讯 Bugly的微信公众帐号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!