iOS開發HTTPS實現之信任SSL證書和自簽名證書

首先來分析一下什麼是HTTPS以及瞭解HTTPS對於iOS開發者的意義html

HTTPS 以及SSL/TSL

  • 什麼是SSL?

SSL(Secure Sockets Layer, 安全套接字層),由於原先互聯網上使用的 HTTP 協議是明文的,存在不少缺點,好比傳輸內容會被偷窺(嗅探)和篡改。 SSL 協議的做用就是在傳輸層對網絡鏈接進行加密。ios

  • 何爲TLS?

到了1999年,SSL 由於應用普遍,已經成爲互聯網上的事實標準。IETF 就在那年把 SSL 標準化。標準化以後的名稱改成 TLS(Transport Layer Security,傳輸層安全協議)。SSL與TLS能夠視做同一個東西的不一樣階段git

  • HTTPS

簡單來講,HTTPS = HTTP + SSL/TLS, 也就是 HTTP over SSL 或 HTTP over TLS,這是後面加 S 的由來 。github

HTTPS和HTTP異同:HTTP和HTTPS使用的是徹底不一樣的鏈接方式,用的端口也不同,前者是80,後者是443。HTTP的鏈接很簡單,是無狀態的;HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網絡協議,比HTTP協議安全。算法

在WWDC 2016開發者大會上,蘋果宣佈了一個最後期限:到2017年1月1日 App Store中的全部應用都必須啓用 App Transport Security安全功能。App Transport Security(ATS)是蘋果在iOS 9中引入的一項隱私保護功能,屏蔽明文HTTP資源加載,鏈接必須通過更安全的HTTPS。蘋果目前容許開發者暫時能夠繼續使用HTTP鏈接,但到年末全部官方商店的應用都必須強制性使用ATS。數組

如下是開發者網站公告原文:安全

應用傳輸安全協議是與iOS9和OS X 10.11一同發佈的,該協議須要應用程序經過HTTPS使用安全的網絡鏈接,以提升用戶的數據和隱私安全。
在2016年WWDC上咱們宣佈在今年年末以前,提交到App Store的應用程序必須支持應用傳輸安全協議。爲了給大家額外的時間去準備,這個截止日期已被延長,當新的截止日期肯定的時候,咱們將及時提供相關信息。服務器

2016年12月21日蘋果更新了截止日期,宣佈延期執行ATS支持要求Supporting App Transport Security網絡

 
開發者網站截圖

** 此舉爲開發者提供了更多時間來作適配和支持。然而,對於iOS開發者來講,儘早解決HTTPS請求的問題仍爲上策。**app

蘋果ATS對HTTPS證書的要求

啓用ATS必須符合如下標準,不知足條件的HTTPS證書,ATS都會拒絕鏈接:

  • 服務器全部的鏈接使用TLS1.2以上版本
  • HTTPS證書必須使用SHA256以上哈希算法簽名
  • HTTPS證書必須使用RSA 2048位或ECC 256位以上公鑰算法
  • 使用前向加密技術

此外,蘋果ATS支持CT證書透明,要求開發者使用支持CT證書透明度的SSL證書,確保SSL證書合法透明,防止中間人攻擊。

發送HTTPS請求信任SSL證書和自簽名證書,分爲三種狀況

1.若是你的app服務端安裝的是SLL頒發的CA,可使用系統方法直接實現信任SSL證書,關於Apple對SSL證書的要求請參考:蘋果官方文檔CertKeyTrustProgGuide

這種方式不須要在Bundle中引入CA文件,能夠交給系統去判斷服務器端的證書是否是SSL證書,驗證過程也不須要咱們去具體實現。
示例代碼:

NSURL *URL = [NSURL URLWithString:URLString];
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:URL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
    //建立同步鏈接
    NSError *error = nil;
    NSData *receivedData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&error];
    NSString *receivedInfo = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];

固然,若是你須要同時信任SSL證書和自簽名證書的話仍是須要在代碼中實現CA的驗證,這種狀況在後面會提到。

