Implement iOS Mutual authentication between client and server?

In one of my application i'm trying to implement certificate Mutual authentication between client and server for my iOS app using URLSession. I was able to extract the identityRef and trust and certificate chain and in didReceivechallenge method i'm checking for the authenticationMethod and creating URLCredential for challenge for URLSession.

// Struct to save values of the Cert.

Code Block
struct IdentityAndTrust {
var identityRef: SecIdentity
var trust: SecTrust
var certificates: [SecCertificate]
}


// Method to extract the identity, certificate chain and trust

Code Block
func extractIdentity(certData: NSData, certPassword: String) -> IdentityAndTrust? {
var identityAndTrust: IdentityAndTrust?
var securityStatus: OSStatus = errSecSuccess
var items: CFArray?
let certOptions: Dictionary = [kSecImportExportPassphrase as String : certPassword]
securityStatus = SecPKCS12Import(certData, certOptions as CFDictionary, &items)
if securityStatus == errSecSuccess {
let certificateItems: CFArray = items! as CFArray
let certItemsArray: Array = certificateItems as Array
let dict: AnyObject? = certItemsArray.first
if let certificateDict: Dictionary = dict as? Dictionary<String, AnyObject> {
// get the identity
let identityPointer: AnyObject? = certificateDict["identity"]
let secIdentityRef: SecIdentity = identityPointer as! SecIdentity
// get the trust
let trustPointer: AnyObject? = certificateDict["trust"]
let trustRef: SecTrust = trustPointer as! SecTrust
// get the certificate chain
var certRef: SecCertificate? // <- write on
SecIdentityCopyCertificate(secIdentityRef, &certRef)
var certificateArray = [SecCertificate]()
certificateArray.append(certRef! as SecCertificate)
let count = SecTrustGetCertificateCount(trustRef)
if count > 1 {
for i in 1..<count {
if let cert = SecTrustGetCertificateAtIndex(trustRef, i) {
certificateArray.append(cert)
}
}
}
identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certificates: certificateArray)
}
}
return identityAndTrust
}

// Delegate method of URLSession

Code Block
public class SessionDelegate : NSObject, URLSessionDelegate {
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if let localCertPath = Bundle.main.url(forResource: "my_client", withExtension: "p12"),
let localCertData = try? Data(contentsOf: localCertPath)
{
let identityAndTrust:IdentityAndTrust = extractIdentity(certData: localCertData as NSData, certPassword: "eIwj5Lurs92xtC9B4CZ0")!
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
let urlCredential:URLCredential = URLCredential(
identity: identityAndTrust.identityRef,
certificates: identityAndTrust.certificates as [AnyObject],
persistence: URLCredential.Persistence.permanent);
completionHandler(URLSession.AuthChallengeDisposition.useCredential, urlCredential);
return
}
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let urlCredential:URLCredential = URLCredential(
identity: identityAndTrust.identityRef,
certificates: identityAndTrust.certificates as [AnyObject],
persistence: URLCredential.Persistence.forSession);
completionHandler(URLSession.AuthChallengeDisposition.useCredential, urlCredential);
// completionHandler (URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!));
return
}
completionHandler (URLSession.AuthChallengeDisposition.performDefaultHandling, Optional.none);
return
}
challenge.sender?.cancel(challenge)
completionHandler(URLSession.AuthChallengeDisposition.rejectProtectionSpace, nil)
}


Code Block
}
Below is the response i'm getting
`Project XXXX[1115:755397] [tcp] tcp_output [C22.1:3] flags=[R.] seq=2988084600,
ack=2995213448, w`in=2047 state=CLOSED rcv_nxt=2995213448, snd_una=2988084600
Project XXXX[1115:755397] Connection 22: received failure notification
2021-05-18 12:39:08.000356+0530 Project XXXX[1115:755397] Connection 22: failed to connect
3:-9816, reason -1
2021-05-18 12:39:08.000429+0530 Project XXXX[1115:755397] Connection 22: encountered
error(3:-9816)
finished with error [-1200] Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://dev.xxxx.net/oauth/xxx/login, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <75A97E53-2AE1-4CE2-9C0D-64AA5965BCBC>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <75A97E53-2AE1-4CE2-9C0D-64AA5965BCBC>.<1>"
), NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://dev.projectzebra.net/oauth/zebra/login, NSUnderlyingError=0x282d26910 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=1, _kCFNetworkCFStreamSSLErrorOriginalValue=-9816, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9816}}, _kCFStreamErrorCodeKey=-9816}

A few things I see:

1) I suspect that this is just for test purposes but I would avoid keeping your p12 on disk in your app. This type of sensitive data should be loaded securely into your app and saved to your Keychain.

2) The second thing I see is sort of related to the first item. It looks like you are loading the identity from disk so the but the identity is not in the Keychain and therefore there could be issues with signing the response and building a chain of trust if the root certificate is not installed on the device.

As a test, I would recommend performing the following: loading the p12 and saving it to the Keychain as a SecIdentity. Next, ensure that if the identity's certificate originates from CA that has a root certificates installed on the client device to build the chain of trust from the leaf in the identity to the root in the trust store. Lastly, the second argument (identityAndTrust.certificates) should not be the identity certificate, but rather the rest of the certs in the chain (with the root being optional.) The leaf certificate will be contained in the identity.

Code Block swift
let urlCredential:URLCredential = URLCredential(
identity: identityAndTrust.identityRef,
certificates: identityAndTrust.certificates as [AnyObject],
persistence: URLCredential.Persistence.forSession);

NOTE: Every server is different and may not require the intermediate or root certificates in the argument for (identityAndTrust.certificates).


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Implement iOS Mutual authentication between client and server?
 
 
Q