mTLS : Guidance on Generating SecIdentity with Existing Private Key and Certificate

Hello,

I am currently working on iOS application development using Swift, targeting iOS 17 and above, and need to implement mTLS for network connections.

In the registration API flow, the app generates a private key and CSR on the device, sends the CSR to the server (via the registration API), and receives back the signed client certificate (CRT) along with the intermediate/CA certificate. These certificates are then imported on the device.

The challenge I am facing is pairing the received CRT with the previously generated private key in order to create a SecIdentity.

Could you please suggest the correct approach to generate a SecIdentity in this scenario? If there are any sample code snippets, WWDC videos, or documentation references available, I would greatly appreciate it if you could share them.

Thank you for your guidance.

Answered by DTS Engineer in 854714022

I’m presuming you want the resulting digital identity to be persistent, that is, to survive your app being terminated and relaunched. If so, you need to store the private key and the certificate somewhere, and the best place for storing credentials like this is the keychain.

Standard practice here is:

  1. Generate the private key in the keychain.
  2. Derive the public key from the private key.
  3. Export the public key bits and send that your certificate issuing infrastructure.
  4. Get back the certificate.
  5. Add that to your keychain.
  6. Get a digital identity from the keychain.

There are various APIs to support this:

  • In step 1, use SecKeyCreateRandomKey.
  • In step 2, use SecKeyCopyPublicKey.
  • In step 3, use SecKeyCopyExternalRepresentation.
  • In step 4, if the server sends you back a PEM, you’ll need to decode that to get a DER. You can then import that using SecCertificateCreateWithData.
  • In step 5, use SecItemAdd.
  • In step 6, use SecItemCopyMatching.

We have a bunch of resources that can help with this. I recommend that you review the Security Resources pinned post. I specifically recommend:

All of the above works for RSA or EC keys. If you’re only interested in EC keys, you can:

  • Do some of these steps more easily using Apple CryptoKit.
  • Protect the key with the Secure Enclave.

Let me know if that’s the case and I can point you at more resources.

Share and Enjoy

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

I’m presuming you want the resulting digital identity to be persistent, that is, to survive your app being terminated and relaunched. If so, you need to store the private key and the certificate somewhere, and the best place for storing credentials like this is the keychain.

Standard practice here is:

  1. Generate the private key in the keychain.
  2. Derive the public key from the private key.
  3. Export the public key bits and send that your certificate issuing infrastructure.
  4. Get back the certificate.
  5. Add that to your keychain.
  6. Get a digital identity from the keychain.

There are various APIs to support this:

  • In step 1, use SecKeyCreateRandomKey.
  • In step 2, use SecKeyCopyPublicKey.
  • In step 3, use SecKeyCopyExternalRepresentation.
  • In step 4, if the server sends you back a PEM, you’ll need to decode that to get a DER. You can then import that using SecCertificateCreateWithData.
  • In step 5, use SecItemAdd.
  • In step 6, use SecItemCopyMatching.

We have a bunch of resources that can help with this. I recommend that you review the Security Resources pinned post. I specifically recommend:

All of the above works for RSA or EC keys. If you’re only interested in EC keys, you can:

  • Do some of these steps more easily using Apple CryptoKit.
  • Protect the key with the Secure Enclave.

Let me know if that’s the case and I can point you at more resources.

Share and Enjoy

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

Hello,

Thank you for sharing the provided solution. I followed all the steps and used the same API set. However, while retrieving the SecIdentity using the certificate fetch API SecItemCopyMatching, I am encountering the following error:

Error: Could not retrieve client identity, status = -25300

Please find the relevant code snippets below:

Step 1 to 3 : Generate the private key in the keychain. Derive the public key from the private key. Export the public key bits and send that your certificate issuing infrastructure.

func generateCSR(_ deviceId: String? = UserProfile.deviceId) -> String? {
        do {
            // Define subject DN for CSR
            let subject = try DistinguishedName([
                .init(type: .NameAttributes.countryName, printableString: "US"),
                .init(type: .NameAttributes.stateOrProvinceName, printableString: "stateProvince_Name"),
                .init(type: .NameAttributes.localityName, printableString: "locality_Name"),
                .init(type: .NameAttributes.organizationName, printableString: "organization_Name"),
                .init(type: .NameAttributes.organizationalUnitName, printableString: "Engineering"),
                .init(type: .NameAttributes.commonName, utf8String: deviceId ?? "NA"),
            ])
            
            // Application tag for persistent key storage in Keychain
            let tagData = MTLSTag.clientkey.data(using: .utf8)! as NSData

            // Generate EC keypair (private key in Keychain, permanent)
            let keyAttributes: [String: Any] = [
                kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
                kSecAttrKeySizeInBits as String: 256,
                kSecAttrIsPermanent as String: true,          // retain key in Keychain
                kSecAttrApplicationTag as String: tagData     // allow lookup by tag
            ]
            // In step 1, use SecKeyCreateRandomKey.
            var error: Unmanaged<CFError>?
            guard let privateKey = SecKeyCreateRandomKey(keyAttributes as CFDictionary, &error) else {
                print("Failed to create private key: \(error!.takeRetainedValue())")
                return nil
            }
            // In step 2, use SecKeyCopyPublicKey.
            // Extract public key (needed for CSR building)
            let publicKey = SecKeyCopyPublicKey(privateKey)!
            
            // Wrap private key so Swift `Certificate.PrivateKey` can use it
            let wrappedPrivateKey = try Certificate.PrivateKey(privateKey)
            // In step 3, use SecKeyCopyExternalRepresentation.
            // (Optional) Validate we can export the public key (for debugging or backend raw SPKI cases)
            if let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? {
                print("Public key exported, length: \(publicKeyData.count) bytes")
            }

            // Extensions to request in CSR
            let extensions = try Certificate.Extensions {
                BasicConstraints.notCertificateAuthority
                KeyUsage(digitalSignature: true, keyCertSign: true)
            }

            let extensionRequest = ExtensionRequest(extensions: extensions)
            let csrAttributes = try CertificateSigningRequest.Attributes([.init(extensionRequest)])

            // Build CSR object with subject & keypair
            let csr = try CertificateSigningRequest(
                version: .v1,
                subject: subject,
                privateKey: wrappedPrivateKey,
                attributes: csrAttributes,
                signatureAlgorithm: .ecdsaWithSHA256
            )

            // Double-check CSR is signed correctly
            guard csr.publicKey.isValidSignature(csr.signature, for: csr) else {
                print("Invalid CSR signature")
                return nil
            }

            // Return PEM string
            return try csr.serializeAsPEM().pemString
        } catch {
            print("CSR generation failed: \(error)")
            return nil
        }
    }

