上篇讲到自签名证书在APP内通不过验证,这篇文章要学习下如何如何正确覆盖TLS链验证。
证书信任评估的步骤
首先建立信任的第一步是检查数字签名。颁发证书的CA使用自己的私钥给叶证书的摘要签名,并将该字节流嵌入到叶证书中。只要CA的证书有效,则叶证书一定有效。CA的证书又由另外的CA签发,这样形成了一个证书链。证书链以一个锚证书结束,该锚证书通常是嵌入在操作系统中的固有受信任的根证书之一。
然后系统根据第一步说的检查数字签名的方式从当前证书开始回溯整个证书链,因为签名链不间断地回到受信任的根证书,可以根据称为信任策略的一组规则对其进行评估。
通常情况下只有根证书是存在于系统维护的根证书列表时,验证才被通过。
以上内容都在第一篇笔记[https]中有详细记载
系统维护的证书列表
各大CA机构的锚证书会作为固有受信任的根证书嵌入到操作系统中,这样系统就维护了一个证书列表,因此CA机构签发的证书验证我们不需要太多操作就能验证通过。
自签名的证书要想验证应该怎么做呢?
Another common problem arises from using a custom, self-signed root certificate as an anchor. By default, the system trusts as an anchor only the root certificates packaged with the operating system (see the lists for iOSand macOS), but you can supplement this list. If you bundle a self-signed certificate with your app, you can indicate to the system that you trust it for use as an anchor by making a call to the SecTrustSetAnchorCertificates(::)
function
通过调用SecTrustSetAnchorCertificates(::)方法给SecTrustRef对象传递一个锚证书列表告诉系统除了系统维护的证书列表我还信任这个列表中的证书,当验证的时候如果证书链的锚证书存在这个列表中,验证也能通过。
SecTrustRef
◇ 是什么
证书信任链的验证工作依赖于一个SecTrustRef对象,该对象包含一些控制执行哪些类型的验证的标志。一般来说我们不用接触这些标志,但是应该知道这些标志的存在。另外trust对象还包含一个policy(SecPolicyRef对象)在修改验证TLS证书修改默认域名验证策略时用的到(不是本篇重点,先不讲)。
◇ 怎么验证
证书的验证过程都已经有系统框架封装好了,开发者只需要调用SecTrustEvaluateAsync(::_:)或者SecTrustEvaluate(::)方法传递SecTrustRef对象。然后等待回调参数判断验证是否通过。
验证使用SecTrustEvaluateAsync(::_:)或者SecTrustEvaluate(::)方法,判断回调返回值,如果结果是kSecTrustResultProceed或者 kSecTrustResultUnspecified则验证通过。
◇ 如何得到
上篇文章说服务器会返回一个包含公钥的受保护空间(challenge.protectionSpace)里边包含了公钥等信息,个人认为说成服务端返回证书(证书里包含公钥信息),苹果框架再次抽取封装成NSURLProtectionSpace(challenge.protectionSpace)对象更合适。
最终一次HTTPS认证质询需要用到的信息被封装成NSURLAuthenticationChallenge对象,通过URLSession:task:didReceiveChallenge:completionHandler:第三个参数challenge传递给我们,我们通过challenge.protectionSpace.serverTrust获取本次认证质询所用到的SecTrustRef对象,而不必再自己去创建
如图challenge.protectionSpace对象包含一个SecTrustRef属性
◇ 优点
- 首先证书的验证过程都封装好了,开发者只需要调用方法SecTrustEvaluateAsync(::_:)传递SecTrustRef对象,而不必先了解HTTPS底层实现、协议机制(具体验证哪些因素,怎么验证都被封装在方法内部了)
- SecTrustRef对象出了包含证书还包含了其他必要信息,URLSession:task:didReceiveChallenge:completionHandler:参数里包含该对象而不是证书的好处,拿到证书开发者还需要自己通过SecTrustCreateWithCertificates(::_:)传递证书再创建SecTrustRef对象,这些都是c方法,封装好后一般开发者就不用解除了,降低开发学习成本,减少犯错的可能性。
操纵SecTrustRef对象
自签名证书需要将证书打包进bundle里,然后在需要的时候建立一个数组存放bundle里证书,调用SecTrustSetAnchorCertificates(::)方法传递这个数组和SecTrustRef对象,告诉系统当前验证除了系统维护的证书列表我还信任这个列表中的证书
代码:
SecTrustRef addAnchorToTrust(SecTrustRef trust, SecCertificateRef trustedCert)
{
// 创建一个新的空array
CFMutableArrayRef newAnchorArray = CFArrayCreateMutable (kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
// 将证书添加到array
CFArrayAppendValue(newAnchorArray, trustedCert);
// 告诉系统当前验证除了系统维护的证书列表我还信任这个列表中的证书
SecTrustSetAnchorCertificates(trust, newAnchorArray);
return trust;
}
操纵信任对象在https的具体应用
- 将自签名证书拖到xcode项目中:
- 获取打包进bundle中的证书:
SecCertificateRef getTrustedCert(){
// 获取bundle指针
NSBundle *bundle = [NSBundle mainBundle];
// 获取本地内置证书路径
NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];
// 获取内置证书data(因为本地只内置一个证书,所以直接用下标0从路径中获取,正式项目中不要这么用)
NSData *certificateData = [NSData dataWithContentsOfFile:paths[0]];
// data转换成SecCertificateRef
SecCertificateRef trustedCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData);
return trustedCert;
}
- 告诉系统当前验证除了系统维护的证书列表我还信任这个列表中的证书
SecTrustRef addAnchorToTrust(SecTrustRef trust, SecCertificateRef trustedCert)
{
// 创建一个新的空array,接下来将用作锚点证书集合
CFMutableArrayRef newAnchorArray = CFArrayCreateMutable (kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
// 将证书添加到array
CFArrayAppendValue(newAnchorArray, trustedCert);
// 设置trust对象内部指针指向我们自己创建的锚点证书集合,接下来的验证会使用这个锚点集合而不是使用系统内置的
SecTrustSetAnchorCertificates(trust, newAnchorArray);
return trust;
}
- 验证:
- (BOOL)trustEvaluate:(SecTrustRef)trust {
SecTrustResultType secresult = kSecTrustResultInvalid;
if (SecTrustEvaluate(trust, &secresult) != errSecSuccess) {
NSLog(@"NO:%d",secresult);
return NO;
}
switch (secresult) {
case kSecTrustResultUnspecified: // The OS trusts this certificate implicitly.
case kSecTrustResultProceed: // The user explicitly told the OS to trust it.
{
NSLog(@"YES");
return YES;
}
default:
NSLog(@"NO:%d",secresult);
return NO;
}
}
验证使用[SecTrustEvaluateAsync(_:_:_:)](https://developer.apple.com/documentation/security/1400632-sectrustevaluateasync)或者[SecTrustEvaluate(_:_:)](https://developer.apple.com/documentation/security/1394363-sectrustevaluate)
方法,在评估回调中检查结果,如果结果是kSecTrustResultProceed或者 kSecTrustResultUnspecified则验证通过。
所有代码:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
BLog();
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecCertificateRef trustedCert = getTrustedCert();
SecTrustRef serverTrust = addAnchorToTrust(challenge.protectionSpace.serverTrust, trustedCert);
if ([self trustEvaluate:serverTrust]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
return;
}
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
SecCertificateRef getTrustedCert(){
// 获取bundle指针
NSBundle *bundle = [NSBundle mainBundle];
// 获取本地内置证书路径
NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];
// 获取内置证书data(因为本地只内置一个证书,所以直接用下标0从路径中获取,正式项目中不要这么用)
NSData *certificateData = [NSData dataWithContentsOfFile:paths[0]];
// data转换成SecCertificateRef
SecCertificateRef trustedCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData);
return trustedCert;
}
SecTrustRef addAnchorToTrust(SecTrustRef trust, SecCertificateRef trustedCert)
{
// 创建一个新的空array,接下来将用作锚点证书集合
CFMutableArrayRef newAnchorArray = CFArrayCreateMutable (kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
// 将证书添加到array
CFArrayAppendValue(newAnchorArray, trustedCert);
// 设置trust对象内部指针指向我们自己创建的锚点证书集合,接下来的验证会使用这个锚点集合而不是使用系统内置的
SecTrustSetAnchorCertificates(trust, newAnchorArray);
return trust;
}
- (BOOL)trustEvaluate:(SecTrustRef)trust {
SecTrustResultType secresult = kSecTrustResultInvalid;
if (SecTrustEvaluate(trust, &secresult) != errSecSuccess) {
NSLog(@"NO:%d",secresult);
return NO;
}
switch (secresult) {
case kSecTrustResultUnspecified:
case kSecTrustResultProceed:
{
NSLog(@"YES");
return YES;
}
default:
NSLog(@"NO:%d",secresult);
return NO;
}
}
其他类型
除了更改锚点证书,还有一些其他类型的执行自定义TLS链验证
覆盖主机名,允许一个特定站点的证书适用于另一个特定站点,或允许证书在通过IP地址连接主机时也能正常工作等情况下必须执行自定义TLS链验证,替换SecTrustRef对象中的SecPolicyRef对象。详见:Overriding TLS Chain Validation Correctly
只验证公钥。证书的目的是为了验证服务端的真实性从而保证客户端能拿到正确主机的正确公钥。而我们已经在客户端内置了一份证书,这样对比两份证书中的公钥是否一样就能确定当前证书是否可信。AFNetworking有这么一个选项(AFSSLPinningModePublicKey)但是我在文档中没找到依据。所以这里只是提一下。
终于iOS上关于https的东西疑惑解的差不多了,现在AFNetworking中的AFSecurityPolicy类能看懂了且明白为什么了吧!
demo
◇ demo地址:
◇ demo教程
1 下载demo里两个文件夹,server和client,server是https服务端代码,client是iOS代码
2 在终端下cd到server文件夹下,执行
python https.py
https服务端就跑起来了
3 运行iOS代码就可以了