iOS TLS/SSL Pinning using NSRequiresCertificateTransparency key in Info.plist

3.5k Views Asked by At

I want to secure my app against man-in-the-middle (mitm) attacks using SSL Pinning.
By default it is possible to use a proxy like Charles or mitmproxy to intercept traffic, and decrypt it using a self-signed certificate.

After extensive research, I found several options:

  1. Adding NSPinnedDomains > MY_DOMAIN > NSPinnedLeafIdentities to Info.plist
    Apple Documentation: Identity Pinning
    Guardsquare: Leveraging Info.plist Based Certificate Pinning
    Pros: Simple
    Cons: App becomes unusable once Certificate/Private Key is renewed (typically after a few months)

  2. Adding NSPinnedDomains > MY_DOMAIN > NSPinnedCAIdentities to Info.plist
    Apple Documentation: same as above
    Pros: Simple. No failure on Leaf Certificate renewal because Root CAs are pinned instead (expiration dates decades out)
    Cons: Seems redundant as most root CAs are already included in the OS

  3. Checking certificates in code URLSessionDelegate > SecTrustEvaluateWithError (or Alamofire wrapper)
    Ray Wenderlich: Preventing Man-in-the-Middle Attacks in iOS with SSL Pinning
    Apple Documentation: Handling an Authentication Challenge
    Medium article: Everything you need to know about SSL Pinning
    Medium article: Securing iOS Applications with SSL Pinning
    Pros: More flexibility. Potentially more secure. Recommended by Apple (see Apple-link above).
    Cons: A more laborious version of (1) or (2). Same Cons as (1) and (2) regarding leaf expirations / root CA redundancies. More complicated.

  4. Adding NSExceptionDomains > MY_DOMAIN > NSRequiresCertificateTransparency to Info.plist
    Apple documentation: Section Info.plist keys 'Certificate Transparency'
    Pros: Very simple. No redundant CA integration.
    Cons: Documentation is unclear whether this should be used for ssl pinning

After evaluation I came to the following conclusion:

  1. Not suitable for a production app because of certificate expiration
  2. Probably best balance between simplicity, security and sustainability – but I don't like the duplication of adding root CAs the system already knows
  3. Too complicated, too risky, any implementation error may lock the app
  4. My preferred way. Simple. Works in my tests but – unclear documentation.

I am tempted to use option (4) but I am not sure if this is really meant for ssl pinning.

In the documentation it says:

Certificate Transparency (CT) is a protocol that ATS can use to identify mistakenly or maliciously issued X.509 certificates. Set the value for the NSRequiresCertificateTransparency key to YES to require that for a given domain, server certificates are supported by valid, signed CT timestamps from at least two CT logs trusted by Apple. For more information about Certificate Transparency, see RFC6962.

and in the linked RFC6962:

This document describes an experimental protocol for publicly logging the existence of Transport Layer Security (TLS) certificates [...]

The terms "experimental protocol" and "publicly logging" raise flags for me and although flipping the feature on in the Info.plist seems to solve SSL pinning I am not sure if I should use it.
I am by no means a security expert and I need a dead simple solution that gives me decent protection while protecting me from choking my own app through possible expired / changed certificates.

My question:

Should I use NSRequiresCertificateTransparency for ssl pinning and preventing mitm-attacks on my app?

And if not:

What should I use instead?


PS:

This same question was essentially already asked in this thread:
https://developer.apple.com/forums/thread/675791

However the answer was vague about NSRequiresCertificateTransparency (4. in my list above):

Right, Certificate Transparency is great tool for verifying that a provided leaf does contain a set of SCTs (Signed Certificate Timestamp) either embedded in the certificate (RFC 6962), through a TLS extension (which can be seen in a Packet Trace), or by checking the OCSP logs of the certificate. When you make a trust decision in your app, I would recommend taking a look at is property via the SecPolicyRef object.

Additional side note:

My expectation from Apple as a security-aware company would have been, that pinning to root CAs was enabled by default, and that I would have to add exceptions manually, e.g. allow proxying with Charles on debug builds. I hear Android does it that way.

3

There are 3 best solutions below

0
On

Additional resource for you is OWASP. It is good to follow their recommendations for all of your platforms.

https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning

As for your points:

  1. The public key does not always change when the certificates change (leaf certs are usualy rotated yearly?). This is one of the advantages of pinning against the public key.

  2. 'Cons: Seems redundant as most root CAs are already included in the OS' As you said here, pinning against the root cert is pointless, as the certificate is likely already trusted by the OS.

However, from the doc you linked to: 'A pinned CA public key must appear in a certificate chain either in an intermediate or root certificate. Pinned keys are always associated with a domain name, and the app will refuse to connect to that domain unless the pinning requirement is met.'

You would be pinning against the intermediate certificate here. For peace of mind you can do a test to print out the public key of your root certs + intermediate certs and prove they don't match.

  1. 'Too complicated, too risky, any implementation error may lock the app.' Apple provides an implementation in their tech note, and you can test all the codepaths yourself manually. Also, as owasp recommends, you can look into trust kit. I seems it has an implementation of this, and again here you have the option to just pin against the intermediate certificate (NOT ROOT) vs leaf node certs. Intermediate certificates typically last for 5-10 years.

  2. I would hold off on this as personally I am not sure, I think this might just be an additional check you might want to use in addition to cert pinning. There also does not seem to be mention of this in the owasp document.

