iOS https认证

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...

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352