Client certificates and identities in iOS


lipoprotein:

I have used SecKeyGeneratePairfunctions to generate private and public keys for a Swift based iOS app . I then generated a "Certificate Signing Request"
using the iOS CSR and my server replied with a certificate chain in PEM format. I use the following code to convert the PEM certificate to DER format:

var modifiedCert = certJson.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
modifiedCert =  modifiedCert.replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
modifiedCert =  modifiedCert.replacingOccurrences(of: "\n", with: "")
let dataDecoded = NSData(base64Encoded: modifiedCert, options: [])

Now, I should create a certificate with DER datalet certificate = SecCertificateCreateWithData(nil, certDer)

My question is the following: How can I concatenate the certificate with the private key created at the beginning and get the identity that these two (key and certificate) belong to?
Maybe add the certificate to the keychain and use to get the identity SecItemCopyMatching? I have followed the procedure described in the question SecIdentityRef procedure

edit:

When adding the certificate to the keychain, I get a status response of 0, which I think means the certificate has been added to the keychain.

let certificate: SecCertificate? = SecCertificateCreateWithData(nil, certDer)
    if certificate != nil{
        let params : [String: Any] = [
            kSecClass as String : kSecClassCertificate,
            kSecValueRef as String : certificate!
        ]
        let status = SecItemAdd(params as CFDictionary, &certRef)
        print(status)
}

Now when I try to get the identity I get a status of -25300 (errSecItemNotFound). The following code is used to get the identity. The label is the private key label that I use to generate the private/public key.

let query: [String: Any] = [
    kSecClass as String : kSecClassIdentity,
    kSecAttrApplicationTag as String : tag,
    kSecReturnRef as String: true
]

var retrievedData: SecIdentity?
var extractedData: AnyObject?
let status = SecItemCopyMatching(query as NSDictionary, &extractedData)

if (status == errSecSuccess) {

    retrievedData = extractedData as! SecIdentity?
}

I can get the private key, public key and certificate from the keychain using SecItemCopyMatching and add the certificate to the keychain, but querying SecIdentity has no effect. Is it possible that my certificate does not match my key? How to check?

I printed the public key in base64 format from iOS. The following is printed:

MIIBCgKCAQEAo/MRST9oZpO3nTl243o+ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy
58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3PcjU2sopdMN35LeO6jZ34auH37gX41Sl
4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYsrSJONbr+74/mI/m1VNtLOM2FIzewVYcL
HHsM38XOg/kjSUsHEUKET/FfJkozgp76r0r3E0khcbxwU70qc77YPgeJHglHcZKF
ZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA
/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4ZwIDAQAB

Then, from the certificate signing request, I extracted the public key using openssl (openssl req -in ios.csr -pubkey -noout). The following response was printed:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo/MRST9oZpO3nTl243o+
ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3
PcjU2sopdMN35LeO6jZ34auH37gX41Sl4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYs
rSJONbr+74/mI/m1VNtLOM2FIzewVYcLHHsM38XOg/kjSUsHEUKET/FfJkozgp76
r0r3E0khcbxwU70qc77YPgeJHglHcZKFZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+
N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4
ZwIDAQAB
-----END PUBLIC KEY----

It seems that there is a slight difference in the beginning of the key generated from the CSR. (MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A). Based on the question RSA encryption , it appears that MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A is the base64 format identifier for RSA encryption "1.2.840.113549.1.1.1". So I guess the public key might be okay?

Orion Edwards:

We're not using the same CSR method, but there is an equivalent when doing the following:

  1. Generate key pair
  2. Send public key to remote server
  3. The remote server uses the public key to generate a signed client certificate
  4. Send the client certificate back to the iOS device
  5. Add client certificate to keychain
  6. Later, use the client certificate in NSURLSession or similar URL.

You seem to have discovered that iOS requires an additional "identity" to bind the client certificate.

