概述
AFN框架中實現HTTPS請求的客戶端校驗是通過AFSecurityPolicy對象實現的,本篇主要分析一下AFSecurityPolicy的相關實現邏輯。
TLS/SSL握手
HTTPS請求首先需要TLS/SSL握手,該協議也是建立在TCP基礎之上,以下是握手的幾個階段:
- 客戶端發出握手請求,請求報文主要包含協議版本號,客戶端提供的加密算法,一個隨機數random_Client。
- 服務端接收到請求,保存隨機數random_Client,然后發送響應給客戶端,包括選擇的加密算法、版本、壓縮算法、一個隨機數random_Server,以及證書鏈。
- 客戶端接收到信息,將隨機數random_Server保存,并且對返回的證書鏈進行校驗,如果檢驗不通過,終止連接。如果校驗通過產生隨機數字Pre_master,并用證書中的公鑰進行加密,將加密內容發送給服務器。同時客戶端根據random_Client、random_Server和Pre_master通過相應算法得到今后雙方通信的密鑰key。客戶端邏輯結束。
- 服務端接收到公鑰加密的信息,通過證書的私鑰解密得到隨機數字Pre_master,然后根據random_Client、random_Server和Pre_master通過算法得到今后雙方通信的密鑰key。
- 握手完畢,客戶端和服務端通過生成的密鑰key和之前約定的對稱加密算法對通信過程的報文數據進行加密。
在握手的過程中,密鑰數字的交換過程使用非對稱加密,且證書的私鑰保存在服務端,如果私鑰不泄露,正常情況下無法破解加密數據。當最終密鑰生成,握手之后的數據傳輸用的是對稱加密,比一直使用非對稱加密性能提升。
TLS/SSL握手的關鍵在于客戶端對服務器返回的證書進行驗證,比較有名的中間人攻擊就是通過偽造證書的方式竊取傳輸過程中加密的數據。
證書校驗
SSL證書是數字證書的一種類型,專門用于HTTPS類型的網絡請求,遵循X.509標準生成。SSL證書由CA(Certificate Authority)機構負責頒發,證書的申請流程如下:
- 申請者提供自己的必要信息(包括身份信息,公鑰、私鑰等)給CA機構。
- CA機構認證申請者的信息。
- 認證通過后創建新證書,并通過哈希算法得到證書的摘要,用自己證書中的私鑰加密摘要,得到新證書的簽名。
下圖是訪問百度網站時,下發的SSL證書:
可以看出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是否相等。
- 驗證證書頒發者的合法性,即驗證上一級證書的簽名,需要用再上一級證書的公鑰解密簽名,然后和哈希算法計算出的摘要進行比較。遞歸驗證,直到驗證根證書,由于根證書沒有上級證書,是最上級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頒發的證書,開發者不用做額外邏輯,使用起來十分方便。