14

The web service I want to consume requires a client certificate. How can I send my certificate to it?

To further elaborate I don't understand how to create the SecIdentityRef.

In my NSURLConnection didReceiveAuthenticationChallenge I've got this conditional after ServerTrust:

else if challenge?.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate
    {
        var secIdent : SecIdentityRef = ?????????
        var certCred = NSURLCredential(identity: secIdent, certificates: [getClientCertificate()], persistence: NSURLCredentialPersistence.Permanent)
        challenge?.sender.useCredential(certCred, forAuthenticationChallenge: challenge!)
    }

The getClientCertificate method:

func getClientCertificate() -> SecCertificateRef
{
    let mainBundle : NSBundle = NSBundle.mainBundle()
    var mainBund = mainBundle.pathForResource("iosClientCert", ofType: "cer") //exported the cert in der format.
    var key : NSData = NSData(contentsOfFile: mainBund!)!
    var turnToCert : SecCertificateRef = SecCertificateCreateWithData(kCFAllocatorDefault, key).takeRetainedValue()

    return turnToCert;
}
Xaver Kapeller
  • 49,491
  • 11
  • 98
  • 86
sk1tt1sh
  • 198
  • 1
  • 2
  • 11
  • Thanks for the bounty @EpicPandaForce – sk1tt1sh Jun 09 '15 at 19:58
  • 1
    We ran into the same problem, but we're still working on it... --- we'd probably have to do a direct port of the following solution http://stackoverflow.com/a/27012819/2413303 to be accessible in Swift, though. – EpicPandaForce Jun 09 '15 at 20:54
  • I had to get the app into a testing stage so I had to switch it to use auth with tokens etc. I will be revisiting this as soon as I can. I almost went the bridging header route and implemented the http client in obj-c... but I didn't have time for that either. – sk1tt1sh Jun 10 '15 at 01:26
  • "reputation from EpicPandaForce ending in 22 hours" blah – EpicPandaForce Jun 15 '15 at 09:27
  • .cer is a simple export which does not have any keys. How are we gonna append this during the request time. Anyone solved this issue ? – Swaroop S May 17 '17 at 06:24

5 Answers5

10

Technically, when someone I know needed the implementation in Swift, he used the following Objective-C implementation in order to get the NSURLCredential object to the connection; based on the private key and X509 Certificate pair contained in a PKCS12 keystore.

Sorry, I don't have access to the source with the Swift solution. All I know is that the NSURLCredential was returned to Swift, and used directly in the http url connection there. It's similar to this one, though.

I'm not an iOS dev so I won't be able to help you out with the "bridging to Swift" part.

- (void)getMessageWithURL:(NSString *)url {

    NSURL *URL = [NSURL URLWithString:url];

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setURL:URL];
    [request setHTTPMethod:@"GET"];
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection self];
}

- (void)postMessageWithURL:(NSString *)url withContent:(NSString *)content {

    NSData *postData = [content dataUsingEncoding:NSUTF8StringEncoding];
    NSString *postLength = [NSString stringWithFormat:@"%d", [postData length]];

    NSURL *myURL = [NSURL URLWithString:url];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:myURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60];

    [request setHTTPMethod:@"POST"];
    [request setValue:postLength forHTTPHeaderField:@"Content-Length"];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:postData];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection self];

}

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    NSLog(@"didReceiveAuthenticationChallenge");
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    responseData = [[NSMutableData alloc] init];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [responseData appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"Unable to fetch data");
    NSLog(@"%@", error);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"Succeeded! Received %lu bytes of data", (unsigned long)[responseData
            length]);

    NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", responseString);

    [bridge callHandler:handlerName data:responseString];

}

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {

    /*
    Reading the certificate and creating the identity
    */
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = paths[0]; // Get documents directory

    NSData *p12data = [CertificateManager getP12Data]; //returns essentially a byte array containing a valid PKCS12 certificate

    if (!p12data) {
      return;
      NSAssert(p12data, @"Couldn't load p12 file...");
    }

    CFStringRef password = CFSTR("password");

    const void *keys[] = {kSecImportExportPassphrase};
    const void *values[] = {password};
    CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    CFArrayRef p12Items;

    OSStatus result = SecPKCS12Import((__bridge CFDataRef) p12data, optionsDictionary, &p12Items);

    if (result == noErr) {
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(p12Items, 0);
        SecIdentityRef identityApp = (SecIdentityRef) CFDictionaryGetValue(identityDict, kSecImportItemIdentity);

        SecCertificateRef certRef;
        SecIdentityCopyCertificate(identityApp, &certRef);

        SecCertificateRef certArray[1] = {certRef};
        CFArrayRef myCerts = CFArrayCreate(NULL, (void *) certArray, 1, NULL);
        CFRelease(certRef);

        NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityApp certificates:nil persistence:NSURLCredentialPersistenceNone];
        CFRelease(myCerts);

        [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
    }
    else {
        // Certificate is invalid or password is invalid given the certificate
        NSLog(@"Invalid certificate or password");
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
        return;
    }
}