We also found it a bit odd for iOS that you need to remove the public key from the keychain before adding the client certificate and identity to it, otherwise the identity doesn't seem to be able to locate the client certificate correctly. We chose to re-add the public key, but as a "common password" (i.e. arbitrary user data) - we do this because iOS doesn't have a sane API to extract the public key from the certificate on the fly, so we need the public key to deal with other weird things we happen to be doing.

If you're just doing TLS client certificate authentication, you don't need an explicit copy of the public key once you get the certificate, so you can simplify the process by simply removing it, and skip the "add-back-in-as - "Generic Password" bit

Forgive a bunch of code, cryptocurrencies always seem to require a lot of work.

Here is some code to perform the above task:

Generate a key pair and delete/resave the public key

/// Returns the public key binary data in ASN1 format (DER encoded without the key usage header)
static func generateKeyPairWithPublicKeyAsGenericPassword(privateKeyTag: String, publicKeyAccount: String, publicKeyService: String) throws -> Data {
    let tempPublicKeyTag = "TMPPUBLICKEY:\(privateKeyTag)" // we delete this public key and replace it with a generic password, but it needs a tag during the transition

    let privateKeyAttr: [NSString: Any] = [
        kSecAttrApplicationTag: privateKeyTag.data(using: .utf8)!,
        kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
        kSecAttrIsPermanent: true ]

    let publicKeyAttr: [NSString: Any] = [
        kSecAttrApplicationTag: tempPublicKeyTag.data(using: .utf8)!,
        kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
        kSecAttrIsPermanent: true ]

    let keyPairAttr: [NSString: Any] = [
        kSecAttrKeyType: kSecAttrKeyTypeRSA,
        kSecAttrKeySizeInBits: 2048,
        kSecPrivateKeyAttrs: privateKeyAttr,
        kSecPublicKeyAttrs: publicKeyAttr ]

    var publicKey: SecKey?, privateKey: SecKey?
    let genKeyPairStatus = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)
    guard genKeyPairStatus == errSecSuccess else {
        log.error("Generation of key pair failed. Error = \(genKeyPairStatus)")
        throw KeychainError.generateKeyPairFailed(genKeyPairStatus)
    }
    // Would need CFRelease(publicKey and privateKey) here but swift does it for us

    // we store the public key in the keychain as a "generic password" so that it doesn't interfere with retrieving certificates
    // The keychain will normally only store the private key and the certificate
    // As we want to keep a reference to the public key itself without having to ASN.1 parse it out of the certificate
    // we can stick it in the keychain as a "generic password" for convenience
    let findPubKeyArgs: [NSString: Any] = [
        kSecClass: kSecClassKey,
        kSecValueRef: publicKey!,
        kSecAttrKeyType: kSecAttrKeyTypeRSA,
        kSecReturnData: true ]

    var resultRef:AnyObject?
    let status = SecItemCopyMatching(findPubKeyArgs as CFDictionary, &resultRef)
    guard status == errSecSuccess, let publicKeyData = resultRef as? Data else {
        log.error("Public Key not found: \(status))")
        throw KeychainError.publicKeyNotFound(status)
    }

    // now we have the public key data, add it in as a generic password
    let attrs: [NSString: Any] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
        kSecAttrService: publicKeyService,
        kSecAttrAccount: publicKeyAccount,
        kSecValueData: publicKeyData ]

    var result: AnyObject?
    let addStatus = SecItemAdd(attrs as CFDictionary, &result)
    if addStatus != errSecSuccess {
        log.error("Adding public key to keychain failed. Error = \(addStatus)")
        throw KeychainError.cannotAddPublicKeyToKeychain(addStatus)
    }

    // delete the "public key" representation of the public key from the keychain or it interferes with looking up the certificate
    let pkattrs: [NSString: Any] = [
        kSecClass: kSecClassKey,
        kSecValueRef: publicKey! ]

    let deleteStatus = SecItemDelete(pkattrs as CFDictionary)
    if deleteStatus != errSecSuccess {
        log.error("Deletion of public key from keychain failed. Error = \(deleteStatus)")
        throw KeychainError.cannotDeletePublicKeyFromKeychain(addStatus)
    }
    // no need to CFRelease, swift does this.
    return publicKeyData
}

