iOS https認證
項目背景
最近在做iOS 熱更新忱详,出于公司信息安全限制沒使用JSPatch平臺來下發(fā)js昆淡,而是把js放自己的對象存儲服務器上匣吊。為了簡單處理,js路徑里加了對應的版本號桌肴。這樣對應版本的app會去下載該js皇筛,能下載到表示該版本有patch,沒有則表示無patch坠七。既然沒有接入JSPatch平臺來下發(fā)水醋,自然存在下發(fā)js安全的問題旗笔。比如對象存儲服務器被劫持后,黑客篡改js拄踪,就可能造成全部版本不可用的風險蝇恶。最簡單的就是讓對象存儲服務器走https,簡單暴力惶桐。
https簡單理解
https是簡單的理解為http secure艘包,就是安全的http。我們知道http是應用層協(xié)議耀盗,它緊接著的一層為運輸層(TCP/UDP)想虎。那如何做到安全的http,且不影響原本的http協(xié)議叛拷,也不影響TCP或者UDP呢舌厨?加一層,也就是SSL(Secure Socket Layer)忿薇。SSL3.0以后開始叫TSL了裙椭,最新的是TSL1.3,不過主流支持到TSL1.2署浩。https具體原理不說了揉燃,有點繁瑣,具體百度筋栋。只要知道客戶端和服務端在握手階段商量出了一個非對稱秘鑰(也就是兩個不一樣的秘鑰炊汤,可以互相解密對方加密的內容,但是比較耗時)弊攘。然后用這個非對稱秘鑰加密傳輸一個秘鑰抢腐,讓彼此知道這個秘鑰,接下來就可以有用這個秘鑰來加密需要傳輸?shù)臄?shù)據(jù)了(也就是對稱加解密)襟交。還是用圖(網(wǎng)圖)來說明吧:
https防中間人攻擊以及利用
通常說https可以防中間人攻擊迈倍,那么怎么做到防中間人攻擊呢?怎么做到A與B通信就是A與B捣域,而不是A與C啼染,不是C與B,甚至是A通過C與B通信焕梅?簡單說給他們各自一個憑證迹鹅,A證證明A是A,B證證明B就是B丘侠。他們在開始通信的時候先亮出彼此的證書徒欣,A驗證后發(fā)現(xiàn)確實與我通信的就是B逐样,B驗證后與我通信的就是A才開始接下里的通信蜗字。好了打肝,大家說我平時訪問https的網(wǎng)站或者網(wǎng)址,都沒用到啥https證書挪捕,這有啥用粗梭?其實我們一直在用,只是他們隱匿的深一點级零。當我們在chrome里鍵入:https://www.baidu.com 的時候断医,瀏覽器里地址欄會出現(xiàn)一個小鎖的圖標,如下圖:
這表示該網(wǎng)站的https證書是CA(可以理解為給你發(fā)身份證的公安局)認證過的奏纪,可以放心使用了鉴嗤。那怎么認證的呢?瀏覽器和操作系統(tǒng)都會內置一些權威CA的證書(MBP里打開鑰匙串序调,選擇系統(tǒng)根證書醉锅,可有看到目前所有的根證書,選擇證書可以看到我們添加的信任的證書)发绢,
在訪問的時候這些網(wǎng)站時候硬耍,把網(wǎng)站亮出的證書與內置的權威證書對比下,如果有一個命中边酒,就表示認證通過(其實驗證的是一個證書鏈)经柴。所以不要隨便把一些未知證書導入系統(tǒng)里,這樣也會是潛在安全隱患墩朦。因為一旦你導入系統(tǒng)并信任坯认,那么它就具有系統(tǒng)內置權威CA證書一樣的功能了。大家常用的抓包工具Charles就是這樣的原理(嚴格意義上也算中間人攻擊氓涣,只是這個中間人是我們自己)鹃操。開啟了Charles后,需要你安裝Charles的證書到系統(tǒng)春哨,不然是無法攔截到并看到明文https請求的荆隘。下面圖中可以看到打開Charles,百度的證書簽發(fā)機構變成Charles了:
這就表明與我們?yōu)g覽器通信的其實是Charles這個中間人赴背。之所以能看到百度網(wǎng)址的內容椰拒,是因為Charles這個中間人訪問百度并將內容返回了給我們?yōu)g覽器。這也是為什么Charles可以看到https請求的response是明文而不是亂碼的原因凰荚。
iOS中的https認證
鑒于越來越多的安全事故燃观,3年前蘋果要求所有的App都要配置ATS開關。如果不配置便瑟,默認所有http請求都打不開而且所有驗證不通過的https請求也打不開缆毁。也是逼著公司,開發(fā)者重視安全到涂,重視用戶隱私并且盡早接入https脊框。但是可能阻力太大颁督,蘋果額外加了個允許任意請求的開關,這樣開發(fā)者就可以繞開蘋果的要求了浇雹。不過對于這個下發(fā)js的項目背景來說沉御,認證是必須的。鑒于AFNetworking(后面簡稱AF)基本上是iOS網(wǎng)絡庫的事實標準昭灵,下面以AF里認證為例說明吠裆。
AF里做證書驗證的主要類是AFSecurityPolicy,負責網(wǎng)絡請求的AFHTTPSessionManager有個該類的實例屬性烂完,用于作證書認證试疙。打開AFSecurityPolicy的m文件,發(fā)現(xiàn)里面的注釋都比較清楚抠蚣,這里只單獨說兩個屬性:
@interface AFSecurityPolicy : NSObject <NSSecureCoding, NSCopying>
/**
The criteria by which server trust should be evaluated against the pinned SSL certificates. Defaults to `AFSSLPinningModeNone`.
*/
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
/**
The certificates used to evaluate server trust according to the SSL pinning mode.
By default, this property is set to any (`.cer`) certificates included in the target compiling AFNetworking. Note that if you are using AFNetworking as embedded framework, no certificates will be pinned by default. Use `certificatesInBundle` to load certificates from your target, and then create a new policy by calling `policyWithPinningMode:withPinnedCertificates`.
Note that if pinning is enabled, `evaluateServerTrust:forDomain:` will return true if any pinned certificate matches.
*/
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
AFSSLPinningMode枚舉定義如下:
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,//對于https請求我們不用作驗證效斑,丟給系統(tǒng)做,也就是去比對系統(tǒng)內置證書
AFSSLPinningModePublicKey,//該模式要求app提供證書柱徙,會比較證書對應的公鑰是否一致
AFSSLPinningModeCertificate,//該模式有要求app提供證書缓屠,會比較整個完整證書,也就是驗證策略最嚴格
};
上面3種枚舉的驗證策略注釋已經(jīng)說的很明確了护侮,可以看出AFSSLPinningModeNone適合網(wǎng)站或者后臺部署了權威CA簽發(fā)的https證書敌完。AFSSLPinningModePublicKey和AFSSLPinningModeCertificate就對應我們自簽名的https證書了。既然自簽名證書羊初,自然需要我們提供證書了滨溉,也就是把證書放在工程里與app一起打包。在初始化AFHTTPSessionManager時設置securityPolicy的pinnedCertificates屬性即可长赞。下面我們來看看自簽名證書的核心驗證邏輯代碼晦攒。在AFUrlSessionManager的m文件里,可以看到有個NSURLSessionDelegate的代理方法:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {//表示驗證服務端證書
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
這個就是NSURLSession在發(fā)https請求時遇到要求證書驗證時的回調得哆。具體的邏輯還是在[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]脯颜,核心代碼如下:
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
return NO;
}//既然選擇系統(tǒng)驗證,就不要還允許無效證書了
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) {//如果是系統(tǒng)驗證贩据,采用系統(tǒng)驗證的結果
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}//系統(tǒng)驗證不通過栋操,但是有設置不允許無效證書,整個驗證結果就是false了
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone://if (self.SSLPinningMode == AFSSLPinningModeNone)已處理饱亮,實際到不了該case
default:
return NO;
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);//設置serverTrust的可信任錨點證書矾芙,也就是我們提供的證書
if (!AFServerTrustIsValid(serverTrust)) {//如果證書無效直接返回
return NO;
}
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}//查看serverTrust的證書鏈是否有一個在我們提供的證書里列表里(與app一起打包的證書)
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;
}
上面的代碼注釋解釋的很清楚了近上。需要注意的是有兩點:
- 對于自簽名的https證書剔宪,需要自己驗證。
- 打包到App里的自簽名證書會存在過期問題,需要在到期之前提前處理葱绒。
最后說一句感帅,我們的存儲服務器就是https證書就是權威CA簽發(fā)的,意味著啥都不用做Orz...