Personally, if I was writing a new app, I would go with option 1. Since android N, the OS provides a similar approach, which means you can also stay in sync with your android counterparts and they will also only have to update when you do and vice versa. https://developer.android.com/training/articles/security-config.html#CertificatePinning

I am not a security expert, but I am giving my thoughts based on my experience working for large coorperations that have their applications penetration tested. If you are really working on an app that requires high security, you really should have a penetration tester test your application. If you are in a big company, you may have a cyber team that can help.

6
On

No, you can’t do ssl certificate pining by using NSRequiresCertificateTransparency, it’s uses for client side TLS. If you want to implement pinning ,you can use server certificate pining to prevent MITM attacks.

Certificate pining

The difference is bellow

1) Client-side certificate transparency

For iOS apps, turning on client-side certificate transparency check is rather simple – you do nothing! Certificate transparency is enforced by default on devices running iOS 12.1.1 and higher. For devices running earlier versions of the iOS, you will need to set the NSRequiresCertificateTransparency option to YES in your Info.plist file.

2) Server-side certificate transparency

Certificate transparency has two aspects:

  1. Pin the certificate: You can download the server’s certificate and bundle it into your app. At runtime, the app compares the server’s certificate to the one you’ve embedded.

  2. Pin the public key: You can retrieve the certificate’s public key and include it in your code as a string. At runtime, the app compares the certificate’s public key to the one hard-coded in your code.

An SSL certificate with an SCT is definitely required. Make sure your server certificate is one with a valid SCT. Almost every CA these days issue certificates with SCTs

1
On

I'm using SecTrustEvaluateWithError to evaluate the certificate. In case if certificate expired or any another case where evaluate return an error, I getting new one from the server. Certificate is stored and received witch keychain. One of the problem I faced with this solution was updating existing certificate at keychain because in apple documentation way to do it, is by using kSecValueRef but that one returns error whenever you try to update it. Instead cert is saved with kSecValueData.

So solution nr 3 (kind of) is used here, but in my case there is a socket connection instead.

First I connect to the socket with settings using CocoaAsyncSocket library

    GCDAsyncSocketManuallyEvaluateTrust: NSNumber(value: true),
    kCFStreamSSLPeerName as String: NSString("name")

next I use delegate to receive trust object

public func socket(_ sock: GCDAsyncSocket, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) 

next evaluate with existing one (from keychain), or update cert and repeat evaluation

if let cert = CertificateManager.shared.getServerCertificate() {
    SecTrustSetAnchorCertificates(trust, [cert] as NSArray)
    SecTrustSetAnchorCertificatesOnly(trust, true)
    var error: CFError?
    let evaluationSucceeded = SecTrustEvaluateWithError(trust, &error)

    guard evaluationSucceeded else {
        CertificateManager.shared.updateCertificate()
        return
    }

    completionHandler(evaluationSucceeded)
} else {
    CertificateManager.shared.updateCertificate()
}

Method for getting certificate is just a regular URLSession dataTask on domain that has certificate with URLSessionDelegate I get a URLAuthenticationChallenge from that object you can retrieve certificate and save it to keychain.

There is info from apple documentation how to store certificate

Best if you read thru it, but how I mentioned above I faced problems with that solution with updating existing one so there there are methods that I use for saving and retrieving certificate

add as data:

public func saveServerCertificate(_ certificate: SecCertificate, completion: @escaping () -> Void) throws {
        let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
                                    kSecAttrLabel as String: attribute]

        let status = SecItemCopyMatching(query as CFDictionary, nil)
        switch status {
        case errSecItemNotFound:
            let certData = SecCertificateCopyData(certificate) as Data
            let saveQuery: [String: Any] = [kSecClass as String: kSecClassCertificate,
                                            kSecAttrLabel as String: attribute,
                                            kSecValueData as String: certData]
            let addStatus = SecItemAdd(saveQuery as CFDictionary, nil)
            guard addStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
            completion()
        case errSecSuccess:
            let certData = SecCertificateCopyData(certificate) as Data
            let attributes: [String: Any] = [kSecValueData as String: certData]
            let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
            guard updateStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
            completion()
        default:
            throw KeychainError.unhandledError(status: status)
        }
    }

get and create certificate with data:

public func getServerCertificate(completion: @escaping (SecCertificate) -> Void) throws {

    let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
                                kSecAttrLabel as String: attribute,
                                kSecReturnAttributes as String: true,
                                kSecMatchLimit as String: kSecMatchLimitOne,
                                kSecReturnData as String: true]
    
    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)

    switch status {
    case errSecItemNotFound:
        throw KeychainError.noCertificate
    case errSecSuccess:
        guard let existingItem = item as? [String : Any],
              let certData = existingItem[kSecValueData as String] as? Data
        else {
            throw KeychainError.unexpectedCertificateData
        }

        if let certificate = SecCertificateCreateWithData(nil, certData as CFData) {
            completion(certificate)
        } else {
            throw KeychainError.unexpectedCertificateData
        }
    default:
        throw KeychainError.unhandledError(status: status)
    }
}