AFNetworking 2.x 阅读笔记(六)

本文主要记录关于 AFSecurityPolicy 模块的相关内容,主要是AFSecurityPolicy.m, 是 HTTPS网络请求中的安全模块.

HTTPS

HTTPs 的基本内容可以参考维基百科

其中的身份认证的方法是基于公开密钥加密算法的身份验证是指通信中的双方分别持有公开密钥和私有密钥,由其中的一方采用私有密钥对特定数据进行加密,而对方采用公开密钥对数据进行解密,如果解密成功,就认为用户是合法用户,否则就认为是身份验证失败。使用基于公开密钥加密算法的身份验证的服务有:SSL, 数字签名等等。

bangs blog 的解释以及有两个重要的问题如下:

HTTPS连接建立过程大致是,客户端和服务端建立一个连接,服务端返回一个证书,客户端里存有各个受信任的证书机构根证书,用这些根证书对服务端返回的证书进行验证,经验证如果证书是可信任的,就生成一个pre-master secret,用这个证书的公钥加密后发送给服务端,服务端用私钥解密后得到pre-master secret,再根据某种算法生成master secret,客户端也同样根据这种算法从pre-master secret生成master secret,随后双方的通信都用这个master secret对传输数据进行加密解密。

以上是简单过程,中间还有很多细节,详细过程和原理已经有很多文章阐述得很好,就不再复述,推荐一些相关文章:
关于非对称加密算法的原理:RSA算法原理<一> <二>
关于整个流程:HTTPS那些事<一> <二> <三>
关于数字证书:浅析数字证书

1.证书是怎样验证的?怎样保证中间人不能伪造证书?

首先要知道非对称加密算法的特点,非对称加密有一对公钥私钥,用公钥加密的数据只能通过对应的私钥解密,用私钥加密的数据只能通过对应的公钥解密。
我们来看最简单的情况:一个证书颁发机构(CA),颁发了一个证书A,服务器用这个证书建立https连接。客户端在信任列表里有这个CA机构的根证书。
首先CA机构颁发的证书A里包含有证书内容F,以及证书加密内容F1,加密内容F1就是用这个证书机构的私钥对内容F加密的结果。(这中间还有一次hash算法,略过。)
建立https连接时,服务端返回证书A给客户端,客户端的系统里的CA机构根证书有这个CA机构的公钥,用这个公钥对证书A的加密内容F1解密得到F2,跟证书A里内容F对比,若相等就通过验证。整个流程大致是:F->CA私钥加密->F1->客户端CA公钥解密->F。因为中间人不会有CA机构的私钥,客户端无法通过CA公钥解密,所以伪造的证书肯定无法通过验证。

2.什么是SSL Pinning?

可以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架构的像手机APP事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。
为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。
为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。

关于Certificate Pinning

在SSL/TLS通信中,客户端通过数字证书判断服务器是否可信,并采用证书的公钥与服务器进行加密通信.
然而在很多移动应用中,开发者不检查服务器证书的有效性,或选择接受所有的证书,这样就会导致中间人攻击.
事实上,app大多只和固定的服务器通信,因此可以在代码更精确地直接验证某张特定的证书,这种方法称为“证书锁定”(certificate pinning)

如果进行 Certificate Pinning

具体步骤如下:
1 首先通过服务器端使用RSA算法生成一对公钥私钥对,服务器端持有私钥,线下将公钥传给客户端。App中将这个值硬编码到本地。
2 App端可以自己实现一个X509TrustManager接口,在其中的CheckServerTrusted()方法里通过证书链拿到PublicKey
3 比较12中进行md5的值,如果匹配则服务器验证通过,否则立即终止与此服务器的通信.

AFNetworking 就是采用的这种方法.

如果引入第三方的支付等

在实际应用中,一款移动应用往往不止一个后台,尤其是在支付类产品中,经常需要去集成第三方支付网关。

但是第三方的服务什么时候会更改证书,这个就说不准了。初期,我们会把所有的这些第三方的服务提供的PublicKey都硬编码在本地,但是有次其中一个服务商自己改掉了,造成用户手上的产品直接不能使用了, 这个就给我们带来了不必要的麻烦。

具体的解决方案如下:
1 自己的服务端使用RSA算法生成一对公钥私钥对,服务器端持有私钥,线下将公钥传给客户端。App中将这个值硬编码到本地
2 自己的服务端提供API,获得当前所有服务器(包括第三方)公钥的SHASUM值.当然这个值必须通过1中存在本地PublicKey签名验证得到
3 再通过最基本Certificate Pinning的办法(上文提到),直接从各服务端拿到各自公钥, 连同本地PublicKey一起计算SHASUM
4 比较2和3中的值,如果相同则Certificate Pinning通过,否则终止app

