前段时间由于工作需要,接触到了App安全相关的冰山一角,在此整理一下以备后续需要。
我们知道无线传输的数据能被第三方轻易截获(如使用抓包工具Charles),如果未使用加密措施,可能直接暴露用户的各种关键数据,例如用户名,密码等。加入了SSL(Secure Socket Layer)子层实现的HTTPS协议可确保数据在网络上加密传输,即使传输的数据被截获,也无法解密和还原。
HTTPS
HTTPS(英语:Hypertext Transfer Protocol Secure,缩写:HTTPS,常称为HTTP over TLS,HTTP over SSL或HTTP Secure,中文名:超文本传输安全协议)是一种透过计算机网络进行安全通信的传输协议,工作在应用层。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包,提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
TLS(英语:Transport Layer Security,缩写作 TLS,中文名:传输层安全性协议),及其前身SSL(Secure Sockets Layer,缩写作SSL,中文名:安全套接层)是一种安全协议,TLS与SSL在传输层对网络连接进行加密,以提供安全及数据完整性保障.
SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等.
TLS/SSL提供以下服务:
- 认证用户和服务器,确保数据发送到正确的客户机和服务器;
- 加密数据以防止数据中途被窃取;
- 维护数据的完整性,确保数据在传输过程中不被改变。
关于SSL/TLS可以阅读阮一峰的这两篇文章:
SSL/TLS协议运行机制的概述
图解SSL/TLS协议
https建立的基本过程是这样的:
从上图可知,客户端与服务端经过SSL握手获得了三个随机数,通过这三个随机数,客户端与服务端能够使用相同的算法生成后续HTTP通信过程中对称加密算法使用的密钥。
HTTPS协议在建立连接过程中使用的是非对称加密,在实际的数据传输时使用的是对称加密。
数字证书
数字证书是一个经证书授权中心(如:CA中心)数字签名的包含公开密钥拥有者信息以及公开密钥的文件。可以对网络上传输的信息进行加密和解密、数字签名和签名验证,确保网上传递信息的机密性、完整性及交易的不可抵赖性。
更多证书相关内容可以阅读:
https://www.cnblogs.com/guogangj/p/4118605.html
认证服务器
TLS/SSL主要是通过校验服务返回的证书来认证服务器的。
证书验证过程:
- 验证证书本身的合法性(验证签名完整性、证书有效期等)
验证证书颁发者的合法性(查找颁发者的证书并检查其合法性,这个过程是递归的)
证书验证的递归过程最终会成功终止,而成功终止的条件是:证书验证过程中遇到了可信任的锚点证书,这些可信任的锚点证书是已加入到系统中的、权威证书颁发机构的根证书。
如果证书验证成功,系统可能会进行TLS的其他验证,如:访问的服务器的域名和证书中的域名是否一致。
综上所诉:建立https的连接过程中,服务器验证失败有以下原因:
- 无法找到证书的颁发者
- 证书过期
- 验证过程中遇到了自签名证书,但该证书不是锚点证书。
- 无法找到锚点证书(即在证书链的顶端没有找到合法的根证书)
- 访问的服务器的域名和证书中的地址不同
在iOS中使用HTTPS
使用NSURLConnection
或NSURLSession
向服务器发送https请求,在建立https连接的过程中(连接的具体过程请阅读SSL/TLS协议运行机制的概述),客户端会收到服务器的授权响应,这时对于NSURLSession
而言,会调用代理方法URLSession:task:didReceiveChallenge:completionHandler:
方法,对于NSURLConnection
而言,会调用代理方法connection:willSendRequestForAuthenticationChallenge:
,我们需要在代理方法中验证服务器返回的证书并决定是否继续请求数据.
以NSURLSession为例:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
//1)获取需要验证的信任对象trust(包含待验证的证书和支持的验证方法等)
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//2)验证证书合法性
OSStatus status = SecTrustEvaluate(trust, &result);
/**
kSecTrustResultProceed表示serverTrust验证成功,且该验证得到了用户认可(例如在弹出的是否信任的alert框中选择always trust)。 kSecTrustResultUnspecified表示 serverTrust验证成功,此证书也被暗中信任了,但是用户并没有显示地决定信任该证书
*/
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
//4)验证失败,取消这次验证流程
[challenge.sender cancelAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
}
}
以上代码是通过系统默认验证流程来验证证书的(iOS系统里存有各个受信任的证书机构的根证书,默认使用这些根证书对服务端返回的证书进行验证)。
如果我们服务器使用的是自签证书,这样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));
if (certificate) {
self.trustedCertificates = @[(__bridge_transfer id)certificate];
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
//1)获取需要验证的信任对象trust
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//2)注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);
//3)验证证书合法性,SecTrustEvaluate会查找前面SecTrustSetAnchorCertificates设置的证书或者系统默认提供的证书,对trust进行验证
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//4)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
//5)验证失败,取消这次验证流程
[challenge.sender cancelAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
}
}
更多在iOS中使用HTTPS的相关内容请阅读:HTTPS Server Trust Evaluation
使用AFNetworking
通常在项目中会使用比较成熟的第三方库AFNetworking来进行https请求,在AFNetworking上对HTTPS的支持是通过AFSecurityPolicy来实现的,AFSecurityPolicy分三种验证模式:
AFSSLPinningModeNone
这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
AFSSLPinningModeCertificate
这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。
AFSSLPinningModePublicKey
这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
如果不做SSL Pinning验证,代码如下:
+ (AFHTTPSessionManager *)sessionManager {
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
//allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
policy.allowInvalidCertificates = NO;
// validatesDomainName 是否需要验证域名,默认为YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
//置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
//如置为NO,建议自己添加对应域名的校验逻辑。
policy.validatesDomainName = YES;
manager.securityPolicy = policy;
return manager;
}
如果做SSL Pinning验证,代码如下:
+ (AFHTTPSessionManager *)sessionManager {
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
NSString *cerPath = @""; // 证书路径
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
NSSet *cerSet = [NSSet setWithObjects:cerData, nil];
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:cerSet];
policy.allowInvalidCertificates = NO;
policy.validatesDomainName = YES;
manager.securityPolicy = policy;
return manager;
}
关于证书更换
如果你的App中存储了服务器的证书,保存的证书过期了或则更换了那么就必须考虑证书的更换:
- 上线新版本
在新证书和旧证书交接的一段时间内,上线新版本,同时包含新旧证书,这样可以保证更新过的用户可以对证书更换无感。但这种方法对于万年不更新版本的用户就不适用了。 - 动态下发
在每次 APP 启动时,就自动连接到服务器下载最新的证书,但这种方式由于网络不可信任,是有风险的。
参考
超文本传输安全协议
传输层安全性协议
SSL
(http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html)
iOS安全系列之一:HTTPS
iOS 中 HTTPS 证书验证浅析
写给 iOS 开发者看的 HTTPS 指南
有关ssl-pinning的总结