iOS源碼解析—AFNetworking(AFSecurityPolicy)

概述

AFN框架中實現(xiàn)HTTPS請求的客戶端校驗是通過AFSecurityPolicy對象實現(xiàn)的,本篇主要分析一下AFSecurityPolicy的相關(guān)實現(xiàn)邏輯郎嫁。

TLS/SSL握手

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

  1. 客戶端發(fā)出握手請求,請求報文主要包含協(xié)議版本號宏浩,客戶端提供的加密算法,一個隨機數(shù)random_Client廓俭。
  2. 服務(wù)端接收到請求云石,保存隨機數(shù)random_Client,然后發(fā)送響應(yīng)給客戶端研乒,包括選擇的加密算法汹忠、版本、壓縮算法雹熬、一個隨機數(shù)random_Server宽菜,以及證書鏈。
  3. 客戶端接收到信息竿报,將隨機數(shù)random_Server保存铅乡,并且對返回的證書鏈進行校驗,如果檢驗不通過烈菌,終止連接阵幸。如果校驗通過產(chǎn)生隨機數(shù)字Pre_master,并用證書中的公鑰進行加密芽世,將加密內(nèi)容發(fā)送給服務(wù)器挚赊。同時客戶端根據(jù)random_Client、random_Server和Pre_master通過相應(yīng)算法得到今后雙方通信的密鑰key济瓢∫螅客戶端邏輯結(jié)束。
  4. 服務(wù)端接收到公鑰加密的信息葬荷,通過證書的私鑰解密得到隨機數(shù)字Pre_master涨共,然后根據(jù)random_Client、random_Server和Pre_master通過算法得到今后雙方通信的密鑰key宠漩。
  5. 握手完畢举反,客戶端和服務(wù)端通過生成的密鑰key和之前約定的對稱加密算法對通信過程的報文數(shù)據(jù)進行加密。

在握手的過程中扒吁,密鑰數(shù)字的交換過程使用非對稱加密火鼻,且證書的私鑰保存在服務(wù)端,如果私鑰不泄露雕崩,正常情況下無法破解加密數(shù)據(jù)魁索。當最終密鑰生成,握手之后的數(shù)據(jù)傳輸用的是對稱加密盼铁,比一直使用非對稱加密性能提升粗蔚。

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

證書校驗

SSL證書是數(shù)字證書的一種類型鹏控,專門用于HTTPS類型的網(wǎng)絡(luò)請求致扯,遵循X.509標準生成。SSL證書由CA(Certificate Authority)機構(gòu)負責頒發(fā)当辐,證書的申請流程如下:

  1. 申請者提供自己的必要信息(包括身份信息抖僵,公鑰、私鑰等)給CA機構(gòu)缘揪。
  2. CA機構(gòu)認證申請者的信息耍群。
  3. 認證通過后創(chuàng)建新證書,并通過哈希算法得到證書的摘要找筝,用自己證書中的私鑰加密摘要世吨,得到新證書的簽名。

下圖是訪問百度網(wǎng)站時呻征,下發(fā)的SSL證書:

5-1.png
5-2.png

可以看出baidu.com證書是由GlobalSign Organization Validation CA的機構(gòu)創(chuàng)建并頒發(fā)的耘婚,而它存在上一級CA機構(gòu),名稱是Global Root CA陆赋,GlobalSign Organization Validation CA的證書是由Global Root CA頒發(fā)的沐祷,且證書的簽名是通過Global Root CA的私鑰生成的。證書的機構(gòu)是鏈式的攒岛。通過上圖赖临,可以知道證書的內(nèi)容主要包括,證書持有者的身份信息灾锯、證書頒發(fā)這的身份信息兢榨、證書的有效期、證書的公鑰顺饮、加密算法類型吵聪、證書的簽名等。當TLS/SSL握手時兼雄,服務(wù)端返回證書鏈吟逝,客戶端校驗證書的流程如下:

  1. 驗證證書的有效期(是否過期)、身份信息等赦肋。
  2. 驗證證書的簽名块攒,首先用哈希算法計算證書的摘要1,然后用證書鏈的上一級證書的公鑰解密簽名佃乘,得到摘要2囱井,然后比較摘要1和摘要2是否相等。
  3. 驗證證書頒發(fā)者的合法性趣避,即驗證上一級證書的簽名庞呕,需要用再上一級證書的公鑰解密簽名,然后和哈希算法計算出的摘要進行比較鹅巍。遞歸驗證千扶,直到驗證根證書,由于根證書沒有上級證書骆捧,是最上級CA頒發(fā)的澎羞,是自簽名的。需要將根證書加入操作系統(tǒng)中作為信任證書敛苇。如果將證書鏈中某一級證書是被設(shè)置成了錨點證書妆绞,則被視為根證書。