Step 4 : The server sends you back a PEM, convert into DER

class CertificateUtils {
    static func getCRTCertificate() -> SecCertificate? {
        if let certificateString = UserAccount.getUserCRT() {
            return Certificates.convertStringToCertificate(certificateString: certificateString)
        }
        return nil
    }
    
    static func getCACertificates() -> [SecCertificate]? {
        if let certificateString = UserAccount.getUserCA() {
            return Certificates.convertMultiCertStringToCertificates(certificatesString: certificateString)
        }
        return nil
    }
}

Step 5 : Add that to your keychain.

    private func storeCertificatesInKeychain() {
        // Get client cert
        guard let clientCert = CertificateUtils.getCRTCertificate() else {
            print("No client certificate found")
            return
        }
        
        // Add client certificate to Keychain
        let clientQuery: [String: Any] = [
            kSecClass as String: kSecClassCertificate,
            kSecValueRef as String: clientCert,
            kSecAttrLabel as String: MTLSTag.clientcert
        ]
        let status = SecItemAdd(clientQuery as CFDictionary, nil)
        if status == errSecDuplicateItem {
            print("Client certificate already in Keychain")
        } else if status != errSecSuccess {
            print("Failed to add client certificate: \(status)")
        } else {
            print("Client certificate added to Keychain")
        }
        
        // Add intermediate CA certs (optional)
        if let caCerts = CertificateUtils.getCACertificates() {
            for (index, cert) in caCerts.enumerated() {
                let caQuery: [String: Any] = [
                    kSecClass as String: kSecClassCertificate,
                    kSecValueRef as String: cert,
                    kSecAttrLabel as String: "\(MTLSTag.clientcert).\(index)"
                ]
                let caStatus = SecItemAdd(caQuery as CFDictionary, nil)
                if caStatus == errSecDuplicateItem {
                    print("CA certificate \(index) already in Keychain")
                } else if caStatus != errSecSuccess {
                    print("Failed to add CA certificate \(index): \(caStatus)")
                } else {
                    print("Added CA certificate \(index)")
                }
            }
        }
    }

Step 6 - Get a digital identity from the keychain:

    private func getClientIdentity() -> SecIdentity? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassIdentity,
            kSecAttrLabel as String: MTLSTag.clientcert, // <-- match by certificate tag
            kSecReturnRef as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var identityRef: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &identityRef)
        
        if status == errSecSuccess, let identity = identityRef as! SecIdentity? {
            print("Retrieved client identity from Keychain")
            return identity
        } else {
            print("Could not retrieve client identity, status=\(status)")
            return nil
        }
    }

Usages :

    func updateCertificates() {
        // Load CA certificates (if needed for server trust validation)
        self.trustedCACerts = CertificateUtils.getCACertificates()

        self.storeCertificatesInKeychain()
        
        if let useridentity = self.getClientIdentity() {
            self.clientCredential = URLCredential(identity: useridentity, certificates: nil, persistence: .forSession)
        }
}

Certificate Tags:

struct MTLSTag {
    static let clientkey    = "com.company.test1.clientkey"
    static let clientcert   = "com.company.test1.clientcert"
}

Sorry I didn’t reply to this earlier. I’m not sure how I missed your replies last August.

I’m also not sure why your code is failing, and there’s too much there for me to check line-by-line. However, I have a general debugging process for issues like this. The basic idea idea is to dump the contents of the keychain after each step to make sure that the stuff you think you’ve set is actually set. In your case that means three probes:

  1. One after you create the private key, to see its attributes.
  2. One after you add the certificate, to see its attributes.
  3. One after the identity lookup fails, to see if any identities are available and what they’re attributes are.

As to what these probes look like, I recently updated SecItem: Pitfalls and Best Practices with some suggestions on that front. First, the Starting from Scratch section explains how to reset the keychain so that each of your tests starts from a known clear state. Second, the Lost Keychain Items, Redux section explains how to dump all the attributes of all the items of a given class.

Note I just tweaked the snippet in that Lost Keychain Items, Redux section to make the output easier to read.

Once you have all this output, you can use the information in the Digital Identities Aren’t Real section, and in SecItem attributes for keys, to confirm that the key and certificate have the attributes necessary to form a digital identity. If they don’t, work backwards from there to figure out where things went wrong.

Share and Enjoy

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

mTLS : Guidance on Generating SecIdentity with Existing Private Key and Certificate
 
 
Q