最近為了滿足蘋果的 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分三種驗證模式:
這個模式表示不做SSL pinning,
只跟浏覽器一樣在系統的信任機構列表裡驗證服務端返回的證書。若證書是信任機構簽發的就會通過,若是自己服務器生成的證書就不會通過。
這個模式表示用證書綁定方式驗證證書,需要客戶端保存有服務端的證書拷貝,這裡驗證分兩步,第一步驗證證書的域名有效期等信息,第二步是對比服務端返回的證書跟客戶端返回的是否一致。
這個模式同樣是用證書綁定方式驗證,客戶端要有服務端的證書拷貝,
只是驗證時只驗證證書裡的公鑰,不驗證證書的有效期等信息。只要公鑰是正確的,就能保證通信不會被竊聽,因為中間人沒有私鑰,無法解開通過公鑰加密的數據。
// 正對是 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在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];
}
}