2.基於AFNetWorking的SSL特定服務器證書信任處理,重寫AFNetWorking的customSecurityPolicy方法,這裏我建立了一個HttpRequest類,分別對GET和POST方法進行了封裝,以GET方法爲例:

+ (void)get:(NSString *)url params:(NSDictionary *)params success:(void (^)(id))success failure:(void (^)(NSError *))failure {
    // 1.得到請求管理者
    AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
    // 2.申明返回的結果是text/html類型
    mgr.responseSerializer = [AFHTTPResponseSerializer serializer];
    // 3.設置超時時間爲10s
    mgr.requestSerializer.timeoutInterval = 10;
    
    // 加上這行代碼,https ssl 驗證。
    if(openHttpsSSL) {
        [mgr setSecurityPolicy:[self customSecurityPolicy]];
    }
    
    // 4.發送GET請求
    [mgr GET:url parameters:params success:^(AFHTTPRequestOperation *operation, id responseObj){
        if (success) {
            success(responseObj);
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        if (error) {
            failure(error);
        }
    }];
}
+ (AFSecurityPolicy*)customSecurityPolicy {
    // /先導入證書
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:certificate ofType:@"cer"];//證書的路徑
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    
    // AFSSLPinningModeCertificate 使用證書驗證模式
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    
    // allowInvalidCertificates 是否容許無效證書(也就是自建的證書),默認爲NO
    // 若是是須要驗證自建證書,須要設置爲YES
    securityPolicy.allowInvalidCertificates = YES;
    
    //validatesDomainName 是否須要驗證域名,默認爲YES;
    //假如證書的域名與你請求的域名不一致,需把該項設置爲NO;如設成NO的話,即服務器使用其餘可信任機構頒發的證書,也能夠創建鏈接,這個很是危險,建議打開。
    //置爲NO,主要用於這種狀況:客戶端請求的是子域名,而證書上的是另一個域名。由於SSL證書上的域名是獨立的,假如證書上註冊的域名是www.google.com,那麼mail.google.com是沒法驗證經過的;固然,有錢能夠註冊通配符的域名*.google.com,但這個仍是比較貴的。
    //如置爲NO,建議本身添加對應域名的校驗邏輯。
    securityPolicy.validatesDomainName = NO;
    
    securityPolicy.pinnedCertificates = @[certData];
    
    return securityPolicy;
}

其中的cerPath就是app bundle中證書路徑,certificate爲證書名稱的宏,僅支持cer格式,securityPolicy的相關配置尤其重要,請仔細閱讀customSecurityPolicy方法並根據實際狀況設置其屬性。

這樣,就可以在AFNetWorking的基礎上使用HTTPS協議訪問特定服務器,可是不能信任根證書的CA文件,所以這種方式存在風險,讀取pinnedCertificates中的證書數組的時候有可能失敗,若是證書不符合,certData就會爲nil。

3.更改系統方法,發送異步NSURLConnection請求。

- (void)getDataWithURLRequest {
    //connection
    NSString *urlStr = @"https://developer.apple.com/cn/";
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
    NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];
    [connection start];
}

