單項認證:客戶端APP包裏保存一份證書 用於校驗服務端證書是否合法
雙向認證:單項認證以外, 客戶端(不是app,這裏指系統)要擁有一份證書 用於傳給服務端用於校驗客戶端證書是否合法
分兩方面講解:一 客戶端配置 二 服務端配置
一 客戶端配置。
單向認證流程:
- 客戶端向服務端發送SSL協議版本號、加密算法種類、隨機數等信息。
- 服務端給客戶端返回SSL協議版本號、加密算法種類、隨機數等信息,同時也返回服務器端的證書,即公鑰證書
- 客戶端使用服務端返回的信息驗證服務器的合法性,包括:
- 證書是否過期
- 發形服務器證書的CA是否可靠
- 返回的公鑰是否能正確解開返回證書中的數字簽名
- 服務器證書上的域名是否和服務器的實際域名相匹配
- 驗證通過後,將繼續進行通信,否則,終止通信
- 客戶端向服務端發送自己所能支持的對稱加密方案,供服務器端進行選擇
- 服務器端在客戶端提供的加密方案中選擇加密程度最高的加密方式。
- 服務器將選擇好的加密方案通過明文方式返回給客戶端
- 客戶端接收到服務端返回的加密方式後,使用該加密方式生成產生隨機碼,用作通信過程中對稱加密的密鑰,使用服務端返回的公鑰進行加密,將加密後的隨機碼發送至服務器
- 服務器收到客戶端返回的加密信息後,使用自己的私鑰進行解密,獲取對稱加密密鑰。在接下來的會話中,服務器和客戶端將會使用該密碼進行對稱加密,保證通信過程中信息的安全。
雙向認證流程:
- 客戶端向服務端發送SSL協議版本號、加密算法種類、隨機數等信息。
- 服務端給客戶端返回SSL協議版本號、加密算法種類、隨機數等信息,同時也返回服務器端的證書,即公鑰證書
- 客戶端使用服務端返回的信息驗證服務器的合法性,包括:
- 證書是否過期
- 發形服務器證書的CA是否可靠
- 返回的公鑰是否能正確解開返回證書中的數字簽名
- 服務器證書上的域名是否和服務器的實際域名相匹配
- 驗證通過後,將繼續進行通信,否則,終止通信
- 服務端要求客戶端發送客戶端的證書,客戶端會將自己的證書發送至服務端
- 驗證客戶端的證書,通過驗證後,會獲得客戶端的公鑰
- 客戶端向服務端發送自己所能支持的對稱加密方案,供服務器端進行選擇
- 服務器端在客戶端提供的加密方案中選擇加密程度最高的加密方式
- 將加密方案通過使用之前獲取到的公鑰進行加密,返回給客戶端
- 客戶端收到服務端返回的加密方案密文後,使用自己的私鑰進行解密,獲取具體加密方式,而後,產生該加密方式的隨機碼,用作加密過程中的密鑰,使用之前從服務端證書中獲取到的公鑰進行加密後,發送給服務端
- 服務端收到客戶端發送的消息後,使用自己的私鑰進行解密,獲取對稱加密的密鑰,在接下來的會話中,服務器和客戶端將會使用該密碼進行對稱加密,保證通信過程中信息的安全。
是單向認證還是雙向認證主要看服務器的配置,與客戶端無關。如果是雙向認證,服務器在配置的時候必須設置clientAuth=“true”,單向則設置爲false,默認值爲false。
單向認證需要服務器的公鑰證書,我們後臺提供給我的server.cer,在網上看到有說AF 3.0之後需要用.der格式的,我用了.cer格式也沒有問題。
雙向認證不僅需要服務器的公鑰證書還需要提供客戶端證書,一般爲.p12格式而且是帶有密碼的。
下面直接上代碼(單向認證和雙向認證適用同一套代碼,只要證書配置的對就可以):
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
// 設置超時時間
[manager.requestSerializer willChangeValueForKey:@"timeoutInterval"];
manager.requestSerializer.timeoutInterval = 30.f;
[manager.requestSerializer didChangeValueForKey:@"timeoutInterval"];
[manager.requestSerializer setValue:@"Content-Type" forHTTPHeaderField:@"application/json; charset=utf-8"];
[manager setSecurityPolicy:[self customSecurityPolicy]];
[self checkCredential:manager];
[manager POST:@"這裏填你的https地址" parameters:nil progress:^(NSProgress * _Nonnull uploadProgress) {
NSLog(@"123");
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"成功:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"失敗:%@",error);
}];
- (AFSecurityPolicy*)customSecurityPolicy {
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
//獲取證書路徑
NSString * cerPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
NSSet *dataSet = [NSSet setWithArray:@[certData]];
[securityPolicy setAllowInvalidCertificates:YES];//是否允許使用自簽名證書
[securityPolicy setPinnedCertificates:dataSet];//設置去匹配服務端證書驗證的證書
[securityPolicy setValidatesDomainName:NO];//是否需要驗證域名,默認YES
return securityPolicy;
}
//校驗證書
- (void)checkCredential:(AFURLSessionManager *)manager
{
[manager setSessionDidBecomeInvalidBlock:^(NSURLSession * _Nonnull session, NSError * _Nonnull error) {
}];
__weak typeof(manager)weakManager = manager;
[manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession*session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing*_credential) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__autoreleasing NSURLCredential *credential =nil;
NSLog(@"authenticationMethod=%@",challenge.protectionSpace.authenticationMethod);
//判斷服務器要求客戶端的接收認證挑戰方式,如果是NSURLAuthenticationMethodServerTrust則表示去檢驗服務端證書是否合法,NSURLAuthenticationMethodClientCertificate則表示需要將客戶端證書發送到服務端進行檢驗
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 基於客戶端的安全策略來決定是否信任該服務器,不信任的話,也就沒必要響應挑戰
if([weakManager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 創建挑戰證書(注:挑戰方式爲UseCredential和PerformDefaultHandling都需要新建挑戰證書)
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 確定挑戰的方式
if (credential) {
//證書挑戰 設計policy,none,則跑到這裏
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else { //只有雙向認證纔會走這裏
// client authentication
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client"ofType:@"p12"];
NSFileManager *fileManager =[NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:p12])
{
NSLog(@"client.p12:not exist");
}
else
{
NSData *PKCS12Data = [NSData dataWithContentsOfFile:p12];
if ([self extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data])
{
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;
}
}
}
*_credential = credential;
return disposition;
}];
}
//讀取p12文件中的密碼
- (BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityError = errSecSuccess;
//client certificate password
NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject:@"123456"
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;
} else {
NSLog(@"Failedwith error code %d",(int)securityError);
return NO;
}
return YES;
}
下面看一下服務端配置
二 服務端配置
服務端環境 :httpd: 2.4.4 openssl:1.0.1 os:ubuntu 12.04 LTS
首先,修改httpd.conf文件,加載必要的模塊
- LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
- LoadModule socache_dbm_module modules/mod_socache_dbm.so
- LoadModule socache_memcache_module modules/mod_socache_memcache.so
- LoadModule ssl_module modules/mod_ssl.so
- # Secure (SSL/TLS) connections
- Include conf/extra/httpd-ssl.conf
- <IfModule ssl_module>
- SSLRandomSeed startup builtin
- SSLRandomSeed connect builtin
- </IfModule>
然後打開httpd-ssl.conf,主要有以下幾個配置
- SSLEngine on
- SSLCertificateFile "/usr/local/httpd/conf/server.cer"
- SSLCertificateKeyFile "/usr/local/httpd/conf/server.key.pem"
- #SSLCACertificateFile "/usr/local/httpd/conf/ca.cer"
- #SSLVerifyClient require
- #SSLVerifyDepth 10
只要開啓前3個,單向的HTTPS認證就配置好了。後面3個目前先註釋掉,是後面雙向認證纔用到
SSLCACertificateFile,這個意思是當客戶端發來客戶端證書的時候,httpd用哪個CA根證書校驗它
九、簽發客戶端證書
1、創建客戶端私鑰
- openssl genrsa -aes256 -out private/client.key.pem 2048
2、創建客戶端證書籤發請求
- openssl req -new -key private/client.key.pem -out private/client.csr -subj "/C=CN/ST=SZ/L=SZ/O=kyfxbl/OU=kyfxbl/CN=kyfxbl"
這裏的不同在於,這裏的CN不是*.kyfxbl.net,也不是www.kyfxbl.net,隨便填一個kyfxbl就好了,或者乾脆叫user都沒問題,反正是一個客戶端證書
3、利用CA根證書,簽發客戶端證書
- openssl x509 -req -days 3650 -sha1 -extensions v3_req -CA certificates/ca.cer -CAkey private/ca.key.pem -CAserial ca.srl -CAcreateserial -in private/client.csr -out certificates/client.cer
這裏和簽發server.cer基本是一樣的
4、把客戶端證書轉換成p12格式
- openssl pkcs12 -export -clcerts -inkey private/client.key.pem -in certificates/client.cer -out certificates/client.p12
這步是必須的,因爲稍後就需要把客戶端證書導入到瀏覽器裏,但是一般瀏覽器都不能直接使用PEM編碼的證書
最後導出p12的方法 根據不同的證書 導出p12也不一樣 要以實際爲準
最後 把p12文件安裝在客戶端 就可以實現雙向認證了