当前位置: 首页 > 科技观察

ATS从服务端到iOS客户端的适配

时间:2023-03-12 11:07:57 科技观察

iOS9系统已经出来了,网络中的ATS(AppTransportSecurity)特性可以说是大家体验到了。而我的博客就是结合我这几天的经验,谈谈ATS从服务端到iOS客户端的适配。一、简单说说ATS(AppTransportSecurity)ATS(AppTransportSecurity)是一种提高App与服务器之间数据安全传输的特性。这个特性从iOS9和OSX10.11开始就出现了。默认需要满足以下条件:服务器TLS版本至少为1.2版本连接加密只允许几个高级加密证书必须使用SHA256或更好的哈希算法签名,2048位或更长的RSA密钥,或256位或更长的ECC密钥。如果您想了解允许使用哪种高级加密,请参阅官方文档AppTransportSecurityTechnote了解详情。一个配置到服务器;另一种是自己创建一个证书,然后配置到服务器上。第一种方式搭建的HTTPS服务器当然是最好的。如果你建立一个网站,你将被直接信任。当您将其用作移动应用服务器时,您无需为ATS做太多适配。虽然说权威机构认证要钱,但现在也有免费的第三方认证机构;第二种方式搭建的HTTPS服务器对于网站来说是完全不可行的,用户打开会直接弹出警告。据说这是一个不受信任的网站,用户是否继续,体验很差,让用户觉得网站不安全。对于移动端,在iOS9出现之前这是没有问题的,但是在iOS9出现之后,第二种方法无法通过ATS特性,需要将NSAllowsArbitraryLoads设置为YES。所以我推荐使用第一种方式搭建HTTPS服务器。接下来,让我们谈谈这两种方法是如何工作的。第一种方式是使用CA机构认证的证书搭建HTTPS服务器。1.创建证书请求,提交给CA机构认证#成私privateKeyopensslgenrsa-des3-outprivate.key2048#生成服务器私钥,去除密钥密码opensslrsa-inprivate.key-outserver.key#Generate证书请求opensslreq-new-keyprivate.key-outserver.csr将生成server.csr并将其提交给CA组织。CA组织对其进行签名后,会将签名后的根证书和服务器生成的证书发送给您,此时的证书就是经过CA认证后的证书。这里我们将根证书和服务器证书分别重命名为ca.crt和serve.crt。2、配置Apache服务器上传ca.crt、server.key、server.crt到阿里云服务器,使用SSH登录进入这三个文件所在目录,执行以下命令mkdirsslcpserver.crt/alidata/server/httpd/conf/ssl/server.crtcpserver.key/alidata/server/httpd/conf/ssl/server.keycpdemoCA/cacert.pem/alidata/server/httpd/conf/ssl/ca.crtcp-rssl/alidata/server/httpd/conf/编辑/alidata/server/httpd/conf/extra/httpd-ssl.conf文件,找到SSLCertificateFile、SSLCertificateKeyFile、SSLCACertificatePath、SSLCACertificateFile修改:#SpecifyservercertificatelocationSSLCertificateFile"/alidata/server/httpd/conf/ssl/server.crt"#指定服务器证书密钥位置SSLCertificateKeyFile"/alidata/server/httpd/conf/ssl/server.key"#证书目录SSLCACertificatePath"/alidata/server/httpd/conf/ssl"#根证书位置SSLCACertificateFile"/alidata/server/httpd/conf/ssl/ca.crt"修改vhost配置vim/alidata/server/httpd/conf/vhosts/phpwind.confSSLCertificateFile/alidata/server/httpd/conf/ssl/server.crtSSLCertificateKeyFile/alidata/server/httpd/conf/ssl/server.keySSLCACertificatePath/alidata/server/httpd/conf/sslSSLCACertificateFile/alidata/server/httpd/conf/ssl/ca.crtServerNamewww.casetree.cnDocumentRoot/alidata/www***,重启Apache服务器,在浏览器输入网址查看是否配置成功。我在这里供个人使用。我申请了免费证书,我申请证书的网站是WoSign。搭建结果:https://www.casetree.cn二、自建证书配置HTTPS服务器请参考我之前的文章自建证书配置HTTPS服务器3.使用nscurl检测服务器搭建好HTTPS服务器后,就可以使用nscurl命令检查建立的HTTPS服务器是否可以通过ATS特性。nscurl--ats-diagnostics--verbosehttps://casetree.cn如果HTTPS服务器能够通过ATS特性,以上测试用例全部为PASS;如果某个项目的Reuslt是FAIL,找ATSDictionary查一下,就可以知道HTTPS服务器不满足ATS的哪个条件。这里之前遇到了一个问题,就是我自己建证书的时候,通过这个命令测试的时候,发现Result全是FAIL,在iOS代码测试中也出现了一个很奇怪的现象,就是,同样的代码,在iOS8.4上请求数据是完全正常的,但是在iOS9上,直接连接失败。最后发现原因是自建证书不可信,不能通过ATS,除非NSAllowsArbitraryLoads设置为YES。4、iOS客户端上面第二步,HTTPS服务器满足ATS默认条件,SSL证书经过权威CA机构认证。那么我们在使用Xcode7开发的时候,网络的适配是怎样的呢?什么都不用做,我们就可以和服务器正常通信了。但是当我们对安全性要求比较高或者是自己创建证书时,就需要在本地导入证书进行验证。那么,如何在本地导入证书进行校验呢?这里先提一下,由于iOS客户端支持的证书是DER格式的,所以我们需要创建一个客户端证书。要创建客户端证书,只需将服务器的CA根证书导出为DER格式即可。opensslx509-informPEM-outformDER-inca.crt-outca.cer导入证书后,我们再来说说使用NSURLSession和AFNetworking进行本地验证。首先说说使用NSURLSession验证验证步骤如下:将CA根证书导入到项目中,也就是我们创建的获取信任对象的ca.cer,通过读取导入证书的数据SecCertificateCreateWithData方法生成一个证书对象,然后通过SecTrustSetAnchorCertificates设置这个证书为信任对象的信任根证书(信任锚),通过SecTrustEvaluate方法验证信任对象。下面是主要的OC实现代码。我也把demo工程放到github上了。有两种语言:OC和Swift。要下载演示,请单击HTTPSConnectDemo。-(void)viewDidLoad{[superviewDidLoad];//导入客户端证书NSString*cerPath=[[NSBundlemainBundle]pathForResource:@"ca"ofType:@"cer"];NSData*data=[NSDatadataWithContentsOfFile:cerPath];SecCertificateRefcertificate=SecCertificateCreateWithData(NULL,(__bridgeCFDataRef)data);self.trustedCerArr=@[(__bridge_transferid)certificate];//发送请求NSURL*testURL=[NSURLURLWithString:@》https://casetree.cn/web/test/demo.php"];NSURLSession*session=[NSURLSessionsessionWithConfiguration:[NSURLSessionConfigurationdefaultSessionConfiguration]delegate:selfdelegateQueue:[NSOperationQueuemainQueue]];NSURLSessionDataTask*task=[sessiondataTaskWithRequest:[NSURLRequestrequestWithURL:testURL]];[taskresume];//Doanyadditionalsetupafterloadingpraganibmark-#typicallyfrompraganibview.,#typicallypraganibviewNSURLSessionDelegate-(void)URLSession:(NSURLSession*)sessiondidReceiveChallenge:(NSURLAuthenticationChallenge*)challengecompletionHandler:(void(^)(NSURLSessionAuthChallengeDispositiondisposition,NSURLCredential*__nullablecredential))completionHandler{OSStatuserr;NSURLSessionAuthChallengeDispositiondisposition=NSURLSessionAuthChallengePerformDefaultHandling;SecTrustResultTypetrustResult=kSecTrustResultInvalid;NSURLCredential*credential=nil;//获取服务器的trustobjectSecTrustRefserverTrust=challenge.protectionSpace.serverTrust;//将读取的证书设置为serverTrust的根Certificateerr=SecTrustSetAnchorCertificates(serverTrust,(__bridgeCFArrayRef)self.trustedCerArr);if(err==noErr){//通过本地导入的证书验证服务器的证书是否可信。如果SecTrustSetAnchorCertificatesOnly设置为NO,则只能通过本地或系统证书链中的任何一方进行认证){//认证成功,然后创建凭证返回给服务器){完成etionHandler(disposition,credential);}}注意:1.SecTrustSetAnchorCertificates方法会设置一个标志来屏蔽信任对象不信任其他根证书;如果你也想信任系统默认的根证书,请调用SecTrustSetAnchorCertificatesOnly方法清除该标志(设置为NO)2.验证方式不止这一种,更多验证方式请参考HTTPS服务器信任评估下面,就来说说AFNetworking是如何验证的,我们是如何使用AFNetworking的AFNetworking的证书验证工作是由AFSecurityPolicy来完成的,所以这里主要看一下AFSecurityPolicy。注意:我这里使用的是AFNetworking2.6.0,与2.5.0不同。说到AFSecurityPolicy,就不得不提到它的三个重要属性,如下:@property(readonly,nonatomic,assign)AFSSLPinningModeSSLPinningMode;@property(nonatomic,assign)BOOLallowInvalidCertificates;@property(nonatomic,assign)BOOLvalidatesDomainName;SSLPingMode是最重要的一个指示如何对AFSecurityPolicy进行身份验证的属性。是一个枚举类型,这个枚举类型有三个值,分别是AFSSLPinningModeNone、AFSSLPinningModePublicKey、AFSSLPinningModeCertificate。其中AFSSLPinningModeNone表示AFSecurityPolicy不进行更严格的验证。只要是系统信任的证书,都可以通过验证。但是,它受allowInvalidCertificates和validatesDomainName的影响;AFSSLPinningModePublicKey是通过比较证书的公钥(PublicKey)部分来验证的。通过SecTrustCopyPublicKey方法获取本地证书和服务器证书,然后进行比较。如果相同,则验证通过。该方式主要适用于自建证书和验证等对安全性要求较高的HTTPS服务器;AFSSLPinningModeCertificate直接使用本地证书设置为可信根证书,然后进行判断,将本地证书的内容与服务器证书的内容进行比较,进行二次判断。该方法适用于对安全性要求较高的验证。allowInvalidCertificates属性表示是否允许不受信任的证书通过验证,默认为NO。validatesDomainName属性表示是否验证主机名,默认为YES。接下来说一下验证过程。验证过程主要放在AFSecurityPolicy的-(BOOL)evaluateServerTrust:(SecTrustRef)serverTrustforDomain:(NSString*)domain方法中。c-(BOOL)evaluateServerTrust:(SecTrustRef)serverTrustforDomain:(NSString*)domain{//使用自建证书验证域名时,需要使用AFSSLPinningModePublicKey或AFSSLPinningModeCertificateif(domain&&self.allowInvalidCertificates&&self.validatesDomainName&&(self.SSLPinningMode==N|one|oneAFSSLPinning[Modeself.pinnedCertificatescount]==0)){NSLog(@"Inordertovalidateadomainnameforselfsignedcertificates,youMUSTusepinning.");returnNO;}NSMutableArray*policies=[NSMutableArrayarray];//需要验证域时name,需要添加域名验证策略//设置认证策略,可以是多个SecTrustSetPolicies(serverTrust,(__bridgeCFArrayRef)policies);//当SSLPinningMode为AFSSLPinningModeNone时,allowInvalidCertificates为YES,which表示可以验证服务器的任何证书;如果为NO,则需要判断服务器证书是否为系统信任的证书if(self.SSLPinningMode==AFSSLPinningModeNone){if(self.allowInvalidCertificates||AFServerTrustIsValid(serverTrust)){returnYES;}else{returnNO;}}elseif(!AFServerTrustIsValid(serverTrust)&&!self.allowInvalidCertificates){returnNO;}//获取服务器证书的内容NSArray*serverCertificates=AFCertificateTrustChainForServerTrust(serverTrust);switch(self.SSLPinningMode){caseAFSSLPinningModeNone:default:returnNO;caseAFSSLPinningModeCertificate:{//AFSSLPinningModeCertificate是直接将本地证书设置为可信根证书,然后进行判断,比较本地证书的内容是否正确与服务端证书相同,如果相同则返回YESNSMutableArray*pinnedCertificates=[NSMutableArrayarray];for(NSData*certificateDatainself.pinnedCertificates){[pinnedCertificatesaddObject:(__bridge_transferid)SecCertificateCreateWithData(NULL,(__bridgeCFDataRef)certificateData)];}//设置本地证书为根证书SecTrustSetAnchorCertificates(serverTrust,(__bridgeCFArrayRef)pinnedCertificates);//使用本地证书判断服务器证书是否可信。服务器证书的内容是否相同?NSUIntegertrustedCertificateCount=0;for(NSData*trustChainCertificateinserverCertificates){if([self.pinnedCertificatescontainsObject:trustChainCertificate]){trustedCertificateCount++;}}returntrustedCertificateCount>0;}caseAFSSLPinningModePublicKey:{//AFSSLPinningModePublicKey是通过比较证书的公钥(PublicKey)部分来验证的,通过SecTrustCopyPublicKey方法得到本地证书和服务器证书,然后比较它们。如果相同则验证NSUIntegertrustedPublicKeyCount=0;NSArray*publicKeys=AFPublicKeyTrustChainForServerTrust(serverTrust);//判断服务器证书的公钥是否与本地证书公钥相同。认证通过for(idtrustChainPublicKeyinpublicKeys){for(idpinnedPublicKeyinself.pinnedPublicKeys){if(AFSecKeyIsEqualToKey((__bridgeSecKeyRef)trustChainPublicKey,(__bridgeSecKeyRef)pinnedPublicKey)){trustedPublicKeyCount+=1;NO}}}return0Key}t验证过程>Put,让我们看看在如何使用AFNetworking,代码如下:_httpClient=[[BGAFHTTPClientalloc]initWithBaseURL:[NSURURRLWithString:baseURL]];AFSecurityPolicy*policy=[AFSecurityPolicypolicyWithPinningMode:AFSSLPinningModeCertificate];//是否允许CA不信任的证书通过policy.allowInvalidCertificates=YES;//是否验证主机名policy.validatesDomainName=YES;_httpClient.securityPolicy=policy;我没有在这里创建演示。想看的话可以看一下我写的一个框架BGNetwork。里面的demo适配ATS,AFNetworking的使用放在BGNetworkConnector类里面——(instancetype)initWithBaseURL:(NSString*)baseURLdelegate:(id)delegate初始化方法5.适配ATS前面的内容描述了情况符合ATS的特性,但是如果服务端是用自建证书搭建的,或者TLS版本是1.0的,服务端不能随便改,那我们的客户端怎么适应呢?不用担心,我们可以在项目中的Info.plist文件中设置,主要参考下图:如果是自建证书,没有权威机构认证的证书,那么需要设置NSAllowsArbitraryLoads为YES即可通过。NSAllowsArbitraryLoads为YES,之前的HTTP请求也可以通过。如果是经过认证的证书,可以使用nscurl--ats-diagnostics--verbosehttps://casetree.cn查看服务器支持的ATSDictionary,然后进行相应的设置。适配部分也可以参考Demo1_iOS9网络适配_ATS:切换到更安全的HTTPS