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)];