2
\$\begingroup\$

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? Specifically myIdentityAndTrust[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 the OSStatus variable like this? What other options are there to structure my code?

\$\endgroup\$
2
\$\begingroup\$
  • In a guard statement with multiple optional bindings, use let 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 the guard 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. to

    var 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 and certificateKey parameters of the function.
  • Instead of printing error messages (which you do only in some error situations) and returning nil, make the function throw a custom defined error. The caller can then decide between calling the function in a do/catch context, or simply with try?.
\$\endgroup\$
  • \$\begingroup\$ Thanks Martin. One small enhancement: I no longer need to declare signedBytesSize as a size_t since the count parameter of the Array initializer takes an Int. As always, great answer. \$\endgroup\$ – JAL Aug 29 '16 at 15:12
  • \$\begingroup\$ I'm trying to use this code on a Swift command line tool, but the SecKeyRaw functions dont seem available, any clue what I can do? \$\endgroup\$ – Kevin R Apr 10 '17 at 21:57
  • 1
    \$\begingroup\$ @KevinR: SecKeyRawSign is not available on macOS, only on iOS. Compare stackoverflow.com/questions/7966829/…. \$\endgroup\$ – Martin R Apr 11 '17 at 3:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.