AFNetworking 源码阅读之安全策略 AFSecurityPolicy

AFNetworking

HTTP请求是不需要的证书的,也不需要什么安全策略。AFNetworking 中的AFSecurityPolicy 的使用对象是HTTPS请求。

在我之前的 HTTPS实现原理 文章中,介绍了 HTTPS 的原理以及为什么它需要证书。AFSecurityPolicy 就是为了解决 AFNetworking 证书相关的问题的模块。这个模块同时也能够脱离 AFNetworking 单独使用。

下面一步一步通过源码来理解 AFSecurityPolicy 如何实现HTTPS安全请求的相关问题。

1. 源码中使用到的相关名词

  • SecCertificateRef
    SecCertificateRef 是 Security.frame 框架下一个的证书引用结构体。
    SecCertificateRef

    它内部引用了一个 X.509 的证书。

X.509 是密码学里公钥证书的格式标准。 X.509 证书己应用在包括TLS/SSL(WWW万维网安全浏览的基石)在内的众多 Intenet协议里.同时它也用在很多非在线应用场景里,比如电子签名服务。X.509证书里含有公钥、身份信息(比如网络主机名,组织的名称或个体名称等)和签名信息(可以是证书签发机构CA的签名,也可以是自签名)。对于一份经由可信的证书签发机构签名或者可以通过其它方式验证的证书,证书的拥有者就可以用证书及相应的私钥来创建安全的通信,对文档进行数字签名.

我们之前说HTTPS请求需要使用到的证书,在Security.frame框架中,就是通过SecCertificateRef来抽象的。

  • SecKeyRef
    同样是 Security.frame 框架下一个结构体的引用。
    SecKeyRef

HTTPS 中的客户端对内容进行加密,很多可逆的加密算法都有秘钥,而 SecKeyRef 就是这些秘钥抽象的结构体引用。

  • SecPolicyRef
    SecPolicyRef 用于描述 X.509 证书的安全策略。

    SecPolicyRef

    SecCertificateRef 中并没有对它具备的功能进行描述,仅仅是抽象了证书的数据,一个证书变得有意义的原因在,它能够抽象出一些规则,这些规则描述如何安全的进行数据间的传输,这个抽象的规则就是安全策略。在 Security.frame 下使用 SecPolicyRef 进行抽象。
    我们可以通过 SecPolicyRef 的一些 API 创建一个 SecPolicyRef 。并且可以指定其中包含的属性,但是我们没有对 SecPolicyRef 修改的能力。

  • SecTrustRef
    X.509 证书的信任评估。

    SecTrustRef

    SecPolicyRef 包含了设定的安全策略,如果要评估一个策略是否适用于一个证书,那么需要通过 SecTrustRef 来进行评估。

  • SecTrustResultType
    SecTrustRef 证书信任评估对一个证书和安全策略评估之后得到的结果。它是一个枚举:

    SecTrustResultType

每一个枚举的具体意义:

    kSecTrustResultInvalid  : 证书无效
    kSecTrustResultProceed : 用户选择信任此证书
    kSecTrustResultConfirm:用户预先选择了证书链中得某一个证书在每次使用前询问允许。这个返回值已经不再使用,只在老版本的OS X中使用。 
    kSecTrustResultDeny: 认证成功,用户拒绝信任
    kSecTrustResultUnspecified : 证书验证成功,但是用户没有明确指出信任此证书。这是最常见的返回值
    kSecTrustResultRecoverableTrustFailure:证书不可信,但是经过较小的改动可以修复问题,例如忽略过期证书、增加信任链节点等。  
    kSecTrustResultFatalTrustFailure : 证书不可信,并且无法通过改动策略修复。
    kSecTrustResultOtherError :其他错误 

以上的几个名词,全部来自于 Security.frame 框架,AFSecurityPolicy 则是依赖这个框架进行开发的。

2. AFSecurityPolicy 类解读

AFSecurityPolicy 的官方介绍:

AFSecurityPolicy evaluates server trust against pinned X.509 certificates and public keys over secure connections.
Adding pinned SSL certificates to your app helps prevent man-in-the-middle attacks and other vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged to route all communication over an HTTPS connection with SSL pinning configured and enabled.
AFSecurityPolicy 通过评估服务的X.509证书和公钥识别可靠的服务器,并与之建立加密连接。向应用程序添加固定SSL证书有助于防止中间人攻击和其他漏洞。强烈鼓励处理敏感客户数据或财务信息的应用程序通过配置并启用的SSLping的HTTPS连接路由所有通信

AFSecurityPolicy.h 文件中,包含了一个枚举AFSSLPinningMode和一个 AFSecurityPolicy 类型接口。

2.1 AFSSLPinningMode枚举

AFSSLPinningMode 主要用在 AFSecurityPolicy.h 对一个 SecTrustRef 有对象的评估中。它包含了三个枚举值:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,         // 直接对获取的信任评估(SecTructRef)进行评估,如果有效或者用户设置不检测则通过
    AFSSLPinningModePublicKey,    // 使用本地的公钥和信任评估(SecTructRef)中的所有进行对比,符合条件则通过。
    AFSSLPinningModeCertificate,  // 使用本地的证书和信任评估(SecTructRef)中证书进行对比,如何条件则通过。证书中,除了公钥还有过期的日期、域名等其他信息,比仅仅是公钥验证更加的严格。
};

这个枚举将在后面的解读中会看到它的使用。

2.2 AFSecurityPolicy的属性

AFSecurityPolicy 中的属性并不多,但是每一个都很关键。 用户设置任何一个属性都会对 AFSecurityPolicy 策略对信任评估的方式产生影响。

/**
    AFSSLPinningMode 枚举,默认为 AFSSLPinningModeNone。
    这个属性只会在初始化的时候被创建,创建之后,为只读!
 */
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;

/**
 项目中可用的证书,默认情况下,会使用项目中所有的以 .cer 结尾的文件作为证书。
我们也可以手动设置这个证书数组,将我们自签名的证书加入到其中。
 */
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;

/**
 允许无效证书默认为 NO, 如果设置为YES,那么实际上项目不会对服务器进行甄别
 */
@property (nonatomic, assign) BOOL allowInvalidCertificates;

/**
 检验域名默认为 YES。 证书中会包含域名,如果我们需要指定域名对证书甄别,那么应该设置这个属性为YES。  反之,设置为NO。
 */
@property (nonatomic, assign) BOOL validatesDomainName;

/**
 这是一个类似 Get 方法的函数。主要是获取项目的证书。默认情况下,会将项目中,所有的以 .cer 结尾的文件作为证书,然后赋值给  pinnedCertificates。 如果我们明确的时候,自己的证书放在项目的某个位置,就可以使用这个方法来获取。
 */
+ (NSSet <NSData *> *)certificatesInBundle:(NSBundle *)bundle;
+ (NSSet *)certificatesInBundle:(NSBundle *)bundle {
    NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];

    NSMutableSet *certificates = [NSMutableSet setWithCapacity:[paths count]];
    for (NSString *path in paths) {
        NSData *certificateData = [NSData dataWithContentsOfFile:path];
        [certificates addObject:certificateData];
    }

    return [NSSet setWithSet:certificates];
}

//  除了上述几个属性之外,在类的内部还有两个私有属性。


/**
.m 文件中,也有一个 SSLPinningMode 属性,不同的是,它是可读写,因此这个属性,对外只读的,但是对内却是可写的。
 */
