iOS 集成 SSL Pinning

一、SSL Pinning 简介

1、使用背景

在开发手机应用时,如何正确的使用HTTPS来提高网络传输的安全性是尤为重要的。HTTPS协议本使用了SSL 加密传输,相比HTTP但依然存在极大的安全隐患----中间人攻击。SSL解决了内容的加密的问题,但是SSL过程中是依靠证书进行验证的,这就需要保证证书绝对的安全。先立一个小目标(伪造证书),万一实现了呢?在立一个小目标(伪造服务器),万一实现了呢?事实证明目标是可以实现的(SSL系统遭入侵发布虚假密钥 微软谷歌受影响 )。SSL Pinning技术就是基于SSL基础上在添加一个本地证书,用来再次验证!

2、中间人攻击

中间人攻击(Man-in-the-middle Attack,简称MITM、MitM、MIM、MiM、MITMA)是一种由来已久的网络入侵手段,并且在今天仍然有着广泛的发展空间,如SMB会话劫持、DNS欺骗等攻击都是典型的中间人攻击。简而言之,所谓的中间人攻击就是通过拦截正常的网络通信数据,并进行数据篡改和嗅探,而通信的双方却毫不知情。

中间人攻击

3、Charles抓包原理

Charles作为一个中间人代理,当浏览器和服务器通信时,Charles接收服务器的证书,但动态生成一张证书发送给浏览器,也就是说Charles作为中间代理在浏览器和服务器之间通信,所以通信的数据可以被Charles拦截并解密。由于Charles更改了证书,浏览器校验不通过会给出安全警告,必须安装Charles的证书后才能进行正常访问。

Charles抓包原理
  • 客户端向服务器发起HTTPS请求
  • Charles拦截客户端的请求,伪装成客户端向服务器进行请求
  • 服务器向“客户端”(实际上是Charles)返回服务器的CA证书
  • Charles拦截服务器的响应,获取服务器证书公钥,然后自己制作一张证书,将服务器证书替换后发送给客户端。(这一步,Charles拿到了服务器证书的公钥)
  • 客户端接收到“服务器”(实际上是Charles)的证书后,生成一个对称密钥,用Charles的公钥加密,发送给“服务器”(Charles)
  • Charles拦截客户端的响应,用自己的私钥解密对称密钥,然后用服务器证书公钥加密,发送给服务器。(这一步,Charles拿到了对称密钥)
  • 服务器用自己的私钥解密对称密钥,向“客户端”(Charles)发送响应
  • Charles拦截服务器的响应,替换成自己的证书后发送给客户端
  • 至此,连接建立,Charles拿到了 服务器证书的公钥 和 客户端与服务器协商的对称密钥,之后就可以解密或者修改加密的报文了。

HTTPS抓包的原理还是挺简单的,简单来说,就是Charles作为“中间人代理”,拿到了 服务器证书公钥 和 HTTPS连接的对称密钥,前提是客户端选择信任并安装Charles的CA证书,否则客户端就会“报警”并中止连接。这样看来,HTTPS还是很安全的。

4、SSL Pinning

SSL Pinning(又叫Certificate Pinning)可以理解为证书绑定。在一些应用场景中,客户端和服务器之间的通信是事先约定好的,既服务器地址和证书是预先知道的,这种情况常见于CS(Client-Server)架构的应用中。这样的话在客户端事先保存好一份服务器的证书(含公钥),每次请求服务器的时候,将服务器返回的证书与客户端保存的证书进行对比,如果证书不符,说明受到中间人攻击,马上可以中断请求。这样的话中间人就无法伪造证书进行攻击了。

我们需要将APP代码内置仅接受指定域名的证书,而不接受操作系统或浏览器内置的CA根证书对应的任何证书,通过这种授权方式,保障了APP与服务端通信的唯一性和安全性。但是CA签发证书都存在有效期问题,所以缺点是在证书续期后需要将证书重新内置到APP中。

公钥锁定则是提取证书中的公钥并内置到移动端APP中,通过与服务器对比公钥值来验证连接的合法性,我们在制作证书密钥时,公钥在证书的续期前后都可以保持不变(即密钥对不变),所以可以避免证书有效期问题。

证书锁定旨在解决移动端APP与服务端通信的唯一性,实际通信过程中,如果锁定过程失败,那么客户端APP将拒绝针对服务器的所有 SSL/TLS 请求,FaceBook/Twitter则通过证书锁定以防止Charles/Fiddler等抓包工具中间人攻击。

