https 安全驗證問題,https驗證
最近為了滿足蘋果的 https 要求, 經過努力終於寫出了方法
驗證 SSL 憑證是否滿足 ATS 要求
nscurl --ats-diagnostics --verbose https://你的網域名稱
PASS 符合要求
輸出滿足 ATS 的認證
openssl s_client -connect 你的網域名稱:443 </dev/null 2>/dev/null | openssl x509 -outform DER > https.cer
1. 針對 AFNetWorking (2.6.0之前的版本)
AFSecurityPolicy分三種驗證模式:
AFSSLPinningModeNone
這個模式表示不做SSL pinning,
只跟瀏覽器一樣在系統的信任機構列表裡驗證服務端返回的認證。若認證是信任機構簽發的就會通過,若是自己伺服器產生的認證就不會通過。
AFSSLPinningModeCertificate
這個模式表示用認證綁定方式驗證認證,需要用戶端儲存有服務端的認證拷貝,這裡驗證分兩步,第一步驗證認證的網域名稱有效期間等資訊,第二步是對比服務端返回的認證跟用戶端返回的是否一致。
AFSSLPinningModePublicKey
這個模式同樣是用認證綁定方式驗證,用戶端要有服務端的認證拷貝,
只是驗證時只驗證認證裡的公開金鑰,不驗證認證的有效期間等資訊。只要公開金鑰是正確的,就能保證通訊不會被竊聽,因為中間人沒有私密金鑰,無法解開通過公開金鑰加密的資料。
// 正對是 app 新人的機構發布的 SSL 憑證
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 = YES;
//validatesCertificateChain 是否驗證整個憑證鏈結,預設為YES
//設定為YES,會將伺服器返回的Trust Object上的憑證鏈結與本地匯入的認證進行對比,這就意味著,假如你的憑證鏈結是這樣的:
//GeoTrust Global CA // Google Internet Authority G2
// *.google.com //那麼,除了匯入*.google.com之外,還需要匯入憑證鏈結上所有的CA認證(GeoTrust Global CA, Google Internet Authority G2);
//如是自建認證的時候,可以設定為YES,增強安全性;假如是信任的CA所簽發的認證,則建議關閉該驗證,因為整個憑證鏈結一一比對是完全沒有必要(請查看原始碼);
securityPolicy.validatesCertificateChain = NO; // 2.6.0之前不需要, 之後需要
requestOperationManager.securityPolicy = securityPolicy;
// 如實自建的認證
還需要把認證匯入本地工程中, 並天添加以下代碼
NSData *cerData = [self getSSLCerByCerName:@"本地SSL認證的名字 "];
[securityPolicy setPinnedCertificates:@[cerData]];
// 擷取 SSL 憑證
+ (NSData *)getSSLCerByCerName:(NSString *)cerName {
NSString *cerPath = [[NSBundle mainBundle] pathForResource:cerName
ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
return certData;
}
2. 針對 NSURLConnection
驗證認證的API
相關的Api在Security Framework中,驗證流程如下:
1). 第一步,先擷取需要驗證的信任物件(Trust Object)。這個Trust Object在不同的應用情境下擷取的方式都不一樣,對於NSURLConnection來說,是從delegate方法-connection:willSendRequestForAuthenticationChallenge:
回調回來的參數challenge中擷取([challenge.protectionSpace serverTrust]
)。
2). 使用系統預設驗證方式驗證Trust Object。SecTrustEvaluate
會根據Trust Object的驗證策略,一級一級往上,驗證憑證鏈結上每一級數位簽章的有效性(上一部分有講解),從而評估認證的有效性。
3). 如第二步驗證通過了,一般的安全要求下,就可以直接驗證通過,進入到下一步:使用Trust Object產生一份憑證([NSURLCredential credentialForTrust:serverTrust]
),傳入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge]
)處理,建立串連。
4). 假如有更強的安全要求,可以繼續對Trust Object進行更嚴格的驗證。常用的方式是在本地匯入認證,驗證Trust Object與匯入的認證是否匹配。更多的方法可以查看Enforcing Stricter Server Trust Evaluation,這一部分在講解AFNetworking源碼中會講解到。
5). 假如驗證失敗,取消此次Challenge-Response Authentication驗證流程,拒絕串連請求。
ps: 假如是自建認證的,則不使用第二步系統預設的驗證方式,因為自建認證的根CA的數位簽章未在作業系統的信任清單中。
iOS授權驗證的API和流程大概瞭解了,下面,我們看看在NSURLConnection中的代碼實現:
// NSURLConnection Https 安全驗證問題
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace{
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
// 針對的是自建認證, 未受安全機構信任
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
static CFArrayRef certs;
if (!certs) {
NSData*certData =[NSData dataWithContentsOfFile:[[NSBundle mainBundle]
pathForResource:@"本地工程中的SSL認證的名字" ofType:@"cer"]];
SecCertificateRef rootcert
=SecCertificateCreateWithData(kCFAllocatorDefault,CFBridgingRetain(certData));
const void *array[1] = { rootcert };
certs = CFArrayCreate(NULL, array, 1, &kCFTypeArrayCallBacks);
CFRelease(rootcert);
}
SecTrustRef trust = [[challenge protectionSpace] serverTrust];
// 針對一個認證對應多個網域名稱, 無需驗證網域名稱
NSMutableArray *policies = [NSMutableArray array];
// BasicX509 不驗證網域名稱是否相同
SecPolicyRef policy = SecPolicyCreateBasicX509();
[policies addObject:(__bridge_transfer id)policy];
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
int err;
SecTrustResultType trustResult = 0;
err = SecTrustSetAnchorCertificates(trust, certs);
if (err == noErr) {
err = SecTrustEvaluate(trust,&trustResult);
}
CFRelease(trust);
// kSecTrustResultUnspecified: 系統隱式地信任這個認證
// kSecTrustResultProceed: 使用者加入自己的信任錨點,顯式地告訴系統這個認證是值得信任的
BOOL trusted = (err == noErr)
&& ((trustResult == kSecTrustResultProceed)
|| (trustResult == kSecTrustResultUnspecified));
if (trusted) {
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]
forAuthenticationChallenge:challenge];
}else{
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}
// SSL 憑證是經過信任的機構授權的, 不用再把認證存放在本地
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)擷取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
// 針對一個認證對應多個網域名稱, 無需驗證網域名稱
NSMutableArray *policies = [NSMutableArray array];
// BasicX509 不驗證網域名稱是否相同
SecPolicyRef policy = SecPolicyCreateBasicX509();
[policies addObject:(__bridge_transfer id)policy];
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
//2)SecTrustEvaluate對trust進行驗證
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)驗證成功,產生NSURLCredential憑證cred,告知challenge的sender使用這個憑證來繼續串連
NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
} else {
//5)驗證失敗,取消這次驗證流程
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}