iOS源码解析—AFNetworking(AFSecurityPolicy)

概述

AFN框架中实现HTTPS请求的客户端校验是通过AFSecurityPolicy对象实现的,本篇主要分析一下AFSecurityPolicy的相关实现逻辑。

TLS/SSL握手

HTTPS请求首先需要TLS/SSL握手,该协议也是建立在TCP基础之上,以下是握手的几个阶段:

  1. 客户端发出握手请求,请求报文主要包含协议版本号,客户端提供的加密算法,一个随机数random_Client。
  2. 服务端接收到请求,保存随机数random_Client,然后发送响应给客户端,包括选择的加密算法、版本、压缩算法、一个随机数random_Server,以及证书链。
  3. 客户端接收到信息,将随机数random_Server保存,并且对返回的证书链进行校验,如果检验不通过,终止连接。如果校验通过产生随机数字Pre_master,并用证书中的公钥进行加密,将加密内容发送给服务器。同时客户端根据random_Client、random_Server和Pre_master通过相应算法得到今后双方通信的密钥key。客户端逻辑结束。
  4. 服务端接收到公钥加密的信息,通过证书的私钥解密得到随机数字Pre_master,然后根据random_Client、random_Server和Pre_master通过算法得到今后双方通信的密钥key。
  5. 握手完毕,客户端和服务端通过生成的密钥key和之前约定的对称加密算法对通信过程的报文数据进行加密。

在握手的过程中,密钥数字的交换过程使用非对称加密,且证书的私钥保存在服务端,如果私钥不泄露,正常情况下无法破解加密数据。当最终密钥生成,握手之后的数据传输用的是对称加密,比一直使用非对称加密性能提升。

TLS/SSL握手的关键在于客户端对服务器返回的证书进行验证,比较有名的中间人攻击就是通过伪造证书的方式窃取传输过程中加密的数据。

证书校验

SSL证书是数字证书的一种类型,专门用于HTTPS类型的网络请求,遵循X.509标准生成。SSL证书由CA(Certificate Authority)机构负责颁发,证书的申请流程如下:

  1. 申请者提供自己的必要信息(包括身份信息,公钥、私钥等)给CA机构。
  2. CA机构认证申请者的信息。
  3. 认证通过后创建新证书,并通过哈希算法得到证书的摘要,用自己证书中的私钥加密摘要,得到新证书的签名。

下图是访问百度网站时,下发的SSL证书:

5-1.png
5-2.png

可以看出baidu.com证书是由GlobalSign Organization Validation CA的机构创建并颁发的,而它存在上一级CA机构,名称是Global Root CA,GlobalSign Organization Validation CA的证书是由Global Root CA颁发的,且证书的签名是通过Global Root CA的私钥生成的。证书的机构是链式的。通过上图,可以知道证书的内容主要包括,证书持有者的身份信息、证书颁发这的身份信息、证书的有效期、证书的公钥、加密算法类型、证书的签名等。当TLS/SSL握手时,服务端返回证书链,客户端校验证书的流程如下:

  1. 验证证书的有效期(是否过期)、身份信息等。
  2. 验证证书的签名,首先用哈希算法计算证书的摘要1,然后用证书链的上一级证书的公钥解密签名,得到摘要2,然后比较摘要1和摘要2是否相等。
  3. 验证证书颁发者的合法性,即验证上一级证书的签名,需要用再上一级证书的公钥解密签名,然后和哈希算法计算出的摘要进行比较。递归验证,直到验证根证书,由于根证书没有上级证书,是最上级CA颁发的,是自签名的。需要将根证书加入操作系统中作为信任证书。如果将证书链中某一级证书是被设置成了锚点证书,则被视为根证书。

其中任何一步流程出现问题,都会导致证书校验失败。此外证书的地址和访问服务端的地址不一致,也会校验失败。

AFSecurityPolicy

在AFN框架中,调用AFSecurityPolicy对象securityPolicy的evaluateServerTrust:forDomain:方法校验,校验的目标对象被封装在SecTrustRef对象serverTrust中,首先看一下AFSecurityPolicy的相关属性:

@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode; //校验模式
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates; //本地绑定的证书
@property (nonatomic, assign) BOOL allowInvalidCertificates; //是否允许无效证书
@property (nonatomic, assign) BOOL validatesDomainName; //是否验证域名

SSLPinningMode是校验证书的模式,是枚举类型,如下:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone, //默认验证方式
    AFSSLPinningModePublicKey, //比较证书的公钥
    AFSSLPinningModeCertificate, //比较证书
};

