AFNetworking源码学习(二)- AFSecurityPolicy

AFSecurityPolicy是关于网络连接安全方面的类

要想了解网络安全方面的工作,首先得了解http/https通信,下面就分别先简单介绍下http/https通信。

HTTP

HTTP(超文本传输协议)是一个客户端和服务器请求和响应的标准,用于客户端和服务器端之间的通信。

下图为HTTP协议建立连接、通讯与关闭连接全过程

HTTP通信

HTTP是一套很简单的通信协议,因此也非常的高效。但是由于通信数据都是明文发送的,很容易被拦截后造成破坏。在互联网越来越发达的时代,对通信数据的安全要求也越来越高,所以HTTPS应运而生。

HTTPS

HTTPS(超文本传输安全协议)是以安全为目标的HTTP通道,是一个通信安全的解决方案。

其实HTTPS就是身披SSL外壳的HTTP。

HTTP + 加密 + 认证 + 完整性保护 = HTTPS

通常HTTP直接和TCP通信,当使用SSL就不同了。要先和SSL通信,再由SSL和TCP通信。

下图就是HTTPS通信的全过程:

HTTPS通信

关于HTTPS通信更加有趣的解释请参考:简单粗暴系列之HTTPS原理
更多HTTPS通信细节可以参考:The First Few Milliseconds of an HTTPS Connection

加密

SSL(安全套接层)是为网络通信提供安全及数据完整性的一种安全协议。SSL在传输层对网络连接进行加密。

常用加密算法分三大类:

  • 对称加密算法(加解密密钥相同):AES、DES、3DES
  • 非对称算法(加密密钥和解密密钥不同):RSA、DSA、ECC
  • HASH算法:MD5、SHA-1

而网络通信常用加密方法有两种:

  • 共享密钥加密:用的就是对称加密算法,加密和解密通用一个密钥。优点是加密解密速度快,缺点是一旦密钥泄露,别人也能解密数据。
  • 公开密钥加密:能解决共享密钥加密的困难。过程是:
    1. 发文方使用对方的公开密钥进行加密;
    2. 接受方在使用自己的私有密钥进行解密。

HTTP中没有加密功能,但是可以通过和SSL组合使用,加密通信内容,HTTP组合SSL也即HTTPS,HTTPS通信采用共享密钥加密和公开密钥加密两者并用的混合加密机制:

  • 使用公开密钥加密方式安全地交换在稍后的共享密钥加密中要使用的密钥;
  • 确保交换的密钥是安全的前提下,使用共享密钥加密方式进行通讯。

关于非对称加密可以参考这篇:RSA算法原理

AFSecurityPolicy.h

AFSecurityPolicy其实就是为了验证证书是否可信任

首先,是一个AFSSLPinningMode枚举:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,        //代表无条件信任服务器的证书
    AFSSLPinningModePublicKey,   //代表会对服务器返回的证书中的PublicKey进行验证
    AFSSLPinningModeCertificate, //代表会对服务器返回的证书同本地证书全部进行校验
};

接下来声明了四个属性:

  • SSLPinningMode:返回SSL Pinning的类型。默认的是AFSSLPinningModeNone。
  • pinnedCertificates:保存着所有的可用做校验的证书的集合。只要在证书集合中任何一个校验通过,evaluateServerTrust:forDomain: 就会返回true,即通过校验。
  • allowInvalidCertificates:使用允许无效或过期的证书,默认是NO不允许。
  • validatesDomainName:是否验证证书中的域名domain,默认是YES。

然后声明了几个方法:

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

返回指定bundle中的证书。如果使用AFNetworking的证书验证 ,就必须实现此方法,并且使用policyWithPinningMode:withPinnedCertificates方法来创建security policy。

+ (instancetype)defaultPolicy;

默认的security policy。其认证设置为:不允许无效或过期的证书;验证domain域名;不对证书和公钥进行验证

+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode;
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet <NSData *> *)pinnedCertificates;

这两个都是创建security policy的方法,只是初始化的参数不同。

最后一个,是核心方法:

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

第一个参数:serverTrust是服务器发放的X.509信任证书;
第二个参数:domain是发放信任证书的服务器域名。
这个方法是基于security policy来验证指定的服务器是否可信任。这个方法应该在响应服务器身份验证挑战时使用。

AFURLSessionManagerURLSession:didReceiveChallenge:completionHandler:方法中有使用。

文件前面有提到过这样一段话:

 `AFSecurityPolicy` evaluates server trust against pinned X.509 certificates and public keys over secure connections.

说的是AFSecurityPolicy 用来验证通过X.509(数字证书的标准)的数字证书和公开密钥进行的安全网络连接是否值得信任。这也就是上面方法需要做的事。

AFSecurityPolicy.m

文件最前面是几个私有方法,可以看到很多方法里面都有以Sec为前缀的方法,这些都是<Security/Security.h>里的,我们可以通过查看Security的文档了解相应方法的功能。

#if !TARGET_OS_IOS && !TARGET_OS_WATCH && !TARGET_OS_TV
static NSData * AFSecKeyGetData(SecKeyRef key) {
    CFDataRef data = NULL;

    __Require_noErr_Quiet(SecItemExport(key, kSecFormatUnknown, kSecItemPemArmour, NULL, &data), _out);

    return (__bridge_transfer NSData *)data;

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

    return nil;
}
#endif

这个方法是把key导出为NSData,前提不是OS_IOS|OS_WATCH|OC_TV。核心为SecItemExport方法,其声明为:

