Search code examples
iosssl-certificateswift5urlsessionurlauthenticationchallenges

How to 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.

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.


Solution

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