SecKeychainItem cast to SecCertificate crashes

Hi,

I'm building on macOS Ventura 13.0.1, usingApple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) and targetting .macOS(.v10_15).

I'm experiencing a crash / exception (illegal hardware instruction) when trying to cast a SecKeychainItem into a SecCertificate. This should be safe according to the docs:

A SecKeychainItem object for a certificate that is stored in a keychain can be safely cast to a SecCertificate for use with Certificate, Key, and Trust Services.

I'm trying to load PKCS12 (PEM-wrapped) data to obtain a SecIdentity from the contained certificate.

To do this I'm loading in an array of SecKeychainItems via SecItemImport.

I can see the resulting output CFArray populated with 2 items as expected. The following is the resulting dump:

▿ Optional(<__NSArrayM 0x600001270210>(
<cert(0x7f804f906fa0) s: Just a test certificate>,
<SecCDSAKeyRef 0x6000012792f0: algorithm id: 1, class=1, algorithm=2a, usage=80000000 attrs=38>
)
)

I extract the SecKeychainItem whose type is a certifcate by finding the item where CFGetTypeID($0) == SecCertificateGetTypeID().

The following is my code:

private static func protection(
    _ passphrase: String
  ) -> SecItemImportExportKeyParameters {
    SecItemImportExportKeyParameters(
      version: UInt32(bitPattern: SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION),
      flags: .noAccessControl,
      passphrase: Unmanaged<AnyObject>.passUnretained(passphrase as AnyObject),
      alertTitle: nil,
      alertPrompt: nil,
      accessRef: nil,
      keyUsage: nil,
      keyAttributes: nil
    )
}

static func load(pkcs12Data: Data, passphrase: String = "") throws -> SecIdentity {

    // setup the import parameters
    let data = pkcs12Data as CFData
    var protection = Self.protection(passphrase)
    var externalFormat: SecExternalFormat = .formatPEMSequence
    var externalItemType: SecExternalItemType = .itemTypeAggregate
    var result: CFArray?

    // import the results
    SecItemImport(
      data,
      nil,
      &externalFormat,
      &externalItemType,
      .pemArmour,
      &protection,
      nil,
      &result
    )

    // cast results to an array of SecKeychainItems
    guard let items = result as? [SecKeychainItem] else {
      print("Failed to cast result to [SecKeychainItem]")
      fatalError()
    }

    // extract the certificate result
    guard let certItem = items.first(where: { CFGetTypeID($0) == SecCertificateGetTypeID() }) else {
      print("Failed to extract certItem from items")
      fatalError()
    }

    // force cast because SecKeychainItem cast will always succeed according to documentation
    // the next line crashes with the exception "illegal hardware instruction"
    print(certItem)
    let certificate = certItem as! SecCertificate

    print("Loading identity from cert:")
    var identity: SecIdentity?
    let identityImportStatus = SecIdentityCreateWithCertificate(nil, certificate, &identity)
    guard identityImportStatus == errSecSuccess,
          let identity = identity else {
      print("Failed to create identity. Status: \(identityImportStatus)")
      fatalError()
    }

    print("Loaded identity: \(identity)")
    return identity
  }

I'd really appreciate any insight or help understanding why the cast (let certificate = certItem as! SecCertificate) causes an exception / crash?

Am I doing something wrong here? How should I obtain the SecCertificate?

Many thanks for reading this :)

Because certItem is nil hence the crash on force unwrap.

Just FYI, SecKeychainItem only really makes sense if you’re using the file-based keychain. With the data protection keychain, SecKeychainItem isn’t really a thing. See TN3137 On Mac keychain APIs and implementations for more background on this.

Casting between SecKeychainItem and SecCertificate is not possible for weird reasons. On the rare occasions that you need to do this, you have to use unsafeBitCast(_:to:). Bletch!

In this case, however, that shouldn’t be necessary. Rather, change result as? [SecKeychainItem] to result as? [CFTypeRef] and you should be all set.


btw, this is not safe:

passphrase: Unmanaged<AnyObject>.passUnretained(passphrase as AnyObject),

The problem is that nothing guarantees that the resulting passphrase value lives beyond the return of the protection(_:) function. Here’s how I usually do it:

let passphrase = Unmanaged.passRetained(password as NSString as CFTypeRef)
defer { passphrase.release() }
var params = SecItemImportExportKeyParameters()
params.version = .init(bitPattern: SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION)
params.passphrase = passphrase
… any other stuff …
err = SecItemImport(…)

By placing the code inline with the call to SecItemImport(…), I ensure that the value lives until the defer block runs.

If you want to maintain your current structure, you’ll need to retain and the balance that with an autorelease.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

SecKeychainItem cast to SecCertificate crashes
 
 
Q