iOS - 程序化信任CA(证书颁发机构)

4
我正在尝试使用一个应用程序通过插座/流以安全地连接到TLS服务器,以进行通用数据交换。为此,我使用mac keychain工具设置了自己的CA,并将证书包含在代码包中。
现在,我的应用程序应该信任由该CA签发的任何服务器证书。(我不关心其他应用程序如何处理这些证书,我假设它们不会信任它,因为它们在沙箱中。)
我在网上找到了几个类似的问题,但似乎出了点问题。
如果我将CA证书拖放到模拟器中并手动接受信任,则与服务器的连接似乎可以正常工作。
但是,当我尝试以编程方式建立对CA证书的信任时,尽管下面的代码没有生成错误,但稍后连接到服务器的尝试被拒绝。
因此,我必须弄错了证书实现部分...有什么想法吗?
先感谢您!
NSString* certPath = [[NSBundle mainBundle] pathForResource:@"MyTestCA2" ofType:@"cer"]; //cer = CA certificate
NSData* certData = [NSData dataWithContentsOfFile:certPath];
SecCertificateRef cert;
if( [certData length] ) {
    cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
    if( cert != NULL ) {
        CFStringRef certSummary = SecCertificateCopySubjectSummary(cert);
        NSString* summaryString = [[NSString alloc] initWithString:(__bridge NSString*)certSummary];
        NSLog(@"CERT SUMMARY: %@", summaryString);
        certSummary = nil;
    } else {
        NSLog(@" *** ERROR *** trying to create the SSL certificate from data located at %@, but failed", certPath);
    }
}

OSStatus err = noErr;
CFTypeRef result;
NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
                      (__bridge id)kSecClassCertificate, kSecClass,
                      cert, kSecValueRef,
                      nil];
err = SecItemAdd((__bridge CFDictionaryRef)dict, &result);
if(err!=noErr) NSLog(@"error while importing");
if (err==errSecDuplicateItem) NSLog(@"Cert already installed");
NSLog(@":%i",(int)err);
assert(err==noErr||err==errSecDuplicateItem);   // accept no errors other than duplicate
err = noErr;
SecTrustRef trust;
err = SecTrustCreateWithCertificates(cert, SecPolicyCreateBasicX509() ,&trust);
assert(err==noErr);
err = noErr;
CFMutableArrayRef newAnchorArray = CFArrayCreateMutable(kCFAllocatorDefault,0,&kCFTypeArrayCallBacks);
CFArrayAppendValue(newAnchorArray,cert);
err = SecTrustSetAnchorCertificates(trust, newAnchorArray);
assert(err==noErr);
SecTrustResultType trustResult;
err=SecTrustEvaluate(trust,&trustResult);
assert(err==noErr);
cert=nil;

跟进:尽管使用 kSecResultUnspecified,但连接仍然失败并显示“CFNetwork SSLHandshake failed (-9807)”,但是当证书已被拖放到模拟器中并由用户接受时,就不会出现此类错误(即使这样,我仍然只能获得 kSecResultUnspecified);如果我解决了问题,我会进行更新的。 - McMini
2个回答

3

我没有尝试运行部分代码,但我有一些代码(如下所示)我知道是有效的。我使用它来信任我的内部CA。

err=SecTrustEvaluate(trust,&trustResult);
assert(err==noErr);
trustResult是您感兴趣的内容,而不是SecTrustEvaluate返回的errerr告诉您API调用是否成功/失败;它并不告诉您信任评估的结果。
我认为这里有两种策略。第一种是在trustResult中查找值为kSecTrustResultProceedkSecTrustResultUnspecified的“成功”。因为它不是“提示”,也不是“尝试恢复”,更不是“失败”。
第二种策略是在trustResult中查找值不为kSecTrustResultDenykSecTrustResultFatalTrustFailurekSecTrustResultOtherError的“不失败”。也就是说,只要trustResult不是这些值之一,那么就可以继续作为成功。忽略提示用户信任证书,因为他们不会理解提示并“点击通过”。
以下是我在NSURLConnection委托的-didReceiveAuthenticationChallenge:中使用的代码。它期望一个ASN.1/DER编码的证书(名为ca-cert.der)。它使用上面描述的第一种策略。如果您在#ifdef 0中使用该代码,则使用第二种策略。
我认为苹果的正确覆盖TLS链验证,苹果的技术说明TN2232,HTTPS服务器信任评估和苹果的技术问答QA1360,描述kSecTrustResultUnspecified错误可能对您有用。
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:
(NSURLAuthenticationChallenge *)challenge
{   
    SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
    return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]
                  forAuthenticationChallenge: challenge];

    if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust])
    {
        do
        {
            SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
            NSCAssert(serverTrust != nil, @"serverTrust is nil");
            if(nil == serverTrust)
                break; /* failed */

            NSData* caCert = [NSData dataWithContentsOfFile:@"ca-cert.der"];
            NSCAssert(caCert != nil, @"caCert is nil");
            if(nil == caCert)
                break; /* failed */

            SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
            NSCAssert(caRef != nil, @"caRef is nil");
            if(nil == caRef)
                break; /* failed */

            NSArray* caArray = [NSArray arrayWithObject:(__bridge id)(caRef)];
            NSCAssert(caArray != nil, @"caArray is nil");
            if(nil == caArray)
                break; /* failed */

            OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
            NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
            if(!(errSecSuccess == status))
                break; /* failed */

            SecTrustResultType result = -1;
            status = SecTrustEvaluate(serverTrust, &result);
            if(!(errSecSuccess == status))
                break; /* failed */

            NSLog(@"Result: %d", result);

            /* https://developer.apple.com/library/ios/technotes/tn2232/_index.html */
            /* https://developer.apple.com/library/mac/qa/qa1360/_index.html */
            /* kSecTrustResultUnspecified and kSecTrustResultProceed are success */
            if(result != kSecTrustResultUnspecified && result != kSecTrustResultProceed)
                break; /* failed */

#if 0
            /* Treat kSecTrustResultConfirm and kSecTrustResultRecoverableTrustFailure as success */
            /*   since the user will likely tap-through to see the dancing bunnies */
            if(result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure || result == kSecTrustResultOtherError)
                break; /* failed to trust cert (good in this case) */
#endif

            // The only good exit point
            return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]
                          forAuthenticationChallenge: challenge];

        } while(0);
    }

    // Bad dog
    return [[challenge sender] cancelAuthenticationChallenge: challenge];
}

我得到了kSecResultUnspecified。由于系统似乎对此结果不满意,我需要完全禁用自动TLS验证,而是手动检查SecTrustEvaluate,然后如果我得到kSecResultUnspecified,再继续吗?由于我需要在服务器和客户端之间进行灵活的通信,所以我认为我需要使用NS/CF Streams而不是NSURLConnections。除了stream:handleEvent:之外,我不知道是否有NS/CF-streams的委托,有吗?有没有任何方法可以在默认情况下不受信任并且不要求用户接受我的根CA的情况下实现kSecResultProceed? - McMini
@MacMini - 请参阅苹果公司的技术问答QA1360,描述kSecTrustResultUnspecified错误。或者查看上面源代码中的注释。当返回kSecResultUnspecified时,系统并不会出现问题。 - jww
谢谢您的快速和详细回答,这段代码肯定会很有价值!在您回复我的评论中,您说:系统可以使用'kSecTrustResultUnspec.'。您还表示您的代码正在工作(程序启用自签名根CA而不禁用自动证书验证)。尽管我已经尝试了所有努力,但我无法使用我的NS/CFStreams复制它,并最终找到了一些讨论非常相似主题的帖子;但现在我很难整合不同的陈述,再次非常感谢您的见解。(->我的部分回答) - McMini

1

尽管我已经尝试了NS/CF-Streams,但无法像jww明显通过NSURLConnection那样复制。 (与以前在模拟器上拖放CA-cert并接受其信任时一样,TLS流可以正常工作,但以编程方式执行不成功。)

因此,我对这个主题进行了更多的搜索,并实际上发现了aeternusrahl发布的其他帖子,得到了Rob Napier的回答: Post

...其中引用了Heath Borders的以下博客文章: Blog

对于所有遇到类似问题的人,我强烈推荐这些链接-它们提供了很好的洞察力!

不需要复制这些链接的所有内容:Rob Napier说:“...就我所知,在那个钥匙链中,只有一个受信任的锚定列表,由所有人共享。这对于socket.io并没有太大帮助,因为它不能让你访问其NSURLConnection代理方法。你必须修改socket.io以接受一个信任锚点。”

由于以上所有作者都有杰出的声誉,而我在iOS编程方面相当新手,所以我不敢反驳任何人!但由于我很难整合这些帖子,如果我无意中跳过了帖子中的前提条件,我非常希望得到任何澄清。

对我来说,jww似乎认为在使用自己的根证书时禁用自动TLS验证并不必要(请参见他对第一篇文章的评论的回答)。然后,Heath Borders/Rob Napier似乎暗示,在ssl设置中禁用证书链验证对于sockets是必要的(如果我理解正确的话)。

基本上,我看到以下可能的解释: A)jww仅涉及NSURLConnections,其代理似乎比使用NS/CF流时卡住的代理更强大 B)自Rob Napier的帖子/Heath Borders的博客发布以来情况已经发生了变化 C)我完全弄错了一切,并在这种情况下提前道歉!

虽然 A) 看起来更有可能,但我有点希望是 B)...

如果您能提供更多见解,我将非常感激!

附注:我希望把以上内容作为答案不会违反任何规定...当然这并不是正确/完整的答案,但重新开始一个全新的问题似乎既不实用,而且很遗憾,这段文本过长无法放在任何评论中。如果有更好的方法可以包括其他信息(如上述新链接),同时列出仍不清楚的要点,请让我知道。


1
我认为你说得对,禁用自动TLS评估是必要的。来自苹果的Quinn在这个答案中也同意,但我们没有他实际的开发者论坛帖子链接。http://stackoverflow.com/a/10002271/9636 - Heath Borders

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接