詳解AFNetworking的HTTPS模組

hard_man發表於2018-11-25

0.0 簡述

文章內容包括:

  • AFNetworking簡介
  • ATS和HTTPS介紹
  • AF中的證書驗證介紹
  • 如何建立服務端和客戶端自簽名證書
  • 如何建立簡單的https伺服器
  • 對CA正式證書和自簽名證書的各種情況進行程式碼驗證

文中所涉及的檔案和指令碼程式碼請看這裡

1.0 AFNetworking簡介

AFNetworking(下面簡稱AF)是一個優秀的網路框架,從事iOS開發工作的同學幾乎都用過它。

同時,AF也是一個簡單,高效的網路框架。

AF3.0版本(3.2.1)是對NSURLSession的封裝。NSURLSession是蘋果公司的HTTP協議實現,它儘可能完整地實現了所有功能,但是同蘋果的Autolayout有相同的問題,就是API複雜難用。

因此在專案實踐中,即使我們不使用AF,我們也需要對NSURLSession進行適度封裝才能夠得心應手。AF幫你做了這件事,而且可能做的更好。

AF將NSURLSession的複雜呼叫封裝到框架內部,並向外提供了更加簡單易懂的介面,它主要包含如下功能:

  • 提供了AFHTTPSessionManager用於HTTP請求(GET,POST,...)
  • 提供AFURLRequestSerialization用於請求封裝,新增引數,設定header,傳遞資料
  • 提供AFURLResponseSerialization用於服務端返回資料的解析和過濾
  • 提供AFSecurityPolicy用於HTTPS協議證書驗證
  • 提供了AFNetworkReachabilityManager用於網路狀態監聽
  • 提供了UIKit主要可用於圖片快取,類似於SDWebImage

AF3.0的程式碼足夠簡單,各個模組也很容易理解,就不過多介紹了,我們著重分析一下AFSecurityPolicy這個模組。

2.0 ATS

iOS9.0版本中,包含了一個叫ATS的驗證機制,要求App網路請求必須是安全的。主要包含2點:

  1. 必須使用https
  2. https證書必須是公信機構頒發的證書

對於其中上面的第二點,在程式碼層次沒有強制要求,使用自簽名證書也是可以正常請求的,可能會在稽核階段有此要求。

3.0 AF中的證書驗證

AF中實現了對服務端證書的驗證功能,驗證通過之後,即可正常進行網路請求。

但是它沒有實現客戶端證書,所以如果伺服器要求雙向驗證的時候,我們就需要對AF進行一些擴充套件了。

關於https的介紹可以參考這裡

服務端驗證證書的程式碼在:AFURLSessionManager.m

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.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;
            }
        }
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

複製程式碼

在NSURLSession中,當請求https的介面時,會觸發- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler回撥,在這個回撥中,你需要驗證服務端傳送過來的證書,並返回一個NSURLCredential物件。

其中 disposition 這個變數用於表示你對證書的驗證結果,NSURLSessionAuthChallengeUseCredential表示驗證通過,其他值都表示驗證失敗。

challenge.protectionSpace.authenticationMethod 這個列舉字串表示的是回撥觸發的原因,其中,NSURLAuthenticationMethodServerTrust表示服務端發來證書,NSURLAuthenticationMethodClientCertificate表示服務端請求驗證客戶端證書。

驗證證書的方法在AFSecurityPolicy.m中

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        // https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
        //  According to the docs, you should only trust your provided certs for evaluation.
        //  Pinned certificates are added to the trust. Without pinned certificates,
        //  there is nothing to evaluate against.
        //
        //  From Apple Docs:
        //          "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
        //           Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
        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);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            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;
            }

            // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {
            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;
}
複製程式碼

