iOS源碼解析—AFNetworking(AFSecurityPolicy)

概述

AFN框架中實(shí)現(xiàn)HTTPS請(qǐng)求的客戶(hù)端校驗(yàn)是通過(guò)AFSecurityPolicy對(duì)象實(shí)現(xiàn)的,本篇主要分析一下AFSecurityPolicy的相關(guān)實(shí)現(xiàn)邏輯。

TLS/SSL握手

HTTPS請(qǐng)求首先需要TLS/SSL握手,該協(xié)議也是建立在TCP基礎(chǔ)之上,以下是握手的幾個(gè)階段:

  1. 客戶(hù)端發(fā)出握手請(qǐng)求,請(qǐng)求報(bào)文主要包含協(xié)議版本號(hào),客戶(hù)端提供的加密算法,一個(gè)隨機(jī)數(shù)random_Client。
  2. 服務(wù)端接收到請(qǐng)求,保存隨機(jī)數(shù)random_Client,然后發(fā)送響應(yīng)給客戶(hù)端,包括選擇的加密算法、版本、壓縮算法、一個(gè)隨機(jī)數(shù)random_Server,以及證書(shū)鏈。
  3. 客戶(hù)端接收到信息,將隨機(jī)數(shù)random_Server保存,并且對(duì)返回的證書(shū)鏈進(jìn)行校驗(yàn),如果檢驗(yàn)不通過(guò),終止連接。如果校驗(yàn)通過(guò)產(chǎn)生隨機(jī)數(shù)字Pre_master,并用證書(shū)中的公鑰進(jìn)行加密,將加密內(nèi)容發(fā)送給服務(wù)器。同時(shí)客戶(hù)端根據(jù)random_Client、random_Server和Pre_master通過(guò)相應(yīng)算法得到今后雙方通信的密鑰key。客戶(hù)端邏輯結(jié)束。
  4. 服務(wù)端接收到公鑰加密的信息,通過(guò)證書(shū)的私鑰解密得到隨機(jī)數(shù)字Pre_master,然后根據(jù)random_Client、random_Server和Pre_master通過(guò)算法得到今后雙方通信的密鑰key。
  5. 握手完畢,客戶(hù)端和服務(wù)端通過(guò)生成的密鑰key和之前約定的對(duì)稱(chēng)加密算法對(duì)通信過(guò)程的報(bào)文數(shù)據(jù)進(jìn)行加密。

在握手的過(guò)程中,密鑰數(shù)字的交換過(guò)程使用非對(duì)稱(chēng)加密,且證書(shū)的私鑰保存在服務(wù)端,如果私鑰不泄露,正常情況下無(wú)法破解加密數(shù)據(jù)。當(dāng)最終密鑰生成,握手之后的數(shù)據(jù)傳輸用的是對(duì)稱(chēng)加密,比一直使用非對(duì)稱(chēng)加密性能提升。

TLS/SSL握手的關(guān)鍵在于客戶(hù)端對(duì)服務(wù)器返回的證書(shū)進(jìn)行驗(yàn)證,比較有名的中間人攻擊就是通過(guò)偽造證書(shū)的方式竊取傳輸過(guò)程中加密的數(shù)據(jù)。

證書(shū)校驗(yàn)

SSL證書(shū)是數(shù)字證書(shū)的一種類(lèi)型,專(zhuān)門(mén)用于HTTPS類(lèi)型的網(wǎng)絡(luò)請(qǐng)求,遵循X.509標(biāo)準(zhǔn)生成。SSL證書(shū)由CA(Certificate Authority)機(jī)構(gòu)負(fù)責(zé)頒發(fā),證書(shū)的申請(qǐng)流程如下:

  1. 申請(qǐng)者提供自己的必要信息(包括身份信息,公鑰、私鑰等)給CA機(jī)構(gòu)。
  2. CA機(jī)構(gòu)認(rèn)證申請(qǐng)者的信息。
  3. 認(rèn)證通過(guò)后創(chuàng)建新證書(shū),并通過(guò)哈希算法得到證書(shū)的摘要,用自己證書(shū)中的私鑰加密摘要,得到新證書(shū)的簽名。

