Question

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?

Was it helpful?

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;
}

OTHER TIPS

Posting the corresponding Swift 5 Version. If your server is validating client certificate, the behavior is two challenges: first NSURLAuthenticationMethodServerTrust and then NSURLAuthenticationMethodClientCertificate

import Foundation

struct DiskCredsProvider{
    let clientCert: URL = Bundle.main.url(forResource: "client", withExtension: "crt")!
    let clientKey: URL = Bundle.main.url(forResource: "client", withExtension: "key")!
    let pkcs12: URL = Bundle.main.url(forResource: "cert", withExtension: "p12")!
    let pkcs12Password: String = "xxx"
    
    func keyData() throws -> Data{
        return try Data.init(contentsOf: clientKey)
    }
    
    
    func certData() throws -> Data{
        return try Data.init(contentsOf: clientCert)
    }
    
    
    func p12Data() throws -> Data{
        return try Data.init(contentsOf: pkcs12)
    }
    
    
    /// Provides URL Session Auth Chanllenge disposition by loading on disk pkcs12 file
    /// - Returns: tuple with URLSession disposition and URLCredential
    func provideUrlSessionDispostionWithPKCS12Data(data: Data) -> (URLSession.AuthChallengeDisposition, URLCredential){
        //You can also import a PKCS #12 file directly into your app using the certificate, key, and trust services API.
        let pcks12Data:CFData = data as CFData
        
        
        // CKTS API won’t even import PKCS #12 data that lacks a password. Password during import, you create an options dictionary with the password string:
        let password = pkcs12Password
        let options = [ kSecImportExportPassphrase as String: password ]
        
        //Because the PKCS #12 format allows for bundling multiple cryptographic objects together, this function populates an array object. In our case, it's the cert&key, we create a c type array to store it.
        var rawItems: CFArray?
        let status = SecPKCS12Import(pcks12Data, options as CFDictionary, &rawItems)
        
        //Check SecPCKCS12 import status code.
        
        precondition(status == errSecSuccess)

        let items = rawItems! as! Array<Dictionary<String, Any>>
        
        precondition(items.count == 1)

        let firstItem = items[0]
        
        let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity?
        
        
        let disposition: URLSession.AuthChallengeDisposition = .useCredential
        
        // In most cases the server does not need any intermediate certificates in order to evaluate trust on your client certificate (it either has a fixed list of client certificates, or requires that the client certificate be issued by a specific issuer, in which case it already has any intermediates leading to that issuer), and thus you don’t need to include them.
        let creds = URLCredential(identity: identity!, certificates: nil, persistence: .forSession)
                
        return (disposition,creds)
        
    }
}

enum APIError: Error {
    case pcks12ImportFailed
    case serverError
}

let endPointUrl: URL = URL(string: "https://\(endPointHost)/xx/xx/conf")!

let endPointHost: String = "xxxxx"


class WGAPIHelper:NSObject{
    
    var session: URLSession!
    
    let credsProvider: DiskCredsProvider = DiskCredsProvider()
    
    
    override init() {
        super.init()
        session = URLSession(configuration: .default, delegate: self, delegateQueue: queue)
    }
    
    lazy var queue: OperationQueue = {
        let oq = OperationQueue()
        oq.name = Bundle.main.bundleIdentifier!
        return oq
    }()
    
    
    
    func requestWGConfig(completionHandler: @escaping (String?,Error?) -> Void){
        
        let dataTask = session.dataTask(with: endPointUrl) { data, response, error in
            
            // check for fundamental networking error
            guard let data = data,
                  let response = response as? HTTPURLResponse,
                  error == nil else {
                      print("request error: \(error?.localizedDescription ?? "unknown error")")
                      completionHandler(nil,APIError.serverError)
                      return
                  }
            
            // check for http errors
            guard (200 ... 299) ~= response.statusCode else {
                print("request statusCode should be 2xx, but is \(response.statusCode)")
                print("response = \(response)")
                completionHandler(nil,APIError.serverError)
                return
            }
            
            completionHandler(String(data: data, encoding: .utf8), nil)

        }
        dataTask.resume()
    }
    
}

//MARK: - URLSessionDelegate
extension WGAPIHelper: URLSessionDelegate{
    
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        switch (challenge.protectionSpace.authenticationMethod,challenge.protectionSpace.host) {
        case (NSURLAuthenticationMethodServerTrust, endPointHost):
            
            
            let servTrust = challenge.protectionSpace.serverTrust!
            let credential = URLCredential(trust: servTrust)
            //By Default trust it. if you are using self sign server cert, overide trust settings here.
            
            completionHandler(.useCredential, credential)
            return
            
        
        case (NSURLAuthenticationMethodClientCertificate, endPointHost):
            let data = try! credsProvider.p12Data()
            let (disposition, creds) = credsProvider.provideUrlSessionDispostionWithPKCS12Data(data: data)
            completionHandler(disposition, creds)
            return
            

        default:
            completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
        }
    }

}

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top