详细的参考资料:
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning

iOS 中实现对 HTTPS 的支持

详细参考资料: http://oncenote.com/2014/10/21/Security-1-HTTPS/

首先,需要明确你使用HTTP/HTTPS的用途,因为OSX和iOS平台提供了多种API,来支持不同的用途,官方文档《Making HTTP and HTTPS Requests》有详细的说明,而文档《HTTPS Server Trust Evaluation》则详细讲解了HTTPS验证相关知识,这里就不多说了。主要讲解我们最常用的NSURLConnection支持HTTPS的实现(NSURLSession的实现方法类似,只是要求授权证明的回调不一样而已),以及怎么样使用AFNetworking这个非常流行的第三方库来支持HTTPS。

验证证书的API

相关的Api在Security Framework中,验证流程如下:

  1. 第一步,先获取需要验证的信任对象(Trust Object)。这个Trust Object在不同的应用场景下获取的方式都不一样,对于NSURLConnection来说,是从delegate方法-connection:willSendRequestForAuthenticationChallenge:
    回调回来的参数challenge中获取([challenge.protectionSpace serverTrust]
    )。
  1. 使用系统默认验证方式验证Trust Object。SecTrustEvaluate
    会根据Trust Object的验证策略,一级一级往上,验证证书链上每一级数字签名的有效性(上一部分有讲解),从而评估证书的有效性。
  2. 如第二步验证通过了,一般的安全要求下,就可以直接验证通过,进入到下一步:使用Trust Object生成一份凭证([NSURLCredential credentialForTrust:serverTrust]
    ),传入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge]
    )处理,建立连接。
  3. 假如有更强的安全要求,可以继续对Trust Object进行更严格的验证。常用的方式是在本地导入证书,验证Trust Object与导入的证书是否匹配。更多的方法可以查看Enforcing Stricter Server Trust Evaluation,这一部分在讲解AFNetworking源码中会讲解到。
  4. 假如验证失败,取消此次Challenge-Response Authentication验证流程,拒绝连接请求。
    ps: 假如是自建证书的,则不使用第二步系统默认的验证方式,因为自建证书的根CA的数字签名未在操作系统的信任列表中。
    iOS授权验证的API和流程大概了解了,下面,我们看看在NSURLConnection中的代码实现:

使用NSURLConnection支持HTTPS的实现

// Now start the connection
NSURL * httpsURL = [NSURL URLWithString:@"https://www.google.com"];
self.connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:httpsURL] delegate:self];
//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    //1)获取trust object
    SecTrustRef trust = challenge.protectionSpace.serverTrust;
    SecTrustResultType result;
    
    //2)SecTrustEvaluate对trust进行验证
    OSStatus status = SecTrustEvaluate(trust, &result);
    if (status == errSecSuccess &&
        (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) {
            //3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
            NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
            [challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
        } else {
            //5)验证失败,取消这次验证流程
            [challenge.sender cancelAuthenticationChallenge:challenge];
    }
}

上面是代码是通过系统默认验证流程来验证证书的。假如我们是自建证书的呢?这样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));
self.trustedCertificates = @[CFBridgingRelease(certificate)];

//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    //1)获取trust object
    SecTrustRef trust = challenge.protectionSpace.serverTrust;
    SecTrustResultType result;
    
    //注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate
    SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);
    
    //2)SecTrustEvaluate会查找前面SecTrustSetAnchorCertificates设置的证书或者系统默认提供的证书,对trust进行验证
    OSStatus status = SecTrustEvaluate(trust, &result);
    if (status == errSecSuccess &&
        (result == kSecTrustResultProceed ||
         result == kSecTrustResultUnspecified)) {
            //3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
            NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
            [challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
        } else {
            //5)验证失败,取消这次验证流程
            [challenge.sender cancelAuthenticationChallenge:challenge];
        }
}

建议采用本地导入证书的方式验证证书,来保证足够的安全性。更多的验证方法,请查看官方文档《HTTPS Server Trust Evaluation》

使用 AFNetworking 的安全设置

Pinning Mode

