Avisi Blog

Cross-platform certificate access with Cordova part 2 - iOS

Geschreven door Avisi | 08 March 2016

In my previous post we took a look at accessing Android's installed certificates through a Cordova plugin. Creating a Cordova plugin and only supporting Android hardly makes sense, so this post will focus on adding support for iOS.

 

In order to add iOS support you will need basic Objective-C knowledge for the native implementation and at times a healthy amount of creativity. Unlike Android you will not find out-of-the-box support for certificate access on iOS, most likely there aren't enough use cases to convince Apple to support this the same way Android does.

 

This post will show the outlines rather than a full implementation of a possible solution, one which I have used myself. I do not claim that this is the best or the only possible solution, but it's something that seemed like a reasonable compromise to me. Let's look at what we can do!

 

Trying a similar approach

Just like the Android implementation we start by creating our native code (in Objective-C). Our header (MyPlugin.h) will look like this:

 

#import<Cordova/CDV.h>
@interface  MyPlugin : CDVPlugin
- (void) selectCert:(CDVInvokedUrlCommand*)invokedCommand;
@end

 

As for the implementation, you might remember that the Android implementation had one method call to handle the entire process of listing available certificates, selecting one and granting access to the selected certificate:

KeyChain.choosePrivateKeyAlias(activity, this, KEYTYPES, null, null, -1, null);

 

I had high hopes of finding something similar for iOS, but to make a long story short: There's no such thing for iOS (that I could find). But that's okay, you can install certificates on your device and thus there's bound to be a way to access those installed certificates, so surely we can mimic Android's functionality. Unfortunately, this turns out to be false, from what I have understood the certificates on your iOS device can only be accessed by Apple's own trusted apps. So the idea of using the system's installed certificates seems impossible and we are now limited to finding alternatives within the strict sandbox of our app. Clearly this means that we will not be able to achieve a similar workflow for iOS and Android.

 

Getting creative

This is the part where some creativity is necessary to think of a solution, the greatest challenge is getting a user's certificate into the app's sandbox. Apple does a great job of documenting ways to use and store certificates (in your app's keychain), but these examples assume that you somehow already have the certificate in a NSData object. One of the possibilities that struck me as viable, both for developers and users, is having the app support the certificate's file extension (generally .pfx or .pk12). Instead of installing a certificate globally onto the device a user would open the certificate in our app after downloading it.

 

This idea turned out to have a flaw, the aforementioned file extensions are automatically handled by iOS. Specifying your app to handle these file extensions will not change a single thing, the option to open downloaded certificates in your app will never show up. The first and most reasonable idea that occured to me was to deliver certificates with a custom file extension to our users. For this example I will use the .mypfx extension. Using this custom extension will make the iOS workflow even more different from the Android worfklow since every certificate now needs to be renamed before it can be used. Without better alternatives at hand I decided to go with this solution regardless.

 

To support a custom extension we need to edit our iOS app's property list, we can do this by adding the following to our plugin.xml file.

<platform name =  "ios" >
     <config-file target= "*-Info.plist"  parent= "CFBundleDocumentTypes" >
         <array>
             <dict>
               <key>CFBundleTypeName</key>
               <string>Custom Certificate</string>
               <key>LSItemContentTypes</key>
                   <array>
                       <string>customextension.certificate</string>
                   </array>
               <key>LSHandlerRank</key>
               <string>Owner</string>
           </dict>
         </array>
     </config-file>
    
     <config-file target= "*-Info.plist"  parent= "UTExportedTypeDeclarations" >
         <array>
             <dict>
                 <key>UTTypeConformsTo</key>
                 <array>
                     <string> public .data</string>
                 </array>
                 <key>UTTypeDescription</key>
                 <string>Custom PKCS12 extension</string>
                 <key>UTTypeIdentifier</key>
                 <string>customextension.certificate</string>
                 <key>UTTypeTagSpecification</key>
                 <dict>
                     <key> public .filename-extension</key>
                     <string>mypfx</string>
                 </dict>
             </dict>
         </array>
     </config-file>
</platform>

 

If the app is installed, users will now have the option to open a .mypfx file in your app. Opened files will be moved to the "/inbox" folder of your app's sandbox. The "handleOpenUrl" function will be triggered whenever a file is opened by your app, so at the very least your plugin's header file should contain the following functions now:

 

#import<Cordova/CDV.h>
@interface MyPlugin : CDVPlugin
- (void) selectCert:(CDVInvokedUrlCommand*)invokedCommand;
- (void) handleOpenURL:(NSNotification*)notification;
@end

 

You will most likely want to do the following when a file is opened in your app:

  • Check the "/inbox" folder for new files.
  • Prompt the user for a password for every new certificate.
  • Save the certificate to your app's keychain.
  • Remove the certificate from the "/inbox" folder after adding it to the keychain.

 

Implementing these steps takes a little bit of time but should not pose too much of a challenge. An important detail to notice is that you will want to store a 'SecIdentityRef' object to the keychain, not a 'SecCertificateRef', the latter does not give you access to the private key afterwards. Once you have obtained the identity object add it to the keychain:


const void * chainKeys[] = { kSecValueRef }; const void * chainValues[] = { identity }; CFDictionaryRef dict = CFDictionaryCreate(kCFAllocatorDefault, chainKeys, chainValues,1, NULL, NULL); status = SecItemAdd(dict, NULL);

 

Selecting certificates

At this point I had certificates available for use in my app, something I could take for granted on Android. It's been a roundabout trip already but perhaps we can continue with what this plugin was supposed to do in the first place: Selecting a certificate to use for signatures at a later time. In order to do this we will need to retrieve all available certificates from the app's keychain again. Luckily it is not so hard to obtain all available certificates as an array:


OSStatus status = errSecSuccess; CFArrayRef result = NULL; const void * keys[] = {kSecClass, kSecReturnRef, kSecMatchLimit}; const void * values[] = {kSecClassIdentity, kCFBooleanTrue, kSecMatchLimitAll}; CFDictionaryRef dict = CFDictionaryCreate(NULL, keys, values,3, NULL, NULL); status = SecItemCopyMatching(dict, &result); //Error handling

 

The information we want to display to a user can now be extracted from every certificate in the array. For example, we could get a certificate's human-readable summary using the following code:

OSStatus status = errSecSuccess;
SecCertificateRef certificate = NULL;
status = SecIdentityCopyCertificate(identityRef, &certificate);
//Error handling
NSString* summary = (__bridge NSString*)SecCertificateCopySubjectSummary(certificate)];

 

