AFNetWorking 源码学习笔记 ☞ Security

AFNetWorking 源码学习笔记.png

一、前言

本文是 AFNetWorking 源码学习笔记的第三篇,本篇将介绍第二部分 -- Security 目录下的类,其实只有一个就是 AFSecurityPolicy。

AFNetWorking-Security.png

二、正文

依照惯例,先从声明文件开始,打开 AFSecurityPolicy.h 文件,我们发现内容非常少,只有 4 个属性和 5 个方法。

首先来看看这几个属性,各自的作用见代码注释。

// 根据 SSL 证书验证 server trust 的标准,默认为不验证。
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;

// 根据 SSL pinning mode 验证 server trust  的证书
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;

// 是否允许无效或已经过期的 SSL 证书。
@property (nonatomic, assign) BOOL allowInvalidCertificates;

// 是否验证证书中的域名,默认为 YES。
@property (nonatomic, assign) BOOL validatesDomainName;

然后再来看看它提供的 5 个代理方法,

+ (NSSet <NSData *> *)certificatesInBundle:(NSBundle *)bundle;

此类方法的作用是获取 Bundle 中的所有证书文件(xxx.cer)存放在 set 里边。

+ (instancetype)defaultPolicy;
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode;
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates;

这 3 个方法都在创建 AFSecurityPolicy 对象,第一个默认将属性 SSLPinningMode 设置为 AFSSLPinningModeNone,即不做验证;第二个可以指定 SSLPinningMode,最后一个还可以指定 pinnedCertificates。

下边这个实例方法负责基于当前安全策略评估传入的 serverTrust 是否可以接受,也就是我们在上一篇中接受认证挑战时掉那个的方法。

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(nullable NSString *)domain;

接下来,我们看看 .m 文件,这里就只介绍其中的两个主要方法实现。第一个是 - (void)setPinnedCertificates:(NSSet *)pinnedCertificates,这里不仅给 _pinnedCertificates 赋值,而且取出了所有证书中的公钥(public key),存放到集合 self.pinnedPublicKeys 里边。

- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
    // 1.给 _pinnedCertificates 赋值
    _pinnedCertificates = pinnedCertificates;

    // 2.给 self.pinnedPublicKeys 赋值,值是从 Certificates 中提取的 publicKeys
    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;
    }
}

提取 Certificate 中的 public key 时,利用了 AFPublicKeyForCertificate() 这个静态函数,下面来看看它的具体实现。

static id AFPublicKeyForCertificate(NSData *certificate) {
    // 1.初始化会用到的局部变量
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;

    // 2.提取公钥
    // ① 根据传入的二进制证书数据生成一个 SecCertificateRef 类型的证书,并验证结果
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    __Require_Quiet(allowedCertificate != NULL, _out);

    // ② 生成一个默认 X.509 policy 的实例
    policy = SecPolicyCreateBasicX509();
    
    // ③ 基于证书 (allowedCertificate) 和安全策略 (policy) 创建一个信任管理对象 (a trust management object)
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust), _out);
    // ④ 利用步骤 ③ 创建的 allowedTrust 校验证书,并将结果写入 result,不过后边好像没有用到它
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);

    // ⑤ 验证通过后,从 allowedTrust 中获取公钥
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

_out:
    // 3.释放内存
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }

    if (policy) {
        CFRelease(policy);
    }

    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }

    return allowedPublicKey;
}

从上边的代码中我们发现,重点在第 2 步获取公钥的代码,具体内容建代码注释,这里就不啰嗦了。其中用到了系统提供的 2 个宏:__Require_Quiet__Require_noErr_Quiet,下边来看看他们的定义:

#ifndef __Require_Quiet
    #define __Require_Quiet(assertion, exceptionLabel)                            \
      do                                                                          \
      {                                                                           \
          if ( __builtin_expect(!(assertion), 0) )                                \
          {                                                                       \
              goto exceptionLabel;                                                \
          }                                                                       \
      } while ( 0 )