其中任何一步流程出現(xiàn)問題枫攀,都會導致證書校驗失敗括饶。此外證書的地址和訪問服務(wù)端的地址不一致,也會校驗失敗来涨。

AFSecurityPolicy

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

@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode; //校驗?zāi)J?@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的方式驗證社裆,該方式把服務(wù)端下發(fā)的證書預(yù)先保存在APP的bundle中拙绊,然后通過比較服務(wù)端下發(fā)的證書和本地證書是否相同來校驗證書。使用該方式的原因是CA機構(gòu)頒發(fā)的證書比較昂貴泳秀,一些企業(yè)或者個人不申請CA頒發(fā)的證書标沪,而是自己手動創(chuàng)建證書,用SSL Pinning的方式只要比較證書內(nèi)容一樣嗜傅,無需驗證證書的權(quán)威性谨娜。促成SSL Pinning使用的另一原因是大多數(shù)APP訪問的服務(wù)端域名相對固定,只需要將相應(yīng)證書導入本地bundle就行了磺陡。AFSSLPinningModeCertificate采用SSL Pinning的方式趴梢,首先驗證服務(wù)器證書的有效期(是否過期)、身份信息等币他,然后將該證書和bundle中證書進行比較坞靶,是否一致。AFSSLPinningModeCertificate同樣采用SSL Pinning的方式蝴悉,但是不驗證證書的有效期等信息彰阴,同時只是比較兩個證書的公鑰是否一致。采用SSL Pinning的方式拍冠,本地buundle中導入的證書數(shù)據(jù)由pinnedCertificates維護尿这。

AFSecurityPolicy還提供了允許無效證書驗證通過的開關(guān)allowInvalidCertificates簇抵,以及是否需要驗證證書域名的開關(guān)validatesDomainName。下面分析一下AFSecurityPolicy相關(guān)方法射众。

AFSecurityPolicy相關(guān)方法

首先調(diào)用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數(shù)組不能為空愉舔。

然后判斷域名是否需要驗證域名,如果需要伙菜,則將域名加入需要驗證的對象中轩缤,代碼注釋如下:

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且不允許無效證書,則調(diào)用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;
}

通過系統(tǒng)方法SecTrustEvaluate校驗證書典奉,將校驗結(jié)果存儲在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

如果該方法調(diào)用過程中失敗丧叽,即errorCode不為0卫玖,則通過goto語句跳轉(zhuǎn),isValid直接返回NO踊淳。如果該方法調(diào)用成功假瞬,則根據(jù)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)];
    }//將本地證書加入數(shù)組
    SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates); //將本地證書設(shè)置為錨點證書

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

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

接下來處理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屬性維護,在之前設(shè)置本地證書的方法中獲得你画,注釋如下:

- (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調(diào)用isEqual:方法進行判斷。

總結(jié)

AFN框架的AFSecurityPolicy類為我們實現(xiàn)了HTTPS證書校驗的功能撬统,且同時支持三種方式校驗證書适滓,開發(fā)者可以根據(jù)不同情況進行選擇,如果是CA頒發(fā)的證書恋追,開發(fā)者不用做額外邏輯凭迹,使用起來十分方便。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末苦囱,一起剝皮案震驚了整個濱河市嗅绸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撕彤,老刑警劉巖鱼鸠,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異羹铅,居然都是意外死亡蚀狰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門职员,熙熙樓的掌柜王于貴愁眉苦臉地迎上來麻蹋,“玉大人,你說我怎么就攤上這事焊切“缡冢” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵专肪,是天一觀的道長糙箍。 經(jīng)常有香客問我,道長牵祟,這世上最難降的妖魔是什么深夯? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上咕晋,老公的妹妹穿的比我還像新娘雹拄。我一直安慰自己,他們只是感情好掌呜,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布滓玖。 她就那樣靜靜地躺著,像睡著了一般质蕉。 火紅的嫁衣襯著肌膚如雪势篡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天模暗,我揣著相機與錄音禁悠,去河邊找鬼。 笑死兑宇,一個胖子當著我的面吹牛碍侦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播隶糕,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼瓷产,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枚驻?” 一聲冷哼從身側(cè)響起濒旦,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎再登,沒想到半個月后尔邓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡霎冯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年铃拇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沈撞。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡慷荔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缠俺,到底是詐尸還是另有隱情显晶,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布壹士,位于F島的核電站磷雇,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏躏救。R本人自食惡果不足惜唯笙,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一螟蒸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧崩掘,春花似錦七嫌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挽放,卻和暖如春绍赛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辑畦。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工吗蚌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人航闺。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓褪测,卻偏偏與公主長得像猴誊,于是被迫代替她去往敵國和親潦刃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容