关于HTTPS证书校验

先插播一个好消息,在我坚持了6篇原创文章之后,微信终于给我发送了赞赏开通的邀请。对我而言这是一个全新的激励和认可。起初做这个公众号只是希望把自己的知识和经验做一个沉淀,所以从始至终既没有在朋友圈推广,也没有写软文或者打广告。以后不管有没有赞赏,我都会继续坚持下去,争取把这个公众号做好做精,给大家带来帮助。

言归正传,我们仍然以实际问题作为文章的引子,这一次我们碰到的是一个HTTPS证书校验的问题。

用户反馈更新新版本客户端之后,登录报错,删除重装之后仍然无法登录。

从描述看,我们开始都认为是登录环节出了问题,因为报错的是通用错误,但是初步跟踪了一下并没有复现。项目组算上开发和测试成员,一共只有两台机器可以稳定复现这个问题,其他设备都正常。中间我们尝试了testFlight及试用版本,连接生产环境运行代码,wireshark抓包等等操作,最终锁定了问题原因,竟然是落点在HTTPS证书的校验上。因为涉及的知识点比较零散,所以分几个部分来完成这次的文章,主要关注下面几个问题:

什么是HTTPS,SSL,TLS

什么是证书,证书链,X.509标准

什么是自签名证书,什么是锚点证书

iOS系统如何校验证书,AFN又如何校验证书

抓包工具的原理是什么

HTTPS

HTTPS = HTTP + SSL/TLS,相信了解的同学对这个公式不会陌生,简单来说HTTPS是在HTTP的基础上增加了SSL/TLS这一层,介于TCP与HTTP之间。

为什么要有HTTPS呢,是因为HTTP本身有缺陷,详细的解释可以看看阮一峰老师的博客,写的非常好。简单来说HTTP的缺点在于三点:

通信使用明文,可能会被窃听。

不验证通信方的身份,可能会被伪装。

不验证报文的完整性,可能会被篡改。

所以在《图解HTTP》这本书中有一个经典的定义,HTTPS = HTTP + 加密 + 认证 + 完整性保护,分别解决上面提到的3点问题。更直白的说,HTTPS是包了一层SSL的HTTP协议,使用握手阶段生成的Session Key进行加密,使用数字证书证明通信方的身份,使用散列算法生成的MAC附加到发送的数据包,保证数据的完整性。

SSL/TLS

这里先介绍一下SSL和TLS的历史,SSL全称Secure Sockets Layer,是Netscape公司在1994年推出的网络安全协议 ,直到SSL3.0版本发布后,互联网标准化组织IETF接替Netscape推出TLS1.0版本,目前最新版本的TLS应该是TLS1.2。

HTTPS采取的策略利用非对称加密算法完成密钥的传输,得到最终加密密钥之后,再采用对称密钥算法完成数据加密。而密钥的生成是这里面的关键环节。我们来确认一下具体的细节:

1.客户端发送Client Hello报文,包括客户端支持的TLS协议版本,客户端生成的随机数1,SessionID,客户端支持的密码套件列表(注意这里的套件其实是一个组合,内部包含了密钥交换算法、加密算法一级散列算法),客户端支持的压缩算法列表;

2.服务器端回应Server Hello报文,包括确认使用的TLS协议版本,服务器端生成的随机数2,确认使用的密码套件,确认使用的压缩算法;

3.服务器端发送Certificate报文,包含公钥证书;

4.服务器端发送Server Hello Done报文,通知客户端最初的SSL握手协商部分结束;

5.客户端发送Client Key Exchange报文作为回应,报文中包含 Premaster secret的随机密码串,使用步骤3中的公钥对其加密;

6.客户端发送Change Ciper Spec报文,提示服务器,这条报文之后的通信都会采用Premaster secret进行加密;

7.客户端发送Finished报文,包含连接至今为止全部报文的整体校验值;

8.服务器端发送Change Ciper Spec报文;

9.服务器端发送Finshed报文,至此SSL连接建立完成。

这里需要注意一点,之所以要使用三个随机数生成最终的加密密钥,是因为SSL协议不信任客户端可以产生完全随机的随机数,所以需要在现有随机数的基础上引入更多的随机因子,更接近真实的随机数。当然如果是使用其他的加密算法,例如DH算法,是不需要传输Premaster secret的,客户端和服务器端各自生成即可。

