本文主要记录关于 AFSecurityPolicy 模块的相关内容,主要是AFSecurityPolicy.m
, 是 HTTPS网络请求中的安全模块.
HTTPS
HTTPs 的基本内容可以参考维基百科
其中的身份认证的方法是基于公开密钥加密算法的身份验证是指通信中的双方分别持有公开密钥和私有密钥,由其中的一方采用私有密钥对特定数据进行加密,而对方采用公开密钥对数据进行解密,如果解密成功,就认为用户是合法用户,否则就认为是身份验证失败。使用基于公开密钥加密算法的身份验证的服务有:SSL, 数字签名等等。
bangs blog 的解释以及有两个重要的问题如下:
HTTPS连接建立过程大致是,客户端和服务端建立一个连接,服务端返回一个证书,客户端里存有各个受信任的证书机构根证书,用这些根证书对服务端返回的证书进行验证,经验证如果证书是可信任的,就生成一个pre-master secret,用这个证书的公钥加密后发送给服务端,服务端用私钥解密后得到pre-master secret,再根据某种算法生成master secret,客户端也同样根据这种算法从pre-master secret生成master secret,随后双方的通信都用这个master secret对传输数据进行加密解密。
以上是简单过程,中间还有很多细节,详细过程和原理已经有很多文章阐述得很好,就不再复述,推荐一些相关文章:
关于非对称加密算法的原理:RSA算法原理<一> <二>
关于整个流程:HTTPS那些事<一> <二> <三>
关于数字证书:浅析数字证书
1.证书是怎样验证的?怎样保证中间人不能伪造证书?
首先要知道非对称加密算法的特点,非对称加密有一对公钥私钥,用公钥加密的数据只能通过对应的私钥解密,用私钥加密的数据只能通过对应的公钥解密。
我们来看最简单的情况:一个证书颁发机构(CA),颁发了一个证书A,服务器用这个证书建立https连接。客户端在信任列表里有这个CA机构的根证书。
首先CA机构颁发的证书A里包含有证书内容F,以及证书加密内容F1,加密内容F1就是用这个证书机构的私钥对内容F加密的结果。(这中间还有一次hash算法,略过。)
建立https连接时,服务端返回证书A给客户端,客户端的系统里的CA机构根证书有这个CA机构的公钥,用这个公钥对证书A的加密内容F1解密得到F2,跟证书A里内容F对比,若相等就通过验证。整个流程大致是:F->CA私钥加密->F1->客户端CA公钥解密->F。因为中间人不会有CA机构的私钥,客户端无法通过CA公钥解密,所以伪造的证书肯定无法通过验证。
2.什么是SSL Pinning?
可以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架构的像手机APP事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。
为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。
为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。
关于Certificate Pinning
在SSL/TLS通信中,客户端通过数字证书判断服务器是否可信,并采用证书的公钥与服务器进行加密通信.
然而在很多移动应用中,开发者不检查服务器证书的有效性,或选择接受所有的证书,这样就会导致中间人攻击.
事实上,app大多只和固定的服务器通信,因此可以在代码更精确地直接验证某张特定的证书,这种方法称为“证书锁定”(certificate pinning)
如果进行 Certificate Pinning
具体步骤如下:
1 首先通过服务器端使用RSA算法生成一对公钥私钥对,服务器端持有私钥,线下将公钥传给客户端。App中将这个值硬编码到本地。
2 App端可以自己实现一个X509TrustManager接口,在其中的CheckServerTrusted()方法里通过证书链拿到PublicKey
3 比较1和2中进行md5的值,如果匹配则服务器验证通过,否则立即终止与此服务器的通信.
AFNetworking 就是采用的这种方法.
如果引入第三方的支付等
在实际应用中,一款移动应用往往不止一个后台,尤其是在支付类产品中,经常需要去集成第三方支付网关。
但是第三方的服务什么时候会更改证书,这个就说不准了。初期,我们会把所有的这些第三方的服务提供的PublicKey都硬编码在本地,但是有次其中一个服务商自己改掉了,造成用户手上的产品直接不能使用了, 这个就给我们带来了不必要的麻烦。
具体的解决方案如下:
1 自己的服务端使用RSA算法生成一对公钥私钥对,服务器端持有私钥,线下将公钥传给客户端。App中将这个值硬编码到本地
2 自己的服务端提供API,获得当前所有服务器(包括第三方)公钥的SHASUM值.当然这个值必须通过1中存在本地PublicKey签名验证得到
3 再通过最基本Certificate Pinning的办法(上文提到),直接从各服务端拿到各自公钥, 连同本地PublicKey一起计算SHASUM
4 比较2和3中的值,如果相同则Certificate Pinning通过,否则终止app
详细的参考资料:
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning
iOS 中实现对 HTTPS 的支持
详细参考资料: http://oncenote.com/2014/10/21/Security-1-HTTPS/
首先,需要明确你使用HTTP/HTTPS的用途,因为OSX和iOS平台提供了多种API,来支持不同的用途,官方文档《Making HTTP and HTTPS Requests》有详细的说明,而文档《HTTPS Server Trust Evaluation》则详细讲解了HTTPS验证相关知识,这里就不多说了。主要讲解我们最常用的NSURLConnection支持HTTPS的实现(NSURLSession的实现方法类似,只是要求授权证明的回调不一样而已),以及怎么样使用AFNetworking这个非常流行的第三方库来支持HTTPS。
验证证书的API
相关的Api在Security Framework中,验证流程如下:
- 第一步,先获取需要验证的信任对象(Trust Object)。这个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与导入的证书是否匹配。更多的方法可以查看Enforcing Stricter Server Trust Evaluation,这一部分在讲解AFNetworking源码中会讲解到。
- 假如验证失败,取消此次Challenge-Response Authentication验证流程,拒绝连接请求。
ps: 假如是自建证书的,则不使用第二步系统默认的验证方式,因为自建证书的根CA的数字签名未在操作系统的信任列表中。
iOS授权验证的API和流程大概了解了,下面,我们看看在NSURLConnection中的代码实现:
使用NSURLConnection支持HTTPS的实现
// 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];
}
}
上面是代码是通过系统默认验证流程来验证证书的。假如我们是自建证书的呢?这样Trust Object里面服务器的证书因为不是可信任的CA签发的,所以直接使用SecTrustEvaluate
进行验证是不会成功。又或者,即使服务器返回的证书是信任CA签发的,又如何确定这证书就是我们想要的特定证书?这就需要先在本地导入证书,设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates
设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),再调用SecTrustEvaluate
来验证。代码如下
//先导入证书
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];
}
}
建议采用本地导入证书的方式验证证书,来保证足够的安全性。更多的验证方法,请查看官方文档《HTTPS Server Trust Evaluation》
使用 AFNetworking 的安全设置
Pinning Mode
AFNetworking 的安全相关的设置在AFSecurityPolicy
,它定义3种 SSL Pinning Mode:
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,
AFSSLPinningModePublicKey,
AFSSLPinningModeCertificate,
};
- AFSSLPinningModeNone : 你不必将凭证跟你的 APP 一起打包,完全信任服务器的凭证
- AFSSLPinningModeCertificate : 对比服务器凭证跟你的凭证是否完全匹配
- AFSSLPinningModePublicKey : 只对比服务器凭证的 publick key跟你的凭证的 public key 是否匹配
那么使用哪种方式比较好?
AFSSLPinningModeCertificate
比较安全同时比较麻烦, 它会对比你打包的凭证和服务器的凭证是否一致.因为你的凭证是和 app 一起打包的,这也就代表说如果你的凭证过期或者变化了,你就必须得更新 app,而且旧版 app 无法使用了.当然也可以在每次 app 启动时候就去某个服务器下载最新的凭证,不过此时下载链接有风险.
AFSSLPinningModePublicKey
则是只有对比凭证里面的 public key, 所以即使服务器的凭证有所变动,只要 public key 没变化, 就能通过验证
一般情况下使用AFSSLPinningModePublicKey
,除非你能保证 app 使用者都只用最新版本的 app.
Certification Chain
/** Whether to evaluate an entire SSL certificate chain, or just the leaf certificate. Defaults to `YES`. */
@property (nonatomic, assign) BOOL validatesCertificateChain;
如果你的凭证是某个机构发出的,该机构的凭证是由另外一家更加高级的机构发出的,一路往上追,这样一串由各个机构发出的凭证称为 certification chain.
如果你把这个属性设置为 Yes,那就得把这一串凭证全部打包到你的 APP,必须每个验证都通过才算通过,如果设置为 No, 只需要打包你自己的凭证就够了.
在2.6x 以后的版本的 AFNetworking 以后已经没有
validatesCertificateChain
这个属性.
参考连接:
http://robnapier.net/pinning-your-ssl-certs
http://stackoverflow.com/questions/24615144/afnetworking-pin-public-key-for-a-trusted-certificate/24625969#24625969
http://oncenote.com/2014/10/21/Security-1-HTTPS/
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning
AFNetworking 中 HTTPS 实例
NSURL * url = [NSURL URLWithString:@"https://www.google.com"];
AFHTTPRequestOperationManager * requestOperationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:url];
dispatch_queue_t requestQueue = dispatch_create_serial_queue_for_name("kRequestCompletionQueue");
requestOperationManager.completionQueue = requestQueue;
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;
requestOperationManager.securityPolicy = securityPolicy;
AFSecurity 在源码中的位置
AFHTTPRequestOperationManager ,AFURLConnectionOperation, 类中有以下属性
The security policy used by created request operations to evaluate server trust for secure connections. `AFHTTPRequestOperationManager` uses the `defaultPolicy` unless otherwise specified.
*/
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;
最后这个属性被设置成AFURLConnectionOperation的以上属性, 通过该属性的注释 -- 用于 request 中验证服务器身份.
具体使用代码:
//把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件,当然也可以自己打包到自己的 xxx.bundle 中,但是需要在源码中进行一定的设置
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode: AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES;
[AFHTTPRequestOperationManager manager].securityPolicy = securityPolicy;
[manager GET:@"[https://example.com/](https://example.com/)"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id, responseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
具体的源码注释可以参考 bangs blog
个人准备总结一份关于 iOS 可能用到的 HTTPS 相关的系列文章