Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Client certificates and identities in iOS

Tags:

I have generated private key and public key to my Swift-based iOS application using SecKeyGeneratePair function.
Then, I generated Certificate Signing Request using iOS CSR generationand my server replied with certificate chain in PEM format.
I converted PEM-certificate to DER-format using following code:

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 certificate from DER-data using let certificate = SecCertificateCreateWithData(nil, certDer)

My question is following : How can I connect the certificate with private key I have created in the beginning and get the identity where both of these(keys and certificate) belongs?
Maybe, add certificate to keychain and get the identity using SecItemCopyMatching? I have followed the procedure presented in question SecIdentityRef procedure

Edit:

When adding the certificate to keychain, I get the status response 0, which I believe means that certificate has been added to 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'm trying to get the identity, I get status -25300 (errSecItemNotFound). Following code is used to get the identity. tag is the private key tag I have used to generate 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'm able to get the private key & public key & certificate from the keychain using SecItemCopyMatching and add the certificate to keychain, but querying the SecIdentity does not work. Is it possible that my certificate does not match to my keys? How is that checked?

I printed public key from iOS in base64 format. The following was 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 minor difference in the beginning of the key generated from CSR. (MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A). Based on the question RSA encryption, it seems that MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A is base64-formatted identifier for RSA encryption "1.2.840.113549.1.1.1". So I guess the public key might be fine?

like image 538
lipponen Avatar asked Oct 11 '16 10:10

lipponen


People also ask

What are certificates in iOS?

An iOS developer certificate is a code-signing certificate, a digital signature that associates you and your digital identity with your applications.

Where are certificates stored in iOS?

On iOS, certificates are stored in the publisher keychain. On Android, they are stored in the system keychain.

What are certificates in Apple?

A certificate is an attachment to an electronic document that allows the safe transfer of information over the internet. Certificates are used by web browsers and mail and texting apps. When you communicate with a secure site, the information exchanged with the site is encrypted.

What are client certificates used for?

Client Certificates are digital certificates for users and individuals to prove their identity to a server. Client certificates tend to be used within private organizations to authenticate requests to remote servers.


1 Answers

We don't use that same method of CSR, but we have an equivalent thing where we do the following:

  1. Generate key pair
  2. Ship the public key to the remote server
  3. Remote server generates a signed client certificate using the public key
  4. Ship the client certificate back to the iOS device
  5. Add the client certificate to the keychain
  6. Later on, use the client certificate in an NSURLSession or similar.

As you seem to have discovered, iOS needs this extra thing called an "identity" to tie the client cert.

We also discovered that iOS has a weird thing where you need to DELETE the public key from the keychain before you add the client cert and identity into it, otherwise the identity doesn't seem to locate the client certificate properly instead. We chose to add the public key back in but as a "generic password" (i.e arbitrary user data) - we only do this because iOS doesn't have a sensible API for extracting a public key from a cert on the fly, and we need the public key for other strange things we happen to be doing.

If you're just doing TLS client certificate auth, once you have the certificate you won't need an explicit copy of the public key so you can simplify the process by simply deleting it, and skip the "add-back-in-as-generic-password" bit

Please excuse the giant pile of code, crypto stuff always seems to require a lot of work.

Here's bits of code to perform the above tasks:

Generating the keypair, and deleting/re-saving 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 isn't strictly in DER format, it's in "DER with the first 24 bytes trimmed off" format. I'm not sure what this is called officially, but both microsoft and apple seem to use it as the raw format for public keys. If your server is a microsoft one running .NET (desktop or core) then it will probably be happy with the public key bytes as-is. If it's Java and expects DER you may need to generate the DER header - this is a fixed sequence of 24 bytes you can probably just concatenate on.

Adding 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 chose to return a label, and then look up the identity as-required using the label, and the following code. You could also chose to just return the identity ref from the above function rather than the label. Here's our getIdentity function anyway

Getting the identity later on

// 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) } 

And finally

Using the identity to authenticate against a server

This bit is in objc because that's how our app happens to work, 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. iOS certificate stuff is exceedingly poorly documented, hopefully this helps.

like image 73
Orion Edwards Avatar answered Sep 18 '22 08:09

Orion Edwards