#endif
#ifndef __Require_noErr_Quiet
    #define __Require_noErr_Quiet(errorCode, exceptionLabel)                      \
      do                                                                          \
      {                                                                           \
          if ( __builtin_expect(0 != (errorCode), 0) )                            \
          {                                                                       \
              goto exceptionLabel;                                                \
          }                                                                       \
      } while ( 0 )
#endif

从定义可以看出,对于 __Require_Quiet,当入参 assertion 为空的时候,会跳转到 exceptionLabel 所在位置去继续执行;而 __Require_noErr_Quiet,是当入参 errorCode != 0 即出错的时候会跳转至 exceptionLabel 所在行继续执行。

核心方法

下边我们来看看 上一篇 提到过的方法 evaluateServerTrust: forDomain:,该方法是 AFNetWorking 自己提前做的一次 HTTPS 认证,用于验证服务器是否合法,如果验证不通过就不再继续调用系统方法验证了。

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    // 1.过滤
    // 域名存在 && 允许自签名证书 && 需要验证域名 && (要求不做验证 || 没有证书)
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
        return NO;
    }

    // 2.准备安全策略,并将其设置到 serverTrust 里边,以备后边验证使用
    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    // Sets the policies to use in an evaluation.
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    // 3.再次过滤
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        // 不验证的情况下
        // 如果允许自签名证书或者 serverTrust 验证成功时,返回 YES
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        // 如果 serverTrust 验证失败 && 不允许自签名证书,则返回 NO
        return NO;
    }

    // 4.验证
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
            /**
             *  1.不验证
             *
             *  但 紧上边的那个 if-else 语句已经处理了 AFSSLPinningModeNone 的情况,这里还做这个判断有意义吗?难道是是为了看起来完整。
             */
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            
            /**
             *  2.验证证书
             *
             *  从serverTrust中去获取证书链,然后和我们一开始初始化设置的证书集合self.pinnedCertificates去匹配,如果有一对能匹配成功的,就返回YES,否则NO。
             */
            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;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {
            
            /**
             *  3.验证公钥
             *
             *  从serverTrust,获取证书链每一个证书的公钥,放到数组中。和我们的self.pinnedPublicKeys,去配对,如果有一个相同的,就返回YES,否则NO。
             */
            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;
        }
    }
    
    return NO;
}

代码稍微有点长,不过主要做了这些事:

  • 过滤一些特殊情况:域名存在 && 允许自签名证书 && 需要验证域名 && (要求不做验证 || 没有证书);
  • 准备安全策略,并将其设置到 serverTrust 里边,以备后边验证使用;
  • 再次过滤:不验证、serverTrust 验证失败 && 不允许自签名证书 的情况;
  • 开始验证,根据 self.SSLPinningMode 分成了几种情况分别验证:
    • 1.不验证
    • 2.验证证书
    • 3.验证公钥

这一步的验证中又用到了几个静态函数,下边来简单看一下。

  • 验证serverTrust 是否有效,调用了系统方法 SecTrustEvaluate() 进行校验。
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);

    // 只有当 "用户未指定验证的策略,即使用系统提供的默认策略" 或者 "要求永远信任" 的时候,才会返回 YES,其他时候都是 NO。
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out:
    return isValid;
}
  • 取出 serverTrust 里边的所有证书,将其二进制文件组成的数组返回。
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];

    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
    }

    return [NSArray arrayWithArray:trustChain];
}
  • 取出 serverTrust 里边的所有证书,然后遍历证书取出其中的 public key,将公钥组成的数组返回。
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);

        SecCertificateRef someCertificates[] = {certificate};
        CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);

        SecTrustRef trust;
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);

        SecTrustResultType result;
        __Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);

        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

    _out:
        if (trust) {
            CFRelease(trust);
        }

        if (certificates) {
            CFRelease(certificates);
        }

        continue;
    }
    CFRelease(policy);

    return [NSArray arrayWithArray:trustChain];
}

小结

以上就是安全验证过程中会使用到的类 AFSecurityPolicy 中的主要内容,其他部分本文暂不做过多讨论,以后有时间再更新吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容