验证挑战和TLS链验证
NSURLRequest对象经常遇到身份验证挑战,或者从其连接的服务器请求凭证。 NSURLSession类在请求遇到身份验证挑战时通知其代理,以便它们可以相应地执行。
重要提示:URL加载系统类不会调用其代理来处理请求挑战,除非服务器响应包含WWW-Authenticate标头。 其他身份验证类型(如代理身份验证和TLS信任验证)不需要此标头。
决定如何应对认证挑战重点内容
如果会话任务需要身份验证,并且没有有效的凭据可用
,作为请求的URL或共享NSURLCredentialStorage的一部分,它会创建身份验证挑战。
它首先发送URLSession:task:didReceiveChallenge:completionHandler:向其任务委托处理身份验证挑战。
如果任务委托不对该消息进行响应,则任务将URLSession:task:didReceiveChallenge:completionHandler发送到其会话委托以处理身份验证挑战。
为了继续连接,代表有三个选项:
提供认证凭证。
尝试继续没有凭据。
取消验证挑战。
为了帮助确定正确的操作过程,传递给该方法的NSURLAuthenticationChallenge实例包含有关触发身份验证挑战的信息,对挑战进行了多少次尝试,任何先前尝试的凭据,需要凭证的NSURLProtectionSpace和发送者挑战。
如果身份验证挑战尝试先前进行身份验证并失败(例如,如果用户在服务器上更改了密码),则可以通过在身份验证挑战中调用proposalCredential来获取尝试的凭据。然后,代理可以使用这些凭据来填充向用户呈现的对话框。
在验证挑战上调用previousFailureCount会返回之前身份验证尝试的总数,包括来自不同身份验证协议的身份验证尝试的总数。委托人可以向用户提供此信息,以确定先前提供的凭据是否发生故障,或限制最大认证尝试次数。
响应认证挑战
以下是对URLSession的三种方法:didReceiveChallenge:completionHandler:或URLSession:task:didReceiveChallenge:completionHandler:delegate方法。
提供凭据
要尝试进行身份验证,应用程序应创建一个具有服务器预期形式的身份验证信息的NSURLCredential对象。您可以通过在提供的身份验证挑战的保护空间上调用authenticationMethod来确定服务器的身份验证方法。 NSURLCredential支持的一些认证方法有:
HTTP基本认证(NSURLAuthenticationMethodHTTPBasic)需要用户名和密码。提示用户获取必要的信息,并使用credentialWithUser创建一个NSURLCredential对象:password:persistence :.
HTTP摘要认证(NSURLAuthenticationMethodHTTPDigest),如基本认证,需要用户名和密码。 (摘要自动生成。)提示用户获取必要的信息,并使用credentialWithUser创建一个NSURLCredential对象:password:persistence :.
客户端证书身份验证(NSURLAuthenticationMethodClientCertificate)需要系统身份和所需的证书才能与服务器进行身份验证。使用credentialWithIdentity创建一个NSURLCredential对象:certificates:persistence :.
服务器信任身份验证(NSURLAuthenticationMethodServerTrust)需要由身份验证挑战的保护空间提供的信任。使用credentialForTrust创建一个NSURLCredential对象。
创建NSURLCredential对象后,使用提供的完成处理程序块将对象传递给身份验证挑战的发件人。
继续无凭据
如果代理人选择不提供身份验证挑战的凭证,则可以尝试继续执行身份验证。 将以下值之一传递给提供的完成处理程序块:
NSURLSessionAuthChallengePerformDefaultHandling处理请求,就像代理没有提供委托方法来处理挑战一样。
NSURLSessionAuthChallengeRejectProtectionSpace拒绝了挑战。 根据服务器响应允许的身份验证类型,URL加载类可能会多次调用此委托方法,以获得更多的保护空间。
取消连接
通过将NSURLSessionAuthChallengeCancelAuthenticationChallenge传递给提供的完成处理程序块,委托也可以选择取消身份验证挑战。
验证示例
清单4-1所示的实现通过使用应用程序的首选项提供的用户名和密码创建一个NSURLCredential实例来应对挑战。 如果以前认证失败,则取消认证挑战并通知用户。
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
if ([challenge previousFailureCount] == 0) {
NSURLCredential *newCredential = [NSURLCredential credentialWithUser:[self preferencesName] password:[self preferencesPassword] persistence:NSURLCredentialPersistenceNone]; completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential);
} else {
// Inform the user that the user name and password are incorrect completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
如果认证挑战未被会话或任务委托处理,并且凭据不可用或者如果它们无法进行身份验证,则继承WidhoutCredentialForAuthenticationChallenge:消息由底层实现发送。
执行自定义TLS链验证
在NSURL系列API中,TLS链验证由您的应用程序的身份认证委托方法处理,而不是提供凭据来将用户(或应用程序)验证到服务器,而应用程序会检查服务器在TLS期间提供的凭据握手,然后告诉URL加载系统是否应该接受或拒绝这些凭据。
如果您需要以非标准方式执行链式验证(例如接受特定的自签名证书进行测试),则应用程序必须实现URLSession:didReceiveChallenge:completionHandler:或URLSession:task:didReceiveChallenge:completionHandler:delegate方法。如果实现两者,则会话级方法负责处理身份验证。
在您的身份验证处理委托方法中,您应该检查挑战保护空间是否具有NSURLAuthenticationMethodServerTrust的身份验证类型,如果是,请从该保护空间获取serverTrust信息。
自定义认证机构
如果您的服务器的证书是默认由系统信任的证书颁发机构颁发的,则可以通过将证书颁发机构的根证书包含在程序中来解决产生的服务器信任评估失败。程序如下:
在您的程序中包含证书颁发机构的根证书的副本
一旦拥有信任对象,从证书数据(SecCertificateCreateWithData)创建证书对象,然后将该证书设置为信任对象(SecTrustSetAnchorCertificates)的可信锚点
SecTrustSetAnchorCertificates设置防止信任对象信任任何其他锚点的标志;如果您还想信任默认系统锚点,请调用SecTrustSetAnchorCertificatesOnly来清除该标志
根据惯例评估信任对象
自签名证书
处理自签名证书是有问题的,因为人们使用自签名证书有多种不同的原因。本节涵盖最常见的情况。
发展
在开发期间,自签名证书使得更容易设置基于TLS的服务器进行测试。这是完全合理使用自签名证书,完全合理的借口完全禁用TLS服务器信任评估。
警告:在将产品发送给用户之前,请确保重新启用服务器信任评估。否则会将您的用户暴露在主动攻击者身上。开发人员在生产应用程序中禁用了服务器信任评估,已经有一些值得注意和尴尬的例子,并且您不希望使用该特定画笔进行打印。
在开发过程中,自签名证书是一种合理的方法,但是有一个更好的方法:创建自己的证书颁发机构(见下文),并为您的测试服务器颁发证书。然后,您可以a)将您的证书颁发机构的根证书连接到您的应用程序(如在自定义证书颁发机构中所述),或b)让每个测试人员通过标准系统用户界面(Safari,邮件)安装您的证书颁发机构的根证书和iOS上的配置配置文件,OS X上的钥匙串访问)。这种方法的优点是,您不需要在开发过程中禁用服务器信任评估,这意味着您在运输生产构建时不能忘记重新启用它。
注意:如果您需要创建自己的证书颁发机构,可以通过以下几种方法来实现:
证书助理 - 此应用程序内置于OS X,并支持相当不错的用户界面来管理您自己的证书颁发机构;技术说明TN2326“创建TLS测试证书”详细说明了如何使用它。
OpenSSL - OpenSSL命令行实用程序允许您创建和管理您自己的证书颁发机构。这是相当复杂的(有关详细信息,请参阅手册页),但互联网上有很多教程
正确覆盖TLS链验证
本文介绍如何覆盖使用传输层安全性(TLS)保护的网络连接的链验证行为。
重要提示:本文假设您已经阅读了安全性概述中的加密服务,或者以其他方式熟悉证书,签名和信任链的概念。
大多数生产软件不应该覆盖连锁验证。但是,在开发过程中这样做是很常见的。通过使用这里描述的技术来安全地覆盖链条验证,如果您无意中发送软件版本而不禁用该调试代码,则您的用户将不会被保护。
当TLS证书被验证时,操作系统验证其信任链。如果该信任链只包含有效的证书并以已知的(受信任的)锚证书结束,则证书被认为是有效的。如果没有,它被认为是无效的。如果您使用主要供应商的商业签名证书,证书应“正常工作”。
但是,如果您做的事情超出了用户规范创建的客户端证书,则为使用不自信的单个证书为多个域提供服务,使用自签名证书,通过IP连接到主机地址(网络堆栈无法确定服务器的主机名),等等 - 您必须采取其他步骤来说服操作系统接受证书。
在高级别,TLS链验证由信任对象(SecTrustRef)执行。此对象包含一些控制执行哪些类型的验证的标志。一般来说,你不应该触摸这些旗帜,但你应该意识到它们的存在。另外,信任对象包含一个策略(SecPolicyRef),它允许您提供评估TLS证书时应使用的主机名。最后,信任对象包含应用程序可以修改的可信锚证书列表。
本文分为多个部分。操作信任对象的第一部分描述了操纵信任对象来改变验证行为的常见方法。剩余部分,信任对象和NSURLConnection和信任对象和NSStream显示了如何将这些更改与各种网络技术集成在一起。
操纵信任对象
操纵信任对象的细节在很大程度上取决于你要覆盖的内容。 要覆盖的两个最常见的事情是主机名(必须匹配叶子证书的公用名称或其主题替代名称扩展名中的一个名称)和一组锚(确定一组受信任的证书颁发机构)。
要将证书添加到受信任的锚证书列表中,您必须将现有的锚证书复制到数组中,创建该数组的可变版本,将新的锚证书添加到可变数组,并告知信任对象使用该新的 更新数组以供将来评估信任。 清单1中列出了一个简单的功能。
清单1将锚点添加到SecTrustRef对象
SecTrustRef addAnchorToTrust(SecTrustRef trust, SecCertificateRef trustedCert)
{
#ifdef PRE_10_6_COMPAT
CFArrayRef oldAnchorArray = NULL;
/* In OS X prior to 10.6, copy the built-in
anchors into a new array. */
if (SecTrustCopyAnchorCertificates(&oldAnchorArray) != errSecSuccess) {
/* Something went wrong. */
return NULL;
}
CFMutableArrayRef newAnchorArray = CFArrayCreateMutableCopy(
kCFAllocatorDefault, 0, oldAnchorArray);
CFRelease(oldAnchorArray);
#else
/* In iOS and OS X v10.6 and later, just create an empty
array. */
CFMutableArrayRef newAnchorArray = CFArrayCreateMutable (
kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
#endif
CFArrayAppendValue(newAnchorArray, trustedCert);
SecTrustSetAnchorCertificates(trust, newAnchorArray);
#ifndef PRE_10_6_COMPAT
/* In iOS or OS X v10.6 and later, reenable the
built-in anchors after adding your own.
*/
SecTrustSetAnchorCertificatesOnly(trust, false);
#endif
return trust;
注意:要构建TrustedCert的SecCertificateRef对象,首先将DER编码的证书加载到CFData对象中,然后调用SecCertificateCreateWithData。
为了更多的灵活性,包括在其他编码中加载证书,还可以在OS X v10.7及更高版本中使用SecItemImport函数。
要覆盖主机名(允许一个特定站点的证书适用于另一个特定站点,或允许证书在通过其IP地址连接到主机时工作),则必须替换信任策略使用的策略对象 以确定如何解释证书。 为此,首先为所需的主机名创建一个新的TLS策略对象。 然后创建一个包含该策略的数组。 最后,告诉信任对象使用该数组来进行信任的评估。 清单2显示了一个执行此操作的函数。
清单2更改SecTrustRef对象的远程主机名
SecTrustRef changeHostForTrust(SecTrustRef trust)
{
CFMutableArrayRef newTrustPolicies = CFArrayCreateMutable(
kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
SecPolicyRef sslPolicy = SecPolicyCreateSSL(true, CFSTR("www.example.com"));
CFArrayAppendValue(newTrustPolicies, sslPolicy);
#ifdef MAC_BACKWARDS_COMPATIBILITY
/* This technique works in OS X (v10.5 and later) */
SecTrustSetPolicies(trust, newTrustPolicies);
CFRelease(oldTrustPolicies);
return trust;
#else
/* This technique works in iOS 2 and later, or
OS X v10.7 and later */
CFMutableArrayRef certificates = CFArrayCreateMutable(
kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
/* Copy the certificates from the original trust object */
CFIndex count = SecTrustGetCertificateCount(trust);
CFIndex i=0;
for (i = 0; i < count; i++) {
SecCertificateRef item = SecTrustGetCertificateAtIndex(trust, i);
CFArrayAppendValue(certificates, item);
}
/* Create a new trust object */
SecTrustRef newtrust = NULL;
if (SecTrustCreateWithCertificates(certificates, newTrustPolicies, &newtrust) != errSecSuccess) {
/* Probably a good spot to log something. */
return NULL;
}
return newtrust;
#endif
}