写给 iOS 开发者看的 HTTPS 指南

春天来了,到了动物交配强制 ATS(App Transport Security)的季节,广袤的 iOS 大草原上到处都弥漫着一种叫 HTTPS 的荷尔蒙气息。

  • 白岩松赵忠祥

苹果在 WWDC 2016 上宣布:2016 年底将要求所有 APP 适配苹果的 App Transport Security,简单地说就是除了特殊情况(浏览器、第三方服务、媒体)外,APP 跟服务端的通信必须使用 HTTPS 协议,否则 iOS 9 和 macOS 10.11 起,操作系统将有能力阻止所有的明文 HTTP 请求。在上面的 session 中,苹果还对具体的细节做出了要求。不过,就在今天,苹果宣布将这个 deadline 无限期推迟

本文将着重以大部分 iOS 开发者能理解的方式介绍 APP 启用 HTTPS 支持的过程中跟 APP 相关的部分,剩余的协议细节将一笔带过。

HTTPS 基础

定义

HTTPS 看似跟 HTTP 一样,其实它只是看起来跟 HTTP 一样,实际上是一种新的网络架构。在当前情况下,HTTPS 的英文全称应该是 HTTP over TLS。

HTTPS 请求 和 HTTP 请求的异同

普通 HTTP 请求直接基于 TCP,在互联网上明文传播,而且没有任何校验,链路上的每一个节点都可以对数据包进行篡改,使用移动、联通等手机网络访问网站被插入流量球甚至广告等运营商劫持行为就是最常见的例子。而 HTTPS 请求运行在 TLS 层之上,TLS 运行在 TCP 上,TLS 有独特的握手、建立连接、数据验证机制,让运行商劫持无处下手:只要任何一个数据包被篡改,数据校验就会失败,这个请求会直接被浏览器等客户端抛弃,网页不会显示。当我们用 HTTP 协议来解释 TLS 层携带的内容时,这个东西就被称为 HTTPS 啦。

HTTP 协议简析

HTTP 协议是一个非常简单而强壮的协议,它规定了以文本方式解析数据后哪一部分该代表什么:头部携带特定信息,正文部分被渲染为网页。所以,任何数据都可以被 HTTP 协议解析,无论他是基于 TCP 还是 TLS 传输,或者只是硬盘上的一个文件。

请注意,此处的 HTTP 协议和上一小节中的 HTTP 请求是两个概念。

HTTPS 证书

证书是什么

下面两张图分别是我的个人博客的一张旧证书的 cer 和 crt 两种格式,在 Finder 中点击空格预览的结果:

《写给 iOS 开发者看的 HTTPS 指南》

《写给 iOS 开发者看的 HTTPS 指南》

