大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
0x00 问题引出
调用NSURLConnection实现HTTPS访问时,如果服务器证书是由CA机构颁发的(全球可信的机构,如verisign),连接方式和HTTP并没有区别。代码如下:
NSString *urlStr = @"https://yourURL";
NSURL *url = [NSURL URLWithString:urlStr];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
系统会帮你建立TLS连接,验证服务器证书(包含是否过期,证书和访问域名是否一致,证书是否为合法机构颁发等),交换对称加密密钥等,几乎不用自己写任何代码。
0x01 ATS
但是iOS9有一个新特性ATS(App Transport Security),需要我们注意一下,在测试访问百度时发现报错:
NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)
错误代码定义详解见文件<Security/SecureTranspot.h>,所有相关的错误代码都定义在这个头文件里面。查阅后发现-9802描述为Fatal alert。常见的还有:-9843(域名和证书不符),-9814(证书链中有过期证书)等。
同样的代码在iOS9以前的系统上就不存在问题,这是因为在iOS9之后,ATS默认打开,然后它要求所有网络请求都为HTTPS,且支持TLS V1.2,对加密方式和证书的签名算法强度均有要求(详情见,iOS App Transport Security研究)。官方文档是这样描述的:
Certificates must be signed with one of the following types of keys:
- Secure Hash Algorithm 2 (SHA-2) key with a digest length of at least 256 (that is, SHA-256 or greater)
- Elliptic-Curve Cryptography (ECC) key with a size of at least 256 bits
- Rivest-Shamir-Adleman (RSA) key with a length of at least 2048 bits
An invalid certificate results in a hard failure and no connection.
也就是说不满足条件的证书,ATS都会拒绝连接,百度证书如下图。由于它采用的sha1,不满足要求,因此会报错。
在info.plist文件中加入以下代码,可以关闭ATS,就不会报错了。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
需要注意的是加入以上代码之后会完全关闭ATS,也就是说iOS系统不再检查你的TLS连接的安全性。如果还是需要保留此特性,可以设置NSExceptionDomains属性,指定某个域名,ATS不进行安全检查。按照如下配置也可以通过,可以更加细粒度的管理(详情见,iOS App Transport Security研究)。
0x02 自定义证书验证
为了更深入的了解证书验证过程,动手自定义证书验证过程,如果证书不是向可信CA机构购买的,而是自己做的证书(比如12306,顺便吐槽一下,最近帮同事买票,验证码真心醉了。。。言归正传,正如12306官网所说,为保障您顺畅购票,请下载安装根证书),就必须订制证书验证过程。订制证书验证过程需要实现协议NSURLConnectionDelegate,并实现其中的方法willSendRequestForAuthenticationChallenge,与之相关的还有canAuthenticateAgainstProtectionSpace和didReceiveAuthenticationChallenge,但是如果实现了willSendRequestForAuthenticationChallenge系统就不会调用后两个方法了,本例调用的前者。上代码:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
// 1.判断本次鉴别挑战的类型,NSURLAuthenticationMethodServerTrust是验证服务器身份
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
// 判断如果失败次数大于5次,则不再验证。直接返回失败。
if(challenge.previousFailureCount < 5) {
// 获取信任管理对象,里面包含了待验证的证书,和自定义的策略
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
// 2.保存验证结果
SecTrustResultType result;
// 注意:该方法是验证证书的关键方法,该方法可能会从网络获取证书,因此不能放在主线程。
// 也可以调用它的异步方法SecTrustEvaluateAsync
SecTrustEvaluate(self.serverTrust, &result);
if(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified
) {
// 3.以下两种方法调用两个都可以,调用一个就可以了
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
// [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
} else if (result == kSecTrustResultRecoverableTrustFailure) {
// 域名和证书中的不匹配,或者证书过期等
// 中断本次连接
[challenge.sender cancelAuthenticationChallenge:challenge];
[connection cancel];
}
else {
// 域名和证书中的不匹配,或者证书过期等
// 中断本次连接
[challenge.sender cancelAuthenticationChallenge:challenge];
[connection cancel];
}
}
}
}
1.NSURLAuthenticationChallenge
NSURLAuthenticationChallenge是一个认证挑战类,服务器向客户端发起挑战,要求客户端提供一个挑战凭证NSURLCredential(用户名密码、客户端证书等信息)来接受挑战。如函数:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]就是用SecTrustRef类创建一个挑战凭证。NSURLAuthenticationChallenge中的protectionSpace中保存了挑战相关的信息(如服务器提供的证书,主机名,端口号,协议等),由于willSendRequestForAuthenticationChallenge回调时不止HTTPS服务器身份鉴别,如Basic、Digest、HTML Form等方式的身份鉴别也会调用该方法,我们关注的是HTTPS证书验证,因此首先判断一下身份鉴别的类型。通过challenge.protectionSpace.authenticationMethod可以获取,可能获取到以下值:
NSURLAuthenticationMethodDefault // 使用默认的身份鉴别方法,默认为Basic方式
NSURLAuthenticationMethodHTTPBasic // 同上
NSURLAuthenticationMethodHTTPDigest // 使用Digest鉴别方式
NSURLAuthenticationMethodHTMLForm // HTML表单方式
NSURLAuthenticationMethodNegotiate
NSURLAuthenticationMethodNTLM
NSURLAuthenticationMethodClientCertificate // 用客户端提供的证书来完成身份鉴别
NSURLAuthenticationMethodServerTrust // 验证服务器证书,该方法主要用于SLL和TLS连接的建立。
2.SecTrustEvaluate
函数定义:
OSStatus SecTrustEvaluate(SecTrustRef trust, SecTrustResultType * __nullable result)
返回值:
OSStatus 定义在SecBase.h,0是无错误。
参数:
Trust 信任管理对象,里面包含了待验证的证书,和自定义的策略。
Result 验证结果,有以下情况:
- kSecTrustResultUnspecified 证书验证成功,但是用户没有明确指出信任此证书。这是最常见的返回值。
- kSecTrustResultProceed 用户选择信任此证书。
- kSecTrustResultDeny 用户选择不信任次证书。
- kSecTrustResultRecoverableTrustFailure 证书不可信,但是经过较小的改动可以修复问题,例如忽略过期证书、增加信任链节点等。在iOS中通常会拒绝该证书,但是收到信息时证书未过期可以通过验证,因此可以验证证书在收到信息时是否过期。
- kSecTrustResultConfirm 用户预先选择了证书链中得某一个证书在每次使用前询问允许。这个返回值已经不再使用,只在老版本的OS X中使用。
该函数评估证书的有效性用于在数字签名、建立SSL连接等情况。在调用函数前,可以调用SecTrustSetVerifyDate来设置用于验证证书的时间,默认是当前的时间。如果证书是自定义的,则需要在调用SecTrustEvaluate之前,自己构造证书链等信息。
如果信任管理对象(trust management object)中缺少验证页证书的上一级证书,它会在以下位置搜索证书:
1.调用者自定义的证书链(SecTrustSetKeychains)
2.通过SecTrustSetAnchorCertificates设置的证书
3.系统的证书链
4.从网络获取
注意:
该函数在调用时可能涉及到网络请求,因此不能在主线程调用,可以调用它的异步方法SecTrustEvaluateAsync
3.响应挑战
证书验证完成后,会根据验证结果响应服务器挑战,响应挑战必须调用NSURLAuthenticationChallenge里的sender发送NSURLAuthenTicationChallengeSender定义了身份鉴别用的sender必须实现的协议,不同的方法提供不同的响应方式。注意,在调用其中一种响应方式后,之后再调用无效,同时,willSendRequestForAuthenticationChallenge
里必须调用一种响应挑战的方式。协议里定义的方法如下:
// 取消挑战。如果证书验证不通过,调用该方法终止本次认证。
- (void)cancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
// 响应挑战。如果是服务器和客户端的单向验证,服务器不需要验证客户端的证书,因此调用该方法即可。
-(void)continueWithoutCredentialForAuthenticationChallenge:(NSURLAuthenticationChallenge *) challenge
// 用凭证响应挑战。如果是双向验证,不仅客户端要验证服务器身份,服务器也需要客户端提供证书,因此需要提供凭证
-(void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
0x03 总结:
由于公司云端使用的HTTPS,与之通信时要使用HTTPS协议,于是开始研究相关的内容,发现网上相关的资料太少,自己动手做了很多实验,查阅了很多文档终于把问题梳理清楚,还有很多疏漏或者错误的地方,欢迎大家指正。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/189184.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...