一、SSL Pinning 簡介
1、使用背景
在開發(fā)手機應(yīng)用時,如何正確的使用HTTPS來提高網(wǎng)絡(luò)傳輸?shù)陌踩允怯葹橹匾?。HTTPS協(xié)議本使用了SSL 加密傳輸,相比HTTP但依然存在極大的安全隱患----中間人攻擊。SSL解決了內(nèi)容的加密的問題,但是SSL過程中是依靠證書進行驗證的,這就需要保證證書絕對的安全。先立一個小目標(偽造證書),萬一實現(xiàn)了呢?在立一個小目標(偽造服務(wù)器),萬一實現(xiàn)了呢?事實證明目標是可以實現(xiàn)的(SSL系統(tǒng)遭入侵發(fā)布虛假密鑰 微軟谷歌受影響 )。SSL Pinning技術(shù)就是基于SSL基礎(chǔ)上在添加一個本地證書,用來再次驗證!
2、中間人攻擊
中間人攻擊(Man-in-the-middle Attack,簡稱MITM、MitM、MIM、MiM、MITMA)是一種由來已久的網(wǎng)絡(luò)入侵手段,并且在今天仍然有著廣泛的發(fā)展空間,如SMB會話劫持、DNS欺騙等攻擊都是典型的中間人攻擊。簡而言之,所謂的中間人攻擊就是通過攔截正常的網(wǎng)絡(luò)通信數(shù)據(jù),并進行數(shù)據(jù)篡改和嗅探,而通信的雙方卻毫不知情。

3、Charles抓包原理
Charles作為一個中間人代理,當(dāng)瀏覽器和服務(wù)器通信時,Charles接收服務(wù)器的證書,但動態(tài)生成一張證書發(fā)送給瀏覽器,也就是說Charles作為中間代理在瀏覽器和服務(wù)器之間通信,所以通信的數(shù)據(jù)可以被Charles攔截并解密。由于Charles更改了證書,瀏覽器校驗不通過會給出安全警告,必須安裝Charles的證書后才能進行正常訪問。