程式碼解析:

  • 函式第一行就是一長串的邏輯判斷,乍一看,這裡看的人很懵,它包含的資訊很多。但實際上它的作用是用來處理服務端自簽名證書的。其他情況無需考慮此處邏輯。根據後面程式碼來看,如果你服務端證書使用的是自簽名證書,AFSecurityPolicyallowInvalidCertificates屬性必須設為YES,所以這裡判斷會帶上self.allowInvalidCertificates

  • 接下來就是驗證服務端證書的過程,SSLPinningMode 有3個值,AFSSLPinningModeNone表示服務端使用的是CA機構簽發的正式證書,另外2個值表示服務端使用的是自簽名證書。

  • AFServerTrustIsValid這個函式使用的是Security.framework中的方法,用於驗證服務端傳送來的證書是否是可信任的,只要證書鏈中任何一個證書是已經信任的證書,那麼這個服務端證書就是合法的。詳細過程已經被Security.framework處理了,不需要我們做額外工作。關於證書鏈可以參考這裡

  • 第三部分程式碼就是服務端自簽名證書的驗證了,這種情況下,需要把服務端證書也放到客戶端中一份。根據SSLPinningMode,你可以選擇使用服務端證書或者服務端證書內的公鑰

  • AFSSLPinningModeCertificate表示客戶端需要儲存一個服務端根證書,用於驗證服務端證書是否合法。客戶端需要將服務端證書的證書鏈上的任意一個證書拖入xcode工程中。

  • 自簽名證書需要設定pinnedCertificates屬性,把拖入xcode的證書載入到記憶體中,儲存在pinnedCertificates陣列中。通過SecTrustSetAnchorCertificates方法把陣列中的證書同服務端返回的證書做證書鏈繫結,然後就可以用AFServerTrustIsValid方法驗證證書是否合法了,如果服務端證書和我們客戶端儲存的證書可以正確匹配,這個函式就會返回YES。

  • AFSSLPinningModePublicKey表示客戶端需要儲存一個服務端根證書公鑰,用於驗證服務端證書是否合法。客戶端需要將服務端證書鏈上的任意一證書的公鑰拖入xcode工程中。

  • 若使用公鑰驗證,則需要從服務端證書中取出公鑰,同時取出客戶端中儲存的公鑰,逐一比較,如果有匹配的就認為驗證成功。

根據上述分析,客戶端對於證書的使用,有下面的3種情況:

  1. 服務端使用CA機構頒發的正式證書
  2. 服務端使用自簽名證書
  3. 服務端要驗證客戶端證書時,客戶端使用自簽名證書

4.0 證書驗證實踐

我們對上面所述3種證書使用情形進行逐一驗證。

驗證之前,我們需要做3個準備工作:

  • 第一是要把所需的證書建立出來
  • 第二是搭建簡單的伺服器用於測試
  • 第三是建立客戶端工程引入AF3.0準備測試

4.1 建立證書

https使用的證書都是基於X.509格式的。

CA機構的正式證書一般是要花錢購買的,當然也有免費的,我之前在阿里雲買過免費的證書。一般申請通過後,你可以把證書下載下來,其中主要包含私鑰和各種格式的證書。

自簽名的證書就比較容易了,在mac中可以使用openssl命令來生成。

我寫了一個簡單的指令碼,用於生成各種自簽名證書,你可以把它儲存到檔案(檔名為:create.sh)中,在終端裡執行。

指令碼會生成3種證書:根證書,客戶端證書,服務端證書。

其中不同的證書沒有本質區別,只是用在不同的地方而已。

每種證書包含5個檔案,分別是:

  • .der格式證書
  • .pem格式證書
  • .p12格式證書
  • .pem格式私鑰
  • .csr格式證書申請檔案
#!/bin/sh

locale='CN' #地區
province='Beijing' #省份
city=$province #城市
company='xxx' #公司
unit='yyy' #部門
hostname='127.0.0.1' #域名
email='hr@suning.com' #郵箱

#clean
function clean(){
	echo '清理檔案...'
	ls | grep -v create.sh | xargs rm -rf
}

#用法
function usage(){
	echo 'usage: ./create.sh 
		[-l [localevalue]]
		[-p [provincevalue]]
		[-c [cityvalue]]
		[-d [companyvalue]]
		[-u [unitvalue]]
		[-h [hostnamevalue]]
		[-e [emailvalue]]
	'
	exit
}

