AFNetworking之于https认证

96
涂耀辉
2016.12.09 14:24* 字数 4455

写在开头:
  • 本来这篇内容准备写在AFNetworking到底做了什么?(三)中的,但是因为我想在三中完结这个系列,碍于篇幅所限、并且这一块内容独立性比较强,所以单独拎出来,写成一篇。
  • 本文从源码的角度,去分析AFNetworking对https的认证过程。旨在让读者明白我们去做https请求:

    • 如果使用AF,需要做什么。
    • 不使用的话,直接用原生NSUrlSession,又需要做什么。
    • 当我们使用自签证书的https,又需要注意哪些问题。
  • 单独看并不影响阅读。如果有需要了解更多AF相关内容,可以关注楼主的系列文章:
    AFNetworking到底做了什么?
    AFNetworking到底做了什么?(二)

那么正文开始了:

简单的理解下https:https在http请求的基础上多加了一个证书认证的流程。认证通过之后,数据传输都是加密进行的。
关于https的更多概念,我就不赘述了,网上有大量的文章,小伙伴们可以自行查阅。在这里大概的讲讲https的认证过程吧,如下图所示:


https单向认证过程.jpg

1. 客户端发起HTTPS请求
  这个没什么好说的,就是用户在浏览器里输入一个https网址,然后连接到server的443端口。
2. 服务端的配置
  采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
3. 传送证书
  这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。
4. 客户端解析证书
  这部分工作是有客户端的TLS/SSL来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随机值。然后用证书对该随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
5. 传送加密信息
  这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
6. 服务段解密信息
  服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。
7. 传输加密后的信息
  这部分信息是服务段用私钥加密后的信息,可以在客户端被还原。
8. 客户端解密信息
  客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。

这就是整个https验证的流程了。简单总结一下:

  • 就是用户发起请求,服务器响应后返回一个证书,证书中包含一些基本信息和公钥。
  • 用户拿到证书后,去验证这个证书是否合法,不合法,则请求终止。
  • 合法则生成一个随机数,作为对称加密的密钥,用服务器返回的公钥对这个随机数加密。然后返回给服务器。
  • 服务器拿到加密后的随机数,利用私钥解密,然后再用解密后的随机数(对称密钥),把需要返回的数据加密,加密完成后数据传输给用户。
  • 最后用户拿到加密的数据,用一开始的那个随机数(对称密钥),进行数据解密。整个过程完成。

当然这仅仅是一个单向认证,https还会有双向认证,相对于单向认证也很简单。仅仅多了服务端验证客户端这一步。感兴趣的可以看看这篇:Https单向认证和双向认证。

了解了https认证流程后,接下来我们来讲讲AFSecurityPolicy这个类,AF就是用这个类来满足我们各种https认证需求。

在这之前我们来看看AF用来做https认证的代理:

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    //挑战处理类型为 默认
    /*
     NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
     NSURLSessionAuthChallengeUseCredential:使用指定的证书
     NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战
     */
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    // sessionDidReceiveAuthenticationChallenge是自定义方法,用来如何应对服务器端的认证挑战

    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
         // 此处服务器要求客户端的接收认证挑战方法是NSURLAuthenticationMethodServerTrust
        // 也就是说服务器端需要客户端返回一个根据认证挑战的保护空间提供的信任(即challenge.protectionSpace.serverTrust)产生的挑战证书。

        // 而这个证书就需要使用credentialForTrust:来创建一个NSURLCredential对象
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {

            // 基于客户端的安全策略来决定是否信任该服务器,不信任的话,也就没必要响应挑战
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                // 创建挑战证书(注:挑战方式为UseCredential和PerformDefaultHandling都需要新建挑战证书)
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                // 确定挑战的方式
                if (credential) {
                    //证书挑战  设计policy,none,则跑到这里
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                //取消挑战
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            //默认挑战方式
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
    //完成挑战
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

更多的这个方法的细节问题,可以看注释,或者查阅楼主之前的相关文章,都有去讲到这个代理方法。在这里我们大概的讲讲这个方法做了什么:
1)首先指定了https为默认的认证方式。
2)判断有没有自定义Block:sessionDidReceiveAuthenticationChallenge,有的话,使用我们自定义Block,生成一个认证方式,并且可以给credential赋值,即我们需要接受认证的证书。然后直接调用completionHandler,去根据这两个参数,执行系统的认证。至于这个系统的认证到底做了什么,可以看文章最后,这里暂且略过。
3)如果没有自定义Block,我们判断如果服务端的认证方法要求是NSURLAuthenticationMethodServerTrust,则只需要验证服务端证书是否安全(即https的单向认证,这是AF默认处理的认证方式,其他的认证方式,只能由我们自定义Block的实现)
4)接着我们就执行了AFSecurityPolicy相关的一个方法,做了一个AF内部的一个https认证:

[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host])

AF默认的处理是,如果这行返回NO、说明AF内部认证失败,则取消https认证,即取消请求。返回YES则进入if块,用服务器返回的一个serverTrust去生成了一个认证证书。(注:这个serverTrust是服务器传过来的,里面包含了服务器的证书信息,是用来我们本地客户端去验证该证书是否合法用的,后面会更详细的去讲这个参数)然后如果有证书,则用证书认证方式,否则还是用默认的验证方式。最后调用completionHandler传递认证方式和要认证的证书,去做系统根证书验证。

  • 总结一下这里securityPolicy存在的作用就是,使得在系统底层自己去验证之前,AF可以先去验证服务端的证书。如果通不过,则直接越过系统的验证,取消https的网络请求。否则,继续去走系统根证书的验证。
接下来我们看看AFSecurityPolicy内部是如果做https认证的:

如下方式,我们可以创建一个securityPolicy

AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];

内部创建:

+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
    return securityPolicy;
}

默认指定了一个SSLPinningMode模式为AFSSLPinningModeNone
对于AFSecurityPolicy,一共有4个重要的属性:

//https验证模式
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
//可以去匹配服务端证书验证的证书
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
//是否支持非法的证书(例如自签名证书)
@property (nonatomic, assign) BOOL allowInvalidCertificates;
//是否去验证证书域名是否匹配
@property (nonatomic, assign) BOOL validatesDomainName;

它们的作用我添加在注释里了,第一条就是AFSSLPinningMode, 共提供了3种验证方式:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    //不验证
    AFSSLPinningModeNone,
    //只验证公钥
    AFSSLPinningModePublicKey,
    //验证证书
    AFSSLPinningModeCertificate,
};

我们接着回到代理https认证的这行代码上:

[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]
  • 我们传了两个参数进去,一个是SecTrustRef类型的serverTrust,这是什么呢?我们看到苹果的文档介绍如下:

    CFType used for performing X.509 certificate trust evaluations.

    大概意思是用于执行X。509证书信任评估,
    再讲简单点,其实就是一个容器,装了服务器端需要验证的证书的基本信息、公钥等等,不仅如此,它还可以装一些评估策略,还有客户端的锚点证书,这个客户端的证书,可以用来和服务端的证书去匹配验证的。

  • 除此之外还把服务器域名传了过去。

我们来到这个方法,代码如下:

//验证服务端是否值得信任
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    //判断矛盾的条件
    //判断有域名,且允许自建证书,需要验证域名,
    //因为要验证域名,所以必须不能是后者两种:AFSSLPinningModeNone或者添加到项目里的证书为0个。
    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) {

        // 如果需要验证domain,那么就使用SecPolicyCreateSSL函数创建验证策略,其中第一个参数为true表示验证整个SSL证书链,第二个参数传入domain,用于判断整个证书链上叶子节点表示的那个domain是否和此处传入domain一致
        //添加验证策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        // 如果不需要验证domain,就使用默认的BasicX509验证策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    //serverTrust:X。509服务器的证书信任。
    // 为serverTrust设置验证策略,即告诉客户端如何验证serverTrust
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);


    //有验证策略了,可以去验证了。如果是AFSSLPinningModeNone,是自签名,直接返回可信任,否则不是自签名的就去系统根证书里去找是否有匹配的证书。
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        //如果支持自签名,直接返回YES,不允许才去判断第二个条件,判断serverTrust是否有效
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    }
    //如果验证无效AFServerTrustIsValid,而且allowInvalidCertificates不允许自签,返回NO
    else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    //判断SSLPinningMode
    switch (self.SSLPinningMode) {
        // 理论上,上面那个部分已经解决了self.SSLPinningMode)为AFSSLPinningModeNone)等情况,所以此处再遇到,就直接返回NO
        case AFSSLPinningModeNone:
        default:
            return NO;

        //验证证书类型
        case AFSSLPinningModeCertificate: {

            NSMutableArray *pinnedCertificates = [NSMutableArray array];

            //把证书data,用系统api转成 SecCertificateRef 类型的数据,SecCertificateCreateWithData函数对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书

            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            // 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证。
            //serverTrust是服务器来的验证,有需要被验证的证书。
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            //自签在之前是验证通过不了的,在这一步,把我们自己设置的证书加进去之后,就能验证成功了。

            //再去调用之前的serverTrust去验证该证书是否有效,有可能:经过这个方法过滤后,serverTrust里面的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);

            //reverseObjectEnumerator逆序
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {

                //如果我们的证书中,有一个和它证书链中的证书匹配的,就返回YES
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            //没有匹配的
            return NO;
        }
            //公钥验证 AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
        case AFSSLPinningModePublicKey: {

            NSUInteger trustedPublicKeyCount = 0;

            // 从serverTrust中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            //遍历服务端公钥
            for (id trustChainPublicKey in publicKeys) {
                //遍历本地公钥
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    //判断如果相同 trustedPublicKeyCount+1
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }

    return NO;
}

代码的注释很多,这一块确实比枯涩,大家可以参照着源码一起看,加深理解。

  • 这个方法是AFSecurityPolicy最核心的方法,其他的都是为了配合这个方法。这个方法完成了服务端的证书的信任评估。我们总结一下这个方法做了什么(细节可以看注释):

    1. 根据模式,如果是AFSSLPinningModeNone,则肯定是返回YES,不论是自签还是公信机构的证书。
    2. 如果是AFSSLPinningModeCertificate,则从serverTrust中去获取证书链,然后和我们一开始初始化设置的证书集合self.pinnedCertificates去匹配,如果有一对能匹配成功的,就返回YES,否则NO。
      看到这可能有小伙伴要问了,什么是证书链?下面这段是我从百科上摘来的:

      证书链由两个环节组成—信任锚(CA 证书)环节和已签名证书环节。自我签名的证书仅有一个环节的长度—信任锚环节就是已签名证书本身。

      简单来说,证书链就是就是根证书,和根据根证书签名派发得到的证书。

    3. 如果是AFSSLPinningModePublicKey公钥验证,则和第二步一样还是从serverTrust,获取证书链每一个证书的公钥,放到数组中。和我们的self.pinnedPublicKeys,去配对,如果有一个相同的,就返回YES,否则NO。至于这个self.pinnedPublicKeys,初始化的地方如下:

      //设置证书数组
      - (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
      
      _pinnedCertificates = pinnedCertificates;
      
      //获取对应公钥集合
      if (self.pinnedCertificates) {
         //创建公钥集合
         NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]];
         //从证书中拿到公钥。
         for (NSData *certificate in self.pinnedCertificates) {
             id publicKey = AFPublicKeyForCertificate(certificate);
             if (!publicKey) {
                 continue;
             }
             [mutablePinnedPublicKeys addObject:publicKey];
         }
         self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys];
      } else {
         self.pinnedPublicKeys = nil;
      }
      }

      AF复写了设置证书的set方法,并同时把证书中每个公钥放在了self.pinnedPublicKeys中。

这个方法中关联了一系列的函数,我在这边按照调用顺序一一列出来(有些是系统函数,不在这里列出,会在下文集体描述作用):

函数一:AFServerTrustIsValid
//判断serverTrust是否有效
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {

    //默认无效
    BOOL isValid = NO;
    //用来装验证结果,枚举
    SecTrustResultType result;  

    //__Require_noErr_Quiet 用来判断前者是0还是非0,如果0则表示没错,就跳到后面的表达式所在位置去执行,否则表示有错就继续往下执行。

    //SecTrustEvaluate系统评估证书的是否可信的函数,去系统根目录找,然后把结果赋值给result。评估结果匹配,返回0,否则出错返回非0
    //do while 0 ,只执行一次,为啥要这样写....
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);

    //评估没出错走掉这,只有两种结果能设置为有效,isValid= 1
    //当result为kSecTrustResultUnspecified(此标志表示serverTrust评估成功,此证书也被暗中信任了,但是用户并没有显示地决定信任该证书)。
    //或者当result为kSecTrustResultProceed(此标志表示评估成功,和上面不同的是该评估得到了用户认可),这两者取其一就可以认为对serverTrust评估成功
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

    //out函数块,如果为SecTrustEvaluate,返回非0,则评估出错,则isValid为NO