SSL证书卸载

SSL虽然保证了数据的传输安全,但是也有其缺点,比如相比HTTP速度会慢。根据《图解HTTP》书中的解释,这里的慢分两种,一种是指通信变慢,一种是处理速度变慢。

通信慢是因为SSL通信部分消耗网络资源,原本HTTP直接与TCP通信,现在增加了SSL中间层,整体的通信量增加;

处理速度慢是因为SSL需要在客户端和服务器端完成加密运算,必然消耗CPU和内存等硬件资源。

目前对于SSL慢的问题,市场上有专门的SSL加速器,可以用来改善这个问题,比如我们的应用就部署了专门的模块去完成SSL卸载,注意这里的卸载并不是说删除SSL协议或者相关模块,而是说因为采用了SSL,保证了安全性的同时,引入了服务器的性能消耗负担,为了解决SSL加密运算的问题,所以把这一部分工作改为由其他模块承担,比如专业的交付设备或者负载均衡设备处理,减轻服务器的负担,加快处理速度。

证书

简单说,证书就是一个文件,主要包含了这个证书的公钥,以及数字证书认证机构CA对这个公钥做的签名,此外还有一些其他的附带信息。如果要获取证书,一般都是服务器端向CA提出申请,CA确认了申请者的有效身份之后,会分配对应的公钥给申请者,然后先对公钥和证书信息做hash操作生成摘要,再使用CA机构自己的密钥对摘要做签名,这样生成的文件我们叫做证书。

我们可以打开mac上的钥匙串,点击其中的某个证书观察一下,一般证书的内容包含:

证书的名称

证书的持有者

证书的签发者

证书包含的公钥证书包含的数字签名

除了合法申请到的证书。我们自己也可以制作自签名证书,也就是自己作为CA去签发。显然这样的证书只能用于证明我是XXX,可以用在平时的开发需要中,比如我们可以使用开源的openSSL去生成完整的自签名证书。

证书链

先搞清楚一些概念:

站点证书:一般是我们直接访问的域名直接对应的证书,或者我们工程里直接使用的证书。

中级证书:一般是签发站点证书的上一级证书,可能有多个,逐级签发。

根证书:证书链上最上层的证书,自己签发自己,是从可信任机构CA申请得到的一级证书。

证书服务提供商:使用较多的是VeriSign、赛门铁克、Godaddy等,提供证书供应服务。

X.509:X.509是一种非常通用的证书格式。所有的证书都符合ITU-T X.509国际标准,因此(理论上)为一种应用创建的证书可以用于任何其他符合X.509标准的应用。

证书链其实就是由上述各种证书按照X.509的格式标准,组成的一条树的路径,从叶子节点到根节点。这里涉及到一个证书链的验证问题,根据前面说的证书特性,验证的步骤会从叶子节点开始先遍历,从证书信息中不断寻找自己的签发证书,直至到达根证书,然后原路返回,根据上一级证书的公钥去验证下一级证书的签名,最终返回到叶子节点。具体流程参见这张图:

证书校验:

iOS系统默认校验策略

网上有好几篇文章都做了很详细的解释,这里简单说一下。

苹果的策略是对证书链进行校验,如果信任链中只包含有效证书并且以anchor certificate锚点证书结束,那么就认定证书有效。

这里提到的锚点证书一般是在系统中存在的根证书,支持开发者自定义证书为锚点。

我们重点关注一下NSURLSession的证书校验(因为NSURLConnection已经被苹果废弃,我们这里就不讨论了,其实原理是一样的),这里涉及到一个代理方法:

根据官方的代码注释,这是一个NSURLSession代理方法,系统默认不去实现这个方法,只是正常校验证书链。如果成功则根据serverTrust生成credential,建立SSL连接。这里要说明一下,之所以代理方法会出现challenge,是因为HTTP协议中存在Challenge-Response这样一种授权模式,大家有兴趣可以了解一下BASIC认证的流程,简单说就是客户端发起请求到达服务器端,服务器端返回401的错误码,标识访问拒绝,客户端收到之后将自己的用户名、密码经过Base64编码之后,附加到HTTP请求的header中重新发送请求到服务器端。

同样的,iOS中的HTTPS请求也采用了类似的模式,只是差别在于服务器端返回401之后,触发的是上面提到的NSURLSession的代理方法。在这个方法内部去处理服务器的要求,生成Credential返回给服务器端。

