你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 【投稿】iOS 9之適配ATS

【投稿】iOS 9之適配ATS

編輯:IOS開發基礎

本文為投稿文章,作者:liuchungui(博客)


iOS 9系統已經出來了,而網絡方面的ATS(App Transport Security)特性可以說每個人都要經歷。而我這篇博客,就是結合我最近幾天的經歷,來談談從服務器到iOS客戶端對ATS的適配。

一、簡單談談ATS(App Transport Security)

ATS(App Transport Security)是為了提高App與服務器之間安全傳輸數據一個特性,這個特性從iOS9和OSX10.11開始出現,它默認需要滿足以下幾個條件:

  • 服務器TLS版本至少是1.2版本

  • 連接加密只允許幾種先進的加密

  • 證書必須使用SHA256或者更好的哈希算法進行簽名,要麼是2048位或者更長的RSA密鑰,要麼就是256位或更長的ECC密鑰。

如果想了解哪幾種先進的加密是被允許的,詳情請見官方文檔App Transport Security Technote

二、搭建HTTPS服務器

搭建HTTPS服務器有兩種方式,一種是創建證書請求,然後到權威機構認證,隨之配置到服務器;另外一種是自建證書,然後配置給服務器。第一種方式搭建的HTTPS服務器當然是最優的了,建立網站的話,直接就會被信任,而作為移動端app的服務器時,也不需要為ATS做過多的適配。雖然說權威的機構認證都是需要錢的,但是如今也不乏存在免費的第三方認證機構;第二種方式搭建的HTTPS服務器,對於網站來說完全不可行,用戶打開時直接彈出一個警告提醒,說這是一個不受信任的網站,讓用戶是否繼續,體驗很差,而且讓用戶感覺網站不安全。對於移動端來說,在iOS9出現之前,這個沒什麼問題,但是在iOS9出來之後,第二種方式是通不過ATS特性,需要將NSAllowsArbitraryLoads設置為YES才行。所以,我推薦使用第一種方式搭建HTTPS服務器。

下面,咱們來說說這兩種方式都如何進行操作。

第一種、使用CA機構認證的證書搭建HTTPS服務器

1、創建證書請求,並提交給CA機構認證

#生成私鑰
openssl genrsa -des3 -out private.key 2048
#生成服務器的私鑰,去除密鑰口令 
openssl rsa -in private.key -out server.key
#生成證書請求
openssl req -new -key private.key -out server.csr

將生成server.csr提交給CA機構,CA機構對它進行簽名之後,然後會生成簽名後的根證書和服務器證書發送給你,這個時候的證書就是CA認證之後的證書。我們這裡將根證書和服務器證書分別改名為ca.crt和serve.crt。

2、配置Apache服務器 

將ca.crt、server.key、server.crt上傳到阿裡雲服務器,使用SSH登陸進入這三個文件的目錄,執行下面命令

mkdir ssl
cp server.crt /alidata/server/httpd/conf/ssl/server.crt
cp server.key /alidata/server/httpd/conf/ssl/server.key
cp demoCA/cacert.pem /alidata/server/httpd/conf/ssl/ca.crt
cp -r ssl /alidata/server/httpd/conf/

編輯/alidata/server/httpd/conf/extra/httpd-ssl.conf文件,找到SSLCertificateFile、SSLCertificateKeyFile、SSLCACertificatePath、SSLCACertificateFile進行修改:

# 指定服務器證書位置
SSLCertificateFile "/alidata/server/httpd/conf/ssl/server.crt"
# 指定服務器證書key位置
SSLCertificateKeyFile "/alidata/server/httpd/conf/ssl/server.key"
# 證書目錄
SSLCACertificatePath "/alidata/server/httpd/conf/ssl"
# 根證書位置
SSLCACertificateFile "/alidata/server/httpd/conf/ssl/ca.crt"

修改vhost配置vim /alidata/server/httpd/conf/vhosts/phpwind.conf

        SSLCertificateFile    /alidata/server/httpd/conf/ssl/server.crt
        SSLCertificateKeyFile /alidata/server/httpd/conf/ssl/server.key
        SSLCACertificatePath /alidata/server/httpd/conf/ssl
        SSLCACertificateFile /alidata/server/httpd/conf/ssl/ca.crt
        ServerName www.casetree.cn
        DocumentRoot /alidata/www

最後,重啟Apache服務器,在浏覽器輸入網址查看是否配置成功。我這裡是個人使用,申請的是免費的證書,我申請證書的網站是沃通。

搭建的成果:https://www.casetree.cn

第二種、自建證書配置HTTPS服務器

請查看我的上一篇自建證書配置HTTPS服務器

三、使用nscurl對服務器進行檢測

