23

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?

Blazej SLEBODA
  • 8,936
  • 7
  • 53
  • 93
lipponen
  • 733
  • 1
  • 5
  • 18
  • Started to wonder, if certificate conversion from pem to der fails – lipponen Oct 15 '16 at 06:38
  • Are you sure that `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` don't have newlines that can be causing issues ? you might be ending up with a new line after removing them – Mostafa Berg Oct 18 '16 at 14:28
  • I'm also removing all the newlines (\n), otherwise SecCertificateCreateWithData throws error. Just added the line to the question, in the actual code the line has been there before. – lipponen Oct 19 '16 at 07:32
  • Aha I see, so this code is now exactly 1:1 to your current code that fails ? – Mostafa Berg Oct 19 '16 at 09:38
  • Yes, code is exactly the one which fails – lipponen Oct 19 '16 at 10:14
  • I have compared the certificate with private key using following commands: openssl x509 -noout -modulus -in certificate.crt | openssl md5 openssl rsa -noout -modulus -in privateKey.key | openssl md5 Result is that the md5 does not match, so I guess that is why SecIdentity is not found – lipponen Oct 19 '16 at 10:17
  • You are using iOS-SCR which is hopelessly insecure code. It is an example project not something you should be using in production. I recommend swapping it out immediately. – Antwan van Houdt Jan 17 '18 at 10:11

2 Answers2

8

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.

Orion Edwards
  • 121,657
  • 64
  • 239
  • 328
  • Please someone save my life and show me how to get the damn issuer and serial with Obj-c : 80hours + into working on cert with NSURLSession. fml @Orion Edwards – Ezos Mar 21 '20 at 00:36
  • @Ezos I haven't done this myself, but I'm guessing you can call SecCertificateCopyNormalizedIssuerSequence to get the issuer and SecCertificateCopySerialNumberData to get the issuer. I'm guessing from the docs (Apple's docs are trash on this) that those give you raw ASN1 sequences which you'll have to parse, but that hopefully isn't too hard – Orion Edwards Mar 24 '20 at 00:02
  • Thank you for this - I wish Apple would provide information as useful as this. We are using CSRs but am curious how your server creates a certificate with just the public key. I think that is a better way, so that the server controls all of the certificate attributes. Can you tell me how you generate the certificate on the server? – svenyonson Jan 10 '21 at 01:24
  • @svenyonson with considerable effort. Our backend is C# / aspnetcore running on either linux or windows, and it does this: https://gist.github.com/borland/5cf356d76904bbb7e83c156e9359dca6 Remember, a certificate is just a public key with some metadata stapled to it, and then signed by another issuer (root) certificate; This is how we achieve that with BouncyCastle in .NET – Orion Edwards Jan 14 '21 at 07:31
  • Thanks, that gist might come in handy at some point. I had to make a few changes to make your solution (above) work: Identity was not being found. Per "Eskimo", I had to gen a public key hash and add it to the private key (kSecAttrLabel & kSecAttrApplicationLabel) and also to the certificate (kSecAttrPublicKeyHash). I also didn't add the public key back in as a generic login as it wasn't needed for anything. – svenyonson Jan 15 '21 at 17:35
  • @OrionEdwards I have things almost working. I can extract the certs and identity, but when I create a URLCredential object using the certs and identity, the result is nil. If I print the SecIdentityRef, it is only 12 bytes - is that right? – svenyonson Jan 16 '21 at 00:59
  • Works great. Looks like no longer necessary to delete public key. Also, creating the credential for challenge needed only the identity, credentials argument nil. – user1055568 Feb 23 '23 at 02:18
-1

The usual way to generate SSL certificates is that private key is used to generate the CSR, Certificate Signing Request info. In fact, you're hidding as well company, email, etc info with that key signature. With that CSR, then, you sign your certificate, so it will be associated with your private key and info stored in CSR, nevermind the public key. I'm currently not able to see in IOS CSR Generation project where you can pass your generated key: seems to me that CSR generated with IOS CSR Generation project is using it's own generated key, or no private key at all. That will got then logic with the fact that you cannot extract private key from CER or DER, because it isn't there.

DvTr
  • 347
  • 1
  • 5
  • Private key is passed to CSR building process in method: -(NSData *) build:(NSData *)publicKeyBits privateKey:(SecKeyRef)privateKey – lipponen Oct 20 '16 at 09:51