First of all, thank you for the superbly detailed response! Quinn always delivers.
So, it sounds like the data protection keychain is the way to go and more portable between macOS and iOS and we are implementing on both. I created a test .app to call my code, added appropriate entitlement, and your suggestions work so far, I can add a key with a tag, find it, etc.
For tracking identities we just generate a GUID to store in core data with some other app specific items for each identity.
When we run our app of course we would like to find the cert/key pair using that ID.
Here is what I plan on doing.
Add the key with a tag (GUID)
Add the certificate
When I need to grab the key/cert I will
a) find the key using that tag
b) find the cert associated with that key
Of course we would also like to delete the identities when we no longer need them.
I'm not clear on your statement: "identify both components using the label from the private key component."
You mentioned I should stop using labels, so I'm not sure what "label" means in this context.
How do I go about finding the cert once i have the key?
Is there an easy way to identify the key associated with a certificate in the keychain access app?
Is there any advantage to getting the certs to show up in the MyCertificates section I see in keychain app or to have our own keychain?
alf
Post
Replies
Boosts
Views
Activity
Sorry, yes, I am trying to create/manage identities.
On a side note I am concerned about the one to may thing with keys and certificates and how to manage that possibility. Multiple certs for a key may (hopefully) be an error condition for us but I still have to handle it in the code.
Back to the main problem. Using the data protection keychain I can add, find, delete keys no problem using kSecAttrApplicationTag. I was also able to add the cert previously discussed without specifying any tag or label but am not able to find it with the label testdeletejune27.invisinet.com, so maybe that label isn't assigned with the data security keychain? In addition, it seems that certs in this keychain aren't visible with the keychain access app or apparently security find-certificate -a, so it's disconcerting not having another way to verify the state of things. Also, now that my cert is added I have no way of deleting it and I know it's there as I get an already exists error if I try to add it again.
I think I understand now that I need to find the cert from the key and I have an example of that via the AI bots, so can move in that direction. Once I can find the cert, I am still not sure how to delete it, since SecItemDelete needs a query that works.
This example I found seems like it may be a good way to manage these identities. Is this the right approach? what about the one to many issue? I'm not sure we will have control as far as how the customer will generate their certs.:
var items = [String: Any]()
items[kSecClass as String] = kSecClassIdentity
items[kSecAttrApplicationTag as String] = tag.data(using: .utf8)
items[kSecImportExportPassphrase as String] = kCFNull // Optional passphrase
items[kSecValueRef as String] = SecPKCS12Import(certificateData: certificateData as CFData, keyData: privateKeyData as CFData, options: [:])
let status = SecItemAdd(items as CFDictionary, nil)
return status
}
func findIdentity(forKeyTag tag: String) -> SecIdentity? {
var query = [String: Any]()
query[kSecClass as String] = kSecClassIdentity
query[kSecAttrApplicationTag as String] = tag.data(using: .utf8)
query[kSecReturnRef as String] = kCFBooleanTrue!
query[kSecUseDataProtectionKeychain as String] = true
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let itemRef = result as? SecCFTypeRef {
return itemRef as? SecIdentity
} else {
// Handle error (e.g., no item found)
print("Error searching keychain: \(status)")
return nil
}
}
func deleteIdentity(forKeyTag tag: String) -> OSStatus {
var query = [String: Any]()
query[kSecClass as String] = kSecClassIdentity
query[kSecAttrApplicationTag as String] = tag.data(using: .utf8)
let status = SecItemDelete(query as CFDictionary)
return status
}
Sorry, missed the first line of that first function:
func addIdentityToKeychain(certificateData: Data, privateKeyData: Data, tag: String) -> OSStatus {
Also, would this code work the same on iOS?
I also now realize I didn't notice that example wasn't using the data protection keychain
When I was able to add a key to the data protection keychain, I then tried the following code to look for all certs, but I think it didn't like the kSecUseDataProtectionKeychain attribute there, as I got:
findCertificatesFromKey(key:): 358, certStatus: -25303 The specified attribute does not exist.
code:
func findCertificatesFromKey(key: SecKey) -> [SecCertificate]? {
let certQuery: [String: Any] = [
kSecUseDataProtectionKeychain as String: true,
kSecClass as String: kSecClassCertificate,
kSecAttrKeyType as String: kSecAttrKeyTypeRSA, // or other key type as required
kSecReturnRef as String: true,
kSecMatchLimit as String: kSecMatchLimitAll
]
var certItems: CFTypeRef?
let certStatus = SecItemCopyMatching(certQuery as CFDictionary, &certItems)
if certStatus == errSecSuccess {
if let certArray = certItems as? [SecCertificate] {
for certificate in certArray {
// Here you can check if the certificate is associated with the key
print("Certificate found: \(certificate)")
// You can use SecCertificateCopyValues to get more info about the certificate
}
return certArray
} else {
print("No certificates found")
}
} else {
print("\(#function): \(#line), certStatus: \(certStatus) \(SecCopyErrorMessageString(certStatus, nil) as String? ?? "Unknown error")")
}
return nil
}
After reading your links and suggestions I decided to try working in the identity space as you suggested.
On iOS, I can convert my key/cert combo to PKCS12 and import it, but when I try to add the identity to the keychain I get: status: -50 One or more parameters passed to a function were not valid. Any idea what param may be wrong? Tried removing tag, password, data protection, still same error.
On macOS, the SecPKCS12Import fails with: status: -25264 MAC verification failed during PKCS12 import (wrong password?) I get the same if I pass "" for the password or just omit it.
func addIdentityToKeychain(certificatePEM: String, privateKeyPEM: String, tag: String) -> OSStatus {
guard let certificateData = certificatePEM.data(using: .utf8) else {
print("Error converting certificate PEM to data")
return errSecParam
}
guard let privateKeyData = privateKeyPEM.data(using: .utf8) else {
print("Error converting private key PEM to data")
return errSecParam
}
guard let secCertificate = convertPEMToSecCertificate(certificatePEM) else {
print("Error converting PEM TO SecCertificate")
return errSecParam
}
guard let secKey = convertPEMToSecKey(privateKeyPEM) else {
print("Error converting PEM TO SecKey")
return errSecParam
}
let (pkcs12Data, err) = createPKCS12Data(certificate: secCertificate, key: secKey, p12Name: "p12Name", p12Password: "")
guard let pkcs12Data = pkcs12Data else {
return errSecParam
}
var items: CFArray?
let status = SecPKCS12Import(pkcs12Data as NSData, [kSecImportExportPassphrase as String: ""] as CFDictionary, &items)
if status != errSecSuccess {
print("\(#function): \(#line), SecPKCS12Import failed, status: \(status) \(SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error")")
return status
}
if let importedItems = items as? Array<Any> {
guard let identity = importedItems.first else {
return errSecItemNotFound
}
let attrs = [
kSecClass: kSecClassIdentity,
kSecAttrApplicationTag: tag,
kSecImportExportPassphrase: kCFNull!, // Optional passphrase (or your CFString passphrase)
kSecValueRef: identity,
kSecUseDataProtectionKeychain: true
] as NSDictionary
let stat = SecItemAdd(attrs, nil)
if stat != errSecSuccess {
print("\(#function): \(#line), SecItemAdd of identity failed, status: \(stat) \(SecCopyErrorMessageString(stat, nil) as String? ?? "Unknown error")")
return stat
}
}
return errSecSuccess
}
Any ideas?
alf
Progress. I noticed the debugger that the "identity" returned by SecPKCS12Import was actually an array where the first element was a dictionary with identity, trust and chain in there. So, I was able to pull out what I think is the real identity:
let dics = items! as! Array<Dictionary<String, Any>>
let firstItem = dics[0]
let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity?
And as long as I don't use the: kSecAttrApplicationTag: tag, element in the query I can add the identity with SecItemAdd. If I try to add a tag to it, I get status: -25303 The specified attribute does not exist.
So I can add it but I won't be able to find/delete later if I can't put a tag on it.
Any suggestions?
Adding a password worked for the pkcs12import, thank you.
On iOS I was able to add/find/delete if I add the identity using kSecAttrLabel and search with that. On mac, add and delete work fine with the same code but find using kSecAttrLabel label fails to find it. I don't have to add key or cert individually, and I can extract them from the identity if I need them.
Reading your link about attributes, it says this is the user visible portion in the keychain access app, but it is not a component of key uniqueness. Is this true for identites? If I use a lable with a UUID in it, it makes it unique for our concern, but maybe what you're saying is that it's not enforced by the keychain? Also, the data protection keychain doesn't show up in the keychain access app so that part is moot I guess.
Previously you suggested not to use kSecAttrLabel, but it works except for the find on mac so far. Is that a "feature" of the shim on mac?
This seems like a clean sequence to me except for the find not working.
I'm not clear on the sequence you are suggesting.
It sounds like you are suggesting I add the key with SecAttrApplicationTag and certificate with no tag since that attribute is not supported for certs to the keychain in addition to adding the identity.
Will I be able to search for the identity by the tag I use for the key?
Are you suggesting sequence of:
add identity with no tag or label
add key with SecAttrApplicationTag
add cert (maybe not necessary since I can extract from identity later)?
I just reread the Pitfalls and Best Practices again to remind me why I should use certain attrbutes, so looking for the magic sequence so I can add, find, and delete identities.
Is the SecAttrApplicationTag supported by CopyMatching, and if so will it find the identity that contains the key I added separately with a SecAttrApplicationTag? if so, I guess this will get my find and delete for me. Will give that a try.
If I add the key and the cert separately, will I be able to find them as an identity without adding the identity explicitly with SecItemAdd or do I still need to do that?
Sorry for all the questions, but some of these details are not obvious.
alf
latest code segment:
func addIdentityToKeychain(certificatePEM: String, privateKeyPEM: String, tag: String) -> OSStatus {
guard let certificateData = certificatePEM.data(using: .utf8) else {
print("Error converting certificate PEM to data")
return errSecParam
}
guard let privateKeyData = privateKeyPEM.data(using: .utf8) else {
print("Error converting private key PEM to data")
return errSecParam
}
guard let secCertificate = convertPEMToSecCertificate(certificatePEM) else {
print("Error converting PEM TO SecCertificate")
return errSecParam
}
guard let secKey = convertPEMToSecKey(privateKeyPEM) else {
print("Error converting PEM TO SecKey")
return errSecParam
}
let (pkcs12Data, err) = createPKCS12Data(certificate: secCertificate, key: secKey, p12Name: "p12Name", p12Password: "p12pass")
guard let pkcs12Data = pkcs12Data else {
return errSecParam
}
do {
try writeDataToFilePath(data: pkcs12Data, filePath: "/Users/alfred_eisenberg/Downloads/pkcs12.p12")
}
catch {
print("Error writing data to file: \(error)")
}
var items: CFArray?
let status = SecPKCS12Import(pkcs12Data as NSData, [kSecImportExportPassphrase: "p12pass"] as NSDictionary, &items)
if status != errSecSuccess {
print("\(#function): \(#line), SecPKCS12Import failed, status: \(status) \(SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error")")
return status
}
let dics = items! as! Array<Dictionary<String, Any>>
let firstItem = dics[0]
let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity?
var privateKey: SecKey?
let statusKey = SecIdentityCopyPrivateKey(identity!, &privateKey)
guard statusKey == errSecSuccess else {
return statusKey
}
var certificate: SecCertificate?
let statuscert = SecIdentityCopyCertificate(identity!, &certificate)
guard statuscert == errSecSuccess else {
return statuscert
}
// dumpCertFromSecCertificate(cert: certificate!)
let attrs = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: tag.data(using: .utf8)!,
// kSecAttrApplicationTag: tag.data(using: .utf8)!,
// kSecAttrApplicationLabel: tag.data(using: .utf8)!,
// kSecImportExportPassphrase: kCFNull!, // Optional passphrase (or your CFString passphrase)
kSecValueRef: identity!,
kSecUseDataProtectionKeychain: true
] as NSDictionary
let stat = SecItemAdd(attrs, nil)
if stat != errSecSuccess {
print("\(#function): \(#line), SecItemAdd of identity failed, status: \(stat) \(SecCopyErrorMessageString(stat, nil) as String? ?? "Unknown error")")
return stat
}
print("\(#function): \(#line), SecItemAdd of identity Succeeded")
dumpCertFromSecCertificate(cert: certificate!)
return errSecSuccess
}
func findIdentity(forKeyTag tag: String) -> SecIdentity? {
var query = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: tag.data(using: .utf8)!,
// kSecAttrApplicationLabel: tag.data(using: .utf8),
// kSecAttrApplicationTag: = tag.data(using: .utf8),
kSecReturnRef : kCFBooleanTrue!,
kSecUseDataProtectionKeychain: true
] as NSDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let item = result {
print("\(#function): \(#line), Find identity Succeeded")
return item as! SecIdentity // Forced cast (use with caution)
} else {
// Handle error (e.g., no item found)
print("\(#function): \(#line), Can't find identity with tag: \(tag), status: \(status) \(SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error")")
}
return nil
}
func deleteIdentity(forKeyTag tag: String) -> OSStatus {
var query = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: tag.data(using: .utf8)!,
// kSecAttrApplicationLabel: tag.data(using: .utf8),
// kSecAttrApplicationTag: tag.data(using: .utf8),
kSecUseDataProtectionKeychain: true
] as NSDictionary
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess {
// Handle error (e.g., no item found)
print("\(#function): \(#line), Can't delete identity with tag: \(tag), status: \(status) \(SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error")")
}
print("\(#function): \(#line), Delete identity Succeeded")
return status
}
I was using group.com.company.appname but in the documentation I saw that on mac that we should use group.$TeamIdentifierPrefix)com.company.appname
Still get popups either way. Is that what you are asking for?
if the entitlments are wrong I'm wondering why they were able to share data in that container in Sonoma.
I did mention in first message it was sandboxed.
What screwed me up a bit was what I assume is an xcode 16 bug/feature. If you edit the entitlements in build settings after you alread put the right format in the entitlements file it jams the group. in there. Anyway, managed to get it working on mac and ios once I got the provisioning set and platform specific entitlement files