Problem
One of the requirements of a third-party SDK I am integrating with is to sign an identifier string with the identity from a p12 certificate. This code loads a p12 certificate from the application’s bundle, opens the cert with the specified key, retrieves the key, signs an input string (the requestorID
) with the information in the certificate, and encodes the signed data as a base64 encoded String.
import Security
// ...
func signRequestorId(requestorID: String) -> String? {
guard let certificatePath = NSBundle.mainBundle().pathForResource("Certificate", ofType: "p12"), certificateData = NSData(contentsOfFile: certificatePath) else {
return nil
}
let certificateKey = "certificateKey"
var signedBytes: UnsafeMutablePointer<UInt8>
var privateKey: SecKeyRef?
var myReturnedCertificate: SecCertificateRef?
var signedRequestorId: String?
var status: OSStatus
let options = [kSecImportExportPassphrase as String : certificateKey]
var items: CFArrayRef?
var securityStatus = SecPKCS12Import(certificateData, options, &items)
if securityStatus == errSecSuccess {
// Unwrap optional CFArrayRef of items from the certificate
if let items = items {
// Cast CFArrayRef to Swift Array
let itemsArray = items as [AnyObject]
// Cast CFDictionaryRef as Swift Dictionary
if let myIdentityAndTrust = itemsArray.first as? [String : AnyObject] {
// Get our SecIdentityRef from the PKCS #12 blob
// TODO: Figure out how to avoid force unwrap here
// unsafeBitCast is just as bad
let outIdentity = myIdentityAndTrust[kSecImportItemIdentity as String] as! SecIdentityRef
status = SecIdentityCopyCertificate(outIdentity, &myReturnedCertificate)
if status != errSecSuccess {
print("Failed to retrieve the certificate associated with the requested identity.")
} else {
// Get the private key associated with our identity
status = SecIdentityCopyPrivateKey(outIdentity, &privateKey)
if status != errSecSuccess {
print("Failed to extract the private key from the keystore.")
} else {
// Retrieve the digital signature and sign the requestor
// Unwrap privateKey from optional SecKeyRef
if let privateKey = privateKey {
// Get the maximum size of the digital signature
var signedBytesSize: size_t = SecKeyGetBlockSize(privateKey)
// alloc a buffer to hold the signature
// TODO: See if we can use a Swift Array here instead of C types
signedBytes = UnsafeMutablePointer<UInt8>.alloc(signedBytesSize * strideof(UInt8.self))
memset(signedBytes, 0x0, signedBytesSize)
// We're calling alloc here, so we need to destroy and deinit
defer {
signedBytes.destroy()
signedBytes.dealloc(signedBytesSize * strideof(UInt8.self))
}
// Sign data
if let requestorData = requestorID.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) {
// Generate a digital signature for our requestor from our cert
securityStatus = SecKeyRawSign(privateKey, SecPadding.PKCS1, UnsafePointer(requestorData.bytes), requestorData.length, signedBytes, &signedBytesSize)
if securityStatus == errSecSuccess {
let encryptedBytes = NSData(bytes: signedBytes, length: signedBytesSize)
signedRequestorId = encryptedBytes.base64EncodedStringWithOptions([])
} else {
print("Cannot sign the device id info: failed obtaining the signed bytes.")
}
}
}
}
}
}
}
} else {
print("Cannot sign the device id info: failed importing keystore.")
}
return signedRequestorId
}
-
I put in a few comments to walk through what was going on for my team. They are not familiar with Swift and even less familiar with low level programming with CoreFoundation. I struggled to wrap my head on exactly what is going on when I first wrote this code, so please let me know if the comments can be improved for clarity.
-
I’m able to use Swift types in place of CoreFoundation types (
CFDictionaryRef
,CFArrayRef
, etc.), but how do I retrieve a CoreFoundation type from a Swift dictionary without force-unwrapping? SpecificallymyIdentityAndTrust[kSecImportItemIdentity as String] as! SecIdentityRef
. -
I
alloc
a buffer to hold the signed bytes, but I’m sure I can use a Swift array instead thanks to Martin’s great answer on a different question of mine about AES-128 bit encryption. -
I’m not too happy with the
if-else
structure of this code. Is it bad practice to reuse theOSStatus
variable like this? What other options are there to structure my code?
Solution
-
In a
guard
statement with multiple optional bindings, uselet
also for the second
and subsequent bindings. That is easier to read (and required in Swift 3). Also start
a new line for each clause in theguard
statement:guard let certificatePath = NSBundle.mainBundle().pathForResource("Certificate", ofType: "p12"), let certificateData = NSData(contentsOfFile: certificatePath) else { return nil }
-
Declare variables at the narrowest scope where they are used, and not at the top
of the function. This applies e.g. tovar signedBytes: UnsafeMutablePointer<UInt8> var privateKey: SecKeyRef? var myReturnedCertificate: SecCertificateRef?
-
Early-return in the case of an error to avoid the deep if-nesting.
-
The size (and stride) of
UInt8
is 1, there is no need to multiply. -
A conversion to UTF-8 cannot fail, it is safe to force-unwrap
let requestorData = requestorID.dataUsingEncoding(NSUTF8StringEncoding)!
-
As a function argument,
SecPadding.PKCS1
can be shortened to.PKCS1
because the
type is inferred from the context. -
Try to avoid very long lines.
-
Reusing the
status
variable is acceptable here in my opinion. The only disadvantage
is that you cannot declare it as a constant. Alternatives would be to use
separate variables for each call, or to use a local scope for each call:do { let status = SecPKCS12Import(certificateData, options, &optItems) if status != errSecSuccess { ... } }
If you reuse the
status
variable then there is no need for another
securityStatus
variable.
The function then looks like this:
func signRequestorId(requestorID: String) -> String? {
guard let certificatePath = NSBundle.mainBundle().pathForResource("Certificate", ofType: "p12"),
let certificateData = NSData(contentsOfFile: certificatePath) else {
return nil
}
var status: OSStatus
let certificateKey = "certificateKey"
let options = [kSecImportExportPassphrase as String : certificateKey]
var optItems: CFArrayRef?
status = SecPKCS12Import(certificateData, options, &optItems)
if status != errSecSuccess {
print("Cannot sign the device id info: failed importing keystore.")
return nil
}
guard let items = optItems else {
return nil
}
// Cast CFArrayRef to Swift Array
let itemsArray = items as [AnyObject]
// Cast CFDictionaryRef as Swift Dictionary
guard let myIdentityAndTrust = itemsArray.first as? [String : AnyObject] else {
return nil
}
// Get our SecIdentityRef from the PKCS #12 blob
let outIdentity = myIdentityAndTrust[kSecImportItemIdentity as String] as! SecIdentityRef
var myReturnedCertificate: SecCertificateRef?
status = SecIdentityCopyCertificate(outIdentity, &myReturnedCertificate)
if status != errSecSuccess {
print("Failed to retrieve the certificate associated with the requested identity.")
return nil
}
// Get the private key associated with our identity
var optPrivateKey: SecKeyRef?
status = SecIdentityCopyPrivateKey(outIdentity, &optPrivateKey)
if status != errSecSuccess {
print("Failed to extract the private key from the keystore.")
return nil
}
// Unwrap privateKey from optional SecKeyRef
guard let privateKey = optPrivateKey else {
return nil
}
// Retrieve the digital signature and sign the requestor
// Get the maximum size of the digital signature
var signedBytesSize: size_t = SecKeyGetBlockSize(privateKey)
var signedBytes: UnsafeMutablePointer<UInt8>
// alloc a buffer to hold the signature
signedBytes = UnsafeMutablePointer<UInt8>.alloc(signedBytesSize)
memset(signedBytes, 0x0, signedBytesSize)
// We're calling alloc here, so we need to destroy and deinit
defer {
signedBytes.destroy()
signedBytes.dealloc(signedBytesSize)
}
// Sign data
let requestorData = requestorID.dataUsingEncoding(NSUTF8StringEncoding)!
// Generate a digital signature for our requestor from our cert
status = SecKeyRawSign(privateKey, .PKCS1, UnsafePointer(requestorData.bytes),
requestorData.length, signedBytes, &signedBytesSize)
if status != errSecSuccess {
print("Cannot sign the device id info: failed obtaining the signed bytes.")
return nil
}
let encryptedBytes = NSData(bytes: signedBytes, length: signedBytesSize)
let signedRequestorId = encryptedBytes.base64EncodedStringWithOptions([])
return signedRequestorId
}
You can use a Swift array instead of allocating a buffer:
var signedBytesSize: size_t = SecKeyGetBlockSize(privateKey)
var signedBytes = [UInt8](count: signedBytesSize, repeatedValue: 0)
// Sign data
let requestorData = requestorID.dataUsingEncoding(NSUTF8StringEncoding)!
// Generate a digital signature for our requestor from our cert
status = SecKeyRawSign(privateKey, .PKCS1, UnsafePointer(requestorData.bytes),
requestorData.length, &signedBytes, &signedBytesSize)
Unfortunately, I did not find a good alternative to the forced cast in
let outIdentity = myIdentityAndTrust[kSecImportItemIdentity as String] as! SecIdentityRef
There seems to be no way to use a conditional cast as?
with CoreFoundation types.
You can do a forced cast to SecIdentityRef?
with an optional binding to check if the
value exists at all in the dictionary:
guard let outIdentity = myIdentityAndTrust[kSecImportItemIdentity as String] as! SecIdentityRef?
else { return nil }
I think it is acceptable, because it is documented that the value of the
kSecImportItemIdentity
is a SecIdentityRef
. If you want to be on the safe side,
verify the identity of the object by checking the Core Foundation “type id”:
guard let outIdentityItem = myIdentityAndTrust[kSecImportItemIdentity as String]
where CFGetTypeID(outIdentityItem) == SecIdentityGetTypeID() else {
return nil
}
let outIdentity = outIdentityItem as! SecIdentityRef
Some more suggestions:
- Make
certificatePath
andcertificateKey
parameters of the function. - Instead of printing error messages (which you do only in some error situations)
and returningnil
, make the functionthrow
a custom defined error.
The caller can then decide between calling the function in ado/catch
context,
or simply withtry?
.