iOS https认证
项目背景
最近在做iOS 热更新,出于公司信息安全限制没使用JSPatch平台来下发js,而是把js放自己的对象存储服务器上。为了简单处理,js路径里加了对应的版本号。这样对应版本的app会去下载该js,能下载到表示该版本有patch,没有则表示无patch。既然没有接入JSPatch平台来下发,自然存在下发js安全的问题。比如对象存储服务器被劫持后,黑客篡改js,就可能造成全部版本不可用的风险。最简单的就是让对象存储服务器走https,简单暴力。
https简单理解
https是简单的理解为http secure,就是安全的http。我们知道http是应用层协议,它紧接着的一层为运输层(TCP/UDP)。那如何做到安全的http,且不影响原本的http协议,也不影响TCP或者UDP呢?加一层,也就是SSL(Secure Socket Layer)。SSL3.0以后开始叫TSL了,最新的是TSL1.3,不过主流支持到TSL1.2。https具体原理不说了,有点繁琐,具体百度。只要知道客户端和服务端在握手阶段商量出了一个非对称秘钥(也就是两个不一样的秘钥,可以互相解密对方加密的内容,但是比较耗时)。然后用这个非对称秘钥加密传输一个秘钥,让彼此知道这个秘钥,接下来就可以有用这个秘钥来加密需要传输的数据了(也就是对称加解密)。还是用图(网图)来说明吧:
https原理图https防中间人攻击以及利用
通常说https可以防中间人攻击,那么怎么做到防中间人攻击呢?怎么做到A与B通信就是A与B,而不是A与C,不是C与B,甚至是A通过C与B通信?简单说给他们各自一个凭证,A证证明A是A,B证证明B就是B。他们在开始通信的时候先亮出彼此的证书,A验证后发现确实与我通信的就是B,B验证后与我通信的就是A才开始接下里的通信。好了,大家说我平时访问https的网站或者网址,都没用到啥https证书,这有啥用?其实我们一直在用,只是他们隐匿的深一点。当我们在chrome里键入:https://www.baidu.com 的时候,浏览器里地址栏会出现一个小锁的图标,如下图:
图片这表示该网站的https证书是CA(可以理解为给你发身份证的公安局)认证过的,可以放心使用了。那怎么认证的呢?浏览器和操作系统都会内置一些权威CA的证书(MBP里打开钥匙串,选择系统根证书,可有看到目前所有的根证书,选择证书可以看到我们添加的信任的证书),
图片在访问的时候这些网站时候,把网站亮出的证书与内置的权威证书对比下,如果有一个命中,就表示认证通过(其实验证的是一个证书链)。所以不要随便把一些未知证书导入系统里,这样也会是潜在安全隐患。因为一旦你导入系统并信任,那么它就具有系统内置权威CA证书一样的功能了。大家常用的抓包工具Charles就是这样的原理(严格意义上也算中间人攻击,只是这个中间人是我们自己)。开启了Charles后,需要你安装Charles的证书到系统,不然是无法拦截到并看到明文https请求的。下面图中可以看到打开Charles,百度的证书签发机构变成Charles了:
图片这就表明与我们浏览器通信的其实是Charles这个中间人。之所以能看到百度网址的内容,是因为Charles这个中间人访问百度并将内容返回了给我们浏览器。这也是为什么Charles可以看到https请求的response是明文而不是乱码的原因。
iOS中的https认证
鉴于越来越多的安全事故,3年前苹果要求所有的App都要配置ATS开关。如果不配置,默认所有http请求都打不开而且所有验证不通过的https请求也打不开。也是逼着公司,开发者重视安全,重视用户隐私并且尽早接入https。但是可能阻力太大,苹果额外加了个允许任意请求的开关,这样开发者就可以绕开苹果的要求了。不过对于这个下发js的项目背景来说,认证是必须的。鉴于AFNetworking(后面简称AF)基本上是iOS网络库的事实标准,下面以AF里认证为例说明。
AF里做证书验证的主要类是AFSecurityPolicy,负责网络请求的AFHTTPSessionManager有个该类的实例属性,用于作证书认证。打开AFSecurityPolicy的m文件,发现里面的注释都比较清楚,这里只单独说两个属性:
@interface AFSecurityPolicy : NSObject <NSSecureCoding, NSCopying>
/**
The criteria by which server trust should be evaluated against the pinned SSL certificates. Defaults to `AFSSLPinningModeNone`.
*/
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
/**
The certificates used to evaluate server trust according to the SSL pinning mode.
By default, this property is set to any (`.cer`) certificates included in the target compiling AFNetworking. Note that if you are using AFNetworking as embedded framework, no certificates will be pinned by default. Use `certificatesInBundle` to load certificates from your target, and then create a new policy by calling `policyWithPinningMode:withPinnedCertificates`.
Note that if pinning is enabled, `evaluateServerTrust:forDomain:` will return true if any pinned certificate matches.
*/
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
AFSSLPinningMode枚举定义如下:
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,//对于https请求我们不用作验证,丢给系统做,也就是去比对系统内置证书
AFSSLPinningModePublicKey,//该模式要求app提供证书,会比较证书对应的公钥是否一致
AFSSLPinningModeCertificate,//该模式有要求app提供证书,会比较整个完整证书,也就是验证策略最严格
};
上面3种枚举的验证策略注释已经说的很明确了,可以看出AFSSLPinningModeNone适合网站或者后台部署了权威CA签发的https证书。AFSSLPinningModePublicKey和AFSSLPinningModeCertificate就对应我们自签名的https证书了。既然自签名证书,自然需要我们提供证书了,也就是把证书放在工程里与app一起打包。在初始化AFHTTPSessionManager时设置securityPolicy的pinnedCertificates属性即可。下面我们来看看自签名证书的核心验证逻辑代码。在AFUrlSessionManager的m文件里,可以看到有个NSURLSessionDelegate的代理方法:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {//表示验证服务端证书
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
这个就是NSURLSession在发https请求时遇到要求证书验证时的回调。具体的逻辑还是在[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host],核心代码如下:
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
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
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
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) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (self.SSLPinningMode == AFSSLPinningModeNone) {//如果是系统验证,采用系统验证的结果
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}//系统验证不通过,但是有设置不允许无效证书,整个验证结果就是false了
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone://if (self.SSLPinningMode == AFSSLPinningModeNone)已处理,实际到不了该case
default:
return NO;
case AFSSLPinningModeCertificate: {//如果验证证书
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);//设置serverTrust的可信任锚点证书,也就是我们提供的证书
if (!AFServerTrustIsValid(serverTrust)) {//如果证书无效直接返回
return NO;
}
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}//查看serverTrust的证书链是否有一个在我们提供的证书里列表里(与app一起打包的证书)
return NO;
}
case AFSSLPinningModePublicKey: {//验证公钥,不验证整个证书
NSUInteger trustedPublicKeyCount = 0;
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
}
return NO;
}
上面的代码注释解释的很清楚了。需要注意的是有两点:
- 对于自签名的https证书,需要自己验证。
- 打包到App里的自签名证书会存在过期问题,需要在到期之前提前处理。
最后说一句,我们的存储服务器就是https证书就是权威CA签发的,意味着啥都不用做Orz...
网友评论