@property (readwrite, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
/**
公钥集合。 每一个证书都有一个公钥,因此,我们一般不会直接获取公钥,而是通过证书获取。
 */
@property (readwrite, nonatomic, strong) NSSet *pinnedPublicKeys;

金手指:如果需要对一个属性设置对外为只读,但是在实现部分设置为可写的话,可以分别在.h和.m定义一个相同的属性,分别用readonly 、readwrite 修饰。

2.3 AFSecurityPolicy的初始化

AFSecurityPolicy 包含了三个初始化方法:

+ (instancetype)defaultPolicy;

+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode;

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

我给这三个函数分别标记为 A, B, C。下面用 ABC 代替。

2.3.1 初始化函数 A
+ (instancetype)defaultPolicy

默认实现中,PinningMode 为AFSSLPinningModeNone。其他的属性都设置为默认值。

2.3.2 初始化函数 B 和 C
policyWithPinningMode

B和C两初始化函数指明了需要使用的 PinningMode ,并且C函数中,用户还能够指定某部分证书。

因为一个项目中,可能会请求对个不同的服务器,假如每一个服务器都有其对应的证书的话,那么就会有不止一个证书了。因此 AFSecurityPolicy 中所有的信任评估操作都是对集合而言,只要有一个证书符合要求,那么评估的结果为真。

2.3 AFSecurityPolicy的核心函数

AFSecurityPolicy 的功能集中点就在这个函数上面,设置相关的属性之后,最终确认一个外来证书是否有效全部通过这个函数来判别。客户端和服务器建立连接之初,会将其证书传输过来,客户端则需要对这个证书进行验证。这个方法便是对证书进行检测核心方法。如果证书不能通过,那么接下来的请求连接将会被中断。

/**
客户端和服务器建立连接之初,会将其证书传输过来,客户端则需要对这个证书进行验证。这个方法便是对证书进行检测核心方法。如果证书不能通过,那么接下来的请求连接将会被中断。
 */
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrus forDomain:(nullable NSString *)domain;

其实现(内附解读):

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /*
     评估必须保证是一个有效的过程。
     如果允许无效证书,但是没有证书或者采用AFSSLPinningModeNone模式,其他信息齐全的时候,这时候会被告知,这是一个无效的验证过程。
    */
    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 安全策略。 否则创建一个基于 X.509 的安全策略。
    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);
    

    // 在 AFSSLPinningModeNone,不会进行公钥或者证书的认证。 只要确保服务器给的信任评估是有效的(能够获取到CA根证书)。  或者,如果用户设置允许无效证书,那么也会直接返回通过。
    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;  // 上面已经对 AFSSLPinningModeNone 做了出了,这里直接当成默认的情况。返回NO
            
            
            
        case AFSSLPinningModeCertificate: {
            
            // 验证本地证书和服务器发过来的信任进行甄别。
            
            // 这里本地使用的证书 "pinnedCertificates" 可能有很多个,于是转化成 CFData 放入数组。
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            
            // 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证。
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }
            
            // 获取所有的服务器的证书链,注意这和 AnchorCertificates 是不相同的。
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            
            // 遍历服务器的证书链
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                // 如果证书链中包含了本地的证书,说明 serverTrust 是有效的服务器信任凭证。返回YES
                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;
}

在这个核心方法中,还包含了一些列的私有方法。相对都比较直观,这里全部贴出来:

// 获取证书的公钥
static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;  // 证书
    SecPolicyRef policy = nil;             // 证书的安全策略(主要是X.509证书)
    SecTrustRef allowedTrust = nil;        // 信任策略抽象,允许对该抽象进行评估
    SecTrustResultType result;             // 对安全策略抽象进行,评估的结果

    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    __Require_Quiet(allowedCertificate != NULL, _out);

    policy = SecPolicyCreateBasicX509();
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust), _out);
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);

    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

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

    if (policy) {
        CFRelease(policy);
    }

    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }

    return allowedPublicKey;
}

//  SecTrustEvaluate 判定一个证书的值,信任、拒绝、未作处理等。
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;
}

//  从一个 信任凭证 中获取它的证书链 (从根证书一直到子证书)
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];
}
//  从一个 信任凭证 中获取它的公钥链 (顺序为从自证书的的公钥到根证书的公钥)
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];
}

这篇文章到此完成,为了更好的理解其中的内容,可以看我的另外两篇文章:
HTTPS协议的实现原理
TCP/IP协议栈 —— IP、TCP、UDP、HTTP协议详解

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