AFNetworking 的安全相关的设置在AFSecurityPolicy,它定义3种 SSL Pinning Mode:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,
    AFSSLPinningModePublicKey,
    AFSSLPinningModeCertificate,
};
  • AFSSLPinningModeNone : 你不必将凭证跟你的 APP 一起打包,完全信任服务器的凭证
  • AFSSLPinningModeCertificate : 对比服务器凭证跟你的凭证是否完全匹配
  • AFSSLPinningModePublicKey : 只对比服务器凭证的 publick key跟你的凭证的 public key 是否匹配

那么使用哪种方式比较好?
AFSSLPinningModeCertificate 比较安全同时比较麻烦, 它会对比你打包的凭证和服务器的凭证是否一致.因为你的凭证是和 app 一起打包的,这也就代表说如果你的凭证过期或者变化了,你就必须得更新 app,而且旧版 app 无法使用了.当然也可以在每次 app 启动时候就去某个服务器下载最新的凭证,不过此时下载链接有风险.

AFSSLPinningModePublicKey 则是只有对比凭证里面的 public key, 所以即使服务器的凭证有所变动,只要 public key 没变化, 就能通过验证

一般情况下使用AFSSLPinningModePublicKey,除非你能保证 app 使用者都只用最新版本的 app.

Certification Chain

/** Whether to evaluate an entire SSL certificate chain, or just the leaf certificate. Defaults to `YES`. */
@property (nonatomic, assign) BOOL validatesCertificateChain;

如果你的凭证是某个机构发出的,该机构的凭证是由另外一家更加高级的机构发出的,一路往上追,这样一串由各个机构发出的凭证称为 certification chain.

如果你把这个属性设置为 Yes,那就得把这一串凭证全部打包到你的 APP,必须每个验证都通过才算通过,如果设置为 No, 只需要打包你自己的凭证就够了.

在2.6x 以后的版本的 AFNetworking 以后已经没有 validatesCertificateChain这个属性.

参考连接:
http://robnapier.net/pinning-your-ssl-certs
http://stackoverflow.com/questions/24615144/afnetworking-pin-public-key-for-a-trusted-certificate/24625969#24625969
http://oncenote.com/2014/10/21/Security-1-HTTPS/
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning

AFNetworking 中 HTTPS 实例

NSURL * url = [NSURL URLWithString:@"https://www.google.com"];
AFHTTPRequestOperationManager * requestOperationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:url];
dispatch_queue_t requestQueue = dispatch_create_serial_queue_for_name("kRequestCompletionQueue");
requestOperationManager.completionQueue = requestQueue;

AFSecurityPolicy * securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];

//allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
//如果是需要验证自建证书,需要设置为YES
securityPolicy.allowInvalidCertificates = YES;

//validatesDomainName 是否需要验证域名,默认为YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
//置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
//如置为NO,建议自己添加对应域名的校验逻辑。
securityPolicy.validatesDomainName = YES;

//validatesCertificateChain 是否验证整个证书链,默认为YES
//设置为YES,会将服务器返回的Trust Object上的证书链与本地导入的证书进行对比,这就意味着,假如你的证书链是这样的:
//GeoTrust Global CA
//    Google Internet Authority G2
//        *.google.com
//那么,除了导入*.google.com之外,还需要导入证书链上所有的CA证书(GeoTrust Global CA, Google Internet Authority G2);
//如是自建证书的时候,可以设置为YES,增强安全性;假如是信任的CA所签发的证书,则建议关闭该验证,因为整个证书链一一比对是完全没有必要(请查看源代码);
securityPolicy.validatesCertificateChain = NO;

requestOperationManager.securityPolicy = securityPolicy;

AFSecurity 在源码中的位置

AFHTTPRequestOperationManager ,AFURLConnectionOperation, 类中有以下属性

 The security policy used by created request operations to evaluate server trust for secure connections. `AFHTTPRequestOperationManager` uses the `defaultPolicy` unless otherwise specified.
 */
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;

最后这个属性被设置成AFURLConnectionOperation的以上属性, 通过该属性的注释 -- 用于 request 中验证服务器身份.

具体使用代码:

//把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件,当然也可以自己打包到自己的 xxx.bundle 中,但是需要在源码中进行一定的设置

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode: AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES;
[AFHTTPRequestOperationManager manager].securityPolicy = securityPolicy;

[manager GET:@"[https://example.com/](https://example.com/)"
    parameters:nil
          success:^(AFHTTPRequestOperation *operation, id, responseObject) {
          } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];

具体的源码注释可以参考 bangs blog

个人准备总结一份关于 iOS 可能用到的 HTTPS 相关的系列文章

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

推荐阅读更多精彩内容