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閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容