Note that publicKeyData is not strictly in DER format, but in "DER trimmed off the first 24 bytes" format. I'm not sure what the official name is for this, but both Microsoft and Apple seem to use it as the public key in raw format. If your server is a Microsoft server running .NET (desktop or core) then it will probably be happy with public key bytes as is. If it's Java, and expecting DER, you may need to generate a DER header - it's a fixed 24-byte sequence that you can concatenate.

Add the client certificate to the keychain, generating an identity

static func addIdentity(clientCertificate: Data, label: String) throws {
    log.info("Adding client certificate to keychain with label \(label)")

    guard let certificateRef = SecCertificateCreateWithData(kCFAllocatorDefault, clientCertificate as CFData) else {
        log.error("Could not create certificate, data was not valid DER encoded X509 cert")
        throw KeychainError.invalidX509Data
    }

    // Add the client certificate to the keychain to create the identity
    let addArgs: [NSString: Any] = [
        kSecClass: kSecClassCertificate,
        kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
        kSecAttrLabel: label,
        kSecValueRef: certificateRef,
        kSecReturnAttributes: true ]

    var resultRef: AnyObject?
    let addStatus = SecItemAdd(addArgs as CFDictionary, &resultRef)
    guard addStatus == errSecSuccess, let certAttrs = resultRef as? [NSString: Any] else {
        log.error("Failed to add certificate to keychain, error: \(addStatus)")
        throw KeychainError.cannotAddCertificateToKeychain(addStatus)
    }

    // Retrieve the client certificate issuer and serial number which will be used to retrieve the identity
    let issuer = certAttrs[kSecAttrIssuer] as! Data
    let serialNumber = certAttrs[kSecAttrSerialNumber] as! Data

    // Retrieve a persistent reference to the identity consisting of the client certificate and the pre-existing private key
    let copyArgs: [NSString: Any] = [
        kSecClass: kSecClassIdentity,
        kSecAttrIssuer: issuer,
        kSecAttrSerialNumber: serialNumber,
        kSecReturnPersistentRef: true] // we need returnPersistentRef here or the keychain makes a temporary identity that doesn't stick around, even though we don't use the persistentRef

    let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef);
    guard copyStatus == errSecSuccess, let _ = resultRef as? Data else {
        log.error("Identity not found, error: \(copyStatus) - returned attributes were \(certAttrs)")
        throw KeychainError.cannotCreateIdentityPersistentRef(addStatus)
    }

    // no CFRelease(identityRef) due to swift
}

In our code, we choose to return a label and then use the label and the following code to find the desired identity. You can also choose to just return the identity ref from the above function instead of the tag. Anyway, here is our getIdentity function

get identity later

// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getIdentity(label: String) -> SecIdentity? {
    let copyArgs: [NSString: Any] = [
        kSecClass: kSecClassIdentity,
        kSecAttrLabel: label,
        kSecReturnRef: true ]

    var resultRef: AnyObject?
    let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
    guard copyStatus == errSecSuccess else {
        log.error("Identity not found, error: \(copyStatus)")
        return nil
    }

    // back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
    // It wants to manage CF types on it's own which is fine, except they release when we return them out
    // back into ObjC code.
    return (resultRef as! SecIdentity)
}

// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getCertificate(label: String) -> SecCertificate? {
    let copyArgs: [NSString: Any] = [
        kSecClass: kSecClassCertificate,
        kSecAttrLabel: label,
        kSecReturnRef: true]

    var resultRef: AnyObject?
    let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
    guard copyStatus == errSecSuccess else {
        log.error("Identity not found, error: \(copyStatus)")
        return nil
    }

    // back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
    // It wants to manage CF types on it's own which is fine, except they release when we return them out
    // back into ObjC code.
    return (resultRef as! SecCertificate)
}

finally

Authenticate to the server using an identity

This is in objc because that's how our app happens to run, but you get the idea:

SecIdentityRef _clientIdentity = [XYZ getClientIdentityWithLabel: certLabel];
if(_clientIdentity) {
    CFRetain(_clientIdentity);
}
SecCertificateRef _clientCertificate = [XYZ getClientCertificateWithLabel:certLabel];
if(_clientCertificate) {
    CFRetain(_clientCertificate);
}
...

- (void)URLSession:(nullable NSURLSession *)session
          task:(nullable NSURLSessionTask *)task
didReceiveChallenge:(nullable NSURLAuthenticationChallenge *)challenge
 completionHandler:(nullable void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {

    if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
        // supply the appropriate client certificate
        id bridgedCert = (__bridge id)_clientCertificate;
        NSArray* certificates = bridgedCert ? @[bridgedCert] : @[];
        NSURLCredential* credential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceForSession];


        completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
    }
}

This code took a lot of time to get right. The contents of iOS certificates are minimally documented, hope this helps.

Related


Client certificates and identities in iOS

lipoprotein: I have used SecKeyGeneratePairfunctions to generate private and public keys for a Swift based iOS app . I then generated a "Certificate Signing Request" using the iOS CSR and my server replied with a certificate chain in PEM format. I use the foll

Client certificates and identities in iOS

lipoprotein: I have used SecKeyGeneratePairfunctions to generate private and public keys for a Swift based iOS app . I then generated a "Certificate Signing Request" using the iOS CSR and my server replied with a certificate chain in PEM format. I use the foll

OCSP revocation of client certificates

gtrak: How to manually check certificate revocation status in Java using OCSP, considering only the client's java.security.cert.X509Certificate? I can't see a clear way to do it. Alternatively, I can have tomcat do this for me automatically, how do you know yo

Compare client certificates in go

User1791139: My use case looks like I know the client's public certificate and just want to allow them. I have a go server configured based on gin and TLS where a method has been assigned to the property "VerifyPeerCertificate". The function looks like func cu

OCSP revocation of client certificates

gtrak: How to manually check certificate revocation status in Java using OCSP, considering only the client's java.security.cert.X509Certificate? I can't see a clear way to do it. Alternatively, I can have tomcat do this for me automatically, how do you know yo

Compare client certificates in go

User1791139: My use case looks like I know the client's public certificate and just want to allow them. I have a go server configured based on gin and TLS where a method has been assigned to the property "VerifyPeerCertificate". The function looks like func cu

Dynamically request client certificates

user 93353 The web server has settings for requesting client certificates - eg. SSLVerifyClient requireIn Apache, use other settings in IIS etc. If this setting is set, the browser will pop up a dialog asking you to select a certificate. Is it possible to requ

Android and client certificates

Anthony B Code I've been searching for weeks and can't seem to find an answer anywhere. I am trying to do the following for Android. The code is from a C# app I wrote but am porting it to Android. Web endpoints require certificates to be attached to mutual aut

Android and client certificates

Anthony B Code I've been searching for weeks and can't seem to find an answer anywhere. I am trying to do the following for Android. The code is from a C# app I wrote but am porting it to Android. Web endpoints require certificates to be attached to mutual aut

OCSP revocation of client certificates

gtrak: How to manually check certificate revocation status in Java using OCSP, considering only the client's java.security.cert.X509Certificate? I can't see a clear way to do it. Alternatively, I can have tomcat do this for me automatically, how do you know yo

Compare client certificates in go

User1791139: My use case looks like I know the client's public certificate and just want to allow them. I have a go server configured based on gin and TLS where a method has been assigned to the property "VerifyPeerCertificate". The function looks like func cu

Dynamically request client certificates

user 93353 The web server has settings for requesting client certificates - eg. SSLVerifyClient requireIn Apache, use other settings in IIS etc. If this setting is set, the browser will pop up a dialog asking you to select a certificate. Is it possible to requ

Using Client Certificates in Curl Commands

sunsin1985 : curl command: curl -k -vvvv --request POST --header "Content-Type: application/json" --cert client.pem:password --key key.pem "https://test.com:8443/testing" I am trying to send a client certificate using the Curl command specified above. I would