搭建完HTTPS服務器之後,可以使用nscurl命令來進行檢測,查看建立的HTTPS服務器是否能通過ATS特性。

nscurl --ats-diagnostics --verbose https://casetree.cn

如果HTTPS服務器能通過ATS特性,則上面所有測試案例都是PASS;如果某一項的Reuslt是FAIL,就找到ATS Dictionary來查看,就能知道HTTPS服務器不滿足ATS哪個條件。 這裡我前面碰到一個問題,就是自建證書的時候,通過此命令進行測試時,發現Result全是FAIL,而且在iOS的代碼測試中也出現了一個很奇怪的現象,就是相同的代碼,在iOS8.4請求數據完全正常,但是在iOS9上,直接是連接失敗。最終發現,其實就是因為自建證書不受信任,是通不過ATS的,除非將NSAllowsArbitraryLoads設置為YES。

四、iOS客戶端

在上面的第二大步驟當中,HTTPS服務器滿足ATS默認的條件,而且SSL證書是通過權威的CA機構認證過的,那麼我們在使用Xcode7開發的時候,對網絡的適配什麼都不用做,我們也能正常與服務器通信。但是,當我們對安全性有更高的要求時或者我們自建證書時,我們需要本地導入證書來進行驗證。

那麼,如何本地導入證書進行驗證呢? 

在這裡先提一下,由於iOS客戶端支持的證書是DER格式的,我們需要創建客戶端證書。創建客戶端證書,直接將服務端的CA根證書導出成DER格式就行。

openssl  x509  -inform PEM  -outform DER -in ca.crt -out ca.cer

導入完證書之後,我們分別來說說使用NSURLSession和AFNetworking來進行本地驗證。

首先,來說說使用NSURLSession驗證

驗證步驟如下: 

  • 導入CA根證書到工程中,即我們創建的ca.cer 

  • 獲取trust object,通過SecCertificateCreateWithData方法讀取導入的證書的數據生成一個證書對象,然後通過SecTrustSetAnchorCertificates 設置這個證書為trust object的信任根證書(trusted anchor)

  • 通過SecTrustEvaluate方法去驗證trust object 

下面是主要OC實現代碼,Demo工程我也放在github上了,有OC和Swift兩種語言,下載Demo請點擊HTTPSConnectDemo。

- (void)viewDidLoad {
    [super viewDidLoad];
    //導入客戶端證書
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];
    NSData *data = [NSData dataWithContentsOfFile:cerPath];
    SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef) data);
    self.trustedCerArr = @[(__bridge_transfer id)certificate];
    //發送請求
    NSURL *testURL = [NSURL URLWithString:@"https://casetree.cn/web/test/demo.php"];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:[NSURLRequest requestWithURL:testURL]];
    [task resume];
    // Do any additional setup after loading the view, typically from a nib.
}

#pragma mark - NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler{
 
    OSStatus err;
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    SecTrustResultType  trustResult = kSecTrustResultInvalid;
    NSURLCredential *credential = nil;
    
    //獲取服務器的trust object
    SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
    //將讀取的證書設置為serverTrust的根證書
    err = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)self.trustedCerArr);
    
    if(err == noErr){
        //通過本地導入的證書來驗證服務器的證書是否可信,如果將SecTrustSetAnchorCertificatesOnly設置為NO,則只要通過本地或者系統證書鏈任何一方認證就行
        err = SecTrustEvaluate(serverTrust, &trustResult);
    }
    
    if (err == errSecSuccess && (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified)){
        //認證成功,則創建一個憑證返回給服務器
        disposition = NSURLSessionAuthChallengeUseCredential;
        credential = [NSURLCredential credentialForTrust:serverTrust];
    }
    else{
        disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
    }
    
    //回調憑證,傳遞給服務器
    if(completionHandler){
        completionHandler(disposition, credential);
    }
}

注意: 

1、SecTrustSetAnchorCertificates方法會設置一個標示去屏蔽trust object對其它根證書的信任;如果你也想信任系統默認的根證書,請調用SecTrustSetAnchorCertificatesOnly方法,清空這個標示(設置為NO) 2、驗證的方法不僅僅只有這一種,更多的驗證方法,請參考HTTPS Server Trust Evaluation

下面,來談談AFNetworking是如何驗證的,我們如何使用AFNetworking。

AFNetworking的證書驗證工作是由AFSecurityPolicy來完成的,所以這裡我們主要來了解一下AFSecurityPolicy。注意:我這裡使用的是AFNetworking2.6.0,它跟2.5.0是有區別的。

說到AFSecurityPolicy,我們必須要提到它三個重要的屬性,如下:

@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
@property (nonatomic, assign) BOOL allowInvalidCertificates;
@property (nonatomic, assign) BOOL validatesDomainName;

