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