OSStatus SecItemExport(CFTypeRef secItemOrArray, SecExternalFormat outputFormat, SecItemImportExportFlags flags, const SecItemImportExportKeyParameters *keyParams, CFDataRef  _Nullable *exportedData);

对应上面方法,key即为secItemOrArray,就是被导出的数组对象,而&data是exportedData,就是导出的对象。

导出的data转化为NSData:

(__bridge_transfer NSData *)data

另外,需要注意__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

当出现异常时,执行标记以后的代码,即_out:标记下的代码。

下面这个方法比较两个key是否相等

static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {
#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV
    return [(__bridge id)key1 isEqual:(__bridge id)key2];
#else
    return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)];
#endif
}

如果是OS_IOS|OS_WATCH|OC_TV,直接进行比较;如果不是,需要先用之前的方法转化为NSData,然后再进行比较,因为SecKeyRef是一个结构体。

下面这个方法很长,但是也是很重要的,其作用是从证书中获取公钥

static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecCertificateRef allowedCertificates[1];
    CFArrayRef tempCertificates = nil;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;

//1.根据certificate生成SecCertificateRef类型证书allowedCertificate
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    __Require_Quiet(allowedCertificate != NULL, _out);

//2.生成SecCertificateRef类型证书数组tempCertificates
    allowedCertificates[0] = allowedCertificate;
    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);

//3.新建policy为X.509
    policy = SecPolicyCreateBasicX509();
    
//4.生成SecTrustRef对象allowedTrust

__Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);

//5.校验allowedTrust,不是异步的
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);

//6.从allowedTrust中获取PublicKey
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

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

    if (policy) {
        CFRelease(policy);
    }

    if (tempCertificates) {
        CFRelease(tempCertificates);
    }

    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }

    return allowedPublicKey;
}

上面注释部分就是获取公钥的过程。带Sec前缀的方法具体就不讲了,在文档中都可以查到。__Require_noErr_Quiet上面也介绍了,而__Require_Quiet其定义是:

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

其用途是:当条件返回false时,执行标记以后的代码。那上面用到的地方就是:判断allowedCertificate是否为空,如果为空,就执行_out标记下的代码。

还有一个地方需要注意,为什么新建policy为SecPolicyCreateBasicX509()?,那是因为数字证书的格式都遵循X.509标准

下面这个方法通过验证证书判断服务器是否可信任

static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);

    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out:
    return isValid;
}

SecTrustEvaluate方法上面方法中也有使用过,其用途就是验证serverTrust证书是否可信任,其结果会赋值给result。而result是一个枚举值,如果是下面两种则表示证书是可信任的:

  • kSecTrustResultUnspecified:非用户定义的,对应验证失败为kSecTrustResultRecoverableTrustFailure
  • kSecTrustResultProceed:用户自定义的,对应验证失败为kSecTrustResultDeny

官方文档HTTPS Server Trust EvaluationListing 3 Evaluating a trust object中就有提到这两个值,有些注意点可以去看下。

下面方法是为了取出服务端返回的所有证书

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

SecTrustRef其定义为:

typedef struct CF_BRIDGED_TYPE(id) __SecTrust *SecTrustRef;

文档描述是:CFType used for performing X.509 certificate trust evaluations. (用于执行X.509证书信任评估)。我们再看上面有关于SecTrustRef的创建:

__Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);

其本身是个结构体,且通过SecCertificateRef类型证书数组和policy来创建,可知其至少包含这两个。那上面方法中关于获取所有证书的实现也就明了了。

最后一个私有方法,其目的是取出服务端返回证书里所有的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];
}

它就像是AFPublicKeyForCertificateAFCertificateTrustChainForServerTrust两个方法的整合,具体就不讲了,可以参考这两个方法。

上面的私有方法中,很多地方用到了__bridge_transfer,它的作用是把CF对象类型转换成OC对象类型,关于这种对象类型转换还有两种:

  • __bridge:CF和OC对象转化时只涉及对象类型不涉及对象所有权的转化;
  • __bridge_retained:常用在将OC对象转换成CF对象时,将OC对象的所有权交给CF对象来管理;(作用同CFBridgingRetain())
  • __bridge_transfer:常用在将CF对象转换成OC对象时,将CF对象的所有权交给OC对象,此时ARC就能自动管理该内存;(作用同CFBridgingRelease())

接下来就是@implementation部分了,先介绍前面几个类方法:

  • certificatesInBundle::获取制定目录下的所有证书,是以NSData的形式;
  • defaultPinnedCertificates:获取默认目录下的所有证书;
  • defaultPolicy:创建默认Policy,即SSLPinningMode为AFSSLPinningModeNone;
  • policyWithPinningMode::创建Policy,并设置SSLPinningMode;
  • policyWithPinningMode:withPinnedCertificates::创建Policy,并设置pinningMode和pinnedCertificates。

接下来分析该文件的核心方法:

- (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) {
    //如果要验证域名,就通过域名来生成Policy
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
    //不验证域名,就默认基于X.509
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    
    //设置policies
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
    //不校验证书,只要允许过期无效证书或者serverTrust验证通过,即可信任
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
    //否则,就不可信任
        return NO;
    }

//到了这里就说明:
//1.通过了证书的验证
//2.allowInvalidCertificates = YES

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

至于后面几个方法就不多讲了,主要关于kvo和自定义对象归档。

总结

关于网络安全方面的知识还有很多,AFSecurityPolicy类是其中一个重要的点,主要是为了验证证书是否可信任。

学习到的知识点:

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

推荐阅读更多精彩内容