iOS开发中的HTTPS

版权声明:本人原创文章,转载请注明原文链接。
前言:自娱自乐的iOS开发学习实践。为实现一个简单社交应用做功课,简要理解基于SSL/TLS协议的HTTPS通信运行机制,实践iOS端到服务器端的安全通信。

提出问题

苹果公司在2016的开发者大会上宣布:到2017年,所有的iOS应用都必须使用HTTPS与服务器进行通信。iOS开发者应该都不会对这个决定感到惊讶,因为自iOS9就已经引入了ATS(应用传输安全App Transport Security)特性,该特性对应用的安全传输做出了以下要求:
  • The protocol Transport Security Layer (TLS) must be at least version 1.2.
  • Connection ciphers are limited to those that provide forward secrecy (see the list of ciphers below.)
  • Certificates must use at least an SHA256 fingerprint with either a 2048 bit or greater RSA key, or a 256 bit or greater Elliptic-Curve (ECC) key.
  • 不符合以上条件的任意一项,网络请求将会被中断并返回空值。

    既然苹果公司划出了最后期限,HTTPS将成为必然,那么一个iOS开发自学者自然会提出以下问题:

    • HTTPS的原理和运行机制是什么?
    • 为实现HTTPS通信,服务器端需要做什么
    • 为实现HTTPS通信,iOS客户端需要做什么

    笔者将以上述问题和学习中遇到的新问题为牵引,逐个进行学习与实践。

    HTTPS的原理及运行机制

    网络上有很多对于SSL/TLS协议进行讲解的文章,比如SSL/TLS协议运行机制的概述图解SSL/TLS协议两篇就很不错,推荐阅读。阅读之后能基本理解HTTPS的原理和运行机制,本小节简要归纳自己的理解。

    首先,不使用SSL/TLS的HTTP通信,是不加密的通信,这显然不能保证客户的信息安全。在上一篇功课iOS开发中使用keyChain保存用户密码中,辛辛苦苦地将用户名密码能够安全地存储在keyChain中(不越狱的前提下),但是如果用户名密码就这样明文地发送出去,一旦被截获,后果可想而知。当然,我们很自然地就会想到将用户密码字符串进行MD5加密后,再通过网络发送给服务器,比如:

    NSString *sourceStr = [NSString stringWithFormat:@"attach=iOS&chartset=utf-8&format=json&partner=google&userid=%@&password=%@”,userid,password];
    NSString *signStr = [NSString md5String:sourceStr];
    

    但即便如此也不能保证安全,因为存在碰撞攻击。关于碰撞攻击,可以阅读安全科普:密码学之碰撞攻击一文。

    于是,为了实现通信的安全,SSL/TLS协议采用公钥加密法,其运行的基本流程是:

    1. 客户端向服务器端索要并验证公钥;
    2. 双方协商生成"对话密钥";
    3. 双方采用"对话密钥"进行加密通信。
    其中,第1和2步被称为握手阶段。握手阶段的细节这里就不赘述,我们只需要知道,通过握手阶段,客户端和服务器端主要交换了3个信息:
    1. 数字证书。该信息是我们进行开发需要关注的!数字证书包含了公钥等信息,一般由服务器发给客户端,接收方通过验证这个证书是不是由信赖的CA签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证;
    2. 3个随机数。3个随机数是用于生成对话密钥的,我们不需要关心这细节
    3. 加密通信协议。客户端和服务器端通信需要采取同样的加密通信协议,我们也不需要太关注

    数字证书与公钥基础设施

    对于不大接触网络安全的同学,看了上面的原理,可能和我一样也是充满问题,这是因为我们是半路出家,既不知道顶层构架,又不接地气。但是如果对于公钥基础设施(PKI)和数字证书的关系有了一个直观的理解,疑惑就会迎刃而解。

    刚才已经了解到HTTPS是基于公钥加密法,而公钥基础设施(PKI)就是一种遵循标准的利用公钥加密技术为电子商务的开展提供一套安全基础平台的技术和规范。完整的PKI系统拥有权威认证机构(CA)、数字证书库、密钥备份及恢复系统、证书作废系统、应用接口(API)等基本构成部分,其中权威认证机构CA将是我们需要打交道的部门

    该系统的逻辑关系可以这样理解:

    1. 申请人向CA提交申请材料;
    2. 数字证书是由证书认证机构(CA)对证书申请者真实身份验证之后,用CA的根证书对申请人的一些基本信息以及申请人的公钥进行签名(相当于加盖发证书机构的公章)后形成的一个数字文件。CA完成签发证书后,会将证书发布在CA的证书库(目录服务器)中,任何人都可以查询和下载,因此数字证书和公钥一样是公开的。
    3. 每个证书持有人都有一对公钥和私钥,这两把密钥可以互为加解密。公钥是公开的,不需要保密,而私钥是由证书持有人自己持有,并且必须妥善保管和注意保密
    4. 简单地说,数字证书就是经过CA认证过的公钥,而私钥一般情况都是由证书持有者在自己本地生成的,由证书持有者自己负责保管。

    至此,逻辑链条就和SSL/TLS协议运行机制衔接上了:申请人事先通过向CA申请,已经有了公钥和私钥。客户端向服务器请求,能够得到包含公钥的数字证书。得到公钥后,双方就按照上节中运行的基本流程生成对话密钥,而后开始加密通信。如果对于数字证书和CA之间的关系还不是很清楚,建议阅读数字证书及CA的扫盲介绍一文。

    思维比较敏锐的同学也许会问:

    我怎么知道数字证书是由CA签发的,而不是第三方伪造的呢?
    问得好!简单地说,就是用CA的组织结构和数字证书的签发流程来保证,具体细节在这里不讨论,感兴趣的同学可以阅读iOS安全系列之一:HTTPS一文。

    小结:客户端向服务器发出请求得到包含公钥的数字证书,数字证书的真实性是由CA来进行保证。基于公钥,客户端和服务器端能够通过“握手”建立加密的通信。

    服务器端如何实现HTTPS

    如何实现,肯定得靠自己搭建一个HTTPS服务器啊。根据网络上的资料,可知有两种方式来搭建HTTPS服务器:

    • 一种是创建证书请求,然后到权威机构认证,随之配置到服务器;
    • 一种是自建证书,然后配置给服务器。

    第一种方式搭建的HTTPS服务器是最优的。建立网站的话,直接就会被信任。而服务器作为移动端app的服务器时,也不需要为ATS做过多的适配正是我所需要积累知识的方向)。虽然说权威的机构认证都是需要钱的,但是如今也不乏存在免费的第三方认证机构;

    第二种方式搭建的HTTPS服务器,对于网站来说完全不可行,用户打开时直接弹出一个警告提醒,说这是一个不受信任的网站,让用户是否继续,体验很差,而且让用户感觉网站不安全。对于移动端来说,在iOS9出现之前,这个没什么问题,但是在iOS9出来之后,第二种方式是通不过ATS特性,需要在info.plist文件中将App Transport Security Settings中的Allow Arbitrary Loads设置为YES才行。

    在本文中,为了快速地验证iOS端与HTTPS服务器能够不需要为ATS做过多的适配,采取了选择一个现成的HTTPS服务器来做验证的方式,学习如何自己搭建的工作,放到下一篇功课中。那么如何才能知道一个HTTPS服务器是符合ATS特性中的要求的呢?使用nscurl命令如下:

    nscurl --ats-diagnostics --verbose https://example.com
    

    命令后接的URL可以使你想要检测的HTTPS服务器地址。接下来,让我们看看知乎能不能通过ATS检测,在终端中输入命令:

    nscurl --ats-diagnostics --verbose https://www.zhihu.com
    

    输出如下:

    Starting ATS Diagnostics
    
    Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https://www.zhihu.com.
    A test will "PASS" if URLSession:task:didCompleteWithError: returns a nil error.
    =======================================================================
    
    Default ATS Secure Connection
    ---
    ATS Default Connection
    ATS Dictionary:
    {
    }
    Result : PASS
    ---
    
    =======================================================================
    
    Allowing Arbitrary Loads
    
    ---
    Allow All Loads
    ATS Dictionary:
    {
        NSAllowsArbitraryLoads = true;
    }
    Result : PASS
    ---
    
    =======================================================================
    
    Configuring TLS exceptions for www.zhihu.com
    
    ---
    TLSv1.2
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionMinimumTLSVersion = "TLSv1.2";
            };
        };
    }
    Result : PASS
    ---
    
    ---
    TLSv1.1
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionMinimumTLSVersion = "TLSv1.1";
            };
        };
    }
    Result : PASS
    ---
    
    ---
    TLSv1.0
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionMinimumTLSVersion = "TLSv1.0";
            };
        };
    }
    Result : PASS
    ---
    
    =======================================================================
    
    Configuring PFS exceptions for www.zhihu.com
    
    ---
    Disabling Perfect Forward Secrecy
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    
    =======================================================================
    
    Configuring PFS exceptions and allowing insecure HTTP for www.zhihu.com
    
    ---
    Disabling Perfect Forward Secrecy and Allowing Insecure HTTP
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionAllowsInsecureHTTPLoads = true;
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    
    =======================================================================
    
    Configuring TLS exceptions with PFS disabled for www.zhihu.com
    
    ---
    TLSv1.2 with PFS disabled
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionMinimumTLSVersion = "TLSv1.2";
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    
    ---
    TLSv1.1 with PFS disabled
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionMinimumTLSVersion = "TLSv1.1";
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    
    ---
    TLSv1.0 with PFS disabled
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionMinimumTLSVersion = "TLSv1.0";
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    
    =======================================================================
    
    Configuring TLS exceptions with PFS disabled and insecure HTTP allowed for www.zhihu.com
    
    ---
    TLSv1.2 with PFS disabled and insecure HTTP allowed
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionAllowsInsecureHTTPLoads = true;
                NSExceptionMinimumTLSVersion = "TLSv1.2";
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    
    ---
    TLSv1.1 with PFS disabled and insecure HTTP allowed
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionAllowsInsecureHTTPLoads = true;
                NSExceptionMinimumTLSVersion = "TLSv1.1";
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    
    ---
    TLSv1.0 with PFS disabled and insecure HTTP allowed
    ATS Dictionary:
    {
        NSExceptionDomains =     {
            "www.zhihu.com" =         {
                NSExceptionAllowsInsecureHTTPLoads = true;
                NSExceptionMinimumTLSVersion = "TLSv1.0";
                NSExceptionRequiresForwardSecrecy = false;
            };
        };
    }
    Result : PASS
    ---
    =====================================================================
    

    可以看到知乎在所有的测试案例中都是PASS,由此证明知乎通过了ATS检测,给知乎对于用户信息安全的保护点个赞。

    然后,进入喜闻乐见的黑百度时间:

    nscurl --ats-diagnostics --verbose https://www.baidu.com
    

    然而结果显示,百度同样通过了ATS测试,所以说百度做得也不错。最后被黑到的是新浪微博,感兴趣的知友可以试试,这里就不赘述了。现在我们已经有了百度和知乎这两个HTTPS服务器,接下来就看客户端是否能够成功访问了。


    iOS端如何实现HTTPS

    基于对HTTPS运行机制的理解,我们知道,在iOS客户端实现与服务器的HTTPS通信,前提条件是你服务器是一个提供了HTTPS的服务器。如果前提得以满足,那么iOS客户端就需要向服务器发出请求索要公钥,而后验证公钥,然后进行握手,左后开始加密通信。那么,具体怎么做呢?难倒这些都需要我自己实现吗?肯定不是的,这种基础性工作,苹果早就做好了,著名的第三方库AFNetworking也早就做好了。本文中主要学习和实践基于AFNetworking的通信。

    在上文中提到过,经过CA认证的HTTPS服务器是最好的,在iOS客户端这里基本上不需要做太多ATS适配,现在我们就尝试一下。在上篇功课使用CocoaPods安装AFNetworking并测试中,已经通过设置ATS能够实现HTTP的GET网络请求,这里我们就基于该项目进行修改如下:

    • 首先,删除掉info.plist文件中App Transport Security Settings及其子项Allow Arbitrary Loads,让ATS恢复到默认状态。
    • 其次,修改viewDidLoad方法中的代码,主要将url修改为HTTPS服务器的url:
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 将上次实验的URL注释掉
        // NSString *urlString = @"http://api.jirengu.com/weather.php";
        // NSString *urlString = @"http://itunes.apple.com/search?term=metallica";
    
        // 使用baidu的HTTPS链接
        NSString *urlString = @"https://www.baidu.com";
        NSURL *url = [NSURL URLWithString:urlString];
        
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        [manager GET:url.absoluteString parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
            
            NSLog(@"results: %@", responseObject);
            
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            
            NSLog(@"results: %@", error);
            
        }];
    
    }
    

    运行项目,得到报错如下:

    ...JSON text did not start with array or object and option to allow fragments not set
    

    通过查询,发现问题是因为AFNetworking默认把请求的相应结果认为是JSON数据,然而实际上我们输入百度的地址,得到的应该是html数据。但是AFNetworking并不知道,它坚信请求的结果就是一个json文本,然后固执地以json的形式去解析,这样显然没办法把一个网页解析成一个字典或者数组,所以产生了上述错误。

    解决办法很简答,在代码中添加一句:

    manager.responseSerializer = [AFHTTPResponseSerializer serializer]
    

    告诉AFNetworking别把这个网页当成JSON数据来解析!于是修改代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // 将上次实验的URL注释掉
        // NSString *urlString = @"http://api.jirengu.com/weather.php";
        // NSString *urlString = @"http://itunes.apple.com/search?term=metallica";
        // 使用baidu的HTTPS链接
        NSString *urlString = @"https://www.baidu.com";
        NSURL *url = [NSURL URLWithString:urlString];
        
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    
        manager.responseSerializer = [AFHTTPResponseSerializer serializer]
    
        [manager GET:url.absoluteString parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
            
            NSLog(@"results: %@", responseObject);
            
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            
            NSLog(@"results: %@", error);
            
        }];
    
    }
    

    运行项目,得到输出如下:

    2016-10-08 20:00:18.052 HTTPS[1916:359510] results: <3c21444f 43545950 45206874 6d6c3e0a 3c68746d 6c3e0a20 2020203c 212d2d53 54415455 53204f4b 2d2d3e0a 20202020 3c686561 643e0a20 20202020 2020203c  ...... 6c3e0a>
    Message from debugger: Terminated due to signal 15
    

    显然,这次成功地获取到了数据。将HTTPS链接换为知乎的URL,得到的结果类似。

    由此可知,对于符合ATS要求的HTTPS服务器,在iOS端不需要对ATS做特殊的适配就能和HTTPS服务器进行通信。而要符合ATS要求,则需要老老实实地创建证书请求,然后到权威机构认证,随之配置到服务器。

    小结:在本篇功课中,对HTTPS通信的原理和机制有了一定了解,实现了利用AFNetworking库向符合ATS要求的HTTPS服务器的通信。

    下一步

    学习自己搭建服务器,创建证书请求,然后到权威机构认证,然后配置到服务器使之成为符合ATS要求的HTTPS服务器,最后实现iOS端与服务器端的安全通信。

    作者反馈

    1. 自娱自乐的iOS开发学习,欢迎专业开发者批评指教;
    2. 对于学习自建证书,然后配置给服务器的方式,没有计划去学习;
    3. 注意:知友@兰卿私信指正:
    文章里的HTTPS请求通过Charles是可以抓到包且查看每一个字段的,Charles是通过代理,AFN没有在有代理的情况下对HTTPS进行限制,对用户来说是不安全的。ASI可以做限制,有代理则请求不成功,保证即使被抓包,也无法得到字段。

    个人对于@兰卿指出的问题不是很清楚,还需要查阅资料进行学习验证,后续搞明白之后在修正到文中。本着对用户安全负责的原则,将这个指正放在这里,请各位知友注意。最后感谢@兰卿的批评指正。

    编辑于 2016-10-11