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/Transport_Layer_Security#Client-authenticated_TLS_handshake
Basically it involves a few things.
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` indicated
// the server requested a client certificate.
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
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. So we abort.
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) } else {
completionHandler(.performDefaultHandling, nil)
}
}
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("ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?")
}
fatalError("SecPKCS12Import returned an 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.