前言
最近項(xiàng)目里一部分接口用到了服務(wù)端的自建證書, 之前一直是用AFNetwork來處理相關(guān)的邏輯, 第一次接觸到https證書認(rèn)證相關(guān)邏輯, 寫篇小筆記記錄下
https在iOS的具體體現(xiàn) (單向驗(yàn)證)
在整個(gè)https通訊流程中, 對(duì)于app來說, 關(guān)鍵點(diǎn)其實(shí)就是公鑰的認(rèn)證. 確保當(dāng)前通訊的服務(wù)端所下發(fā)的公鑰與公司所在host的服務(wù)端是同一個(gè)證書所導(dǎo)出的公鑰, 才開始使用該公鑰來加密傳輸數(shù)據(jù).
AFNetworking要使用自建證書的代碼邏輯
先上處理代碼, 要使用自建證書時(shí), 把證書的數(shù)據(jù)放入AFHTTPSessionManager單例
//cerPath為證書在bundle里的路徑
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef httpBinCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(certData));
NSSet *certSet = [[NSSet alloc] initWithObjects:(__bridge_transfer NSData *)SecCertificateCopyData(httpBinCertificate), nil];
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
// 是否允許,NO-- 不允許無效的證書
[securityPolicy setAllowInvalidCertificates:NO];
// 設(shè)置證書
[securityPolicy setPinnedCertificates:certSet];
[AFHTTPSessionManager manager].securityPolicy = securityPolicy;
核心方法
如果是用的自建證書是無法通過系統(tǒng)的默認(rèn)認(rèn)證規(guī)則的, 這時(shí)https連接就會(huì)被中斷, 如果要令自建證書也可以建立https連接, 就需要實(shí)現(xiàn)以下這個(gè)代理方法
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
如果你響應(yīng)這個(gè)方法, 系統(tǒng)會(huì)將本次TCP鏈接中的證書數(shù)據(jù)通過這個(gè)代理方法中的challenge參數(shù)拋給你, 當(dāng)你驗(yàn)證完成后, 通過調(diào)用completionHandler回調(diào)告知系統(tǒng)你的驗(yàn)證結(jié)果
AFNetworking處理自建證書的邏輯
- (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);
}
}
這里的關(guān)鍵是你驗(yàn)證證書后通過completionHandler告知系統(tǒng)你的驗(yàn)證結(jié)果, 該block回調(diào)第一個(gè)參數(shù)就是你的驗(yàn)證結(jié)果
typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) {
NSURLSessionAuthChallengeUseCredential = 0, /* Use the specified credential, which may be nil */
NSURLSessionAuthChallengePerformDefaultHandling = 1, /* Default handling for the challenge - as if this delegate were not implemented; the credential parameter is ignored. */
NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, /* The entire request will be canceled; the credential parameter is ignored. */
NSURLSessionAuthChallengeRejectProtectionSpace = 3, /* This challenge is rejected and the next authentication protection space should be tried; the credential parameter is ignored. */
}
這些結(jié)果枚舉基本能望文生義就不多做解釋了, completionHandler第二個(gè)參數(shù)一般就是把challenge里的證書對(duì)象回傳進(jìn)去, 這一步的關(guān)鍵是如何驗(yàn)證這個(gè)證書對(duì)象是合法的, 在afn中, 驗(yàn)證的邏輯放在這個(gè)方法里
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
AFNetworking處理https驗(yàn)證的幾種方式
證書里面包含里公鑰, SHA-256指紋等多種信息, 根據(jù)實(shí)際需要, 驗(yàn)證策略可以有所不同, 下面
二進(jìn)制數(shù)據(jù)比對(duì)
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;
}
// 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]) {
//直接比對(duì)NSData是否一致
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
最簡單粗暴的方式, 直接將證書轉(zhuǎn)成NSData進(jìn)行比對(duì), 完全一致就可以確定是同一張證書
公鑰比對(duì)
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;
}
只比對(duì)證書的公鑰, 畢竟只要公鑰沒被修改, 即使傳輸了數(shù)據(jù), 對(duì)方也沒有對(duì)應(yīng)的私鑰可以解密你的數(shù)據(jù), 相對(duì)沒那么嚴(yán)謹(jǐn)?shù)尿?yàn)證方式, 由于證書鏈里會(huì)有個(gè)多證書, 注意afn里面的邏輯是主要證書鏈上有任意一個(gè)節(jié)點(diǎn)的證書公鑰與self.pinnedPublicKeys這個(gè)受信任的公鑰是相同時(shí), 就會(huì)認(rèn)為這次認(rèn)證是成功的
疑問
看完之后還是有幾點(diǎn)小疑問
第一是如果證書里面帶有該證書的SHA-256簽名, 那是不是等于不需要在客戶端預(yù)埋證書呢? 只需要寫死SHA-256的值, 比對(duì)服務(wù)端下發(fā)的證書的SHA-256是否一致就可以提取公鑰了.
第二個(gè)是錨點(diǎn)證書的問題, 在afn里如果使用自建證書, afn會(huì)幫你將該自建證書設(shè)置為錨點(diǎn)證書. 但其實(shí)注釋了錨點(diǎn)證書的設(shè)置也一樣能建立起https連接, 那設(shè)置錨點(diǎn)證書的意義在哪呢?
待續(xù)...