#引數
if [ $# -gt 0 ]; then
	while getopts "cl:p:c:d:u:h:e" arg;
	do
		case $arg in
			c)
				clean && exit
				;;
			l)
				locale=$OPTARG
				;;
			p)
				province=$OPTARG
				;;
			c)
				city=$OPTARG
				;;
			d)
				company=$OPTARG
				;;
			u)
				unit=$OPTARG
				;;
			h)
				hostname=$OPTARG
				;;
			e)
				email=$OPTARG
				;;
			?)
				usage
				;;
		esac
	done
fi

clean

echo '開始建立根證書...'

openssl genrsa -out ca-private-key.pem 1024
openssl req -new -out ca-req.csr -key ca-private-key.pem <<EOF
${locale}
${province}
${city}
${company}
${unit}
${hostname}
${email}

EOF
openssl x509 -req -in ca-req.csr -out ca-cert.pem -outform PEM -signkey ca-private-key.pem -days 3650
openssl x509 -req -in ca-req.csr -out ca-cert.der -outform DER -signkey ca-private-key.pem -days 3650
echo '請輸入根證書p12檔案密碼,直接回車表示密碼為空字串...'
openssl pkcs12 -export -clcerts -in ca-cert.pem -inkey ca-private-key.pem -out ca-cert.p12

echo '開始建立服務端證書...'

openssl genrsa -out server-private-key.pem 1024
openssl req -new -out server-req.csr -key server-private-key.pem << EOF
${locale}
${province}
${city}
${company}
${unit}
${hostname}
${email}

EOF
openssl x509 -req -in server-req.csr -out server-cert.pem -outform PEM -signkey server-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
openssl x509 -req -in server-req.csr -out server-cert.der -outform DER -signkey server-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
echo '請輸入服務端證書p12檔案密碼,直接回車表示密碼為空字串...'
openssl pkcs12 -export -clcerts -in server-cert.pem -inkey server-private-key.pem -out server-cert.p12

echo '開始建立客戶端證書...'

openssl genrsa -out client-private-key.pem 1024
openssl req -new -out client-req.csr -key client-private-key.pem << EOF
${locale}
${province}
${city}
${company}
${unit}
${hostname}
${email}

EOF
openssl x509 -req -in client-req.csr -out client-cert.pem -outform PEM -signkey client-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
openssl x509 -req -in client-req.csr -out client-cert.der -outform DER -signkey client-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
echo '請輸入客戶端證書p12檔案密碼,直接回車表示密碼為空字串...'
openssl pkcs12 -export -clcerts -in client-cert.pem -inkey client-private-key.pem -out client-cert.p12

echo 'finishied'

複製程式碼

你可以按照步驟操作:

  1. 複製指令碼內容,儲存到檔案中,檔名為create.sh

  2. 開啟終端,通過cd命令進入create.sh所在的資料夾

  3. 在終端內輸入:chmod +x create.sh 點選回車

  4. 在終端輸入:./create.sh -h,此時會列印用法

    usage: ./create.sh 
    	[-l [localevalue]]
    	[-p [provincevalue]]
    	[-c [cityvalue]]
    	[-d [companyvalue]]
    	[-u [unitvalue]]
    	[-h [hostnamevalue]]
    	[-e [emailvalue]]
    複製程式碼

    指令碼有下面幾種用法:

    • ./create.sh -h 列印用法
    • ./create.sh -c 會清空生成的所有檔案
    • ./create.sh 直接回車,會使用預設引數生成證書
    • ./create.sh + 用法中所述選項 會使用自定義的引數生成證書

指令碼執行成功後,應該會生成下面的檔案:

詳解AFNetworking的HTTPS模組

4.2 搭建簡單的HTTPS伺服器

我們使用nodejs來搭建https伺服器,請按照如下步驟操作:

  • 首先下載nodejs並安裝
  • 建立一個資料夾,資料夾內建立一個檔案,名字為package.json,內容如下:
{
    "name": "test-https",
    "version": "1.0.0",
    "main": "app.js",
    "scripts": {
        "start": "node app.js"
    },
    "debug": true,
    "dependencies": {
        "koa": "2.5.2",
        "koa-router": "7.4.0"
    }
}
複製程式碼
  • 建立另一個檔案,名字為app.js,內容如下:
const Koa = require('koa');
const https = require('https');
const fs = require('fs');
const router = require('koa-router')();

const app = new Koa();

//路由
router.get('/', (ctx, next) => {
    ctx.response.body = 'this is a simple node js https server response';
})
app.use(router.routes());

//https
https.createServer({
    key: fs.readFileSync('./yourServerCertPrivatekey.key'),
    cert: fs.readFileSync('./yourServerCert.pem'),
    requestCert: true,
    ca:[fs.readFileSync('./yourClientCert.pem')]
}, app.callback()).listen(3000);

console.log(`https app started at port 3000`)
複製程式碼
  • 開啟終端,使用cd命令進入我們建立的伺服器資料夾,然後執行命令:npm install,等待命令完成(可能會比較慢,根據網路情況而定)。如出現下列字樣表示安裝成功(不一定完全相同):
added 40 packages from 21 contributors and audited 53 packages in 8.446s
found 0 vulnerabilities
複製程式碼
  • 至此我們的簡易https伺服器就搭建完成了。我們可以使用命令:node app.js 來啟動伺服器。但是你會發現會報錯,這是因為fs.readFileSync(filename)這句程式碼表示要讀取一個證書檔案,要確保檔案存在才可以。我們後續根據需求來修改此處檔案路徑即可。
  • 伺服器啟動成功後,你可以在終端看到下面的文字:
https app started at port 3000
複製程式碼

4.3 建立客戶端工程

這個比較簡單,就不多說了。我們使用下列基本程式碼來做證書測試。

-(void) test{
	AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
    
    //HTTPS驗證程式碼,我們主要修改這裡
    AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];
    policy.validatesDomainName = NO;//不驗證域名,是為了測試方便,否則你需要修改host檔案了
    manager.securityPolicy = policy;
    
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    //請求地址就寫這個
    [manager GET:@"https://127.0.0.1:3000/" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"succ and response = [%@]", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"fail");
    }];
}
複製程式碼

4.4 服務端使用CA機構頒發的正式證書

這個是最簡單的情況,AF已經支援,我們不需要做任何額外工作就能夠支援。

首先,我們將服務端的程式碼中的證書路徑指向我們在CA機構申請好的服務端證書路徑,其中key表示證書私鑰,cert表示pem格式證書。另外將requestCertca這兩個欄位先刪除,然後重新啟動伺服器。像下面這樣:

... ...
//https
https.createServer({
    key: fs.readFileSync(這裡改成你的私鑰路徑),
    cert: fs.readFileSync(這裡改成你的pem格式證書路徑)
}, app.callback()).listen(3000);
... ...
複製程式碼

然後,客戶端的程式碼不需要修改。直接執行xcode,正常情況下你可以看到如下輸出:

succ and response = [this is a simple node js https server response]
複製程式碼

4.5 服務端使用自簽名證書

服務端程式碼不變,只是將證書和私鑰路徑修改為我們自簽名的證書路徑。

上文中,我們已經建立過自簽名的證書。

首先把證書資料夾的私鑰檔案server-private-key.pem和證書檔案server-cert.pem複製到伺服器資料夾下。

然後伺服器程式碼修改如下:

... ...
//https
https.createServer({
    key: fs.readFileSync('./server-private-key.pem'),
    cert: fs.readFileSync('./server-cert.pem')
}, app.callback()).listen(3000);
... ...
複製程式碼

重啟伺服器。

客戶端需要把證書資料夾內的server-cert.der檔案拖入xcode中,然後將xcode中的證書修改名字為server-cert.cer

客戶端程式碼做如下修改(請看註釋):