为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。

为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。

二、NSURLSession方式

1、获取证书

客户端需要证书(Certification file), .cer格式的文件。可以跟服务器端索取。如果他们给个.pem文件,要使用命令行转换:
openssl x509 -inform PEM -in name.pem -outform DER -out name.cer

如果给了个.crt文件,请这样转换:
openssl x509 -in name.crt -out name.cer -outform der

如果啥都不给你,你只能自己动手了,这里以github.com为例子,获取证书:
openssl s_client -connect github.com:443 </dev/null 2>/dev/null | openssl x509 -outform DER > github.com.cer

2、NSURLSession实现

当谈到NSURLSession使用SSL pinning有点棘手,因为在AFNetworking中,其本身已经有封装好的类可以使用来进行配置。这里没有办法去设置一组证书来自动取消所有本地证书不匹配的response。我们需要手动执行检查来实现在NSURLSession上的SSL pinning。我们很荣幸的是我们可以用Security's framework C API。

创建默认会话配置的NSURLSession对象,及发送请求,执行任务

    // 设置地址
    NSURL *testURL = [NSURL URLWithString:@"https://github.com"];

    // 创建默认会话配置的NSURLSession对象
    NSURLSessionConfiguration *seeConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    seeConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
    NSURLSession *session = [NSURLSession sessionWithConfiguration:seeConfig
                                                          delegate:self
                                                     delegateQueue:nil];
    
    // NSURLSession使用NSURLSessionTask来发送一个请求,
    // 我们使用dataTaskWithURL:completionHandler:方法来进行SSL pinning 测试
    NSURLSessionDataTask *task =
    [session dataTaskWithURL:testURL
           completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
               if (!error) {
                   NSString *str =
                   [[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding];
                   NSLog(@"str : %@", str);
               } else {
                   NSLog(@"error : %@", error);
               }
               
           }];
    [task resume];

在代理回调方法中,校验证书是否合法

// 代理回调
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
    
    // 得到远程证书
    SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
    SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);
    
    // 设置ssl政策来检测主域名
    NSMutableArray *policies = [NSMutableArray array];
    [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)];
    
    // 验证服务器证书
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    BOOL certificateIsValid =
    (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
    
    // 得到远程和本地证书data
    NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
    NSString *pathToCert = [[NSBundle mainBundle] pathForResource:@"github2018" ofType:@"cer"];
    NSData *localCertificate = [NSData dataWithContentsOfFile:pathToCert];
    
    // 检查
    if (certificateIsValid && [remoteCertificateData isEqualToData:localCertificate]) {
        // 验证通过
        NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }else {
        // 验证不通过
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,NULL);
    }
}

上述方法的开始,我们使用SecTrustGetCertificateAtIndex来得到服务器的SSL证书数据。然后使用证书评估设置policies。证书使用SecTrustEvaluate评估,然后返回以下几种认证结果类型之一:

typedef uint32_t SecTrustResultType;
enum {
    kSecTrustResultInvalid = 0,
    kSecTrustResultProceed = 1,
    kSecTrustResultConfirm SEC_DEPRECATED_ATTRIBUTE = 2,
    kSecTrustResultDeny = 3,
    kSecTrustResultUnspecified = 4,
    kSecTrustResultRecoverableTrustFailure = 5,
    kSecTrustResultFatalTrustFailure = 6,
    kSecTrustResultOtherError = 7
};

如果我们得到kSecTrustResultProceedkSecTrustResultUnspecified之外的类型结果,我们可以认为证书是无效的(不被信任的)。

至今为止我们除了检测远程服务器证书评估外,还没有做其他事情,对于SSL pinning 检测我们需要通过SecCertificateRef来得到他的NSData。这个SecCertificateRef来自于challenge.protectionSpace.serverTrust。而本地的NSData来自本地的.cer证书文件。然后我们使用isEqual来进行SSL pinning。

如果远程服务器证书的NSData等于本地的证书data,那么就可以通过评估,我们可以验证服务器身份然后进行通信,而且还要使用completionHandler(NSURLSessionAuthChallengeUseCredential,credential)执行request。

然而如果两个data不相等,我们使用completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,NULL)方法来取消dataTask的执行,这样就可以拒绝和服务器沟通。

这就是在NSURLSession中使用SSL pinning。

三、AFNetworking方式

1、AFSecurityPolicy

安全模式设置