对于自签名证书的校验,区别在于锚点证书的设置,需要开发人员手动调用SecTrustSetAnchorCertificates去设置,一般我们可以选择将本地的证书打包进应用,选择不同的校验策略去认定信任动作。

AFNetWorking证书校验策略

AFNetWorking证书验证主要是在AFSerurityPolicy.m和AFURLSessionmManager.m这两个文件中。

其中AFSerurityPolicy.m主要负责配置是否允许无效证书、是否开启域名校验、设定不同的证书校验策略等。AFURLSessionmManager.m则包含了真正的证书验证逻辑实现。

我们主要关注下这三种校验模式:

AFSSLPinningModeNone:AFSSLPinningMode枚举的默认值,校验结果返回self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);也就是外部调用的配置以及对服务器证书链的信任结果;

AFSSLPinningModePublicKey:对比本地证书的公钥信息与服务器返回的证书链中的公钥信息,如果一致则认为证书信任通过;

AFSSLPinningModeCertificate:对比本地证书与服务器返回的证书链中的证书信息,全部一致则认为证书信任通过;

AFN的核心判断逻辑就是调用下面的方法:

返回验证结果,然后在didReceiveChallenge方法中去设置不同的disposition,如果验证结果有效,则生成credential,否则取消这次challenge。

我们应用设定的校验策略

我们的应用对AFN中AFSSLPinningModePublicKey模式进行了定制,总体的策略是将多个根证书的公钥以宏定义的方式写死在客户端,验证服务器返回的证书链中的公钥信息与本地公钥是否能匹配上,如果有至少一个匹配则认为验证通过。具体代码如下:

解决问题

说了这么多,回到我们的问题上来。我们经过了一下排查的步骤:

从服务器端的同事那里去查询出问题的用户日志,发现HTTP请求根本就没有到达服务器端。所以问题锁定在了客户端本地,发生的时间在HTTP请求之前。

我们直接连生产环境运行工程,打断点跟踪SSL握手的流程,发现正常校验通过,并未复现问题。

我们试图通过Chales抓包去获取线索,本来抓包的原理本是利用中间人攻击模式,信任一个由我们的工具发布的证书,充当一个假的中间人服务器,将PC端作为代理去拦截处理网络请求,但是因为这一次证书校验的策略是我们自定义的,导致无法抓到HTTP层面的数据报文。

既然HTTP层没办法,那么试试wireShark吧,获取到了TCP/IP SSL的通讯报文,但是经过分析仍然没有得到有用的线索。

我把出问题的设备连接到开发的Mac机,试图通过控制台看看。这次倒是有点收获,基本上确认了是SSL连接建立失败导致,而且显示代码执行到了系统进程securityd对keyChain的操作部分,结果见下图

其实具体的错误就在下面这个方法:

方法中注释的那行keyChain删除操作原本是没有的,这段代码原本是提供给我们的SDK代码,后来得知其实是从网上开源社区copy而来。程序逻辑上很简单,这个方法的作用是将SecKeyRef类型的公钥数据转化成NSData输出,但是在某些特殊的场景下,代码执行SecItemAdd之后没有执行到SecItemDelete,导致下次继续SecItemAdd一直失败。又因为keyChain是硬件级别的存储,即便是用户删除了客户端重新安装,SecItemAdd仍然失败,导致evaluateServerTrust的返回结果为NO。

那么问题来了,到底什么情况下回导致代码没有正常执行SecItemDelete操作呢?

这个问题我在网上找了好久,我们自己也尝试着复现,都没有一个明确的原因。只有一种说法表示在设备的内存压力过大的时候,可能导致钥匙串的操作受影响。

最终我们的修改方案是在SecItemAdd之前每次都先执行SecItemDelete,确保SecItemAdd一定成功,后续新版本上线之后,问题成功解决。

总结

现在我们对文章开头的几个问题应该有答案了,这次排查经历让我对HTTPS和证书的理解又加深了一层,对计算机网络的知识也有了新的认识,希望对看到这篇文章的读者也能带来帮助和启发。

最后推荐大家看看《图解HTTP》这本书,日本人写的东西确实有一套,图文并茂,浅显易懂,作为工具书平时多看看也是极好的。

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20180711G0ZFKC00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券