EDIT: Har har, very funny, downvoting me twice when you yourself didn't bother while the bounty was up. *grumble *

Anyways, to use the following above, you just need to access it from Swift.

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {
    if let p12Data = UserManager.currentP12,
       let credential = CertificateManager.getCredentialsForP12(p12Data) as? NSURLCredential {
            challenge.sender.useCredential(credential, forAuthenticationChallenge: challenge)
    } else {
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    }   
}

That uses this.

+ (id)getCredentialsForP12:(NSData *)p12 {
    NSData* p12data = p12;
    const void *keys[] = {kSecImportExportPassphrase};
    const void *values[] = {CFSTR("thePassword")};
    CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    CFArrayRef p12Items;
    OSStatus result = SecPKCS12Import((__bridge CFDataRef) p12data, optionsDictionary, &p12Items);
    if (result == noErr) {
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(p12Items, 0);
        SecIdentityRef identityApp = (SecIdentityRef) CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
        SecCertificateRef certRef;
        SecIdentityCopyCertificate(identityApp, &certRef);
        SecCertificateRef certArray[1] = {certRef};
        CFArrayRef myCerts = CFArrayCreate(NULL, (void *) certArray, 1, NULL);
        CFRelease(certRef);

        NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityApp certificates:nil persistence:NSURLCredentialPersistenceNone];
        CFRelease(myCerts);
        return credential;

    }
    else {
        // Certificate is invalid or password is invalid given the certificate
        NSLog(@"Invalid certificate or password");

        UIAlertView* av = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Invalid cert or pass" delegate:nil cancelButtonTitle:@"ok" otherButtonTitles: nil];
        [av show];
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
        return nil;
    }

EDIT: A swift version of the above is here, although it was messy enough that we rather just didn't use it.

            var p12items : Unmanaged<CFArrayRef>?

            let index: CFIndex = 1
            let password: CFString = "password"
            let key = kSecImportExportPassphrase.takeRetainedValue() as String
            var values = [unsafeAddressOf(password)]
            var keys = [unsafeAddressOf(key)]

            var keyCallbacks = kCFTypeDictionaryKeyCallBacks
            var valueCallbacks = kCFTypeDictionaryValueCallBacks

            let length: CFIndex = p12Data.length
            let p12CfData: CFData = CFDataCreate(kCFAllocatorDefault, UnsafePointer<UInt8>(p12Data.bytes), length)

            let options = CFDictionaryCreate(kCFAllocatorDefault, &keys, &values, index, &keyCallbacks, &valueCallbacks)
            let result = SecPKCS12Import(p12CfData, options, &p12items)

            if result == noErr {

                let idIndex: CFIndex = 0
                var items = p12items?.takeRetainedValue()
                var identityDict = CFArrayGetValueAtIndex(items!, idIndex) 

                var key = kSecImportItemIdentity.takeRetainedValue() as String
                var keyAddress = unsafeAddressOf(key)
                var identityApp: SecIdentityRef = CFDictionaryGetValue(identityDict, keyAddress) 
                var certRef : Unmanaged<SecCertificateRef>?
                SecIdentityCopyCertificate(identityApp, &certRef)

                var cert: SecCertificateRef = certRef!.takeRetainedValue()
                var certArray = [unsafeAddressOf(cert)]
                var arrayCallback = kCFTypeArrayCallBacks
                var myCerts: CFArrayRef = CFArrayCreate(kCFAllocatorDefault, &certArray, index, &arrayCallback);

                let credential: NSURLCredential = NSURLCredential(identity: identityApp, certificates: [AnyObject](), persistence: NSURLCredentialPersistence.None)
Community
  • 1
  • 1
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • Interesting. It's a bit off topic I suppose, but why does the entire public & private need to be included? Thank you for your answer. I will try it this week and see if I can get an implementation in Swift. Thanks EPF. – sk1tt1sh Jun 15 '15 at 03:24
  • Sorry about the Obj-C code, I really didn't have access to the Swift bridging. Which part do you mean by "public & private" here? – EpicPandaForce Jun 15 '15 at 05:37
  • I mean the key has to be PKCS which means it will have to include the public and private key of the cert. In other languages I've been able to use a .cer file which is just a direct export without the private key and doesn't require the cert password. Not a huge deal since we have a CA signed cert but it's a little weird to me that it's this way. – sk1tt1sh Jun 16 '15 at 12:37
  • 1
    Well technically this is how it was done in Java, and a friend here managed to map the whole thing I did with BouncyCastle to using OpenSSL on iOS. But in order to make client authentication work, you *do* need both a private key and a public key; because you encrypt things with the public key of the recipient, and they encrypt it with your public key; but to decrypt it you need your private key, which is stored in a keystore. That's imported in the `SecPKCS12Import` call to have both available for the SSL connection. Client auth is magic, and it's weird that there's nothing about it anywhere. – EpicPandaForce Jun 16 '15 at 12:43
  • 2
    So I got access to the Swift code, and added a portion of it. I'm not deleting my answer just because some people who have zero clue about client auth felt like barging in here and acting all smart and giving downvotes. – EpicPandaForce Jun 18 '15 at 13:14
  • I marked it as the answer because that's enough to work with. Potential to convert it to 100% swift? Who knows but at least you tried. Thanks, and sorry that you got those down votes. I'm pretty surprised. – sk1tt1sh Jun 19 '15 at 00:19
  • Nah it is okay, it was objective c after all. (I still think it's kinda funny that they didn't bother while the bounty was up, there is nothing about this anywhere! My coworker solved the issue of generating P12 because no one ever even asked). There was an attempt to make it more swift-like, but it essentially meant using a bunch of takeRetainedValue and unsafePointerOf and all that and was essentially the same code just a lot uglier, and less stable. I might add that when I get to work, but no guarantees on that one. – EpicPandaForce Jun 19 '15 at 07:00
4

Find in this Gist the Swift3 working implementation :

https://gist.github.com/celian-m/8da09ad293507940a0081507f057def5

CZ54
  • 5,488
  • 1
  • 24
  • 39
2

In order to respond to the authentication challenge you need to extract the identity from your client certificate.

struct IdentityAndTrust {

    var identityRef:SecIdentityRef
    var trust:SecTrustRef
    var certArray:NSArray
}

func extractIdentity(certData:NSData, certPassword:String) -> IdentityAndTrust {

    var identityAndTrust:IdentityAndTrust!
    var securityError:OSStatus = errSecSuccess

    var items:Unmanaged<CFArray>?
    let certOptions:CFDictionary = [ kSecImportExportPassphrase.takeRetainedValue() as String: certPassword ];

    // import certificate to read its entries
    securityError = SecPKCS12Import(certData, certOptions, &items);

    if securityError == errSecSuccess {

        let certItems:CFArray = items?.takeUnretainedValue() as CFArray!;
        let certItemsArray:Array = certItems as Array
        let dict:AnyObject? = certItemsArray.first;

        if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {

            // grab the identity
            let identityPointer:AnyObject? = certEntry["identity"];
            let secIdentityRef:SecIdentityRef = identityPointer as! SecIdentityRef!;

            // grab the trust
            let trustPointer:AnyObject? = certEntry["trust"];
            let trustRef:SecTrustRef = trustPointer as! SecTrustRef;

            // grab the certificate chain
            var certRef:Unmanaged<SecCertificate>?
            SecIdentityCopyCertificate(secIdentityRef, &certRef);
            let certArray:NSMutableArray = NSMutableArray();
            certArray.addObject(certRef?.takeRetainedValue() as SecCertificateRef!);

            identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: certArray);
        }
    }

    return identityAndTrust;
}

In NSURLSessionDelegate respond to the authentication challenge like this:

public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {

    let bundle:NSBundle = NSBundle(forClass: self.dynamicType);
    let bundleCertPath:NSString = bundle.pathForResource("clientCertificateName", ofType: "p12")!;
    let certData:NSData = NSData(contentsOfFile: bundleCertPath as String)!;
    let identityAndTrust:IdentityAndTrust = self.certificateHelper.extractIdentity(certData, certPassword: "C00lp@assword");

    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {

        let urlCredential:NSURLCredential = NSURLCredential(
                identity: identityAndTrust.identityRef,
                certificates: identityAndTrust.certArray as [AnyObject],
                persistence: NSURLCredentialPersistence.ForSession);

        completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, urlCredential);


    } else {

        // nothing here but us chickens
    }
}
Xaver Kapeller
  • 49,491
  • 11
  • 98
  • 86
Bins Ich
  • 1,822
  • 6
  • 33
  • 47
0

I am using last xcode and swift version, and this code work for me, using a client certificate .pfx, based on Bins Ich answer:

 func extractIdentity(certData:NSData) -> IdentityAndTrust { 
    var identityAndTrust:IdentityAndTrust!
    var securityError:OSStatus = errSecSuccess
    var items:Unmanaged<CFArray>?
    let certOptions:CFDictionary = [ kSecImportExportPassphrase.takeRetainedValue() as String: "password" ];

    // import certificate to read its entries
    securityError = SecPKCS12Import(certData, certOptions, &items);

    if securityError == errSecSuccess {
        let certItems:CFArray = items?.takeUnretainedValue() as CFArray!;
        let certItemsArray:Array = certItems as Array
        let dict:AnyObject? = certItemsArray.first;
        if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {

            // grab the identity
            let identityPointer:AnyObject? = certEntry["identity"];
            let secIdentityRef:SecIdentityRef = identityPointer as! SecIdentityRef!;

            // grab the trust
            let trustPointer:AnyObject? = certEntry["trust"];
            let trustRef:SecTrustRef = trustPointer as! SecTrustRef;

            // grab the cert
            let chainPointer:AnyObject? = certEntry["chain"];
            let chainRef:SecCertificateRef = chainPointer as! SecCertificateRef;
            let  certArray:CFArrayRef = chainRef as! CFArrayRef

            identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray:  certArray);
        }
    }
    return identityAndTrust;
}

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {

    let strTemp = challenge.protectionSpace.authenticationMethod

    if(strTemp == NSURLAuthenticationMethodServerTrust) {
         challenge.sender.continueWithoutCredentialForAuthenticationChallenge(challenge)
    }

    if(strTemp == NSURLAuthenticationMethodClientCertificate) {

        let certFile = NSBundle.mainBundle().pathForResource("mycert", ofType:"pfx")

        let p12Data = NSData(contentsOfFile:certFile!)
        let identityAndTrust:IdentityAndTrust = extractIdentity(p12Data!)

        let urlCredential:NSURLCredential = NSURLCredential(
            identity: identityAndTrust.identityRef,
            certificates:identityAndTrust.certArray as [AnyObject],
            persistence: NSURLCredentialPersistence.Permanent)

        challenge.sender.useCredential(urlCredential ,forAuthenticationChallenge:challenge)
    }
}
  • Thanks Jose! Do you happen to know what changed that made this work? It's very similar to some of the first code I used when trying to get this to work. – sk1tt1sh Jul 22 '15 at 15:25
  • I get certificate chain using let chainPointer:AnyObject? = certEntry["chain"]; let chainRef:SecCertificateRef = chainPointer as! SecCertificateRef; let certArray:CFArrayRef = chainRef as! CFArrayRef – Jose Sanchez Jul 27 '15 at 18:37
