iOS使用自簽名證書實現HTTPS請求

概述

在16年的WWDC中,Apple已表示將從2017年1月1日起,全部新提交的App必須強制性應用HTTPS協議來進行網絡請求。
默認狀況下非HTTPS的網絡訪問是禁止的而且不能再經過簡單粗暴的向Info.plist中添加NSAllowsArbitraryLoads設置繞過ATS(App Transport Security)的限制(不然須在應用審覈時進行說明並極可能會被拒)。因此還未進行相應配置的公司須要儘快將升級爲HTTPS的事項提上進程了。html

Https

HTTPS就是HTTP協議上再加一層加密處理的SSL協議,即HTTP安全版。相比HTTP,HTTPS能夠保證內容在傳輸過程當中不會被第三方查看、及時發現被第三方篡改的傳輸內容、防止身份冒充,從而更有效的保證網絡數據的安全。至於深層次的原理和介紹請查詢相關資料和文檔。
HTTPS客戶端與服務器交互過程:
一、 客戶端第一次請求時,服務器會返回一個包含公鑰的數字證書給客戶端;
二、 客戶端生成對稱加密密鑰並用其獲得的公鑰對其加密後返回給服務器;
三、 服務器使用本身私鑰對收到的加密數據解密,獲得對稱加密密鑰並保存;
四、 而後雙方經過對稱加密的數據進行傳輸。
這裏寫圖片描述java

數字證書

在HTTPS客戶端與服務器第一次交互時,服務端返回給客戶端的數字證書是讓客戶端驗證這個數字證書是否是服務端的,證書全部者是否是該服務器,確保數據由正確的服務端發來,沒有被第三方篡改。數字證書能夠保證數字證書裏的公鑰確實是這個證書的全部者(Subject)的,或者證書能夠用來確認對方身份。證書由公鑰、證書主題(Subject)、數字簽名(digital signature)等內容組成。其中數字簽名就是證書的防僞標籤,目前使用最普遍的SHA-RSA加密。
證書通常分爲兩種:
一種是向權威認證機構購買的證書,服務端使用該種證書時,由於蘋果系統內置了其受信任的簽名根證書,因此客戶端不需額外的配置。爲了證書安全,在證書發佈機構公佈證書時,證書的指紋算法都會加密後再和證書放到一塊兒公佈以防止他人僞造數字證書。而證書機構使用本身的私鑰對其指紋算法加密,能夠用內置在操做系統裏的機構簽名根證書來解密,以此保證證書的安全。如x50九、RSA。
另外一種是本身製做的證書,即自簽名證書。好處是不須要花錢購買,但使用這種證書是不會受信任的,因此須要咱們在代碼中將該證書配置爲信任證書。這就是本文的主要目的。如12306官網的證書。ios

建立自定義證書

咱們在使用自簽名證書來實現HTTPS請求時,由於不像機構頒發的證書同樣其簽名根證書在系統中已經內置了,因此咱們須要在App中內置本身服務器的簽名根證書來驗證數字證書。
首先將服務端生成的.cer格式的根證書添加到項目中,注意在添加證書要必定要記得勾選要添加的targets。這裏有個地方要注意:蘋果的ATS要求服務端必須支持TLS 1.2或以上版本;必須使用支持前向保密的密碼;證書必須使用SHA-256或者更好的簽名hash算法來簽名,若是證書無效,則會致使鏈接失敗。因爲我在生成的根證書時簽名hash算法低於其要求,在配置完請求時一直報NSURLErrorServerCertificateUntrusted = -1202錯誤,但願你們能夠注意到這一點。
本文使用AFNetworking 3.0來配置證書校驗。其中AFSecurityPolicy類中封裝了證書校驗的過程。
AFSecurityPolicy分三種驗證模式:
一、AFSSLPinningModeNone:只驗證證書是否在新人列表中
二、AFSSLPinningModeCertificate:驗證證書是否在信任列表中,而後再對比服務端證書和客戶端證書是否一致
三、 AFSSLPinningModePublicKey:只驗證服務端與客戶端證書的公鑰是否一致
這裏咱們選第二種模式,而且對AFSecurityPolicy的allowInvalidCertificates和 validatesDomainName進行設置。git

準備證書

我這邊使用的是xca來製做了根證書,製做流程請參考http://www.2cto.com/Article/201411/347512.html,因爲xca沒法導出.jsk的後綴,所以咱們只要製做完根證書後以.p12的格式導出就好了,以後的證書製做由命令行來完成。自制一個批處理文件,添加以下命令:web

set ip=%1%
md %ip%
keytool -importkeystore -srckeystore ca.p12 -srcstoretype PKCS12 -srcstorepass 123456 -destkeystore ca.jks -deststoretype JKS -deststorepass 123456
keytool -genkeypair -alias server-%ip% -keyalg RSA -keystore ca.jks -storepass 123456 -keypass 123456 -validity 3650 -dname "CN=%ip%, OU=ly, O=hik, L=hz, ST=zj, C=cn"
keytool -certreq -alias server-%ip% -storepass 123456 -file %ip%\server-%ip%.certreq -keystore ca.jks
keytool -gencert -alias ca -storepass 123456 -infile %ip%\server-%ip%.certreq -outfile %ip%\server-%ip%.cer -validity 3650 -keystore ca.jks  
keytool -importcert -trustcacerts -storepass 123456 -alias server-%ip% -file %ip%\server-%ip%.cer -keystore ca.jks
keytool -delete -keystore ca.jks -alias ca -storepass 123456

