Madhu
Madhu

Reputation: 929

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.

Upvotes: 3

Views: 2156

Answers (2)

Madhu
Madhu

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

CSmith
CSmith

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

Related Questions