Categories
Security Swift

Client certificate with URLSession in Swift

Recently we I needed to implement an API in an app requiring a TLS client certificate.

It proved to be pretty simple, but I did need connect various bits and pieces together to get to a working solution. In this post I’ll show what worked for me.

To learn more about client certificate: https://en.wikipedia.org/wiki/TransportLayerSecurity#Client-authenticatedTLShandshake

Basically it involves a few things.

  • A client certificate, in my case packaged as a p12 file.
  • A password for the p12 files.
  • A cocoa pod called ASN1Decoder, to allow interpretation of ASN.1 formatted data.
  • An URLSessionDelegate
  • Once successfully obtaining the client certificate identity, we can create an URLCredential to respond to the challenge.

So here is the lowdown extracted from the session delegate I created:

public class MyURLSessionDelegate: NSObject, URLSessionDelegate {
    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        // `NSURLAuthenticationMethodClientCertificate`
        // indicates the server requested a client certificate.
        if challenge.protectionSpace.authenticationMethod
             != NSURLAuthenticationMethodClientCertificate {
                completionHandler(.performDefaultHandling, nil)
                return
        }

        guard let file = Bundle(for: HTTPAccessURLSessionDelegate.self).url(forResource: p12Filename, withExtension: "p12"),
              let p12Data = try? Data(contentsOf: file) else {
            // Loading of the p12 file's data failed.
            completionHandler(.performDefaultHandling, nil)
            return
        }

        // Interpret the data in the P12 data blob with
        // a little helper class called `PKCS12`.
        let password = "MyP12Password" // Obviously this should be stored or entered more securely.
        let p12Contents = PKCS12(pkcs12Data: p12Data, password: password)
        guard let identity = p12Contents.identity else {
            // Creating a PKCS12 never fails, but interpretting th contained data can. So again, no identity? We fall back to default.
            completionHandler(.performDefaultHandling, nil)
            return
        }

        // In my case, and as Apple recommends,
        // we do not pass the certificate chain into
        // the URLCredential used to respond to the challenge.
        let credential = URLCredential(identity: identity,
                                   certificates: nil,
                                    persistence: .none)
        challenge.sender?.use(credential, for: challenge)
        completionHandler(.useCredential, credential)
    }
}

As you can see, there is a lot of “if it fails we go to default” going on. This is security related code, so if things don’t work, we do not try any recovery, default handling just implies that no client certificate will be used and thus the connect should fail.

Here is the PKCS12 implementation. It is actually based on https://gist.github.com/algal/66703927b8379182640a42294e5f3c0b It is basically some helper code to bridge Core Foundation types into the memory safety of Swift.

private class PKCS12 {
    let label: String?
    let keyID: NSData?
    let trust: SecTrust?
    let certChain: [SecTrust]?
    let identity: SecIdentity?

    /// Creates a PKCS12 instance from a piece of data.
    /// - Parameters:
    ///   - pkcs12Data:
              the actual data we want to parse.
    ///   - password:
              The password required to unlock the PKCS12 data.
    public init(pkcs12Data: Data, password: String) {
        let importPasswordOption: NSDictionary
          = [kSecImportExportPassphrase as NSString: password]
        var items: CFArray?
        let secError: OSStatus
          = SecPKCS12Import(pkcs12Data as NSData,
                            importPasswordOption, &items)
        guard secError == errSecSuccess else {
            if secError == errSecAuthFailed {
                NSLog("Incorrect password?")

            }
            fatalError("Error trying to import PKCS12 data")
        }
        guard let theItemsCFArray = items else { fatalError() }
        let theItemsNSArray: NSArray = theItemsCFArray as NSArray
        guard let dictArray
          = theItemsNSArray as? [[String: AnyObject]] else {
            fatalError()
          }
        func f<T>(key: CFString) -> T? {
            for dict in dictArray {
                if let value = dict[key as String] as? T {
                    return value
                }
              }
            return nil
        }
        self.label = f(key: kSecImportItemLabel)
        self.keyID = f(key: kSecImportItemKeyID)
        self.trust = f(key: kSecImportItemTrust)
        self.certChain = f(key: kSecImportItemCertChain)
        self.identity = f(key: kSecImportItemIdentity)

    }
}

Any question? I’ll gladly answer any questions you might have.

By Jeroen

I’m Jeroen a dutch software developer employed by a large insurance company. Mostly I write about Swift code, CocoaHeadsNL and other related topics.

11 replies on “Client certificate with URLSession in Swift”

Hello mr. Jeroen. Thank you for interesting topic!

1) I think you missed some if keyword at the end of urlsession(_: challenge: completionhandler).

} else {
completionHandler(.performDefaultHandling, nil)
}

2) func f(key:) must be f(key:)

@Maxim

Ah yes, that `} else {` was indeed incorrect.

The `func f` is correct though. I do create an inline function and use that function later.

Thanks for you guide !
I’ve tried the code and I received the following error : “error: cannot convert value of type ‘CFArray’ to type ‘NSArray’ in coercion”, how can we fix it ?
Other question, does this code work with self-signed client certificates ?

Thanks !

Hi, it could be a valid error, my guess is it is comming from the PKCS12 helper, since that’s the only explicit cast to NSArray in my sample code.

Best would be to inspect what is actually in items. My guess is that it is null and thus a cast to a non-optional fails.

You question on self-signed client certs is a bit odd. A client certificate challenge basicly comes down to the server requesting a certificate. The client can pass in any certificate it likes, the server then decides if it is a valid on the server’s terms. So the server decides if a self signed client cert is considered ok. In my code you actually see me passing only the identity into the credential which will be used to respond to the client certificate challenge.

I’m having an issue where I’m going through an OAuth2 flow. I can login and get an access token. When I get the access token I receive a challenge and do like you do above to use a client certificate. I then try to call another endpoint on this server, but it fails saying it requires a client certificate, but my delegate doesn’t get called for the second endpoint. Any ideas?

Comments are closed.