下面看一个普通的HTTPS请求过程:
https.png
客户端发起请求,以明文传输请求信息,包含版本信息,加密套件候选列表,压缩算法候选列表,随机数random_C,扩展字段等信息。
iOS Request.jpeg
如上图可以看出这个阶段包含4个过程( 有的服务器是单条发送,有的是合并一起发送)。服务端返回协商的信息结果,包括选择使用的协议版本,选择的加密套件,选择的压缩算法、随机数random_S等,其中随机数用于后续的密钥协商。服务器也会配置并返回对应的证书链Certificate,用于身份验证与密钥交换。然后会发送ServerHelloDone信息用于通知服务器信息发送结束。
在上图中的5-6之间,客户端这边还需要对服务器返回的证书进行校验。只有证书验证通过后,才能进行后续的通信。(具体分析可参看后续的证书验证过程)
服务器返回的证书验证合法后, 客户端计算产生随机数字Pre-master,并用server证书中公钥加密,发送给服务器。同时客户端会根据已有的三个随机数根据相应的生成协商密钥。客户端会通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信。然后客户端发送Finished消息用于通知客户端信息发送结束。
服务器也会根据已有的三个随机数使用相应的算法生成协商密钥,会通知客户端后续的通信都采用协商的通信密钥和加密算法进行加密通信。然后发送Finished消息用于通知服务器信息发送结束
在握手阶段结束后,客户端和服务器数据传输开始使用协商密钥进行加密通信。
简单来说,HTTPS请求整个过程主要分为两部分。一是握手过程:用于客户端和服务器验证双方身份,协商后续数据传输时使用到的密钥等。二是数据传输过程:身份验证通过并协商好密钥后,通信双方使用协商好的密钥加密数据并进行通信。在握手过程协商密钥时,使用的是非对称密钥交换算法, 密钥交换算法本身非常复杂,密钥交换过程涉及到随机数生成,模指数运算,空白补齐,加密,签名等操作。在数据传输过程中,客户端和服务器端使用协商好的密钥进行对称加密解密。
PKI (Public Key Infrastructure),公开密钥基础设施。它是一个标准,在这个标准之下发展出的为了实现安全基础服务目的的技术统称为PKI。 权威的第三方机构CA(认证中心)是PKI的核心, CA负责核实公钥的拥有者的信息,并颁发认证”证书”,同时能够为使用者提供证书验证服务。 x.509是PKI中最重要的标准,它定义了公钥证书的基本结构。
证书申请者向颁发证书的可信第三方CA提交申请证书相关信息,包括:申请者域名、申请者生成的公钥(私钥自己保存)及证书请求文件.cer等 CA通过线上、线下等多种手段验证证书申请者提供的信息合法和真实性。 当证书申请者提供的信息审核通过后,CA向证书申请者颁发证书,证书内容包括明文信息和签名信息。其中明文信息包括证书颁发机构、证书有效期、域名、申请者相关信息及申请者公钥等,签名信息是使用CA私钥进行加密的明文信息。当证书申请者获取到证书后,可以通过安装的CA证书中的公钥对签名信息进行解密并与明文信息进行对比来验证签名的完整性。
在OC中当使用NSURLConnection或NSURLSession建立URL并向服务器发送https请求获取资源时,服务器会使用HTTP状态码401进行响应(即访问拒绝)。此时NSURLConnection或NSURLSession会接收到服务器需要授权的响应,当客户端授权通过后,才能继续从服务器获取数据。如下图所示:
在接收到服务器返回的状态码为401的响应后,对于NSURLSession而言,需要代理对象实现URLSession:task:didReceiveChallenge:completionHandler:方法。对于NSURLConnection而言,需要代理对象实现connection:willSendRequestForAuthenticationChallenge: 方法(OS X v10.7和iOS5及以上),对于早期的版本代理对象需要实现代理对象要实现connection:canAuthenticateAgainstProtectionSpace:和connection:didReceiveAuthenticationChallenge:方法。代码如下:
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//2)使用系统SecTrustEvaluate对trust object验证
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess && (status == kSecTrustResultProceed || status == kSecTrustResultUnspecified)) {
//3)验证成功继续请求操作
[challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge];
} else {
//4)不成功则马上取消请求
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}
当客户端发送https请求后,服务器会返回需要授权的相关信息,然后connection:willSendRequestForAuthenticationChallenge:方法被调用。客户端根据返回的challenge信息,首先获取需要验证的信任对象trust,然后调用SecTrustEvaluate方法是用系统默认的验证方式对信任对象进行验证,当验证通过时,使用该信任对象trust生成证书凭证,然后self.connection使用该凭证继续连接。如下详解:
表示需要验证的信任对象(Trust Object),在此指的是challenge.protectionSpace.serverTrust。包含待验证的证书和支持的验证方法等。
函数内部递归地从叶节点证书到根证书验证。使用系统默认的验证方式验证Trust Object,根据上述证书链的验证可知,系统会根据Trust Object的验证策略,一级一级往上,验证证书链上每一级证书有效性。
表示验证结果。其中 kSecTrustResultProceed表示serverTrust验证成功,且该验证得到了用户认可(例如在弹出的是否信任的alert框中选择always trust)。 kSecTrustResultUnspecified表示 serverTrust验证成功,此证书也被暗中信任了,但是用户并没有显示地决定信任该证书。 两者取其一就可以认为对serverTrust验证成功。
表示身份验证证书。URL Lodaing支持3种类型证书:password-based user credentials, certificate-based user credentials, 和certificate-based server credentials(需要验证服务器身份时使用)。因此NSURLCredential可以表示由用户名/密码组合、客户端证书及服务器信任创建的认证信息,适合大部分的认证请求。对于NSURLCredential也存在三种持久化机制:
对于已经验证通过的信任对象,客户端也可以不提供证书凭证。
NSURLSessionAuthChallengePerformDefaultHandling处理请求,就好像代理没有提供一个代理方法来处理认证请求 NSURLSessionAuthChallengeRejectProtectionSpace拒接认证请求。基于服务器响应的认证类型,URL加载类可能会多次调用代理方法。
//3)验证成功继续请求操作
//[challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge];
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
即使服务器返回的证书是信任的CA颁发的,而为了确定返回的证书正是客户端需要的证书,这需要本地导入证书,并将证书设置成需要参与验证的锚点证书,再调用SecTrustEvaluate通过本地导入的证书来验证服务器证书是否是可信的。如果服务器证书是这个锚点证书对应CA或者子CA颁发的,或服务器证书本身就是这个锚点证书,则证书信任通过。如下代码(参考文档):
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"xxx" ofType:@"cer"];
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData);
self.trustedCertificates = @[CFBridgingRelease(certificate)];
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//将上面导入Xcode项目中的证书设置成下面验证trust object的锚点证书
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);
//2)使用系统SecTrustEvaluate对trust object验证
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess && (status == kSecTrustResultProceed || status == kSecTrustResultUnspecified)) {
//3)验证成功继续请求操作
[challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge];
// [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
} else {
//4)不成功则马上取消请求
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}
对于自签名证书,这样Trust Object中的服务器证书是不可信任的CA颁发的,直接使用SecTrustEvaluate验证是不会成功的。可以采取下述简单代码绕过HTTPS的验证:
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
//判断如果是要验证证书的话 直接使用它期望的证书
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
} else {
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}
上述代码一般用于当服务器使用自签名证书时,为了方便测试,客户端可以通过该方法信任所有自签名证书。