iOS 集成 SSL Pinning

一、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的證書后才能進行正常訪問。

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
};

如果我們得到kSecTrustResultProceedkSecTrustResultUnspecified之外的類型結(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前后抓包對比

使用SLL Pinning前
使用SLL Pinning后

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容