iOS 移动开发网络 part4:HTTPS

作者: 破弓 | 来源:发表于2017-10-13 15:50 被阅读20次

    1.准备

    1.1 SSL/TLS

    普通HTTP协议,信息都是以明文传送的.这个过程中,如果我们需要传送一些敏感信息(如:银行卡账号,密码等),就存在着消息在中间节点泄漏的风险.HTTPS就是为了保障传送过程中的安全而生的.HTTPS保证了数据仅在发送方和目的方可见,而对中间任何一个节点都不可见.

    HTTPS == HTTP over SSL/TLS.一切为了安全.

    SSL/TLS
    
    1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。
    1995年,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。
    1996年,SSL 3.0版问世,得到大规模应用。
    1999年,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版[TLS](http://en.wikipedia.org/wiki/Secure_Sockets_Layer) 1.0版。
    2006年和2008年,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的[修订版](http://tools.ietf.org/html/rfc6176)。
    
    目前,应用最广泛的是TLS 1.0,接下来是SSL 3.0。但是,主流浏览器都已经实现了TLS 1.2的支持。
    TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。
    
    所以SSL就是TLS,可以视作同一个东西的不同阶段.
    
    1.2 加密方式

    在讲HTTPS的流程前,先要说两种加密的方式.

    • 对称加密

    假如你想与小红进行通信,又不想在传递过程中,被别人看懂明文.
    你就和小红约定,你发的内容统一加4再发送,小红收到后,统一减4再查看.

    加密:520==>964
    解密:964==>520

    这样通信双方持有相同密钥的加密方式就是对称加密.

    • 非对称加密

    非对称加密就是通信双方持有不同密钥的加密方式,这两个密钥称为公钥和私钥.公钥加密的结果只有私钥能解开,私钥加密的结果只有公钥能解开.

    非对称加密算法:RSA,DSA/DSS
    对称加密算法:AES,RC4,3DES
    

    那么HTTPS用的是哪种加密方式呢?非对称加密与对称加密都用了,具体见下文.

    2.流程

    HTTPS可以总结为:非对称加密约定双方的对话密钥,然后双方利用对话密钥进行对称加密的对话.

    非对称加密十分损耗硬件性能,所以不适合做多频次的加密对话.
    
    SSL Handshake.png
    第一步,客户端给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
    第二步,服务端确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
    第三步,客户端确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给服务端。
    第四步,服务端使用自己的私钥,获取客户端发来的随机数(即Premaster secret)。
    第五步,客户端和服务端根据约定的加密方法,使用前面的三个随机数,生成"对话密钥"(session key),用来加密接下来的整个对话过程。
    

    以上5步就是非对称加密约定双方的对话密钥的过程.接下去就是利用对话密钥进行对称加密的对话.

    在阅读许多博客的时候,很多作者都在HTTPS的流程中说只用了一个随机数就生成了对话密钥,读起来也真是一点毛病没有,那为什么要用三个随机数生成了对话密钥呢?
    这是由于SSL/TLS设计,就假设服务器不相信所有的客户端都能够提供完全随机数,假如某个客户端提供的随机数不随机的话,就大大增加了对话密钥被破解的风险,所以由三个随机数组成最后的随机数,保证了随机数的随机性,以此来保证每次生成的对话密钥安全性.

    3.HTTPS证书

    3.1 没有证书的公钥传递不安全

    由上面的流程可以知道,公钥是放在证书内,然后传给客户端的.不放在证书行不行呢?以下就是公钥是不放在证书内产生的中间人攻击.

    man_in_mid_1.png

    另一个问题来了,证书是个什么玩意儿,为什么公钥放在证书就可以信得过?

    3.2 证书构造+证书验证

    数字证书是一个电子文档,其中包含了持有者的信息、公钥以及证明该证书有效的数字签名。而数字证书以及相关的公钥管理和验证等技术组成了PKI(公钥基础设施)规范体系。一般来说,数字证书是由数字证书认证机构(Certificate authority,即CA)来负责签发和管理,并承担PKI体系中公钥合法性的检验责任;数字证书的类型有很多,而HTTPS使用的是SSL证书。
    怎么来验证数字证书是由CA签发的,而不是第三方伪造的呢? 在回答这个问题前,我们需要先了解CA的组织结构。首先,CA组织结构中,最顶层的就是根CA,根CA下可以授权给多个二级CA,而二级CA又可以授权多个三级CA,所以CA的组织结构是一个树结构。对于SSL证书市场来说,主要被Symantec(旗下有VeriSign和GeoTrust)、Comodo SSL、Go Daddy 和 GlobalSign 瓜分.

    以浏览器访问百度为例,在浏览器内查看证书信息:


    DingTalk20171012105043.png
    一级:GlobalSign Root CA
    二级:GlobalSign Organization Validation CA - SHA256 - G2
    三级:baidu.com
    

    一级 签发 二级
    二级 签发 三级(百度交钱给CA机构,CA结构签发一个三级证书给百度)

    • 签发

    证书中的持有者信息和公钥都是由申请者提供的,而数字签名则是CA机构对证书内容进行hash加密后得到的,而这个数字签名就是我们验证证书是否是有可信CA签发的证据.

    证书基本内容得到hash摘要,hash摘要经过签发机构的私钥加密生成数字签名.
    数字签名+证书基本内容==>最终的证书.

    CA.png
    • 配置

    一般操作系统和浏览器只包含CA根证书(如下图),而在配置Web服务器的HTTPS时,也会将配置整个证书链.

    CA Root in Mac
    • 验证

    当客户端走 HTTPS 访问站点时,服务器会返回整个证书链.
    浏览器或者操作系统都会内置CA根证书.

    客户端拿到整个证书链,先由下向上找CA根证书,看服务器传过来的CA根证书是否在客户端内置CA根证书列表内.
    不在列表内,验证失败.
    在列表内,再由上向下,验证证书签名.

    一级:GlobalSign Root CA
    二级:GlobalSign Organization Validation CA - SHA256 - G2
    三级:baidu.com
    

    一级证书的公钥解开二级证书的证书签名得到证书基本内容的hash摘要==>摘要1;
    由二级证书的证书基本内容得到hash摘要==>摘要2;
    比对两个摘要.
    相等,则表示证书没有被篡改,继续向下重复以上验证流程,直到验证到末端的证书==>验证完成.

    digtal_certificate_sign
    • 锚点

    在上面的验证过程中,客户端拿到整个证书链,先由下向上找CA根证书,看服务器传过来的CA根证书是否在客户端内置CA根证书列表内.
    这些系统隐式信任的证书(即:CA根证书),被称为可信锚点(trusted anchor).
    可信锚点通常指的就是系统中的CA根证书.不过我们也可以在验证证书链时,设置自定义证书作为可信锚点.(下文将结合代码讲解设置自定义证书作为可信锚点)

    3.3 有证书的公钥传递也不一定安全

    以上的证书签名可以防止篡改证书.而如果中间人玩得更绝==>调包证书.

    中间人调包了服务器下发的证书.毕竟赚钱的生意,权威CA机构也不会拒绝向中间人签发证书.

    调包证书和3.1的调包公钥最终效果是一样的.

    中间人企图直接调包证书,客户端可以校验证书中的域名与自己访问的域名是否一致来防止这种中间人攻击.

    4.HTTPS in AFNetworking

    AFNetworking内主管HTTPS验证的是AFSecurityPolicy.以下是AFSecurityPolicy多个属性的解读.

    /*
     1.SSLPinningMode 模式:默认==>AFSSLPinningModeNone
     AFSSLPinningModeNone==>不进行验证
     AFSSLPinningModePublicKey==>用本地的证书的公钥进行验证
     AFSSLPinningModeCertificate==>用本地的证书进行验证
     */
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    
    //2.allowInvalidCertificates 是否允许自签名:默认==>NO
    securityPolicy.allowInvalidCertificates = NO;
    
    //3.validatesDomainName 是否验证域名:默认==>YES
    securityPolicy.validatesDomainName = NO;
    
    //4.pinnedCertificates 本地导入的证书:默认==>app包内所有的.cer全会被添加进来
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    securityPolicy.pinnedCertificates = [NSSet setWithObject:certData];
    
    //5.pinnedPublicKeys 设置pinnedCertificates的时候,会同时读取证书内的公钥,然后设置pinnedPublicKeys
    

    AFNetworking内的AFURLSessionManager会在NSURLSession的代理方法中调用AFSecurityPolicy.

    AFNetworkingAFURLSessionManagerAFSecurityPolicy的所属关系就不再赘述.有疑问请戳==>iOS 移动开发网络 part2

    - (void)URLSession:(NSURLSession *)session
    didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
     completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
    {
    
    /*
    NSURLSessionAuthChallengePerformDefaultHandling   由系统默认处理挑战
    NSURLSessionAuthChallengeCancelAuthenticationChallenge   取消挑战
    NSURLSessionAuthChallengeUseCredential   本地创建信任状应答挑战
    */
    
        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        __block NSURLCredential *credential = nil;
    
       //AFURLSessionManager是否有处理认证挑战的block,有就交由block处理
        if (self.sessionDidReceiveAuthenticationChallenge) {
            disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
        } else {
            //服务器发过来的认证挑战方法是否为NSURLAuthenticationMethodServerTrust
            if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
                //验证服务器发过来的证书是否值得信任
                if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                    //创建应答服务器挑战的信任状
                    credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                    if (credential) {
                        disposition = NSURLSessionAuthChallengeUseCredential;
                    } else {
                        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                    }
                } else {
                    disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
                }
            } else {
                disposition = NSURLSessionAuthChallengePerformDefaultHandling;
            }
        }
    
        if (completionHandler) {
            completionHandler(disposition, credential);
        }
    }
    

    可以看出- (BOOL)evaluateServerTrust:forDomain:方法是AFSecurityPolicy做HTTPS验证的关键.

    - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
          forDomain:(NSString *)domain
    {
     if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
      // https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
      //  According to the docs, you should only trust your provided certs for evaluation.
      //  Pinned certificates are added to the trust. Without pinned certificates,
      //  there is nothing to evaluate against.
      //
      //  From Apple Docs:
      //          "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
      //           Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
      NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
      return NO;
     }
     
     NSMutableArray *policies = [NSMutableArray array];
     if (self.validatesDomainName) {
      [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
     } else {
      [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
     }
     
     SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
     
     if (self.SSLPinningMode == AFSSLPinningModeNone) {
      return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
     } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
      return NO;
     }
     
     switch (self.SSLPinningMode) {
      case AFSSLPinningModeNone:
      default:
       return NO;
      case AFSSLPinningModeCertificate: {
       NSMutableArray *pinnedCertificates = [NSMutableArray array];
       for (NSData *certificateData in self.pinnedCertificates) {
        [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
       }
       SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
       
       if (!AFServerTrustIsValid(serverTrust)) {
        return NO;
       }
       
       // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
       NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
       
       for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
        if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
         return YES;//zc read:有一个对得上就,代表验证成功
        }
       }
       
       return NO;
      }
      case AFSSLPinningModePublicKey: {
       NSUInteger trustedPublicKeyCount = 0;
       NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
       
       for (id trustChainPublicKey in publicKeys) {
        for (id pinnedPublicKey in self.pinnedPublicKeys) {
         if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
          trustedPublicKeyCount += 1;
         }
        }
       }
       return trustedPublicKeyCount > 0;//zc read:有一个对得上就,代表验证成功
      }
     }
     
     return NO;
    }
    

    个人觉得这个方法内部实在是太繁琐了,一些条件重复出现,我就不对源码进行逐行加注释来讲解了.我根据我了解的AFSecurityPolicy各项属性的功能,改写了一个简单的版本,如下:

    - (BOOL)zc_evaluateServerTrust:(SecTrustRef)serverTrust
                      forDomain:(NSString *)domain
    {
        /*part1:*/
        //验证签名,却没有签名 返回NO
        if (self.validatesDomainName && domain == nil) {
            return NO;
        }
        
        //验证证书或公钥 却没有导入证书 返回NO
        if (self.SSLPinningMode != AFSSLPinningModeNone && self.pinnedCertificates.count <= 0) {
            return NO;
        }
        
        /*part2:*/
        //设置验证策略
        NSMutableArray *policies = [NSMutableArray array];
        if (self.validatesDomainName) {
            [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
        } else {
            [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
        }
        SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
        
        /*part3:*/
        //做分支判断
        if (self.SSLPinningMode == AFSSLPinningModeNone){
            //验证模式为不验证,结果取决于是否同意自签名和系统验证结果
            return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
        }else{
            //验证模式为验证证书或者验证公钥,系统验证为NO+self.allowInvalidCertificates为==>返回NO
            if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
                return NO;
            }
            //验证模式为验证公钥,获取传入证书链内所有的公钥,然后与本地的公钥进行比对.有一个对得上就,代表验证成功
            if (self.SSLPinningMode == AFSSLPinningModePublicKey){
                NSUInteger trustedPublicKeyCount = 0;
                NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
                
                for (id trustChainPublicKey in publicKeys) {
                    for (id pinnedPublicKey in self.pinnedPublicKeys) {
                        if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                            trustedPublicKeyCount += 1;
                        }
                    }
                }
                return trustedPublicKeyCount > 0;//zc read:有一个对得上就,代表验证成功
            }else if (self.SSLPinningMode == AFSSLPinningModeCertificate){
                
                //将本地的证书加入到系统的锚点证书列表,再进行系统验证
                NSMutableArray *pinnedCertificates = [NSMutableArray array];
                for (NSData *certificateData in self.pinnedCertificates) {
                    [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
                }
                SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
                
                if (!AFServerTrustIsValid(serverTrust)) {
                    return NO;
                }
    
                //如果以上系统验证成功,还要获取传入证书链内所有的证书,进行二进制数据比对.有一个对得上就,代表验证成功
                NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
                
                for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                    if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                        return YES;//zc read:有一个对得上就,代表验证成功
                    }
                }
            }
        }
        
        return NO;
    }
    

    简化后的代码我不敢保证没问题,我也在请教高人!
    但用于理解各个条件==>返回结果的流程应该是没问题的.

    SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)certificates);
    只调用SecTrustSetAnchorCertificates()这个函数的话,那么就只有作为参数被传入的证书作为可信锚点,连系统本身信任的CA根证书都不能作为可信锚点验证证书链.
    要想恢复系统中CA根证书作为可信锚点,还要再调用下面这个函数:
    
    SecTrustSetAnchorCertificatesOnly(trust, false);
    //true代表仅被传入的证书作为可信锚点,false允许系统CA根证书也作为可信锚点
    

    5.误区

    以下是我对于HTTPS的'S'错误理解:

    HTTP 可以 over TLS;
    WebSocket 也可以 over TLS;
    FTP 也可以 over TLS;
    
    错误理解:HTTPS,C/S协商密钥阶段,用TLS(服务端端口443),密钥协商好后数据传输走的是HTTP(服务端端口80).我概念里的HTTP始终离不开80端口.
    正确理解:HTTPS,C/S协商密钥阶段,用TLS(服务端端口443),密钥协商好后数据传输也一直是用TLS(服务端端口443).
    
    可以给予网页内容的不一定服务端的端口80,任何端口都可以.(如:http://xxhh:1312/?page=index.md 自家的api文档页面用就是端口1312)
    
    密钥约定好后,服务端通过'用TLS约定好的密钥'把网页内容加密,然后通过端口443发出,客户端收到后'用TLS约定好的密钥'做解密,得到可以做显示的HTML,浏览器显示.
    
    HTTPS跟80端口一点关系也没有,数据是数据,端口是端口,没有规定说,HTTP的数据必须走服务端的80端口发出.
    
    当然对于WebSocket和FTP也是一样的.
    

    文章参考:
    iOS安全系列之一:HTTPS
    简单粗暴系列之HTTPS原理
    看完还不懂HTTPS我直播吃翔
    SSL/TLS协议运行机制的概述
    iOS 中对 HTTPS 证书链的验证

    相关文章

      网友评论

        本文标题:iOS 移动开发网络 part4:HTTPS

        本文链接:https://www.haomeiwen.com/subject/wfxpyxtx.html