_out:
    return isValid;
}
  • 这个方法用来验证serverTrust是否有效,其中主要是交由系统APISecTrustEvaluate来验证的,它验证完之后会返回一个SecTrustResultType枚举类型的result,然后我们根据这个result去判断是否证书是否有效。
  • 其中比较有意思的是,它调用了一个系统定义的宏函数__Require_noErr_Quiet,函数定义如下:
    #ifndef __Require_noErr_Quiet
      #define __Require_noErr_Quiet(errorCode, exceptionLabel)                      \
        do                                                                          \
        {                                                                           \
            if ( __builtin_expect(0 != (errorCode), 0) )                            \
            {                                                                       \
                goto exceptionLabel;                                                \
            }                                                                       \
        } while ( 0 )
    #endif
    这个函数主要作用就是,判断errorCode是否为0,不为0则,程序用goto跳到exceptionLabel位置去执行。这个exceptionLabel就是一个代码位置标识,类似上面的_out
    说它有意思的地方是在于,它用了一个do...while(0)循环,循环条件为0,也就是只执行一次循环就结束。对这么做的原因,楼主百思不得其解...看来系统原生API更是高深莫测...经冰霜大神的提醒,这么做是为了适配早期的API??!
函数二、三(两个函数类似,所以放在一起):获取serverTrust证书链证书,获取serverTrust证书链公钥
//获取证书链
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    //使用SecTrustGetCertificateCount函数获取到serverTrust中需要评估的证书链中的证书数目,并保存到certificateCount中
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    //创建数组
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];

    //// 使用SecTrustGetCertificateAtIndex函数获取到证书链中的每个证书,并添加到trustChain中,最后返回trustChain
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
    }

    return [NSArray arrayWithArray:trustChain];
}
// 从serverTrust中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {

    // 接下来的一小段代码和上面AFCertificateTrustChainForServerTrust函数的作用基本一致,都是为了获取到serverTrust中证书链上的所有证书,并依次遍历,取出公钥。
    //安全策略
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    //遍历serverTrust里证书的证书链。
    for (CFIndex i = 0; i < certificateCount; i++) {
        //从证书链取证书
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        //数组
        SecCertificateRef someCertificates[] = {certificate};
        //CF数组
        CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);

        SecTrustRef trust;

        // 根据给定的certificates和policy来生成一个trust对象
        //不成功跳到 _out。
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);

        SecTrustResultType result;

        // 使用SecTrustEvaluate来评估上面构建的trust
        //评估失败跳到 _out
        __Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);

        // 如果该trust符合X.509证书格式,那么先使用SecTrustCopyPublicKey获取到trust的公钥,再将此公钥添加到trustChain中
        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

    _out:
        //释放资源
        if (trust) {
            CFRelease(trust);
        }

        if (certificates) {
            CFRelease(certificates);
        }

        continue;
    }
    CFRelease(policy);

    // 返回对应的一组公钥
    return [NSArray arrayWithArray:trustChain];
}

两个方法功能类似,都是调用了一些系统的API,利用For循环,获取证书链上每一个证书或者公钥。具体内容看源码很好理解。唯一需要注意的是,这个获取的证书排序,是从证书链的叶节点,到根节点的。

函数四:判断公钥是否相同
//判断两个公钥是否相同
static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {

#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV
    //iOS 判断二者地址
    return [(__bridge id)key1 isEqual:(__bridge id)key2];
#else
    return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)];
#endif
}

方法适配了各种运行环境,做了匹配的判断。

