Retrieving private-public-certificate triples from keychain

Hi there, I'm currently working on a compatibility feature for Apple that allows the user to manage their keys and certificates from within our internal API. For this I need to retrieve all the items contained within keychains. I am looking at the documentation for SecItem API but so far I have not really found an obvious way to link these items together. My best guess so far is to perform two queries, grabbing all SecKeys from the keychains, pairing them up with public keys through SecKeyCopyPublicKey, then downloading all CertItems and pairing them with public keys with SecCertificateCopyKey, and then join the two using public keys. This sounds however somewhat involved and I was wondering if there was a better way of going about the process?

Answered by DTS Engineer in 814638022

What platform are you targeting?

This matters because the keychain story is radically different on macOS than on our other platforms. See TN3137 On Mac keychain APIs and implementations.

the user to manage their keys and certificates from within our internal API.

By “user” do you mean a developer that calls your API?

It sounds like you’re trying to work with digital identities, that is, the combination of a certificate and the private key that matches the public key within that certificate. Is that right?

If so, the SecItem API has a specific class for that, kSecClassIdentity. This works in terms of SecIdentity objects.

Digital identities aren’t stored as such, by instead synthesised by combining key and certificate items. I talk about this in the Digital Identities Aren’t Real section of SecItem: Pitfalls and Best Practices.

Oh, and speaking of that you’ll probably want to real it and its platonic life partner, SecItem: Fundamentals.

If you have follow-up questions, feel free to post them here.

Share and Enjoy

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

Accepted Answer

What platform are you targeting?

This matters because the keychain story is radically different on macOS than on our other platforms. See TN3137 On Mac keychain APIs and implementations.

the user to manage their keys and certificates from within our internal API.

By “user” do you mean a developer that calls your API?

It sounds like you’re trying to work with digital identities, that is, the combination of a certificate and the private key that matches the public key within that certificate. Is that right?

If so, the SecItem API has a specific class for that, kSecClassIdentity. This works in terms of SecIdentity objects.

Digital identities aren’t stored as such, by instead synthesised by combining key and certificate items. I talk about this in the Digital Identities Aren’t Real section of SecItem: Pitfalls and Best Practices.

Oh, and speaking of that you’ll probably want to real it and its platonic life partner, SecItem: Fundamentals.

If you have follow-up questions, feel free to post them here.

Share and Enjoy

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

If so, the SecItem API has a specific class for that, kSecClassIdentity. This works in terms of SecIdentity objects.

Ah, thank you, that's exactly what I was looking for.

Okay I've made some progress, to better understand the structure and the data that is stored within the keychain I tried debugPrinting out all the attributes within the identity, however it seems like only the kSecClassCertificate related attributes are present, and none of the kSecClassKey, even though I can retrieve the private key:

let query: [String: Any] = [kSecClass as String: kSecClassIdentity,
                            kSecMatchLimit as String: kSecMatchLimitAll,
                            kSecReturnAttributes as String: true,
                            kSecReturnRef as String: true,
];
var item: CFTypeRef?;
let status = SecItemCopyMatching(query as CFDictionary, &item);
debugPrint(status);
let output = item as! [[String: Any]];
for entry in output
{
    do {
        debugPrint("Certificate attributes:");
        let label = entry[kSecAttrLabel as String];
        debugPrint("Label: ", label!);
        let certType: CSSM_CERT_TYPE = entry[kSecAttrCertificateType as String] as! CSSM_CERT_TYPE;
        debugPrint("Cert item type: ", certType);
        let issuer = entry[kSecAttrIssuer as String];
        debugPrint("Issuer: ", String(data: issuer! as! Data, encoding: .utf8)!);
        let serialNumber = entry[kSecAttrSerialNumber as String];
        debugPrint("Serial number: ", serialNumber!);
        var cert: SecCertificate?;
        SecIdentityCopyCertificate(entry[kSecValueRef as String] as! SecIdentity, &cert);
        debugPrint("Certificate: ", cert!);
        debugPrint();
    }
    do {
        debugPrint("Key attributes:");
        let keyClass = entry[kSecAttrKeyClass as String];
        debugPrint("Key class: ", keyClass);
        let keyType = entry[kSecAttrKeyType as String];
        debugPrint("Key type: ", keyType);
        let applicationLabel = entry[kSecAttrApplicationLabel as String];
        debugPrint("Application label: ", applicationLabel);
        let applicationTag = entry[kSecAttrApplicationTag as String];
        debugPrint("Application tag: ", applicationTag);
        let sizeInBits = entry[kSecAttrKeySizeInBits as String];
        debugPrint("Size in bits: ", sizeInBits);
        let effectiveKeySize = entry[kSecAttrEffectiveKeySize as String];
        debugPrint("Effective key size: ", effectiveKeySize);
        var pkey: SecKey?;
        SecIdentityCopyPrivateKey(entry[kSecValueRef as String] as! SecIdentity, &pkey);
        debugPrint("Private key: ", pkey!);
        debugPrint();
    }
}
it seems like only the kSecClassCertificate related attributes are present, and none of the kSecClassKey