下面是 crt 格式的证书内容:

  -----BEGIN CERTIFICATE-----   MIIErzCCA5egAwIBAgIQYrp2Mj1s3GeeVubYEGEguTANBgkqhkiG9w0BAQsFADBV   MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV   BAMTIVdvU2lnbiBDQSBGcmVlIFNTTCBDZXJ0aWZpY2F0ZSBHMjAeFw0xNTA5Mjkx   MjMyMDFaFw0xNjA5MjkxMjMyMDFaMBcxFTATBgNVBAMMDGx2d2VuaGFuLmNvbTCC   ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOJMpMrIcfdxScuslpFWWLAP   sLq45P/b7V8WXfTZnMY9nZGc012olTI/Su8DlrBxWfks/iBS1WkGgxkhxirz096c   IPvlddRXyz1LI1Npl0BCbjD1j+jx40e3PIVbcpff5Z1OLlvu/5ehub4SyDl3wIRM   6zwTSzldbAkQ4yXFl2OUyoSecQEqRXBdpbvAcL5/Q1M/wlxNi4KBGmemckABMqNg   30N9OipTCxbGwYbH/RBMYrxncwbIoCAC/P189nQ+RJ3szx3tgjiTpAtHref4uHdv   XI6wy6tP8FXh2HIXbx1kNfY+8YbSWDnvHiN8dL+fkKsr5KU5nNG54KGkdOdlljkC   AwEAAaOCAbcwggGzMAsGA1UdDwQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAgYI   KwYBBQUHAwEwCQYDVR0TBAIwADAdBgNVHQ4EFgQUkjyOi+6xj5KkgATKLXS12K9x   SvMwHwYDVR0jBBgwFoAU0qcWIHyv2ZWe60MKGfLguXQOqMcwfQYIKwYBBQUHAQEE   cTBvMDQGCCsGAQUFBzABhihodHRwOi8vb2NzcDEud29zaWduLmNvbS9jYTYvc2Vy   dmVyMS9mcmVlMDcGCCsGAQUFBzAChitodHRwOi8vYWlhMS53b3NpZ24uY29tL2Nh   Ni5zZXJ2ZXIxLmZyZWUuY2VyMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmxz   MS53b3NpZ24uY29tL2NhNi1zZXJ2ZXIxLWZyZWUuY3JsMCkGA1UdEQQiMCCCDGx2   d2VuaGFuLmNvbYIQd3d3Lmx2d2VuaGFuLmNvbTBRBgNVHSAESjBIMAgGBmeBDAEC   ATA8Bg0rBgEEAYKbUQYBAgIBMCswKQYIKwYBBQUHAgEWHWh0dHA6Ly93d3cud29z   aWduLmNvbS9wb2xpY3kvMA0GCSqGSIb3DQEBCwUAA4IBAQApspu1V2KlscwQints   4zqm6xnfoXkjZJjaanXmbOER8iRd1hmzN6vRYcOJ2t0QmkjBisRgi4ffJVZ9QF30   LzmaZYg+PQUX15CxDlG9XTRB6ufqdlsGdc44NqGdHuo8uIsko1MjubS8mQw7ClLc   4VEvP2KUBZFgaenfG6Y8v5DWWV+NfiCkmCMFXu6nbqEvEf+ev3arnaQSiDhH98Y+   ivysKu8aYlsb2VsRaF8e9bMvE2PgfZCg93lSou/vQS1VUED7ih5lLb2CQqW8ksMp   XKK1E/BNFR7GR8i1NfL15KdIGUmsklT60vooRd7zM9ai8vtmkg9xykwpgUPbTjcd   mRAb   -----END CERTIFICATE----- 

证书就是一种使用特殊格式加密的一段字符串,可以被读取并拿出关键信息。iOS 中 NSURLSession 验证的就是 cer 格式的这个证书。

证书周边知识

下面是一个 HTTPS 证书典型的购买、部署流程:

  1. 在 *UNIX 环境下使用 openssl 工具生成一对一匹配的 私钥 和 CERTIFICATE REQUEST 文件(以 —–BEGIN CERTIFICATE REQUEST—– 开头)。私钥为绝密,绝对不能泄露,最好在生产服务器直接生成,这样就不需要网络传输,更加安全。
  2. 将 CERTIFICATE REQUEST 文件提交到证书服务机构,服务机构根据证书级别进行 域名认证、公司认证、安全认证 等不同级别的验证信息。
  3. 验证通过后,服务机构将基于我们提交的 CERTIFICATE REQUEST 内容,使用他已有的证书派生出子证书,提供给我们下载。
  4. 我们拿到服务机构办法的两个 crt 格式的证书(root 证书 及我们的域名证书),再配合本地的私钥,到 Apache、Nginx 上部署,部署时会验证“私钥”是否和“域名证书”匹配。在这一点上各个 web 服务软件不一致,例如 Nginx 就是把两个 crt 证书的内容放到一个 crt 文件中部署。
  5. 用户在以 HTTPS 协议访问网站时,会进行如下几步安全验证:

    • 域名证书中实际域名是否一致
    • 域名证书和 root 证书是否匹配
    • root 证书是否可信

需要注意的点

  1. 在某些低安全级别证书申请中(如仅验证域名所有权的证书),私钥可以让签发服务器代为生成,但这样做有一定的安全风险。
  2. root 证书也可以在我们本机生成,但这样不会被普通浏览器信任,例如 12306 的自签名证书
  3. 私钥为绝密,因为证书全部都是公开的,任何人都可以提取,如果私钥被别人获取,被部署到服务器中,那所有人就会认为那台服务器是完全合法的。这已经不是中间人攻击了,这时候他就是你。

NSURLSession 对证书的验证

证书验证方法

在 URLSessionDelegate 中有一个方法 func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) 专门用来处理 HTTPS 证书的处理。具体操作可以参考 Pitaya 中的代码