下圖是訪(fǎng)問(wèn)百度網(wǎng)站時(shí),下發(fā)的SSL證書(shū):

5-1.png
5-2.png

可以看出baidu.com證書(shū)是由GlobalSign Organization Validation CA的機(jī)構(gòu)創(chuàng)建并頒發(fā)的,而它存在上一級(jí)CA機(jī)構(gòu),名稱(chēng)是Global Root CA,GlobalSign Organization Validation CA的證書(shū)是由Global Root CA頒發(fā)的,且證書(shū)的簽名是通過(guò)Global Root CA的私鑰生成的。證書(shū)的機(jī)構(gòu)是鏈?zhǔn)降?。通過(guò)上圖,可以知道證書(shū)的內(nèi)容主要包括,證書(shū)持有者的身份信息、證書(shū)頒發(fā)這的身份信息、證書(shū)的有效期、證書(shū)的公鑰、加密算法類(lèi)型、證書(shū)的簽名等。當(dāng)TLS/SSL握手時(shí),服務(wù)端返回證書(shū)鏈,客戶(hù)端校驗(yàn)證書(shū)的流程如下:

  1. 驗(yàn)證證書(shū)的有效期(是否過(guò)期)、身份信息等。
  2. 驗(yàn)證證書(shū)的簽名,首先用哈希算法計(jì)算證書(shū)的摘要1,然后用證書(shū)鏈的上一級(jí)證書(shū)的公鑰解密簽名,得到摘要2,然后比較摘要1和摘要2是否相等。
  3. 驗(yàn)證證書(shū)頒發(fā)者的合法性,即驗(yàn)證上一級(jí)證書(shū)的簽名,需要用再上一級(jí)證書(shū)的公鑰解密簽名,然后和哈希算法計(jì)算出的摘要進(jìn)行比較。遞歸驗(yàn)證,直到驗(yàn)證根證書(shū),由于根證書(shū)沒(méi)有上級(jí)證書(shū),是最上級(jí)CA頒發(fā)的,是自簽名的。需要將根證書(shū)加入操作系統(tǒng)中作為信任證書(shū)。如果將證書(shū)鏈中某一級(jí)證書(shū)是被設(shè)置成了錨點(diǎn)證書(shū),則被視為根證書(shū)。

其中任何一步流程出現(xiàn)問(wèn)題,都會(huì)導(dǎo)致證書(shū)校驗(yàn)失敗。此外證書(shū)的地址和訪(fǎng)問(wèn)服務(wù)端的地址不一致,也會(huì)校驗(yàn)失敗。

AFSecurityPolicy

在AFN框架中,調(diào)用AFSecurityPolicy對(duì)象securityPolicy的evaluateServerTrust:forDomain:方法校驗(yàn),校驗(yàn)的目標(biāo)對(duì)象被封裝在SecTrustRef對(duì)象serverTrust中,首先看一下AFSecurityPolicy的相關(guān)屬性:

@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode; //校驗(yàn)?zāi)J?@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates; //本地綁定的證書(shū)
@property (nonatomic, assign) BOOL allowInvalidCertificates; //是否允許無(wú)效證書(shū)
@property (nonatomic, assign) BOOL validatesDomainName; //是否驗(yàn)證域名

SSLPinningMode是校驗(yàn)證書(shū)的模式,是枚舉類(lèi)型,如下:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone, //默認(rèn)驗(yàn)證方式
    AFSSLPinningModePublicKey, //比較證書(shū)的公鑰
    AFSSLPinningModeCertificate, //比較證書(shū)
};