How this info is then presented and how one of the certificates is selected is up to the developer, personally I used a simple actionsheet to present all available certificates. In the end the global outline of my implementation looked something like this:

#import "MyPlugin.h"
@implementation  MyPlugin
 
- ( void )selectCert:(CDVInvokedUrlCommand*)invokedCommand
{
   // Get an array of all available certificates.
   // Display the available certificates to the user.
   // Save the name of the selected certificate into a NSString* (selectedCertificateName).
  NSString* callbackId = [invokedCommand callbackId];
   CDVPluginResult* result = [CDVPluginResult
                              resultWithStatus:CDVCommandStatus_OK
                              messageAsString: selectedCertificateName];
   [self success:result callbackId:callbackId];
}
 
- ( void ) handleOpenURL:(NSNotification*)notification
{
   // Check the app's inbox folder.
   // Prompt the user for certificate passwords.
   // Load certificates into the keychain.
   // Remove the certificate from the inbox folder.
}
 
 
@end

 

Now that your app has access to certificates, you will probably want to use it's associated private key at some point. You can find a certificate using it's name and create a method for extracting the private key:

 

- (SecKeyRef)getPrivateKeyFromIdentity:(SecIdentityRef) identity
{
     OSStatus error = errSecSuccess;
     SecKeyRef pkRef = NULL;
     error = SecIdentityCopyPrivateKey(identity, &pkRef);
     // Error handling
     return  pkRef;
}

 

Finishing up

In order to use this plugin from our Cordova application we need to finish up by adding the following to our plugin.xml file and then reinstalling the plugin to our Cordova project:

<platform name =  "ios" >
     <config-file target= "config.xml"  parent= "/widget" >
         <feature name= "MyPlugin" >
             <param name= "ios-package"  value= "MyPlugin" />
         </feature>
     </config-file>
     <framework src= "Security.framework" />
     <header-file src= "src/ios/MyPlugin.h"  target-dir= "MyPlugin" />
     <source-file src= "src/ios/MyPlugin.m"  target-dir= "MyPlugin" />
</platform>

The final directory structure for my plugin is as follows:

  • Myplugin (root folder)
    • plugin.xml
    • src/
      • android/
        • MyPlugin.java
      • ios/
        • MyPlugin.h
        • MyPlugin.m
    • www/
      • MyPlugin.js

 

Final thoughts

I hope these posts have given you an impression of what it takes to create a cross-platform solution for accessing certificates. While these posts are in no way supposed to be an Android vs iOS discussion, you might be able to guess which platform gave me the least amount of trouble. Due to numerous restrictions found in iOS I have not found a way to gain a similar workflow for both supported platforms, something that annoys me greatly as a developer. Apart from my complaining I hope that this post has been of some help to developers who need to work with certificates in their cross-platform apps. In case anyone has a different (and perhaps better) approach for certificates and iOS I would be very interested in hearing more about it.