我简单复述一下处理流程:

  1. 发现是 HTTPS 请求,取出证书
  2. 进入证书处理逻辑:

    1. ① 本地存储着证书服务机构颁发的 crt 文件转换而来的 cer 文件,使用 NSData 进行内容对比 ② 本地存储着自签名的 cer 格式的证书,使用 NSData 进行内容对比
    2. 匹配成功,手动让请求继续,这一步可以让自签名证书绕过 iOS 系统的证书合法性验证
    3. 匹配失败,进入错误处理流程
    4. 如果自签名证书不做手动处理,那么在这个方法结束后就会被系统关闭,因为 root 证书不合法。

所以,在 APP 提交的时候,苹果会检查是否将 ATS 配置为了“全部无脑通过”,这种操作是被禁止的。当然,苹果也可以在系统层面一刀切,但是那样得挂多少个 APP 啊,苹果不会那么做。所以会被影响的应该只是新提交的 ipa。

SSL 钢钉原理

如果有人通过一些手段通过了域名所有权认证(非常容易,域名邮箱、DNS指向甚至在根目录放一个文件都可以验证通过),拿到了一个合法的对应你的域名的 HTTPS 证书,这时候他在广场开放了一个没有密码的 wifi,命名为 CMCC,这样,几乎所有的开着 wifi 的手机都会自动连接,这时候他只需要做一个简单的 DNS 劫持,就可以把所有应该向你网站发送的需求劫持到他那里。

但是:如果你做了 SSL 钢钉验证,那么他的证书就不会被验证通过,因为私钥和证书的验证是 Apache 和 Nginx 强制做的,他没有私钥,Apache 都没法启动。所以,私钥千万不能泄露。

关于自签名证书

自签名证书不会被浏览器信任,因为每次有新的 HTTPS 证书到达某个操作系统时,系统会去访问 root 证书的服务器以确定域名证书的身份,这些合法的 root 证书服务商就是固定的那几家,显然自签名证书不会被信任,所以我们在 12306 抢票的时候需要先下载他的自签名证书并手动信任,不然就打不开页面。

我们可以看到,自签名证书的验证在代码层面,在审核的时候是完全不可感知的,所以就没有什么“苹果不接受自签名证书”之类的问题了。而且,自签名证书被广泛的用于各种系统内部的连接加密,不是苹果可以一刀切的:如果粗暴的在操作系统层面阻止了自签名证书,导致企业客户的系统突然挂掉,后果不可想象。

关于证书更换

证书都有有效期,在过期之前需要申请新证书,这时候 SSL 钢钉该怎么处理呢?动态下发当然是不行的,为什么要验证证书?就是因为网络不可信任。Pitaya 前两天加入了这个逻辑:在新证书和旧证书交接的一段时间内,上线新版本,同时包含新旧证书,这样可以保证更新过的用户可以对证书更换无感。

另外,设置 SSL 钢钉不适合面向普通用户的 APP,因为总是有人万年不更新,这更适合企业内部 APP,可以通过行政手段及自驱力(业绩啊,提成啊)推动更新。

关于所谓的双向验证

感觉这里是大家误解最大的地方:就是自签名证书的验证并手动继续而已。

HTTPS 是支持双向认证的,不过那指的是客户端(浏览器或 APP)也像服务端一样,在发送请求给服务端的时候带上证书,再由服务端使用对应的私钥进行验证。一般 APP 不需要这么做。

苹果的要求

后端

《写给 iOS 开发者看的 HTTPS 指南》

前两条拿给后端看就行,第三条用 Nginx 也很容易实现。最后一条 Exception 就是苹果马上就不支持的。

iOS 端

《写给 iOS 开发者看的 HTTPS 指南》

使用这些 API 可以单独绕过。

《写给 iOS 开发者看的 HTTPS 指南》

流媒体文件可以添加例外,WKWebView 可以直接设置为绕过。

总结

购买的证书:什么都不用管,改一下服务器地址就行,代码完全不用改。

自签名证书:找后端哥们儿要一个 crt,自己提取也行,转换成 cer ,放到相应的方法里,手动让处理流程继续即可。

    原文作者:小蜜蜂
    原文地址: https://juejin.im/entry/585c881c61ff4b00688b8fdd
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