For a digital identity, kSecReturnAttributes should return a dictionary containing both the certificate and the key attributes. And just to be sure I wrote a small test for that. Consider this code:

func test() throws {
    let identity = try secCall { Bundle.main.identityNamed("Frankie", password: "test") }
    try secCall { SecItemAdd([
        kSecValueRef: identity
    ] as NSDictionary, nil) }
    let attrsForIdentities = try secCall { SecItemCopyMatching([
        kSecClass: kSecClassIdentity,
        kSecMatchLimit: kSecMatchLimitAll,
        kSecReturnAttributes: true,
    ] as NSDictionary, $0) } as! [[String: Any]]
    for attrs in attrsForIdentities {
        print("--)")
        for (k, v) in attrs.sorted(by: { $0.key < $1.key }) {
            print("\(k): \(v)")
        }
    }
}

Note This uses the SecItem helpers from here and the bundle helper shown below.

When I ran this (on the iOS 18.1 simulator) I see the following output:

accc: <SecAccessControlRef: dk>
agrp: SKMME9E2Y8.com.example.apple-samplecode.Test768855
asen: 0
atag: 
bsiz: 2048
cdat: 2024-12-12 16:22:11 +0000
cenc: 3
crtr: 0
ctyp: 3
decr: 1
drve: 0
edat: 2001-01-01 00:00:00 +0000
encr: 0
esiz: 2048
extr: 1
issr: {length = 31, bytes = 0x3110300e 06035504 030c074d 6f757365 ... 03550406 13024742 }
kcls: 1
klbl: {length = 20, bytes = 0xf753bddd552c9e7934f879ff1337033b4ea1ee8f}
labl: Frankie
mdat: 2024-12-12 16:22:11 +0000
modi: 1
musr: {length = 0, bytes = 0x}
next: 0
pdmn: dk
perm: 1
pkhh: {length = 20, bytes = 0xf753bddd552c9e7934f879ff1337033b4ea1ee8f}
priv: 1
sdat: 2001-01-01 00:00:00 +0000
sens: 0
sha1: {length = 20, bytes = 0xf31ababa6a2f595066c6cc1464608ac18f3e0be9}
sign: 1
slnr: {length = 1, bytes = 0x02}
snrc: 0
subj: {length = 31, bytes = 0x3110300e 06035504 030c0746 72616e6b ... 03550406 13024742 }
sync: 0
tomb: 0
type: 42
unwp: 1
vrfy: 0
vyrc: 0
wrap: 0

This includes:

  • kSecAttrSerialNumber (slnr), which is a certificate attribute

  • kSecAttrApplicationTag (atag), which is a key attribute

Oh, one thing. Earlier I wrote:

What platform are you targeting?

and you never replied. That’s important because the keychain gets super complex on macOS (see TN3137). So, if you’re on iOS or any of its child platforms, the above test is valid. If you’re on macOS… well… we need to talk |-:

Share and Enjoy

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


extension Bundle {
    
    func identityNamed(_ name: String, password: String) -> SecIdentity? {
        guard
            let pkcs12URL = self.url(forResource: name, withExtension: "p12"),
            let pkcs12Data = try? Data(contentsOf: pkcs12URL)
        else {
            return nil
        }
        var importedCF: CFArray? = nil
        let err = SecPKCS12Import(pkcs12Data as NSData, [
            kSecImportExportPassphrase: password
        ] as NSDictionary, &importedCF)
        guard err == errSecSuccess else {
            return nil
        }
        guard
            let importedNS = importedCF as NSArray?,
            let imported = importedNS as? [[String:Any]],
            let firstImport = imported.first,
            let identityAny = firstImport[kSecImportItemIdentity as String],
            CFGetTypeID(identityAny as CFTypeRef) == SecIdentityGetTypeID()
        else {
            return nil
        }
        return (identityAny as! SecIdentity)
    }
}
Retrieving private-public-certificate triples from keychain
 
 
Q