- 客戶端向服務(wù)器發(fā)起HTTPS請求
- Charles攔截客戶端的請求,偽裝成客戶端向服務(wù)器進行請求
- 服務(wù)器向“客戶端”(實際上是Charles)返回服務(wù)器的CA證書
- Charles攔截服務(wù)器的響應(yīng),獲取服務(wù)器證書公鑰,然后自己制作一張證書,將服務(wù)器證書替換后發(fā)送給客戶端。(這一步,Charles拿到了服務(wù)器證書的公鑰)
- 客戶端接收到“服務(wù)器”(實際上是Charles)的證書后,生成一個對稱密鑰,用Charles的公鑰加密,發(fā)送給“服務(wù)器”(Charles)
- Charles攔截客戶端的響應(yīng),用自己的私鑰解密對稱密鑰,然后用服務(wù)器證書公鑰加密,發(fā)送給服務(wù)器。(這一步,Charles拿到了對稱密鑰)
- 服務(wù)器用自己的私鑰解密對稱密鑰,向“客戶端”(Charles)發(fā)送響應(yīng)
- Charles攔截服務(wù)器的響應(yīng),替換成自己的證書后發(fā)送給客戶端
- 至此,連接建立,Charles拿到了 服務(wù)器證書的公鑰 和 客戶端與服務(wù)器協(xié)商的對稱密鑰,之后就可以解密或者修改加密的報文了。
HTTPS抓包的原理還是挺簡單的,簡單來說,就是Charles作為“中間人代理”,拿到了 服務(wù)器證書公鑰 和 HTTPS連接的對稱密鑰,前提是客戶端選擇信任并安裝Charles的CA證書,否則客戶端就會“報警”并中止連接。這樣看來,HTTPS還是很安全的。
4、SSL Pinning
SSL Pinning(又叫Certificate Pinning)可以理解為證書綁定。在一些應(yīng)用場景中,客戶端和服務(wù)器之間的通信是事先約定好的,既服務(wù)器地址和證書是預(yù)先知道的,這種情況常見于CS(Client-Server)架構(gòu)的應(yīng)用中。這樣的話在客戶端事先保存好一份服務(wù)器的證書(含公鑰),每次請求服務(wù)器的時候,將服務(wù)器返回的證書與客戶端保存的證書進行對比,如果證書不符,說明受到中間人攻擊,馬上可以中斷請求。這樣的話中間人就無法偽造證書進行攻擊了。
我們需要將APP代碼內(nèi)置僅接受指定域名的證書,而不接受操作系統(tǒng)或瀏覽器內(nèi)置的CA根證書對應(yīng)的任何證書,通過這種授權(quán)方式,保障了APP與服務(wù)端通信的唯一性和安全性。但是CA簽發(fā)證書都存在有效期問題,所以缺點是在證書續(xù)期后需要將證書重新內(nèi)置到APP中。
公鑰鎖定則是提取證書中的公鑰并內(nèi)置到移動端APP中,通過與服務(wù)器對比公鑰值來驗證連接的合法性,我們在制作證書密鑰時,公鑰在證書的續(xù)期前后都可以保持不變(即密鑰對不變),所以可以避免證書有效期問題。
證書鎖定旨在解決移動端APP與服務(wù)端通信的唯一性,實際通信過程中,如果鎖定過程失敗,那么客戶端APP將拒絕針對服務(wù)器的所有 SSL/TLS 請求,F(xiàn)aceBook/Twitter則通過證書鎖定以防止Charles/Fiddler等抓包工具中間人攻擊。
為什么直接對比就能保證證書沒問題?如果中間人從客戶端取出證書,再偽裝成服務(wù)端跟其他客戶端通信,它發(fā)送給客戶端的這個證書不就能通過驗證嗎?確實可以通過驗證,但后續(xù)的流程走不下去,因為下一步客戶端會用證書里的公鑰加密,中間人沒有這個證書的私鑰就解不出內(nèi)容,也就截獲不到數(shù)據(jù),這個證書的私鑰只有真正的服務(wù)端有,中間人偽造證書主要偽造的是公鑰。
為什么要用SSL Pinning?正常的驗證方式不夠嗎?如果服務(wù)端的證書是從受信任的的CA機構(gòu)頒發(fā)的,驗證是沒問題的,但CA機構(gòu)頒發(fā)證書比較昂貴,小企業(yè)或個人用戶可能會選擇自己頒發(fā)證書,這樣就無法通過系統(tǒng)受信任的CA機構(gòu)列表驗證這個證書的真?zhèn)瘟?,所以需要SSL Pinning這樣的方式去驗證。
二、NSURLSession方式
1、獲取證書
客戶端需要證書(Certification file), .cer格式的文件??梢愿?wù)器端索取。如果他們給個.pem文件,要使用命令行轉(zhuǎn)換:
openssl x509 -inform PEM -in name.pem -outform DER -out name.cer
如果給了個.crt文件,請這樣轉(zhuǎn)換:
openssl x509 -in name.crt -out name.cer -outform der
如果啥都不給你,你只能自己動手了,這里以github.com為例子,獲取證書:
openssl s_client -connect github.com:443 </dev/null 2>/dev/null | openssl x509 -outform DER > github.com.cer
2、NSURLSession實現(xiàn)
當(dāng)談到NSURLSession使用SSL pinning有點棘手,因為在AFNetworking中,其本身已經(jīng)有封裝好的類可以使用來進行配置。這里沒有辦法去設(shè)置一組證書來自動取消所有本地證書不匹配的response。我們需要手動執(zhí)行檢查來實現(xiàn)在NSURLSession上的SSL pinning。我們很榮幸的是我們可以用Security's framework C API。
創(chuàng)建默認會話配置的NSURLSession對象,及發(fā)送請求,執(zhí)行任務(wù)
// 設(shè)置地址
NSURL *testURL = [NSURL URLWithString:@"https://github.com"];
// 創(chuàng)建默認會話配置的NSURLSession對象
NSURLSessionConfiguration *seeConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
seeConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
NSURLSession *session = [NSURLSession sessionWithConfiguration:seeConfig
delegate:self
delegateQueue:nil];
// NSURLSession使用NSURLSessionTask來發(fā)送一個請求,
// 我們使用dataTaskWithURL:completionHandler:方法來進行SSL pinning 測試
NSURLSessionDataTask *task =
[session dataTaskWithURL:testURL
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error) {
NSString *str =
[[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding];
NSLog(@"str : %@", str);
} else {
NSLog(@"error : %@", error);
}
}];
[task resume];
在代理回調(diào)方法中,校驗證書是否合法
// 代理回調(diào)
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
// 得到遠程證書
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);
// 設(shè)置ssl政策來檢測主域名
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)];
// 驗證服務(wù)器證書
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
BOOL certificateIsValid =
(result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
// 得到遠程和本地證書data
NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
NSString *pathToCert = [[NSBundle mainBundle] pathForResource:@"github2018" ofType:@"cer"];
NSData *localCertificate = [NSData dataWithContentsOfFile:pathToCert];
// 檢查
if (certificateIsValid && [remoteCertificateData isEqualToData:localCertificate]) {
// 驗證通過
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}else {
// 驗證不通過
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,NULL);
}
}
上述方法的開始,我們使用SecTrustGetCertificateAtIndex來得到服務(wù)器的SSL證書數(shù)據(jù)。然后使用證書評估設(shè)置policies。證書使用SecTrustEvaluate評估,然后返回以下幾種認證結(jié)果類型之一:
typedef uint32_t SecTrustResultType;
enum {
kSecTrustResultInvalid = 0,
kSecTrustResultProceed = 1,
kSecTrustResultConfirm SEC_DEPRECATED_ATTRIBUTE = 2,
kSecTrustResultDeny = 3,
kSecTrustResultUnspecified = 4,
kSecTrustResultRecoverableTrustFailure = 5,
kSecTrustResultFatalTrustFailure = 6,
kSecTrustResultOtherError = 7
};
如果我們得到kSecTrustResultProceed和kSecTrustResultUnspecified之外的類型結(jié)果,我們可以認為證書是無效的(不被信任的)。
至今為止我們除了檢測遠程服務(wù)器證書評估外,還沒有做其他事情,對于SSL pinning 檢測我們需要通過SecCertificateRef來得到他的NSData。這個SecCertificateRef來自于challenge.protectionSpace.serverTrust。而本地的NSData來自本地的.cer證書文件。然后我們使用isEqual來進行SSL pinning。
如果遠程服務(wù)器證書的NSData等于本地的證書data,那么就可以通過評估,我們可以驗證服務(wù)器身份然后進行通信,而且還要使用completionHandler(NSURLSessionAuthChallengeUseCredential,credential)執(zhí)行request。
然而如果兩個data不相等,我們使用completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,NULL)方法來取消dataTask的執(zhí)行,這樣就可以拒絕和服務(wù)器溝通。
這就是在NSURLSession中使用SSL pinning。
三、AFNetworking方式
1、AFSecurityPolicy
安全模式設(shè)置
AFSecurityPolicy是AFNetworking中三種安全策略模塊,提供了證書鎖定模式
- AFSSLPinningModeNone:
這個模式表示不做SSL pinning,只跟瀏覽器一樣在系統(tǒng)的信任機構(gòu)列表里驗證服務(wù)端返回的證書。
若證書是信任機構(gòu)簽發(fā)的就會通過,若是自己服務(wù)器生成的證書,這里是不會通過的。
- AFSSLPinningModeCertificate:
這個模式表示用證書綁定方式驗證證書,需要客戶端保存有服務(wù)端的證書拷貝,
這里驗證分兩步,第一步驗證證書的域名/有效期等信息,
第二步是對比服務(wù)端返回的證書跟客戶端返回的是否一致。
- AFSSLPinningModePublicKey:
這個模式同樣是用證書綁定方式驗證,客戶端要有服務(wù)端的證書拷貝,
只是驗證時只驗證證書里的公鑰,不驗證證書的有效期等信息。
只要公鑰是正確的,就能保證通信不會被竊聽,
因為中間人沒有私鑰,無法解開通過公鑰加密的數(shù)據(jù)。
選擇那種模式呢?
AFSSLPinningModeCertificate最安全的比對模式。但是也比較麻煩,因為證書是打包在APP中,如果服務(wù)器證書改變或者到期,舊版本無法使用了,我們就需要用戶更新APP來使用最新的證書。
AFSSLPinningModePublicKey只比對證書的Public Key,只要Public Key沒有改變,證書的其他變動都不會影響使用。
如果你不能保證你的用戶總是使用你的APP的最新版本,所以我們使用AFSSLPinningModePublicKey。
屬性
/**
服務(wù)器證書驗證模式,默認是不驗證
*/
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
/**
驗證服務(wù)器的證書的集合,默認情況下,AFNetworking會搜索工程中所有.cer的證書文件,但不會將某個證書作為默認。如果想創(chuàng)建AFSecurityPolicy對象,就先調(diào)用certificatesInBundle方法加載證書,然后調(diào)用policyWithPinningMode:withPinnedCertificates方法創(chuàng)建對象
*/
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
/**
是否信任無效或者過期的證書,默認為否
*/
@property (nonatomic, assign) BOOL allowInvalidCertificates;
/**
是否驗證證書中的域名,默認為是
*/
@property (nonatomic, assign) BOOL validatesDomainName;
2、AFNetworking實現(xiàn)
創(chuàng)建自定義安全策略
// 自定義安全策略
- (AFSecurityPolicy *)customSecurityPolicy {
// 獲取證書
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"github2020" ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
NSSet *pinnedCertificates = [[NSSet alloc] initWithObjects:certData, nil];
/*
安全模式
AFSSLPinningModeNone:完全信任服務(wù)器證書;
AFSSLPinningModePublicKey:只比對服務(wù)器證書和本地證書的Public Key是否一致,如果一致則信任服務(wù)器證書;
AFSSLPinningModeCertificate:比對服務(wù)器證書和本地證書的所有內(nèi)容,完全一致則信任服務(wù)器證書
*/
AFSecurityPolicy *securityPolicy =
[AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey
withPinnedCertificates:pinnedCertificates];
// allowInvalidCertificates 是否允許無效證書(也就是自建的證書),默認為NO
// 如果是需要驗證自建證書,需要設(shè)置為YES
securityPolicy.allowInvalidCertificates = YES;
/*
validatesDomainName 是否需要驗證域名,默認為YES;
假如證書的域名與你請求的域名不一致,需把該項設(shè)置為NO;
如設(shè)成NO的話,即服務(wù)器使用其他可信任機構(gòu)頒發(fā)的證書,也可以建立連接,這個非常危險,建議打開。
置為NO,主要用于這種情況:客戶端請求的是子域名,而證書上的是另外一個域名。
因為SSL證書上的域名是獨立的,假如證書上注冊的域名是www.google.com,那么mail.google.com是無法驗證通過的;
當(dāng)然,有錢可以注冊通配符的域名*.google.com,但這個還是比較貴的。
如置為NO,建議自己添加對應(yīng)域名的校驗邏輯。
*/
securityPolicy.validatesDomainName = YES;
return securityPolicy;
}
創(chuàng)建網(wǎng)絡(luò)會話管理
- (AFHTTPSessionManager *)manager {
if (!_manager) {
// 設(shè)置BaseUrl
NSURL *baseUrl = [NSURL URLWithString:@"https://github.com"];
AFHTTPSessionManager *manager =
[[AFHTTPSessionManager manager] initWithBaseURL:baseUrl];
manager.securityPolicy = [self customSecurityPolicy];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
_manager = manager;
}
return _manager;
}
發(fā)送請求
// 發(fā)送請求
- (void)sendRequest {
NSString *urlStr = @"https://github.com/AFNetworking/AFNetworking";
[self.manager GET:urlStr
parameters:nil
headers:nil
progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *str = [[NSString alloc] initWithData:responseObject
encoding:NSUTF8StringEncoding];
NSLog(@"%@",str);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%@", error);
}];
}
附Demo鏈接:https://github.com/ZhangJingHao/ZJHSSLPinning
Charles對使用SSL Pinning前后抓包對比


參考鏈接:
如何使用SSL pinning來使你的iOS APP更加安全
證書鎖定SSL Pinning簡介及用途
AFNetworking + SSL Pinning
SSL pinning using AFNetworking and NSURLSession
淺談HTTPS通信機制和Charles抓包原理