几个概念
SSL/TSL
HTTPS就是将HTTP协议数据包放到SSL/TSL层加密后,在TCP/IP层组成IP数据报去传输,以此保证传输数据的安全;而对于接收端,在SSL/TSL将接收的数据包解密之后,将数据传给HTTP协议层,就是普通的HTTP数据。 准确的说SSL/TSL是会话层,http是应用层。
基本原理:
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
(1)如何保证公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
(2)公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个"对话密钥"(session key),用它来加密信息。由于"对话密钥"是对称加密,所以运算速度非常快,而服务器公钥只用于加密"对话密钥"本身,这样就减少了加密运算的消耗时间。
简单的来说,SSL/TSL通过四次握手,主要交换三个信息:
数字证书: 该证书包含了公钥等信息,一般是由服务器发给客户端,接收方通过验证这个证书是不是由信赖的CA签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证;
三个随机数:这三个随机数构成了后续通信过程中用来对数据进行对称加密解密的“对话密钥”。首先客户端先发第一个随机数N1,然后服务器回了第二个随机数N2(这个过程同时把之前提到的证书发给客户端),这两个随机数都是明文的;而第三个随机数N3(这个随机数被称为Premaster secret),客户端用数字证书的公钥进行非对称加密,发给服务器;而服务器用只有自己知道的私钥来解密,获取第三个随机数。只有,服务端和客户端都有了三个随机数N1+N2+N3,然后两端就使用这三个随机数来生成“对话密钥”,在此之后的通信都是使用这个“对话密钥”来进行对称加密解密。因为这个过程中,服务端的私钥只用来解密第三个随机数,从来没有在网络中传输过,这样的话,只要私钥没有被泄露,那么数据就是安全的。
加密通信协议:就是双方商量使用哪一种加密方式,假如两者支持的加密方式不匹配,则无法进行通信;
有个常见的问题,关于随机数为什么要三个?只最后一个随机数N3不可以么?
这是由于SSL/TLS设计,就假设服务器不相信所有的客户端都能够提供完全随机数,假如某个客户端提供的随机数不随机的话,就大大增加了“对话密钥”被破解的风险,所以由三组随机数组成最后的随机数,保证了随机数的随机性,以此来保证每次生成的“对话密钥”安全性。
数字证书
什么是CA证书?
CA 证书,顾名思义,就是CA颁发的证书。
前面已经说了,人人都可以找工具制作证书。但是你一个小破孩制作出来的证书是没啥用处的。因为你不是权威的CA机关,你自己搞的证书不具有权威性。
这就好比上述的例子里,某个坏人自己刻了一个公章,盖到介绍信上。但是别人一看,不是受信任的中介公司的公章,就不予理睬。坏蛋的阴谋就不能得逞啦。
什么是证书信任链?
实际上,证书之间的信任关系,是可以嵌套的。比如,C 信任 A1,A1 信任 A2,A2 信任 A3......这个叫做证书的信任链。只要你信任链上的头一个证书,那后续的证书,都是可以信任滴。
什么是根证书?
“根证书”的洋文叫“root certificate”,专业的解释看“这里”。为了说清楚根证书是咋回事,再来看个稍微复杂点的例子。
假设 C 证书信任 A 和 B;然后 A 信任 A1 和 A2;B 信任 B1 和 B2。则它们之间,构成如下的一个树形关系(一个倒立的树)。
处于最顶上的树根位置的那个证书,就是“根证书”。除了根证书,其它证书都要依靠上一级的证书,来证明自己。那谁来证明“根证书”可靠捏?实际上,根证书自己证明自己是可靠滴(或者换句话说,根证书是不需要被证明滴)。
聪明的同学此刻应该意识到了:根证书是整个证书体系安全的根本。所以,如果某个证书体系中,根证书出了问题(不再可信了),那么所有被根证书所信任的其它证书,也就不再可信了。这个后果是相当相当滴严重(简直可以说是灾难性的)。
这也是中间人攻击的一个入口。所以如果不小心安装过非权威机构的根证书,比如客户端链接恶意的 Wi-Fi等情况下装上恶意证书,这时候设备上就多了一个预设的公钥,那么用恶意私钥加密的证书就能被正常解析出来。所以千万不要随便装根证书,这等于是为那些恶意证书留了一扇门。
iOS的Https安全校验
现在为加强ios App的安全,基本所有的app都采用https访问接口和H5页面。
低安全NSURLSession默认校验
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler {
NSURLProtectionSpace *protectionSpace = challenge.protectionSpace;
SecTrustRef serverTrust = protectionSpace.serverTrust;
__block NSURLCredential *credential = nil;
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (BFServerTrustIsValid(serverTrust)) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
static BOOL BFServerTrustIsValid(SecTrustRef serverTrust) {
BOOL isValid = NO;
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
_out:
return isValid;
}
中级安全CA证书默认校验+域名host白名单
我们可以建立一个host white list, 在得到对象NSURLProtectionSpace的时候,首先校验host。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler {
NSURLProtectionSpace *protectionSpace = challenge.protectionSpace;
SecTrustRef serverTrust = protectionSpace.serverTrust;
__block NSURLCredential *credential = nil;
// 这里到host白名单中校验是否安全
BOOL trust = [[AppStatus sharedManager] matchHost:challenge.protectionSpace.host isWebView:NO];
if (!trust) {
if (completionHandler) {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (BFServerTrustIsValid(serverTrust)) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
高安全使用自签名证书
这种方式就是在app中存放一份服务端的证书。然后客户端证书和服务器证书进行强校验。
这里也有几种方式:
- 只校验证书里的公钥,不校验域名和有效期等,只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
- 设置本地为锚点证书,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书)。从服务器端证书链的根节点往下遍历,看看是否有与客户端的绑定证书一致的,有的话,就说明服务器端是可信的。
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain{
self.allowInvalidCertificates==YES表示如果此处允许使用自建证书(服务器自己弄的CA证书,非官方),并且还想验证domain是否有效(self.validatesDomainName == YES),也就是说你想验证自建证书的domain是否有效。那么你必须使用pinnedCertificates(就是在客户端保存服务器端颁发的证书拷贝)才可以。但是你的SSLPinningMode为AFSSLPinningModeNone,表示你不使用SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。所以当然你的客户端上没有你导入的pinnedCertificates,同样表示你无法验证该自建证书。所以都返回NO。最终结论就是要使用服务器端自建证书,那么就得将对应的证书拷贝到iOS客户端,并使用AFSSLPinningMode或AFSSLPinningModePublicKey
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
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) {
// 如果需要验证domain,那么就使用SecPolicyCreateSSL函数创建验证策略,其中第一个参数为true表示验证整个SSL证书链,第二个参数传入domain,用于判断整个证书链上叶子节点表示的那个domain是否和此处传入domain一致
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
// 如果不需要验证domain,就使用默认的BasicX509验证策略
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 为serverTrust设置验证策略,即告诉客户端如何验证serverTrust
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
// 如果SSLPinningMode为 AFSSLPinningModeNone,表示你不使用SSL pinning,但是我允许自建证书,那么返回YES,或者使用AFServerTrustIsValid函数看看serverTrust是否可信任,如果信任,也返回YES
if (self.SSLPinningMode == AFSSLPinningModeNone) {
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
// 既不允许自建证书,而且使用AFServerTrustIsValid函数又返回NO,那么该serverTrust就真的不能通过验证了
return NO;
}
switch (self.SSLPinningMode) {
// 理论上,上面那个部分已经解决了self.SSLPinningMode)为AFSSLPinningModeNone)等情况,所以此处再遇到,就直接返回NO
case AFSSLPinningModeNone:
default:
return NO;
// 这个模式表示用证书绑定(SSL Pinning)方式验证证书,需要客户端保存有服务端的证书拷贝
// 注意客户端保存的证书存放在self.pinnedCertificates中
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
// 这里使用SecCertificateCreateWithData函数对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
// 服务器端的证书链,注意此处返回的证书链顺序是从叶节点到根节点
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
// 从服务器端证书链的根节点往下遍历,看看是否有与客户端的绑定证书一致的,有的话,就说明服务器端是可信的。因为遍历顺序正好相反,所以使用reverseObjectEnumerator
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]){
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
// AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
// 从serverTrust中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
// 依次遍历这些公钥,如果和客户端绑定证书的公钥一致,那么就给trustedPublicKeyCount加一
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
// trustedPublicKeyCount大于0说明服务器端中的某个证书和客户端绑定的证书公钥一致,认为服务器端是可信的
return trustedPublicKeyCount > 0;
}
}
return NO;
}
AFN提供了各种校验证书的方式。可以参数源码
AFSecurityPolicy - 网络安全策略
总结
不安全的 HTTP 协议,很容易被网络嗅探,中间人劫持和篡改,从而泄露隐私信息,冒充伪造虚 假的请求等。虽然 HTTPS 可以从理论上很好的解决窃听、冒充、篡改等风险,但是由于客户端 和服务器实现和配置不当,很可能导致 HTTPS 被绕过。客户端证书校验不全面或有误就是比较 典型的场景,通常表现为未对证书域名做校验,忽略证书校验错误等。
建议采用 HTTPS,对证书域名做校验,不要忽略证书校验错误,也可以采用固定证书的形式,但 要注意固定证书的更新机制。
参考文献
数字证书及CA的扫盲介绍
iOS关于HTTPS支持并防止中间人攻击
打造安全的App!iOS安全系列之 HTTPS
iOS对HTTPS证书链的验证
九个问题从入门到熟悉HTTPS