校驗(yàn)證書(shū)的方式有三種,其中AFSSLPinningModeNone表示按照上文的方式驗(yàn)證證書(shū)鏈,除了這種方式,AF還提供了SSL Pinning的方式驗(yàn)證,該方式把服務(wù)端下發(fā)的證書(shū)預(yù)先保存在APP的bundle中,然后通過(guò)比較服務(wù)端下發(fā)的證書(shū)和本地證書(shū)是否相同來(lái)校驗(yàn)證書(shū)。使用該方式的原因是CA機(jī)構(gòu)頒發(fā)的證書(shū)比較昂貴,一些企業(yè)或者個(gè)人不申請(qǐng)CA頒發(fā)的證書(shū),而是自己手動(dòng)創(chuàng)建證書(shū),用SSL Pinning的方式只要比較證書(shū)內(nèi)容一樣,無(wú)需驗(yàn)證證書(shū)的權(quán)威性。促成SSL Pinning使用的另一原因是大多數(shù)APP訪(fǎng)問(wèn)的服務(wù)端域名相對(duì)固定,只需要將相應(yīng)證書(shū)導(dǎo)入本地bundle就行了。AFSSLPinningModeCertificate采用SSL Pinning的方式,首先驗(yàn)證服務(wù)器證書(shū)的有效期(是否過(guò)期)、身份信息等,然后將該證書(shū)和bundle中證書(shū)進(jìn)行比較,是否一致。AFSSLPinningModeCertificate同樣采用SSL Pinning的方式,但是不驗(yàn)證證書(shū)的有效期等信息,同時(shí)只是比較兩個(gè)證書(shū)的公鑰是否一致。采用SSL Pinning的方式,本地buundle中導(dǎo)入的證書(shū)數(shù)據(jù)由pinnedCertificates維護(hù)。

AFSecurityPolicy還提供了允許無(wú)效證書(shū)驗(yàn)證通過(guò)的開(kāi)關(guān)allowInvalidCertificates,以及是否需要驗(yàn)證證書(shū)域名的開(kāi)關(guān)validatesDomainName。下面分析一下AFSecurityPolicy相關(guān)方法。

AFSecurityPolicy相關(guān)方法

首先調(diào)用AFSecurityPolicy的evaluateServerTrust:forDomain:方法,首先做了一個(gè)判斷:

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

如果允許無(wú)效的證書(shū),同時(shí)希望驗(yàn)證證書(shū)的域名,則需要用SSL Pinning的方式驗(yàn)證,即驗(yàn)證證書(shū)的方式不能是AFSSLPinningModeNone,或者SSL Pinng需要本地導(dǎo)入證書(shū),即pinnedCertificates數(shù)組不能為空。

然后判斷域名是否需要驗(yàn)證域名,如果需要,則將域名加入需要驗(yàn)證的對(duì)象中,代碼注釋如下:

NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) { //需要驗(yàn)證域名
    [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)]; //將域名加入驗(yàn)證對(duì)象中
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

if (self.SSLPinningMode == AFSSLPinningModeNone) { //默認(rèn)驗(yàn)證方式
    return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust); //加驗(yàn)證書(shū)
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
    return NO;
}

然后判斷驗(yàn)證方式如果是AFSSLPinningModeNone且不允許無(wú)效證書(shū),則調(diào)用AFServerTrustIsValid方法進(jìn)行校驗(yàn)。代碼注釋如下:

static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out); //方法驗(yàn)證
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out: //goto語(yǔ)句直接
    return isValid;
}

通過(guò)系統(tǒng)方法SecTrustEvaluate校驗(yàn)證書(shū),將校驗(yàn)結(jié)果存儲(chǔ)在result中,同時(shí)通過(guò)__Require_noErr_Quiet宏來(lái)處理該方法返回error的情況:

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

如果該方法調(diào)用過(guò)程中失敗,即errorCode不為0,則通過(guò)goto語(yǔ)句跳轉(zhuǎn),isValid直接返回NO。如果該方法調(diào)用成功,則根據(jù)result來(lái)判斷isValid是否為YES。當(dāng)值為kSecTrustResultUnspecified或者kSecTrustResultProceed時(shí),驗(yàn)證通過(guò)。

回到evaluateServerTrust:forDomain:方法中,接下來(lái)處理AFSSLPinningModeCertificate的情況,代碼注釋如下:

case AFSSLPinningModeCertificate: {
    NSMutableArray *pinnedCertificates = [NSMutableArray array];
    for (NSData *certificateData in self.pinnedCertificates) {
        [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
    }//將本地證書(shū)加入數(shù)組
    SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates); //將本地證書(shū)設(shè)置為錨點(diǎn)證書(shū)

    if (!AFServerTrustIsValid(serverTrust)) { //校驗(yàn)證書(shū)
        return NO;
    }
    NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
    for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) { //本地證書(shū)數(shù)組中是否包含和服務(wù)端下發(fā)的證書(shū)內(nèi)容一樣的證書(shū)
        if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
            return YES; //如果包含,則校驗(yàn)通過(guò)
        }
    }
    return NO; //否則不通過(guò)
}

因?yàn)閷?dǎo)入APP Bundle中的證書(shū)不是CA頒發(fā)的,不受信任,所以調(diào)用SecTrustSetAnchorCertificates方法將先將這些證書(shū)設(shè)置為serverTrust證書(shū)鏈上的錨點(diǎn)證書(shū),類(lèi)似于將這些證書(shū)設(shè)置為系統(tǒng)信任的根證書(shū),然后調(diào)用AFServerTrustIsValid方法校驗(yàn)serverTrust證書(shū)鏈時(shí),如果遇到錨點(diǎn)證書(shū),則終止驗(yàn)證。然后調(diào)用AFCertificateTrustChainForServerTrust方法獲取serverTrust的證書(shū)鏈serverCertificates,遍歷證書(shū)鏈直到發(fā)現(xiàn)本地證書(shū)pinnedCertificates中有內(nèi)容相同的證書(shū),服務(wù)端下發(fā)的證書(shū)在本地認(rèn)可的證書(shū)范圍內(nèi),校驗(yàn)成功,如果沒(méi)有則校驗(yàn)失敗。?

接下來(lái)處理AFSSLPinningModePublicKey的方式,代碼注釋如下:

case AFSSLPinningModePublicKey: {
    NSUInteger trustedPublicKeyCount = 0;
    NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust); //獲取serverTrust證書(shū)鏈的公鑰
    for (id trustChainPublicKey in publicKeys) { //匹配本地的證書(shū)公鑰和serverTrust的公鑰
        for (id pinnedPublicKey in self.pinnedPublicKeys) {
            if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                trustedPublicKeyCount += 1;
            }
        }
    }
    return trustedPublicKeyCount > 0; //匹配成功,校驗(yàn)成功
}

該方法首先獲取serverTrust證書(shū)鏈的公鑰,然后匹配本地的證書(shū)公鑰和serverTrust的公鑰,本地的公鑰通過(guò)self.pinnedPublicKeys屬性維護(hù),在之前設(shè)置本地證書(shū)的方法中獲得,注釋如下:

- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
    _pinnedCertificates = pinnedCertificates;
    if (self.pinnedCertificates) { //遍歷本地證書(shū)
        NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]];
        for (NSData *certificate in self.pinnedCertificates) {
            id publicKey = AFPublicKeyForCertificate(certificate); //獲取證書(shū)的公鑰
            if (!publicKey) {
                continue;
            }
            [mutablePinnedPublicKeys addObject:publicKey];
        }
        self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys]; //存放在pinnedPublicKeys屬性中
    } else {
        self.pinnedPublicKeys = nil;
    }
}

如果匹配成功,則返回校驗(yàn)成功,否則失敗。匹配方法AFSecKeyIsEqualToKey調(diào)用isEqual:方法進(jìn)行判斷。

總結(jié)

AFN框架的AFSecurityPolicy類(lèi)為我們實(shí)現(xiàn)了HTTPS證書(shū)校驗(yàn)的功能,且同時(shí)支持三種方式校驗(yàn)證書(shū),開(kāi)發(fā)者可以根據(jù)不同情況進(jìn)行選擇,如果是CA頒發(fā)的證書(shū),開(kāi)發(fā)者不用做額外邏輯,使用起來(lái)十分方便。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容