iossslhttpsopensslactivesync

iOS NSURLAuthenticationMethodClientCertificate not requested vs ActiveSync server


I am trying to implement certificate authentication in an ActiveSync client I am developing. The code to use certificate auth might work, but as of now the server, or more accurately, the iOS library's interpretation of the server's response, seems incorrect to me. Here is my code:

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
    NSString *authenticationMethod = [protectionSpace authenticationMethod];

    if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate])
    {
        NSURLCredential* credential = [ self buildCredentialClientCert];

        if ( credential == nil )
        {
            [[challenge sender] cancelAuthenticationChallenge:challenge];
        }
        else
        {
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
        }
    }
    else if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
    {
        .... // do other stuff

The problem is, even though I know the server supports client certificate auth, when I set a breakpoint then authenticationMethod is always set to NSURLAuthenticationMethodServerTrust.

The raw HTTPS server response contains the following:

Error Code: 403 Forbidden. The page requires a client certificate as part of the authentication process. If you are using a smart card, you will need to insert your smart card to select an appropriate certificate. Otherwise, contact your server administrator. (12213)

My question is, what determines if the authentication challenge is NSURLAuthenticationMethodServerTrust versus NSURLAuthenticationMethodClientCertificate?


Solution

  • Since nobody answered this, and I did arrive at a working solution eventually, here it is.

    Server trust is not a challenge from the server to the client, it's an opportunity for the client to validate the trust offered by the server. With that in mind, the code below doesn't verify that trust, but it could.

    Typically you get the NSURLAuthenticationMethodServerTrust, then subsequently you get the NSURLAuthenticationMethodClientCertificate. It's not either-or. Here's the working code.

    - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
    {
        NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
        NSString *authenticationMethod = [protectionSpace authenticationMethod];
    
        if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate] && self.accountCertKeychainRef != nil)
        {
            SecIdentityRef identity = [KeychainUtilities retrieveIdentityWithPersistentRef:self.accountCertKeychainRef];
    
            NSURLCredential* credential = [CertificateUtilities getCredentialFromCert:identity];
    
            if ( credential == nil )
            {
                [[challenge sender] cancelAuthenticationChallenge:challenge];
            }
            else
            {
                [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
            }
        }
        else if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
        {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
        }
        else if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodNTLM] || [authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic])
        {
            self.lastProtSpace = [challenge protectionSpace];
            if ([challenge previousFailureCount] > 2)
            {
                [[challenge sender] cancelAuthenticationChallenge:challenge];
            }
            else
            {
                [[challenge sender]  useCredential:[self buildCredential] forAuthenticationChallenge:challenge];
            }
    
        }
        else
        {
            [[challenge sender] cancelAuthenticationChallenge:challenge];
        }
    }
    

    For the question below, here's how you can get the identity:

    + (SecIdentityRef)copyIdentityAndTrustWithCertData:(CFDataRef)inPKCS12Data password:(CFStringRef)keyPassword
    {
        SecIdentityRef extractedIdentity = nil;
        OSStatus securityError = errSecSuccess;
    
        const void *keys[] = {kSecImportExportPassphrase};
        const void *values[] = {keyPassword};
        CFDictionaryRef optionsDictionary = NULL;
    
        optionsDictionary = CFDictionaryCreate(NULL, keys, values, (keyPassword ? 1 : 0), NULL, NULL);
    
        CFArrayRef items = NULL;
        securityError = SecPKCS12Import(inPKCS12Data, optionsDictionary, &items);
    
        if (securityError == errSecSuccess) {
            CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex(items, 0);
    
            // get identity from dictionary
            extractedIdentity = (SecIdentityRef)CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemIdentity);
            CFRetain(extractedIdentity);
        }
    
        if (optionsDictionary) {
            CFRelease(optionsDictionary);
        }
    
        if (items) {
            CFRelease(items);
        }
    
        return extractedIdentity;
    }
    

    For those interested, here is getCredentialForCert:

    + (NSURLCredential *)getCredentialFromCert:(SecIdentityRef)identity
    {
        SecCertificateRef certificateRef = NULL;
        SecIdentityCopyCertificate(identity, &certificateRef);
    
        NSArray *certificateArray = [[NSArray alloc] initWithObjects:(__bridge_transfer id)(certificateRef), nil];
        NSURLCredentialPersistence persistence = NSURLCredentialPersistenceForSession;
    
        NSURLCredential *credential = [[NSURLCredential alloc] initWithIdentity:identity
                                                                   certificates:certificateArray
                                                                    persistence:persistence];
    
        return credential;
    }