AFSecurityPolicy是AFNetworking中三种安全策略模块,提供了证书锁定模式

  • AFSSLPinningModeNone:
这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。
若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
  • AFSSLPinningModeCertificate:
这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,
这里验证分两步,第一步验证证书的域名/有效期等信息,
第二步是对比服务端返回的证书跟客户端返回的是否一致。
  • AFSSLPinningModePublicKey:
这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,
只是验证时只验证证书里的公钥,不验证证书的有效期等信息。
只要公钥是正确的,就能保证通信不会被窃听,
因为中间人没有私钥,无法解开通过公钥加密的数据。

选择那种模式呢?
AFSSLPinningModeCertificate最安全的比对模式。但是也比较麻烦,因为证书是打包在APP中,如果服务器证书改变或者到期,旧版本无法使用了,我们就需要用户更新APP来使用最新的证书。
AFSSLPinningModePublicKey只比对证书的Public Key,只要Public Key没有改变,证书的其他变动都不会影响使用。
如果你不能保证你的用户总是使用你的APP的最新版本,所以我们使用AFSSLPinningModePublicKey

属性

/**
 服务器证书验证模式,默认是不验证
 */
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;

/**
 验证服务器的证书的集合,默认情况下,AFNetworking会搜索工程中所有.cer的证书文件,但不会将某个证书作为默认。如果想创建AFSecurityPolicy对象,就先调用certificatesInBundle方法加载证书,然后调用policyWithPinningMode:withPinnedCertificates方法创建对象
 */
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;

/**
 是否信任无效或者过期的证书,默认为否
 */
@property (nonatomic, assign) BOOL allowInvalidCertificates;

/**
 是否验证证书中的域名,默认为是
 */
@property (nonatomic, assign) BOOL validatesDomainName;

2、AFNetworking实现

创建自定义安全策略

// 自定义安全策略
- (AFSecurityPolicy *)customSecurityPolicy {
    
    // 获取证书
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"github2020" ofType:@"cer"];
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    NSSet *pinnedCertificates = [[NSSet alloc] initWithObjects:certData, nil];

    /*
     安全模式
     AFSSLPinningModeNone:完全信任服务器证书;
     AFSSLPinningModePublicKey:只比对服务器证书和本地证书的Public Key是否一致,如果一致则信任服务器证书;
     AFSSLPinningModeCertificate:比对服务器证书和本地证书的所有内容,完全一致则信任服务器证书
     */
    AFSecurityPolicy *securityPolicy =
    [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey
                     withPinnedCertificates:pinnedCertificates];
    
    // allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
    // 如果是需要验证自建证书,需要设置为YES
    securityPolicy.allowInvalidCertificates = YES;
    
    /*
    validatesDomainName 是否需要验证域名,默认为YES;
    假如证书的域名与你请求的域名不一致,需把该项设置为NO;
    如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
    置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。
    因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;
    当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
    如置为NO,建议自己添加对应域名的校验逻辑。
     */
    securityPolicy.validatesDomainName = YES;
    
    return securityPolicy;
}

创建网络会话管理

- (AFHTTPSessionManager *)manager {
    if (!_manager) {
        // 设置BaseUrl
        NSURL *baseUrl = [NSURL URLWithString:@"https://github.com"];
        AFHTTPSessionManager *manager =
        [[AFHTTPSessionManager manager] initWithBaseURL:baseUrl];
        
        manager.securityPolicy = [self customSecurityPolicy];
        
        manager.responseSerializer = [AFHTTPResponseSerializer serializer];
        manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
        _manager = manager;
    }
    return _manager;
}

发送请求

// 发送请求
- (void)sendRequest {
    NSString *urlStr = @"https://github.com/AFNetworking/AFNetworking";
    [self.manager GET:urlStr
           parameters:nil
              headers:nil
             progress:nil
              success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                  NSString *str = [[NSString alloc] initWithData:responseObject
                                                        encoding:NSUTF8StringEncoding];
                  NSLog(@"%@",str);
              } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                  NSLog(@"%@", error);
              }];
}

附Demo链接:https://github.com/ZhangJingHao/ZJHSSLPinning

Charles对使用SSL Pinning前后抓包对比

使用SLL Pinning前
使用SLL Pinning后

参考链接:
如何使用SSL pinning来使你的iOS APP更加安全
证书锁定SSL Pinning简介及用途
AFNetworking + SSL Pinning
SSL pinning using AFNetworking and NSURLSession
浅谈HTTPS通信机制和Charles抓包原理

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