0

People who are using the latest version of Xcode +13 will find this helpful.

Finally got it worked using .p12 file and using PKCS12 approach of fetching all the details from the .p12 like identity, certChain, trust, keyID(PrivateKey, & publicKey) and signing those keys into an object to make a Signature!

The below code will help you in achieving the mTLS authentication between the client and server without needing to handle the .pfx file. You just need the base 64 sting certificate data.

struct IdentityAndTrust {
   var identityRef:SecIdentity
   var trust:SecTrust
   var certArray:NSArray
   var privateKey:SecKey?
   var publicKey:SecKey?
   var signatureData:String?
}

func extractIdentity(certData:NSData, certPassword:String, signingData:String) -> IdentityAndTrust {

  let certString = String.init(data: certData as Data, encoding: .utf8) ?? ""
  let base64 = Data(base64Encoded: certString, options: .ignoreUnknownCharacters)
  var identityAndTrust:IdentityAndTrust!
  var items:CFArray?
  var privateKey:SecKey?
  var publicKey:SecKey?
  var signatureData:String?
  var error: Unmanaged<CFError>?

  let importPasswordOption:NSDictionary = [kSecImportExportPassphrase as NSString:certPassword]

// import certificate to read its entries
  let secError:OSStatus = SecPKCS12Import((base64 ?? Data()) as CFData, importPasswordOption, &items)
  guard secError == errSecSuccess else {
      if secError == errSecAuthFailed {
          NSLog("ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?")
      }
      NSLog("SecPKCS12Import returned an error trying to import PKCS12 data")
      return identityAndTrust
  }

  if let certItems:CFArray = items {
      let certItemsArray:Array = certItems as Array
      let dict:AnyObject? = certItemsArray.first
    
      if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {
        
        // grab the identity
          let identityPointer:AnyObject? = certEntry["identity"];
          let secIdentityRef:SecIdentity = identityPointer as! SecIdentity
        
        // grab the trust
          let trustPointer:AnyObject? = certEntry["trust"];
          let trustRef:SecTrust = trustPointer as! SecTrust;
        
        // grab the certificate chain
          var certRef:SecCertificate?
          SecIdentityCopyCertificate(secIdentityRef, &certRef);
          let certArray:NSMutableArray = NSMutableArray();
          certArray.add(certRef!);
        
        //To get the Private Key
          var key: SecKey?
          SecIdentityCopyPrivateKey(secIdentityRef , &key)
          if let privateKeyData = key{
              print("privatekeyData: \(privateKeyData)")
              privateKey = privateKeyData
            
            //Make sure the key supports signing with the algorithm
              guard SecKeyIsAlgorithmSupported(privateKeyData, .sign, .rsaSignatureMessagePKCS1v15SHA256) else {
                  return identityAndTrust
              }
            //Making signature
              if let  signature = SecKeyCreateSignature(privateKeyData, .rsaSignatureMessagePKCS1v15SHA256, Data(signingData.utf8) as CFData, &error) {
                  signatureData =  (signature as Data).base64EncodedString()
                  print("signature: \(signature)")
              } else {
                  NSLog("\(error!.takeUnretainedValue())")
              }
            //To get the Public Key from Private key
              if let publickey = SecKeyCopyPublicKey(privateKeyData) {
                  publicKey = publickey
                  print("publickey: \(publickey)")
              }
          }
          identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: certArray, privateKey: privateKey, publicKey:publicKey, signatureData:signatureData)
      }
  }
 
    print("identityAndTrust \(String(describing: identityAndTrust))")
    return identityAndTrust
 }