使用HTTPS最主要的考虑就是数据的安全性。对于HTTPS如何确保数据安全,依靠的就是SSL/TSL层的加密机制。HTTPS也就是在原来的HTTP层的基础上增加了一个安全层。
我们需要了解的是HTTPS在建立安全连接时采取四次握手机制,会话密钥的产生。使用会话密钥主要是因为建立安全机制的密钥算法是非对称算法(一般是RSA),计算比较耗时,而会话密钥使用的是对称密钥,可以提高会话的效率。
- 客户端向服务器端发起连接请求,上送信息有:支持协议版本、随机数1、加密方法、支持压缩算法;
- 服务器回应,下发信息有:确认协议版本、随机数2、确认加密方法、服务器公钥证书;
- 客户端再次上送信息:随机数3(采用公钥加密)、编码改变通知、客户端握手结束通知;
- 服务器采用随机数1、随机数2和随机数3组成会话密钥,并回应:编码改变通知、服务器握手结束通知。
以上四次握手中,完成了几件事情,第一,证书下发;第二,会话密钥;第三,确认双方参数(双方协议版本、加密方法等)。其中随机数1和随机数2是明文,随机数3是密文,之所以采用三个随机数来确定会话密钥,主要是三组随机数组成的随机数,确保了随机数的随机性,从而保证每次生成的会话密钥的安全性。
在默认情况下,iOS要求连接的HTTPS站点必须为CA签名过得合法证书。AFNetworking基于iOS的HTTP网络通讯库,自然在证书方面要求和系统是一致,也需要合法的站点证书。所以探讨HTTPS在iOS开发中的使用,我们主要考虑两个问题,一个是合法证书,一个是自建证书。
先来了解一下系统API的验证流程,HTTPS的相关API在Security.Framework中,如下:
- 获取需要验证的信任对象(Trust Object),在NSURLConnection来说,是从delegate方法-connection:willSendRequestForAuthenticationChallenge:回调回来的参数challenge中获取([challenge.protectionSpace serverTrust]);
- 采用系统默认方式验证Trust Object,SecTrustEvaluate会根据Trust Object的验证策略,一级一级往上,验证证书上每一级数字签名的有效性,从而评估证书的有效性;
- 通过上一步,一般安全要求下,直接验证通过,下一步使用Trust Object生成一份凭证([NSURLCredential credentialForTrust:serverTrust]),传入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge])处理,建立连接;
- 如果有更强的安全要求,还可以继续对Trust Object进行更严格验证,常用方式为在本地导入证书,验证Trust Object与导入证书是否匹配;
- 如果验证失败,取消此次Challenge-Response Authentication验证流程,拒绝连接请求。
如果是自建证书,那么会跳过第二步,因为自建证书的根CA数字签名不在系统的信任列表中。
上代码
// Now start the connection
NSURL * httpsURL = [NSURL URLWithString:@"https://www.google.com"];
self.connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:httpsURL] delegate:self];
//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//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];
}
}
对于自建证书我们需要先在本地导入证书,设置成需要验证的Anchor Certificate,再来验证。如下:
//先导入证书
NSString * cerPath = ...; //证书的路径
NSData * cerData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(cerData));
self.trustedCertificates = @[CFBridgingRelease(certificate)];
//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);
//2)SecTrustEvaluate会查找前面SecTrustSetAnchorCertificates设置的证书或者系统默认提供的证书,对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];
}
}
一般我们很少直接使用原生API,而是使用AFNetworking封装好的接口来完成以上繁琐的过程,目前AFNetworking也已经更新到2.x版本了。
一般我们只需要如下代码就可使接口支持HTTPS:
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
securityPolicy.allowInvalidCertificates = YES;
对于自建证书,AFNetworking2也是支持的,需要将pem格式的证书转成cer格式的,可以在mac下使用如下命令行:
openssl x509 -in <你的服务器证书>.pem -outform der -out server.cer
将该证书引入到ap的bundle目录里,AFNetworking会自动扫描boundle中的.cer文件。
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModeCertificate];
或者
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES; //还是必须设成YES
上面只列了一个参数,我想下面技术最好都要知道:
//allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
//如果是需要验证自建证书,需要设置为YES
securityPolicy.allowInvalidCertificates = YES;
//validatesDomainName 是否需要验证域名,默认为YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为NO
//主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
securityPolicy.validatesDomainName = NO;
//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;
另外需要知道的是,验证站点证书时通过域名的,如果服务器站点没有绑定域名,仅靠IP地址,上面的方法是不行的,解决的办法就是修改AFNetworking2的代码,在AFSecurityPolicy.m文件中,修改: - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain 方法,将里面的部分校验代码注释掉:
// SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//
// if (!AFServerTrustIsValid(serverTrust)) {
// return NO;
// }
//
// if (!self.validatesCertificateChain) {
// return YES;
// }
有了上面的了解,我们就可以去实践一下HTTPS的开发了。Enjoy it!