校验证书的方式有三种,其中AFSSLPinningModeNone表示按照上文的方式验证证书链,除了这种方式,AF还提供了SSL Pinning的方式验证,该方式把服务端下发的证书预先保存在APP的bundle中,然后通过比较服务端下发的证书和本地证书是否相同来校验证书。使用该方式的原因是CA机构颁发的证书比较昂贵,一些企业或者个人不申请CA颁发的证书,而是自己手动创建证书,用SSL Pinning的方式只要比较证书内容一样,无需验证证书的权威性。促成SSL Pinning使用的另一原因是大多数APP访问的服务端域名相对固定,只需要将相应证书导入本地bundle就行了。AFSSLPinningModeCertificate采用SSL Pinning的方式,首先验证服务器证书的有效期(是否过期)、身份信息等,然后将该证书和bundle中证书进行比较,是否一致。AFSSLPinningModeCertificate同样采用SSL Pinning的方式,但是不验证证书的有效期等信息,同时只是比较两个证书的公钥是否一致。采用SSL Pinning的方式,本地buundle中导入的证书数据由pinnedCertificates维护。

AFSecurityPolicy还提供了允许无效证书验证通过的开关allowInvalidCertificates,以及是否需要验证证书域名的开关validatesDomainName。下面分析一下AFSecurityPolicy相关方法。

AFSecurityPolicy相关方法

首先调用AFSecurityPolicy的evaluateServerTrust:forDomain:方法,首先做了一个判断:

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    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;
    }
    ...
}

如果允许无效的证书,同时希望验证证书的域名,则需要用SSL Pinning的方式验证,即验证证书的方式不能是AFSSLPinningModeNone,或者SSL Pinng需要本地导入证书,即pinnedCertificates数组不能为空。

然后判断域名是否需要验证域名,如果需要,则将域名加入需要验证的对象中,代码注释如下:

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;
}

然后判断验证方式如果是AFSSLPinningModeNone且不允许无效证书,则调用AFServerTrustIsValid方法进行校验。代码注释如下:

static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out); //方法验证
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out: //goto语句直接
    return isValid;
}

通过系统方法SecTrustEvaluate校验证书,将校验结果存储在result中,同时通过__Require_noErr_Quiet宏来处理该方法返回error的情况:

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

如果该方法调用过程中失败,即errorCode不为0,则通过goto语句跳转,isValid直接返回NO。如果该方法调用成功,则根据result来判断isValid是否为YES。当值为kSecTrustResultUnspecified或者kSecTrustResultProceed时,验证通过。

回到evaluateServerTrust:forDomain:方法中,接下来处理AFSSLPinningModeCertificate的情况,代码注释如下:

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;
    }
    NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
    for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) { //本地证书数组中是否包含和服务端下发的证书内容一样的证书
        if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
            return YES; //如果包含,则校验通过
        }
    }
    return NO; //否则不通过
}

因为导入APP Bundle中的证书不是CA颁发的,不受信任,所以调用SecTrustSetAnchorCertificates方法将先将这些证书设置为serverTrust证书链上的锚点证书,类似于将这些证书设置为系统信任的根证书,然后调用AFServerTrustIsValid方法校验serverTrust证书链时,如果遇到锚点证书,则终止验证。然后调用AFCertificateTrustChainForServerTrust方法获取serverTrust的证书链serverCertificates,遍历证书链直到发现本地证书pinnedCertificates中有内容相同的证书,服务端下发的证书在本地认可的证书范围内,校验成功,如果没有则校验失败。�

接下来处理AFSSLPinningModePublicKey的方式,代码注释如下:

case AFSSLPinningModePublicKey: {
    NSUInteger trustedPublicKeyCount = 0;
    NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust); //获取serverTrust证书链的公钥
    for (id trustChainPublicKey in publicKeys) { //匹配本地的证书公钥和serverTrust的公钥
        for (id pinnedPublicKey in self.pinnedPublicKeys) {
            if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                trustedPublicKeyCount += 1;
            }
        }
    }
    return trustedPublicKeyCount > 0; //匹配成功,校验成功
}

该方法首先获取serverTrust证书链的公钥,然后匹配本地的证书公钥和serverTrust的公钥,本地的公钥通过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]; //存放在pinnedPublicKeys属性中
    } else {
        self.pinnedPublicKeys = nil;
    }
}

如果匹配成功,则返回校验成功,否则失败。匹配方法AFSecKeyIsEqualToKey调用isEqual:方法进行判断。

总结

AFN框架的AFSecurityPolicy类为我们实现了HTTPS证书校验的功能,且同时支持三种方式校验证书,开发者可以根据不同情况进行选择,如果是CA颁发的证书,开发者不用做额外逻辑,使用起来十分方便。

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

推荐阅读更多精彩内容