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