美文网首页
iOS https认证

iOS https认证

作者: TerryD | 来源:发表于2019-03-01 13:58 被阅读8次

    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;
    }
    

    上面的代码注释解释的很清楚了。需要注意的是有两点:

    1. 对于自签名的https证书,需要自己验证。
    2. 打包到App里的自签名证书会存在过期问题,需要在到期之前提前处理。

    最后说一句,我们的存储服务器就是https证书就是权威CA签发的,意味着啥都不用做Orz...

    参考链接

    相关文章

      网友评论

          本文标题:iOS https认证

          本文链接:https://www.haomeiwen.com/subject/audduqtx.html