重點在於處理NSURLConnection的didReceiveAuthenticationChallenge代理方法,對CA文件進行驗證,並創建信任鏈接。

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
    
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    
 /*
    //直接驗證服務器是否被認證(serverTrust),這種方式直接忽略證書驗證,直接創建鏈接,但不能過濾其它URL鏈接,能夠理解爲一種折衷的處理方式,實際上並不安全,所以不推薦。
    SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
    return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]
                  forAuthenticationChallenge: challenge];
     */
    if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust]) {
        do
        {
            SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
            NSCAssert(serverTrust != nil, @"serverTrust is nil");
            if(nil == serverTrust)
                break; /* failed */
            /**
             *  導入多張CA證書(Certification Authority,支持SSL證書以及自簽名的CA)
             */
            NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"cloudwin" ofType:@"cer"];//自簽名證書
            NSData* caCert = [NSData dataWithContentsOfFile:cerPath];
            
            NSString *cerPath2 = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"cer"];//SSL證書
            NSData * caCert2 = [NSData dataWithContentsOfFile:cerPath2];
            
            NSCAssert(caCert != nil, @"caCert is nil");
            if(nil == caCert)
                break; /* failed */
            
            NSCAssert(caCert2 != nil, @"caCert2 is nil");
            if (nil == caCert2) {
                break;
            }
            
            SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
            NSCAssert(caRef != nil, @"caRef is nil");
            if(nil == caRef)
                break; /* failed */
            
            SecCertificateRef caRef2 = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert2);
            NSCAssert(caRef2 != nil, @"caRef2 is nil");
            if(nil == caRef2)
                break; /* failed */
            
            NSArray *caArray = @[(__bridge id)(caRef),(__bridge id)(caRef2)];
        
            NSCAssert(caArray != nil, @"caArray is nil");
            if(nil == caArray)
                break; /* failed */
            
            OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
            NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
            if(!(errSecSuccess == status))
                break; /* failed */
            
            SecTrustResultType result = -1;
            status = SecTrustEvaluate(serverTrust, &result);
            if(!(errSecSuccess == status))
                break; /* failed */
            NSLog(@"stutas:%d",(int)status);
            NSLog(@"Result: %d", result);
            
            BOOL allowConnect = (result == kSecTrustResultUnspecified) || (result == kSecTrustResultProceed);
            if (allowConnect) {
                NSLog(@"success");
            }else {
                NSLog(@"error");
            }
            /* https://developer.apple.com/library/ios/technotes/tn2232/_index.html */
            /* https://developer.apple.com/library/mac/qa/qa1360/_index.html */
            /* kSecTrustResultUnspecified and kSecTrustResultProceed are success */
            if(! allowConnect)
            {
            break; /* failed */
            }
            
#if 0
            /* Treat kSecTrustResultConfirm and kSecTrustResultRecoverableTrustFailure as success */
            /*   since the user will likely tap-through to see the dancing bunnies */
            if(result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure || result == kSecTrustResultOtherError)
                break; /* failed to trust cert (good in this case) */
#endif
            
            // The only good exit point
            return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]
                          forAuthenticationChallenge: challenge];
            
        } while(0);
    }
    
    // Bad dog
    return [[challenge sender] cancelAuthenticationChallenge: challenge];
    
}

這裏的關鍵在於result參數的值,根據官方文檔的說明,判斷(result == kSecTrustResultUnspecified) || (result == kSecTrustResultProceed)的值,若爲1,則該網站的CA被app信任成功,能夠創建數據鏈接,這意味着全部由該CA簽發的各個服務器證書都被信任,而訪問其它沒有被信任的任何網站都會鏈接失敗。該CA文件既能夠是SLL也能夠是自簽名。

NSURLConnection的其它代理方法實現

#pragma mark -- connect的異步代理方法
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSLog(@"請求被響應");
    _mData = [[NSMutableData alloc]init];
}

-(void)connection:(NSURLConnection *)connection didReceiveData:(nonnull NSData *)data {
    NSLog(@"開始返回數據片斷");
    
    [_mData appendData:data];
}

-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"連接完成");
    //能夠在此解析數據
    NSString *receiveInfo = [NSJSONSerialization JSONObjectWithData:self.mData options:NSJSONReadingAllowFragments error:nil];
    NSLog(@"received data:\\\\n%@",self.mData);
    NSLog(@"received info:\\\\n%@",receiveInfo);
}

//連接出錯
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"error - %@",error);
}

至此,HTTPS信任證書的問題得以解決,這不只是爲了響應Apple強制性使用ATS的要求,也是爲了實際生產環境安全性的考慮,HTTPS是將來的趨勢,建議儘早支持。

如需參考Demo請移步本人在Github上的開源項目

相關文章
相關標籤/搜索