SecItemCopyMatching not saving permanent key

I am writing a MacOS app that uses the Apple crypto libraries to create, save, and use an RSA key pair. I am not using a Secure Enclave so that the private key can later the retrieved through the keychain. The problem I am running into is that on my and multiple other systems the creation and retrieval works fine. On a different system -- running MacOS 15.3 just like the working systems -- the SecKeyCreateRandomKey function appears to work fine and I get a key reference back, but on subsequent runs SecItemCopyMatching results in errSecItemNotFound. Why would it appear to save properly on some systems and not others?

var error: Unmanaged<CFError>?
let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                               kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                               .biometryAny,
                               &error)!
let tag = TAG.data(using: .utf8)! // com.example.myapp.rsakey
let attributes: [String: Any] = [
            kSecAttrKeyType as String: KEY_TYPE, // set to kSecAttrKeyTypeRSA
            kSecAttrKeySizeInBits as String: 3072,
            kSecPrivateKeyAttrs as String: [
                kSecAttrIsPermanent as String: true,
                kSecAttrApplicationTag as String: tag,
                kSecAttrAccessControl as String: access,
            ],
        ]
guard let newKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
    throw error!.takeRetainedValue() as Error
}
return newKey

This runs fine on both systems, getting a valid key reference that I can use. But then if I immediately try to pull the key, it works on my system but not the other.

let query = [ kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: tag,
            kSecReturnRef as String: true, ]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
let msg = SecCopyErrorMessageString(status, nil)
if status == errSecItemNotFound {
    print("key not found")
 }
guard status == errSecSuccess else { print("other retrieval error") }
return item as! SecKey

I've also tried a separate query using the secCall function from here (https://developer.apple.com/forums/thread/710961) that gets ALL kSecClassKey items before and after the "create the key" function and it'll report the same amount of keys before and after on the bugged system. On the other machines where it works, it'll show one more key as expected.

In the Signing & Capabilities section of the project config, I have Keychain Sharing set up with a group like com.example.myapp where my key uses a tag like com.example.myapp.rsakey. The entitlements file has an associated entry for Keychain Access Groups with value $(AppIdentifierPrefix)com.example.myapp.

Answered by DTS Engineer in 824938022

It’s hard to say exactly what’s going on here. It very much depends on the context.

First, I recommend that you read TN3137 On Mac keychain APIs and implementations. It describes the difference between the data protection and file-based keychains. If you don’t understand that, none of this makes any sense at all.

Second, is this a Mac Catalyst app? Those always target the data protection keychain [1]. OTOH, standard Mac apps can use both keychain types. So, unless you happen to be using Mac Catalyst, it’s really important that you opt in to the data protection keychain [2].

Finally, my general advice is that you not create a key in the keychain but rather create the key and then add it to the keychain [3]. That gives you more direct control over how the key is added. For example, you can request that the add operation return you a permanent reference, which makes it easy to get back to that key.

This is one of the many suggestions I outline in my two SecItem posts:

I recommend that you read them before continuing.

Share and Enjoy

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

[1] Likewise for iOS Apps on Mac.

[2] If you can. Some programs, like a launchd daemon, can only use the file-based keychain )-:

[3] I’ll admit my approach is less secure, but that only really matters if you care about non-extractability.

It’s hard to say exactly what’s going on here. It very much depends on the context.

First, I recommend that you read TN3137 On Mac keychain APIs and implementations. It describes the difference between the data protection and file-based keychains. If you don’t understand that, none of this makes any sense at all.

Second, is this a Mac Catalyst app? Those always target the data protection keychain [1]. OTOH, standard Mac apps can use both keychain types. So, unless you happen to be using Mac Catalyst, it’s really important that you opt in to the data protection keychain [2].

Finally, my general advice is that you not create a key in the keychain but rather create the key and then add it to the keychain [3]. That gives you more direct control over how the key is added. For example, you can request that the add operation return you a permanent reference, which makes it easy to get back to that key.

This is one of the many suggestions I outline in my two SecItem posts:

I recommend that you read them before continuing.

Share and Enjoy

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

[1] Likewise for iOS Apps on Mac.

[2] If you can. Some programs, like a launchd daemon, can only use the file-based keychain )-:

[3] I’ll admit my approach is less secure, but that only really matters if you care about non-extractability.

Thanks for the response Quinn!

I've made some adjustments to my code based on your response, but still have the same problem. I am still using SecCreateRandomKey but instead of saving to the keychain from this call, I get a reference and use this to save the key with SecItemAdd. I also tried this code first converting the new key into Data, then passing it to SecItemAdd as kSecValueData but that made no difference.

You can see I query for a count of all keys before and after saving. On working systems, the count goes up by one, but on the failing systems the count does not go up but I get no error from SecItemAdd. The countAllKeys is making a call to the secCall function mentioned in the linked forum post. It simply asks for all entries of type kSecClassKey and no limit (i.e. kSecMatchLimitAll), then returns the count of the result. This is an attempt to eliminate the variable of “is the SecItemCopyMatching query correct for finding this specific key”.

This is basically my code, with a few cosmetic edits to simplify.

var error: Unmanaged<CFError>?
guard let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, .biometryAny, &error)
else {
  throw error!.takeRetainedValue() as Error
}
let attributes: [String: Any] = [
  kSecAttrKeyType as String: KeyManager.KEY_TYPE,
  kSecAttrKeySizeInBits as String: 3072,
]
            
guard let newKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
  throw error!.takeRetainedValue() as Error
}
            
let query: [String: Any] = [
  kSecClass as String: kSecClassKey,
  kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
  kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
  kSecAttrApplicationTag as String: "com.example.key".data(using: .utf8)!,
  kSecAttrLabel as String: "com.example.key",
  kSecAttrAccessControl as String: access,
  kSecValueRef as String: newKey,
  kSecAttrIsPermanent as String: true,
  kSecUseDataProtectionKeychain as String: true,
]
print("... Tagged key count: " + String(describing: try countAllKeys())) // see note below
let status = SecItemAdd(query as CFDictionary, nil)
print("... Added key with code: " + String(status))
print("... Tagged key count: " + String(describing: try countAllKeys())) 
guard status == errSecSuccess else {
  print(SecCopyErrorMessageString(status, nil))
}

Adding that the working and non-working systems are M1 Max, so it shouldn't be a hardware difference.

SecItemCopyMatching not saving permanent key
 
 
Q