接下来列出验证过程中调用过得系统原生函数:
//1.创建一个验证SSL的策略,两个参数,第一个参数true则表示验证整个证书链
//第二个参数传入domain,用于判断整个证书链上叶子节点表示的那个domain是否和此处传入domain一致
SecPolicyCreateSSL(<#Boolean server#>, <#CFStringRef  _Nullable hostname#>)
SecPolicyCreateBasicX509();
//2.默认的BasicX509验证策略,不验证域名。
SecPolicyCreateBasicX509();
//3.为serverTrust设置验证策略,即告诉客户端如何验证serverTrust
SecTrustSetPolicies(<#SecTrustRef  _Nonnull trust#>, <#CFTypeRef  _Nonnull policies#>)
//4.验证serverTrust,并且把验证结果返回给第二参数 result
SecTrustEvaluate(<#SecTrustRef  _Nonnull trust#>, <#SecTrustResultType * _Nullable result#>)
//5.判断前者errorCode是否为0,为0则跳到exceptionLabel处执行代码
__Require_noErr(<#errorCode#>, <#exceptionLabel#>)
//6.根据证书data,去创建SecCertificateRef类型的数据。
SecCertificateCreateWithData(<#CFAllocatorRef  _Nullable allocator#>, <#CFDataRef  _Nonnull data#>)
//7.给serverTrust设置锚点证书,即如果以后再次去验证serverTrust,会从锚点证书去找是否匹配。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//8.拿到证书链中的证书个数
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
//9.去取得证书链中对应下标的证书。
SecTrustGetCertificateAtIndex(serverTrust, i)
//10.根据证书获取公钥。
SecTrustCopyPublicKey(trust)

其功能如注释,大家可以对比着源码,去加以理解~


分割图.png


可能看到这,又有些小伙伴迷糊了,讲了这么多,那如果做https请求,真正需要我们自己做的到底是什么呢?这里来解答一下,分为以下两种情况:

  1. 如果你用的是付费的公信机构颁发的证书,标准的https,那么无论你用的是AF还是NSUrlSession,什么都不用做,代理方法也不用实现。你的网络请求就能正常完成。
  2. 如果你用的是自签名的证书:

    • 首先你需要在plist文件中,设置可以返回不安全的请求(关闭该域名的ATS)。
    • 其次,如果是NSUrlSesion,那么需要在代理方法实现如下:

      - (void)URLSession:(NSURLSession *)session
      didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
      completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
      {
           __block NSURLCredential *credential = nil;
      
          credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; 
         // 确定挑战的方式
          if (credential) { 
              //证书挑战 则跑到这里
              disposition = NSURLSessionAuthChallengeUseCredential; 
          }
         //完成挑战
          if (completionHandler) {
              completionHandler(disposition, credential);
          }
      }

      其实上述就是AF的相对于自签证书的实现的简化版。
      如果是AF,你则需要设置policy:

      //允许自签名证书,必须的
      policy.allowInvalidCertificates = YES;
      //是否验证域名的CN字段
      //不是必须的,但是如果写YES,则必须导入证书。
      policy.validatesDomainName = NO;

      当然还可以根据需求,你可以去验证证书或者公钥,前提是,你把自签的服务端证书,或者自签的CA根证书导入到项目中:


      证书.png


      并且如下设置证书:

      NSString *certFilePath = [[NSBundle mainBundle] pathForResource:@"AFUse_server.cer" ofType:nil];
      NSData *certData = [NSData dataWithContentsOfFile:certFilePath];
      NSSet *certSet = [NSSet setWithObjects:certData,certData, nil];    
      policy.pinnedCertificates = certSet;

      这样你就可以使用AF的不同AFSSLPinningMode去验证了。

最后总结一下,AF之于https到底做了什么:
  • AF可以让你在系统验证证书之前,就去自主验证。然后如果自己验证不正确,直接取消网络请求。否则验证通过则继续进行系统验证。

  • 讲到这,顺便提一下,系统验证的流程:

    • 系统的验证,首先是去系统的根证书找,看是否有能匹配服务端的证书,如果匹配,则验证成功,返回https的安全数据。
    • 如果不匹配则去判断ATS是否关闭,如果关闭,则返回https不安全连接的数据。如果开启ATS,则拒绝这个请求,请求失败。

总之一句话:AF的验证方式不是必须的,但是对有特殊验证需求的用户确是必要的

写在结尾:

  • 看完之后,有些小伙伴可能还是会比较迷惑,建议还是不清楚的小伙伴,可以自己生成一个自签名的证书或者用百度地址等做请求,然后设置AFSecurityPolicy不同参数,打断点,一步步的看AF是如何去调用函数作证书验证的。相信这样能加深你的理解。

  • 最后关于自签名证书的问题,等2017年1月1日,也没多久了...一个月不到。除非有特殊原因说明,否则已经无法审核通过了。详细的可以看看这篇文章:iOS 10 适配 ATS(app支持https通过App Store审核)

  • 最后的最后,希望大家能点个赞,关注一下~(楼主看到赞和关注会很开心...) 有什么不同意见或者建议可以评论或者简信我~万一有人转载,麻烦注明出处,谢谢~~
苹果官网最新消息:原定于2017.1.1强制的https被延期了,具体延期到什么时候不确定,得等官方通知:

苹果官方新闻.png
后续文章:

AFNetworking之UIKit扩展与缓存实现
AFNetworking到底做了什么?(终)

iOS