SSLPingMode是最重要的屬性,它標明了AFSecurityPolicy是以何種方式來驗證。它是一個枚舉類型,這個枚舉類型有三個值,分別是AFSSLPinningModeNone、AFSSLPinningModePublicKey、AFSSLPinningModeCertificate。其中,AFSSLPinningModeNone代表了AFSecurityPolicy不做更嚴格的驗證,只要是系統信任的證書就可以通過驗證,不過,它受到allowInvalidCertificates和validatesDomainName的影響;AFSSLPinningModePublicKey是通過比較證書當中公鑰(PublicKey)部分來進行驗證,通過SecTrustCopyPublicKey方法獲取本地證書和服務器證書,然後進行比較,如果有一個相同,則通過驗證,此方式主要適用於自建證書搭建的HTTPS服務器和需要較高安全要求的驗證;AFSSLPinningModeCertificate則是直接將本地的證書設置為信任的根證書,然後來進行判斷,並且比較本地證書的內容和服務器證書內容是否相同,來進行二次判斷,此方式適用於較高安全要求的驗證。 

allowInvalidCertificates屬性代表是否允許不信任的證書通過驗證,默認為NO。

validatesDomainName屬性代表是否驗證主機名,默認為YES。

接下來,我們說下驗證流程。驗證流程主要放在AFSecurityPolicy的- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain方法當中。

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    //當使用自建證書驗證域名時,需要使用AFSSLPinningModePublicKey或者AFSSLPinningModeCertificate
    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;
    }
    
    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);
    //SSLPinningMode為AFSSLPinningModeNone時,allowInvalidCertificates為YES,則代表服務器任何證書都能驗證通過;如果它為NO,則需要判斷此服務器證書是否是系統信任的證書
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        if (self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust)){
            return YES;
        } else {
            return NO;
        }
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }
    
    //獲取服務器證書的內容
    NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            //AFSSLPinningModeCertificate是直接將本地的證書設置為信任的根證書,然後來進行判斷,並且比較本地證書的內容和服務器證書內容是否相同,如果有一個相同則返回YES
            
            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;
            }
            
            //判斷本地證書和服務器證書的內容是否相同
            NSUInteger trustedCertificateCount = 0;
            for (NSData *trustChainCertificate in serverCertificates) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    trustedCertificateCount++;
                }
            }
            return trustedCertificateCount > 0;
        }
        case AFSSLPinningModePublicKey: {
            //AFSSLPinningModePublicKey是通過比較證書當中公鑰(PublicKey)部分來進行驗證,通過SecTrustCopyPublicKey方法獲取本地證書和服務器證書,然後進行比較,如果有一個相同,則通過驗證
            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;
}

說了驗證流程,我們最後來看看AFNetworking怎麼使用,代碼如下:

 _httpClient = [[BGAFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];
 AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
 //是否允許CA不信任的證書通過
policy.allowInvalidCertificates = YES;
//是否驗證主機名
policy.validatesDomainName = YES;
_httpClient.securityPolicy = policy;

這裡我就沒有建立Demo了,如果要看的話,可以看看我寫的一個框架BGNetwork,裡面的Demo對ATS進行了適配,AFNetworking的使用放在BGNetworkConnector類裡面的- (instancetype)initWithBaseURL:(NSString *)baseURL delegate:(id)delegate初始化方法中。

五、適配ATS

前面的內容講述都是滿足ATS特性的情況,但若是服務器是自建證書搭建的,或者TLS版本是1.0的話,服務器又不能輕易改動,那麼我們客戶端如何適配呢? 不急,我們可以在工程中的Info.plist文件當中進行設置,主要參照下圖:

1.jpg

  • 如果是自建證書,沒有經過權威機構認證的證書,那麼需要將NSAllowsArbitraryLoads設置為YES才能通過。NSAllowsArbitraryLoads為YES,以前的HTTP請求也能通過。

  • 如果是認證過的證書,那麼可以通過nscurl --ats-diagnostics --verbose https://casetree.cn這樣的命令來查看服務器支持的ATS Dictionary,然後進行對應的設置。

適配的部分,也可以參照Demo1_iOS9網絡適配_ATS:改用更安全的HTTPS

總結

回顧前面的內容,總結一下,主要講了一下幾點內容:

  • ATS需要滿足的條件 

  • 如何建立證書,搭建HTTPS服務器 

  • 使用nscurl命令來檢測HTTPS服務器是否滿足ATS特性 

  • 客戶端的適配,講述了NSURLSession和AFNetworking的使用 

  • 講述了如果建立的服務器不滿足ATS的條件時,我們如何適配

參考

  • App Transport Security Technote 

  • HTTPS Server Trust Evaluation 

  • 打造安全的App!iOS安全系列之 HTTPS 

  • AFNetworking源碼解析<三>

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved