4

I'm trying around with an app that is to securely connect from an iPhone client to a TLS server via sockets/streams for general data-exchange. For that purpose I set up an own CA with the mac keychain-tool and included the certificat in the code bundle.

Now my app should trust any server certificate issued by that CA. (I do not care how other apps treat those certs, I assume that they will not trust it because of the sandbox.)

I have found several similar issues online but seem to have gotten something wrong.

Connecting with the server seems to work fine if I drag & drop the CA certificate into the Simulator and manually accept to trust it.

However, when I try to establish trust for the CA cert programmatically my connection attempts later to the server are refused, despite the code below does not generate errors.

Therefore I must have got the certificate implementation part wrong... Any ideas?

Many thanks in advance!

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;
jww
  • 97,681
  • 90
  • 411
  • 885
McMini
  • 305
  • 3
  • 15
  • Follow up: despite having `kSecResultUnspecified` the connection fails with 'CFNetwork SSLHandshake failed (-9807)'; however there is no such error when the cert has been drag dropped into the simulator and accepted by the user (though even then I only get `kSecResultUnspecified`); I will update if I figure it out – McMini Aug 06 '14 at 10:04

2 Answers2

3

I did not try to run the partial code, but I have some code (provided below) that I know works. I use it to trust my internal CA.

err=SecTrustEvaluate(trust,&trustResult);
assert(err==noErr);

trustResult is what you are interested in, not the err return from SecTrustEvaluate. err tells you if the API call succeeded/failed; it does not tell you the result of the trust evaluation.

I think you have two strategies here. First is to look for "success" in trustResult with values kSecTrustResultProceed or kSecTrustResultUnspecified. Its "success" because its not "prompt", its not "try to recover" and its not "failure".

The second strategy is "not failure" in trustResult with values kSecTrustResultDeny, kSecTrustResultFatalTrustFailure or kSecTrustResultOtherError. That is, as long as trustResult is not one of those values, then proceed as success. Ignore prompting the user to trust a certificate because they will not understand the prompt and "tap through".

Below is the code I use in NSURLConnection delegate's -didReceiveAuthenticationChallenge:. It expects an ASN.1/DER encoded certificate (named ca-cert.der). It uses Strategy 1 described above. If you use the code in the #ifdef 0, then its using Strategy 2.

I think Apple's Overriding TLS Chain Validation Correctly, Apple's Tech Note TN2232, HTTPS Server Trust Evaluation and Apple's Technical Q&A QA1360, Describing the kSecTrustResultUnspecified error might be useful to you.


- (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];
}
jww
  • 97,681
  • 90
  • 411
  • 885
  • I get kSecResultUnspecified. Since the system seems unhappy with this result, do I need to disable auto TLS validation entirely and instead manually check SecTrustEvaluate and then if I get kSecResultUnspecified proceed? Since I need flexible communication between server and client I believe I need to use NS/CF Streams instead of NSURLConnections. Other than stream:handleEvent: I am not aware of a delegate for NS/CF-streams, is there? Is there any way at all for me to achieve a kSecResultProceed without beeing trusted by default and without asking the user to accept my root CA? – McMini Aug 06 '14 at 00:53
  • @MacMini - see Apple's Technical Q&A QA1360, [Describing the kSecTrustResultUnspecified error](http://developer.apple.com/library/mac/qa/qa1360/_index.html). Or see the comments in the source code above. The system is *not* unhappy when it returns `kSecResultUnspecified`. – jww Aug 06 '14 at 00:59
  • Thank you for your quick and detailed answer, the code will certainly prove valuable! In your reply to my comment you say: the system is ok with 'kSecTrustResultUnspec.'. You're also saying that your code is working (program. enabling self sign. root CA without disabling automatic certificate validation). Desp. all efforts I have not been able to duplicate that with my NS/CFStreams and finally found a few posts that discuss a very similar topic; but now have a difficult time consolidating the different statements and would again very much appreciate your insight.(-> my partial answer) – McMini Aug 07 '14 at 14:29
1

Despite all efforts I have not been able to duplicate with NS/CF-Streams, what jww obviously has managed with NSURLConnection. (As before when drag-dropping the CA-cert on the simulator and accepting it as trusted, the TLS stream works fine, but doing it programmatically does not succeed.)

Therefore I've done a little more search on the topic and actually found an additional post by aeternusrahl that was answered by Rob Napier:Post

...in which the following blog entry from Heath Borders is cited: Blog

For everyone that has a similar issue as me, I can very much recommend those links – they provide good insight!

Without replicating the entire content of those links: Rob Napier says '...there is, as far as I've ever discovered, only one trusted anchor list in that keychain, shared by everyone. This doesn't help you a lot with socket.io, because it doesn't give you access to its NSURLConnection delegate methods. You'd have to modify socket.io to accept a trust anchor.'

As all authors above have outstanding reputation and I am pretty new to iOS programming, I would not dare to disagree with either one! But since I am having a really hard time consolidating the posts, I would very much appreciate any clarification on preconditions in the posts I might have accidentally skipped.

For me it seems, jww does not consider disabling automatic TLS validation necessary when using own root-certs (see his answer to my comment on his first post). Then, Heath Borders/Rob Napier, seem to suggest that disabling certificate chain validation in ssl settings is necessary for sockets (again if I got it right.)

Basically I see the following possible explanations: A) jww is referring to NSURLConnections only, whose delegate seems more powerful than the one you're stuck with when working with NS/CF streams B) the situation has changed since the post from Rob Napier / the blog from Heath Borders C) I got everything entirely wrong and apologize in advance in that case!

While A) seems somewhat more probable I'm kind of hoping for B)...

I'd be very thankful for additional insight!

PS. I hope placing the above as an answer does not violate any rules... It's certainly not the correct / complete answer, but starting a entirely new question seems neither useful and unfortunately the text is too long for any comment. If there is a better way to include additional information (as the above new links) while listing points that are still unclear, let me know.

Community
  • 1
  • 1
McMini
  • 305
  • 3
  • 15
  • 1
    I think you're right that it is necessary to disable automatic TLS evaluation. Quinn from Apple agrees according to this answer, but we don't have a link back to his actual devforums post. http://stackoverflow.com/a/10002271/9636 – Heath Borders Apr 06 '17 at 22:16