Reputation: 929
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.
Below is my code
// Struct to save values of the Cert.
struct IdentityAndTrust {
var identityRef: SecIdentity
var trust: SecTrust
var certificates: [SecCertificate]
}
// Method to extract the identity, certificate chain and trust
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
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)
}
}
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}
I'm not sure about the response i have recieved and unable to proceed further, please help if anyone faced similar issue.
Any help is appreciated. Thanks.
Upvotes: 3
Views: 2156
Reputation: 929
Finally got it worked using .p12 file and using PKCS12 approach of fetching all the details from the .p12 like identity, certChain, trust, keyID and assigning those contents to URLCredentials object and pass that object to challenge sender.
Below code will help you in achieving the mTLS authentication between client and server.
public class PKCS12 {
var label:String?
var keyID:NSData?
var trust:SecTrust?
var certChain:[SecTrust]?
var identity:SecIdentity?
public init(PKCS12Data:NSData,password:String)
{
let importPasswordOption:NSDictionary = [kSecImportExportPassphrase as NSString:password]
var items : CFArray?
let secError:OSStatus = SecPKCS12Import(PKCS12Data, 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 d in dictArray {
if let v = d[key as String] as? T {
return v
}
if(key == kSecImportItemLabel || key == kSecImportItemKeyID){
var cert: SecCertificate?
if let cd = d["identity"]{
SecIdentityCopyCertificate(cd as! SecIdentity, &cert)
if let certData = cert{
if(key == kSecImportItemLabel){
let lblDer = SecCertificateCopySubjectSummary(certData)
if let lblVallue = lblDer {
return lblVallue as? T
}
}
var key: SecKey?
SecIdentityCopyPrivateKey(cd as! SecIdentity, &key)
if let keyData = key{
let keyDict = SecKeyCopyAttributes(keyData)
if let keyDictUnwrapped = keyDict, let keyValue = (keyDictUnwrapped as NSDictionary)["v_Data"] as? NSData {
return keyValue as? T
}
}
}
}
}
}
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)
}
}
extension URLCredential {
public convenience init?(PKCS12 thePKCS12:PKCS12) {
if let identity = thePKCS12.identity {
self.init(
identity: identity,
certificates: thePKCS12.certChain,
persistence: URLCredential.Persistence.forSession)
}
else { return nil }
}
}
Upvotes: 1
Reputation: 13458
I believe the problem might be related to this code:
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);
return
}
specifically you're answering the NSURLAuthenticationMethodServerTrust
challenge by offering up your client certificate.
I suggest replacing that code with this:
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust
{
if let serverTrust: ServerTrust = challenge.protectionSpace.serverTrust
{
let credential: URLCredential = URLCredential(trust: serverTrust)
completionHandler(.performDefaultHandling, credential)
return
}
}
Your server should be configured to require client certificates which will create the NSURLAuthenticationMethodClientCertificate
challenge...it appears you're handling this Ok.
Additionally, a web client needs to trust the server certificate, this is where NSURLAuthenticationMethodServerTrust
comes into play. Default handling will ensure that the server certificates root CA (Certificate Authority) is in the list of trusted authorities in iOS, that the certificate isn't expired, etc.
It's not clear from your question whether your intention is to also inspect the server certificate, e.g. for certificate pinning, this answer assumes you're not.
Please try the above and report back any differences?
Upvotes: 4