写给 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 无限期推迟:https://developer.apple.com/news/?id=12212016b
本文将着重以大部分 iOS 开发者能理解的方式介绍 APP 启用 HTTPS 支持的过程中跟 APP 相关的部分,剩余的协议细节将一笔带过。
HTTPS 基础
定义
HTTPS 看似跟 HTTP 一样,其实它只是看起来跟 HTTP 一样,实际上是一种新的网络架构。在当前情况下,HTTPS 的英文全称应该是 HTTP over TLS。
HTTPS 请求 和 HTTP 请求的异同
普通 HTTP 请求直接基于 TCP,在互联网上明文传播,而且没有任何校验,链路上的每一个节点都可以对数据包进行篡改,使用手机网络访问 HTTP 网站被插入流量球甚至广告等运营商劫持行为就是最常见的例子。而 HTTPS 请求运行在 TLS 层之上,TLS 运行在 TCP 上,TLS 有独特的握手、建立连接、数据验证机制,让运行商劫持无处下手:只要任何一个数据包被篡改,数据校验就会失败,这个请求会客户端直接抛弃,网页不会显示。当我们用 HTTP 协议来解释 TLS 层携带的内容时,这个东西就被称为 HTTPS 啦。
HTTP 协议简析
HTTP 协议是一个非常简单而强壮的协议,它规定了以文本方式解析数据后哪一部分该代表什么:头部携带特定信息,正文部分被渲染为网页。所以,任何数据都可以被 HTTP 协议解析,无论他是基于 TCP 还是 TLS 传输,或者只是硬盘上的一个文件。
请注意,此处的 HTTP 协议和上一小节中的 HTTP 请求是两个概念。
HTTPS 证书
证书是什么
下面两张图分别是我的个人博客的一张旧证书的 cer 和 crt 两种格式,在 Finder 中点击空格预览的结果:
下面是 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 server 上部署,部署时会验证“私钥”是否和“域名证书”匹配。
5. 用户在以 HTTPS 协议访问网站时,浏览器会进行如下几步安全验证:
* 域名证书中的域名和实际域名是否一致
* 域名证书和 root 证书是否匹配
* root 证书是否可信
需要注意的点
- 在某些低安全级别证书申请中(如仅验证域名所有权的证书),私钥可以让签发服务器代为生成,但这样做有一定的安全风险。
- root 证书也可以在我们本机生成,如 12306 的自签名证书,但这样不会被普通浏览器信任。
- 私钥为绝密,因为证书全部都是公开的,任何人都可以提取,如果私钥被别人获取,被部署到别人的服务器中,那所有人就会认为那台服务器是完全合法的。这已经不是中间人攻击了,这时候他就是你。
NSURLSession 对证书的验证
证书验证方法
在 URLSessionDelegate 中有一个方法 `func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)` 专门用来处理 HTTPS 证书的处理。具体操作可以参考 Pitaya 中的代码。
我简单复述一下处理流程:
1. 发现是 HTTPS 请求,取出证书
2. 进入证书处理流程:
1. 本地证书分两种情况:① 本地存储着证书服务机构颁发的 crt 文件[转换而来的 cer 文件](https://lvwenhan.com/ios/464.html),使用 NSData 进行内容对比 ② 本地存储着自签名的 cer 格式的证书,使用 NSData 进行内容对比
3. 匹配成功,手动让请求继续,这一步可以让自签名证书绕过 iOS 系统的证书合法性验证
4. 匹配失败,进入错误处理流程
5. 如果自签名证书不做手动处理,那么在这个方法结束后链接就会被系统关闭,因为 root 证书不合法。
所以,在 APP 提交的时候,苹果会检查是否将 ATS 配置为了“全部无脑通过”,这种操作是被禁止的。当然,苹果也可以在系统层面一刀切,但是那样得挂多少个 APP 啊,苹果不会那么做。所以会被影响的应该只是新提交的 ipa。
SSL 钢钉原理
如果有人通过一些手段通过了域名所有权认证(非常容易,域名邮箱、DNS指向甚至在根目录放一个文件都可以验证通过),拿到了一个合法的对应你的域名的 HTTPS 证书,这时候他在广场开放了一个没有密码的 wifi,命名为 CMCC,这样,几乎所有的开着 wifi 的手机都会自动连接,这时候他只需要做一个简单的 DNS 劫持,就可以把所有应该向你网站发送的需求劫持到他那里。
但是:如果你做了 SSL 钢钉验证,那么他的证书就不会被验证通过。他也没法用你的证书启动服务,因为私钥和证书的验证是 SSL 协议强制做的,他没有你的私钥,他的 web server 就没法启动。所以,私钥千万不能泄露。
关于自签名证书
自签名证书不会被浏览器信任,因为每次有新的 HTTPS 证书到达某个操作系统时,系统会去访问 root 证书的服务器以确定域名证书的身份,这些合法的 root 证书服务商就是固定的那几家,显然自签名证书不会被信任,所以我们在 12306 抢票的时候需要先下载他的自签名证书的 root 证书并手动信任,不然就打不开页面。
我们可以看到,自签名证书的验证在代码层面,在审核的时候是完全不可感知的,所以就没有什么“苹果不接受自签名证书”之类的问题了。而且,自签名证书被广泛的用于各种系统内部的连接加密,不是苹果可以一刀切的:如果粗暴的在操作系统层面阻止了自签名证书,导致企业客户的系统突然挂掉,后果不可想象。
关于证书更换
证书都有有效期,在过期之前需要申请新证书,这时候 SSL 钢钉该怎么处理呢?动态下发当然是不行的,为什么要验证证书?就是因为网络不可信任。Pitaya 前两天加入了这个逻辑:在新证书和旧证书交接的一段时间内,上线新版本,同时包含新旧证书,这样可以保证更新过的用户可以对证书更换无感。
另外,设置 SSL 钢钉不适合面向普通用户的 APP,因为总是有人万年不更新,这更适合企业内部 APP,可以通过行政手段及自驱力(业绩啊,提成啊)推动更新。
关于所谓的双向验证
感觉这里是大家误解最大的地方:大部分人所谓的“双向验证”就是自签名证书的验证并手动继续而已。
HTTPS 是支持双向认证的,不过那指的是客户端(浏览器或 APP)也像服务端一样,在发送请求给服务端的时候带上证书,再由服务端使用对应的私钥进行验证。一般 APP 不需要这么做。
苹果的要求
后端
前两条拿给后端看就行,第三条用 Nginx 也很容易实现。最后一条 Exception 就是苹果马上就不支持的。
iOS 端
使用这些 API 可以单独绕过。
流媒体文件可以添加例外,WKWebView 可以直接设置为绕过。
总结
购买的证书:什么都不用管,改一下服务器地址就行,代码完全不用改。
自签名证书:找后端哥们儿要一个 crt,自己提取也行,转换成 cer ,放到相应的方法里,手动让处理流程继续即可。
评论:
2017-11-09 16:21