-(void) test{
	//使用伺服器自簽名證書,需要指定baseUrl屬性。
    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://127.0.0.1:3000"]];
    
    //AFSSLPinningModeCertificate表示使用自簽名證書
    AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    //為了測試方便不驗證域名,若要驗證域名,則請求時的域名要和建立證書(建立證書的指令碼執行時可指定域名)時的域名一致
    policy.validatesDomainName = NO;
    //自簽名伺服器證書需要設定allowInvalidCertificates為YES
    policy.allowInvalidCertificates = YES;
    //指定本地證書路徑
    policy.pinnedCertificates = [AFSecurityPolicy certificatesInBundle:[NSBundle mainBundle]];
    
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    [manager GET:@"https://127.0.0.1:3000/" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"succ and response = [%@]", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"fail");
    }];
}
複製程式碼

執行工程,正常情況下,可以看到正確輸出。

4.6 服務端驗證客戶端證書

這叫做雙向驗證,客戶端驗證服務端無誤之後,服務端也可以驗證客戶端證書,這樣可以保證資料傳輸雙方都是自己想要的目標。

首先,把證書資料夾內的client-cert.pem檔案複製到伺服器資料夾內。

然後修改服務端程式碼:

... ...
//https
https.createServer({
    key: fs.readFileSync('./server-private-key.pem'),
    cert: fs.readFileSync('./server-cert.pem'),
    requestCert: true,//表示客戶端需要證書
    ca:[fs.readFileSync('./client-cert.pem')]//用於匹配客戶端證書
}, app.callback()).listen(3000);
... ...
複製程式碼

重啟伺服器。

客戶端需要把證書資料夾內的client-cert.p12檔案拖到xcode中。

客戶端請求程式碼不需要修改。

因為AF3.0並沒有提供對客戶端證書的支援,所以我們需要修改AF的程式碼。

找到AFURLSessionManager.m檔案,在- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler方法。

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
        NSString *authMethod = challenge.protectionSpace.authenticationMethod;
        if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.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 if([authMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]){
            NSData *p12Data = [NSData dataWithContentsOfFile:[NSBundle pathForResource:@"client-cert" ofType:@"p12" inDirectory:[NSBundle mainBundle].bundlePath]];
            if([p12Data isKindOfClass:[NSData class]]){
                SecTrustRef trust = NULL;
                SecIdentityRef identity = NULL;
                [[self class] extractIdentity:&identity andTrust:&trust fromPKCS12Data:p12Data];
                if(identity){
                    SecCertificateRef certificate = NULL;
                    SecIdentityCopyCertificate(identity, &certificate);
                    const void *certs[] = {certificate};
                    CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
                    credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge  NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
                    disposition = NSURLSessionAuthChallengeUseCredential;
                }else{
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            }else{
                disposition = NSURLSessionAuthChallengePerformDefaultHandling;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
    
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

+ (BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
    OSStatus securityError = errSecSuccess;
    //客戶端證書密碼
    NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject: @""
                                                                 forKey: (__bridge id)kSecImportExportPassphrase];
    
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data,(__bridge CFDictionaryRef)optionsDictionary ,&items);
    
    if(securityError == 0) {
        CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex(items, 0);
        const void *tempIdentity = NULL;
        tempIdentity = CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemIdentity);
        *outIdentity = (SecIdentityRef)tempIdentity;
        const void *tempTrust = NULL;
        tempTrust = CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemTrust);
        *outTrust = (SecTrustRef)tempTrust;
        return YES;
    } else {
        NSLog(@"SecPKCS12Import is failed with error code %d", (int)securityError);
        return NO;
    }
}

複製程式碼

上述程式碼參考自這裡

值得注意的有2個地方:

  • 一個是p12檔案的檔名,我們這裡寫死了client-cert.p12,可以根據具體情況做修改。
  • 還有一個是p12檔案的密碼,在extractIdentity:方法的第三行,可以改成你的p12檔案密碼,密碼可以為空。

程式碼修改好之後,執行工程,可以得到正確的服務端返回。

5.0 總結

文中內容均已經過測試,但仍然可能有錯誤之處,如發現請留言。

文中所涉及的指令碼證書伺服器程式碼, 客戶端程式碼已經上傳到github中,點這裡,都已經包含了安裝環境,下載後直接開啟就能使用。

--完--

相關文章