WSDL client authentication and multiple certificates

other: So I'm having issues with wsdls here, with multiple certificates selected in Java. For example, a smart card has multiple certificates on it for signing, encryption and identification. I have a WSDL that generates code for client auth connections, but a

Using Client Certificates in Alamofire 2.0

Paul On Alamofire 1 and Swift 1.2, I use the following code to make a request and submit my own client certificate: Alamofire.request(.POST, url!, parameters: params, encoding: .JSON) .authenticate(usingCredential: credential) .responseJSON { (request,

Using Client Certificates in Curl Commands

sunsin1985 : curl command: curl -k -vvvv --request POST --header "Content-Type: application/json" --cert client.pem:password --key key.pem "https://test.com:8443/testing" I am trying to send a client certificate using the Curl command specified above. I would

WSDL client authentication and multiple certificates

other: So I'm having issues with wsdls here, with multiple certificates selected in Java. For example, a smart card has multiple certificates on it for signing, encryption and identification. I have a WSDL that generates code for client auth connections, but a

AWS Multiple VPN Client Certificates

Ludo21 South With AWS, I need to make sure I have multiple clients using the VPN network. Each client will use the same server certificate I created earlier. Now, using this document, I managed to set up my own VPN and was able to connect to it using the gener

Client Certificates on Google Cloud Functions

microphone From my google cloud function, I try to request another api that requires a dummy certificate in the test environment. So my server is the client here. Is there a way to send client certificate in google cloud function? I managed to make it work in

Using Client Certificates in Alamofire 2.0

Paul On Alamofire 1 and Swift 1.2, I use the following code to make a request and submit my own client certificate: Alamofire.request(.POST, url!, parameters: params, encoding: .JSON) .authenticate(usingCredential: credential) .responseJSON { (request,

Client certificates in Dotnet Core on Ubuntu

mdavisi All - I wrote a dotnet core API set which works flawlessly on Windows. On Ubuntu 14.04 everything works fine except for one SOAP request to a vendor that uses client certificates for authentication. The request always times out. Netstat trace shows tha

Filter client certificates (like browsers)

Mika Hoover I have a smart card reader. When I try to access a website that accepts client certificates, the browser presents me with a list of 2 or 3 client certificates. All of these certificate options are tied closely to the card used on my machine. When I

Client Certificates on Google Cloud Functions

microphone From my google cloud function, I try to request another api that requires a dummy certificate in the test environment. So my server is the client here. Is there a way to send client certificate in google cloud function? I managed to make it work in

AWS Multiple VPN Client Certificates

Ludo21 South With AWS, I need to make sure I have multiple clients using the VPN network. Each client will use the same server certificate I created earlier. Now, using this document, I managed to set up my own VPN and was able to connect to it using the gener

How do client certificates work?

Sunil I'm working with a REST service provider and they want me to use the client certificate they provide when making HTTP calls. How do client certificates implement authentication? If someone has a copy of the client certificate, then they can also authenti

Generate client certificates for TLS asterisks

Duckett I'm trying to enable TLS b/w (voip server and client (Android device) powered by asterisk) following the guide mentioned here . Used to generate client certificates ./ast_tls_cert -m client -c /etc/asterisk/keys/ca.crt -k /etc/asterisk/keys/ca.key -C p

Using Client Certificates in Curl Commands

sunsin1985 : curl command: curl -k -vvvv --request POST --header "Content-Type: application/json" --cert client.pem:password --key key.pem "https://test.com:8443/testing" I am trying to send a client certificate using the Curl command specified above. I would

WSDL client authentication and multiple certificates

other: So I'm having issues with wsdls here, with multiple certificates selected in Java. For example, a smart card has multiple certificates on it for signing, encryption and identification. I have a WSDL that generates code for client auth connections, but a

AWS Multiple VPN Client Certificates

Ludo21 South With AWS, I need to make sure I have multiple clients using the VPN network. Each client will use the same server certificate I created earlier. Now, using this document, I managed to set up my own VPN and was able to connect to it using the gener