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"
}
mTLS : Guidance on Generating SecIdentity with Existing Private Key and Certificate
 
 
Q