I'm having several issues with managing certificates in the default keychain using swift on macOS.
I have a self containd command line test program with hardcoded pem format cert and private key.
I can convert both pem formats to der via openssl.
Issue 1, For Certificate: I can create a certificate and add it to the keychain. I am not able to find or delete the certificate after I add it.
Issue 2, For the key: I can create the key but when I try to add it to the keychain I get "A required entitlement isn't present."
In our actual app, I can add certs but can't find them (success but cert returned does not match). I can add keys and find them. All using similar code to my test app, so I decided to write the test and got stuck. I don't see any special entitlements for keychain access in our app.
Looking for answers on issue 1 and issue 2.
I have a self contained public github project here as it won't let me attach a zip: https://github.com/alfieeisenberg/cgcertmgr
It won't let me attach a zip of the project or my source.
In both cases below I tried with just labels, just tags, and both with same results.
Here is how I'm trying to add keys: func addPrivateKeyToKeychain(privateKey: SecKey, label: String) -> Bool { let addQuery: [NSString: Any] = [ kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrLabel: label, kSecAttrApplicationTag: label, kSecValueRef: privateKey ]
let status = SecItemAdd(addQuery as CFDictionary, nil)
if status != errSecSuccess {
if status == errSecDuplicateItem {
print("\(#function): \(#line), Key already exists: errSecDuplicateItem")
}
print("\(#function): \(#line), status: \(status) \(SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error")")
}
return status == errSecSuccess
}
Here is adding certs: func addCertificateToKeychain(certificate: SecCertificate, label: String) -> Bool { let addQuery: [NSString: Any] = [ kSecClass: kSecClassCertificate, kSecAttrLabel: label, kSecAttrApplicationTag: label, kSecValueRef: certificate ]
let status = SecItemAdd(addQuery as CFDictionary, nil)
if status != errSecSuccess {
print("\(#function): \(#line), status: \(status) \(SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error")")
}
return status == errSecSuccess
}
And finding a cert: func findCertificateInKeychain(label: String) -> SecCertificate? { let query: [NSString: Any] = [ kSecClass: kSecClassCertificate, kSecAttrLabel: label, kSecAttrApplicationTag: label, kSecReturnRef: kCFBooleanTrue!, kSecMatchLimit: kSecMatchLimitOne ]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
print("\(#function): \(#line), status: \(status)")
if status != errSecSuccess {
print("\(#function): \(#line), status: \(status) \(SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error")")
}
guard status == errSecSuccess, let certificate = item else {
print("\(#function): \(#line), Certificate not found")
return nil
}
return (certificate as! SecCertificate)
}
Output: ===Trying Certs=== tryCerts(pemCertificate:): 338, Certificate added: true findCertificateInKeychain(label:): 272, status: -25300 findCertificateInKeychain(label:): 274, status: -25300 The specified item could not be found in the keychain. findCertificateInKeychain(label:): 277, Certificate not found tryCerts(pemCertificate:): 340, Certificate found: nil deleteCertificateFromKeychain(label:): 314, status: -25300 The specified item could not be found in the keychain. tryCerts(pemCertificate:): 342, Certificate deleted: false ===Trying Keys=== addPrivateKeyToKeychain(privateKey🏷️ ): 256, status: -34018 A required entitlement isn't present. Program ended with exit code: 0
[Just when you thought that the keychain API couldn’t get any weirder…]
First up, some background info:
-
TN3137 On Mac keychain APIs and implementations explains the difference between the data protection and file-based keychain.
-
SecItem: Fundamentals and SecItem: Pitfalls and Best Practices provide lots of important backstory, including more details about the keychain shim on macOS.
-
Sharing access to keychain items among a collection of apps explains how the data protection keychain uses entitlements for access control.
-
TN3125 Inside Code Signing: Provisioning Profiles talks about restricted entitlements and provisioning profiles.
-
SecItem attributes for keys has some info about the semantics of each of the label-ish keychain attributes.
You’ve described two issues here:
-
When you add a certificate to the keychain with a custom label, you can’t then find that certificate using that label.
-
When you add a keychain to the keychain from your command-line tool, that fails with -34018.
I’ll explain the cause of each in turn, and then offer advice as to how to proceed.
The first issue is one of the many incompatibilities caused by the keychain shim on macOS. When you add an object reference to the keychain using kSecValueRef
, the system populates the keychain attributes from the attributes associated with that object. That raises the question of what happens when you also specify attributes via the dictionary. For example, if the object has an internal label and you specify a label yourself using kSecAttrLabel
, what do you get?
Annoyingly, the results differ between the file-based keychain, through the shim, and the data protection keychain:
-
The shim uses the label attached to the certificate object.
-
The data protection keychain uses the label you supply via
kSecAttrLabel
.
)-:
Hence the behaviour you’re seeing. The certificate ends up in the file-based keychain with a kSecAttrLabel
value of testdeletejune27.invisinet.com
, so you can’t find it when you query for a certificate with the kSecAttrLabel
of com.example.mycert
.
The second issues seems to be related to a recent change in SecItemAdd
when dealing with keys. In general, SecItemAdd
no macOS goes through the shim and thus targets the file-based keychain by default. However, it seems that on modern systems (I’m testing on macOS 14.5) it’s targeting the data protection keychain. So, your key ends up in the data protection keychain, which is all well and good until you try to do this from a command-line tool.
The data protection keychain relies on keychain access groups, and the use of keychain access groups is mediated by entitlements. Those are restricted entitlements, which means that they must be authorised by a provisioning profile. That’s problematic for a command-line tool because there’s no place to store that profile (r. 125850707)
So, when you call SecItemAdd
it tries to add the key to the data protection keychain, which fails because you don’t have the entitlements necessary to establish a default keychain access group, and hence the -34018 error, aka errSecMissingEntitlement
.
So, what should you do? First up, stop using kSecAttrLabel
. Regardless of what else is going on here, that attribute is meant for a user-visible label, not for you the app developer. The correct option is kSecAttrApplicationTag
.
Now, that attribute is only supported on keys, not certificates. What to do with certificates is tricky. Most folks who import a certificate and a private key are trying to form a digital identity. Is that what you’re doing here? If so, you don’t need to label the certificate because you can search for a digital identity and then identify both components using the label from the private key component.
If you’re not trying to form a digital identity then things get trickier. In that case, please post back with more details about how you’re tracking certificates.
Regarding the errSecMissingEntitlement
error, you need to make a decision about whether you want to target the data protection keychain or the file-based keychain. IMO it’s better to use the former rather than the latter. Quoting TN3137:
The file-based keychain is on the road to deprecation.
If you want to stick with the file-base keychain, the simple fix is to pass false to the kSecUseDataProtectionKeychain
on SecItemAdd
.
If you want to move forward to the data protection keychain, you need to sign your command-line tool with the necessary entitlements (specifically, the App ID entitlement). This is possible, but a little painful. I wrote up my recommendations in Signing a daemon with a restricted entitlement.
*phew*
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"