將上面加粗的ca.p12改爲你導出的.p12文件的名稱,123456改成你建立證書的密碼。
而後在文件夾空白處按住ctrl+shift點擊右鍵,選擇在此處打開命令窗口,在命令窗口中輸入「start.bat ip/域名」來執行批處理文件,其中start.bat是添加了上述命令的批處理文件,ip/域名即你服務器的ip或者域名。執行成功後會生成一個.jks文件和一個以你的ip或域名命名的文件夾,文件夾中有一個.cer的證書,這邊的.jks文件將在服務端使用.cer文件將在客戶端使用,到這裏證書的準備工做就完成了。算法

服務端配置

打開tomcat/conf目錄下的server.xml文件將HTTPS的配置打開,並進行以下配置:apache

<Connector URIEncoding="UTF-8" protocol="org.apache.coyote.http11.Http11NioProtocol" port="8443" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" sslProtocol="TLSv1.2" sslEnabledProtocols="TLSv1.2" keystoreFile="${catalina.base}/ca/ca.jks" keystorePass="123456" clientAuth="false" SSLVerifyClient="off" netZone="你的ip或域名"/>

keystoreFile是你.jks文件放置的目錄,keystorePass是你製做證書時設置的密碼,netZone填寫你的ip或域名。注意蘋果要求協議要TLSv1.2以上。json

iOS端配置

首先把前面生成的.cer文件添加到項目中,注意在添加的時候選擇要添加的targets。tomcat

使用NSURLSession進行請求

NSString *urlString = @"https://xxxxxxx"; NSURL *url = [NSURL URLWithString:urlString]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0f]; NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request]; [task resume];

須要實現NSURLSessionDataDelegate中的URLSession:didReceiveChallenge:completionHandler:方法來進行證書的校驗:安全

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    NSLog(@"證書認證");
    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:@"ca" ofType:@"cer"];//自簽名證書
            NSData* caCert = [NSData dataWithContentsOfFile:cerPath];

            NSCAssert(caCert != nil, @"caCert is nil");
            if(nil == caCert)
                break; /* failed */

            SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
            NSCAssert(caRef != nil, @"caRef is nil");
            if(nil == caRef)
                break; /* failed */

            //能夠添加多張證書
            NSArray *caArray = @[(__bridge id)(caRef)];

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

            /* 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
            NSLog(@"信任該證書");

            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
            return [[challenge sender] useCredential: credential
                          forAuthenticationChallenge: challenge];

        }
        while(0);
    }

    // Bad dog
    NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,credential);
    return [[challenge sender] cancelAuthenticationChallenge: challenge];
}

此時便可成功請求到服務端。

使用AFNetworking進行請求

AFNetworking首先須要配置AFSecurityPolicy類,AFSecurityPolicy類封裝了證書校驗的過程。

/** AFSecurityPolicy分三種驗證模式: AFSSLPinningModeNone:只是驗證證書是否在信任列表中 AFSSLPinningModeCertificate:該模式會驗證證書是否在信任列表中,而後再對比服務端證書和客戶端證書是否一致 AFSSLPinningModePublicKey:只驗證服務端證書與客戶端證書的公鑰是否一致 */

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    securityPolicy.allowInvalidCertificates = YES;//是否容許使用自簽名證書
    securityPolicy.validatesDomainName = NO;//是否須要驗證域名,默認YES

    AFHTTPSessionManager *_manager = [AFHTTPSessionManager manager];
    _manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    _manager.securityPolicy = securityPolicy;
    //設置超時
    [_manager.requestSerializer willChangeValueForKey:@"timeoutinterval"];
    _manager.requestSerializer.timeoutInterval = 20.f;
    [_manager.requestSerializer didChangeValueForKey:@"timeoutinterval"];
    _manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringCacheData;
    _manager.responseSerializer.acceptableContentTypes  = [NSSet setWithObjects:@"application/xml",@"text/xml",@"text/plain",@"application/json",nil];

    __weak typeof(self) weakSelf = self;
    [_manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *_credential) {

        SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
        /** * 導入多張CA證書 */
        NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自簽名證書
        NSData* caCert = [NSData dataWithContentsOfFile:cerPath];
        NSArray *cerArray = @[caCert];
        weakSelf.manager.securityPolicy.pinnedCertificates = cerArray;

        SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
        NSCAssert(caRef != nil, @"caRef is nil");

        NSArray *caArray = @[(__bridge id)(caRef)];
        NSCAssert(caArray != nil, @"caArray is nil");

        OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
        SecTrustSetAnchorCertificatesOnly(serverTrust,NO);
        NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");

        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        __autoreleasing NSURLCredential *credential = nil;
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([weakSelf.manager.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;
        }

        return disposition;
    }];

因爲服務端使用.jks是一個證書庫,客戶端獲取到的證書可能不止一本,我這邊獲取到了兩本,具體獲取到基本可經過SecTrustGetCertificateCount方法獲取證書個數,AFNetworking在evaluateServerTrust:forDomain:方法中,AFSSLPinningMode的類型爲AFSSLPinningModeCertificate和AFSSLPinningModePublicKey的時候都有校驗服務端的證書個數與客戶端信任的證書數量是否同樣,若是不同的話沒法請求成功,因此這邊我就修改他的源碼,當有一個校驗成功時即算成功。
參考:http://www.jianshu.com/p/e6a26ecd84aa

本文同步分享在 博客「xiangzhihong8」(CSDN)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索