1

I have a PEM file of the ISRG Root X1 certificate which I downloaded from https://letsencrypt.org/certificates/ and I'm trying to implement certificate pinning in my iOS app. I'm specifically interested in public key pinning and I'm targeting iOS 12 and above.

I have two main questions:

  1. How can I generate a SHA256 hex string from the PEM file?

  2. Once I have the SHA256 hex string, how can I implement root certificate public key pinning in Swift using URLSession, without relying on any external libraries?

I would greatly appreciate any assistance or resources that could shed light on this matter. Thank you in advance!

-- Edited

According to what I found on StackOverflow and other sources, the SHA-256 hex string I generated using OpenSSL differs from the one I obtained in the code during TLS connections.

Command used:

openssl rsa -pubin -inform PEM -outform DER -in public_key.pem | openssl enc -base64

Question - why it is different is it expected?

6
  • What have you tried so far? How to get the certificate of a TLS connections, extract the public key, get the raw data and then hash it. For all those steps I am sure you will find answers here on stackoverflow. In my opinion the easiest way is to implement the certificate public key pinning and first set a dummy hash in code. If cert pinning goes wrong print the real certificate public key hash in log so that you can copy it in XCode log and paste it into your code. Commented Sep 16, 2024 at 8:39
  • @Robert Edited the question Commented Sep 16, 2024 at 9:18
  • "In my opinion the easiest way is to implement the certificate public key pinning and first set a dummy hash in code. If cert pinning goes wrong print the real certificate public key hash in log so that you can copy it in XCode log and paste it into your code" is it recommended? Commented Sep 16, 2024 at 9:26
  • You get the hash of the public key contained in the certificate. That is the way you implement public key pinning. Not sure what you mean by "is it it recommended?". Commented Sep 16, 2024 at 10:18
  • I mean how could I validate the hash is correct? or the code I have written is working? we often follow this approach 1- Get Certificate 2- Generate Key 3- In Swift write code that generate certificate keys and 4- compare both if it matches then return true otherwise false Commented Sep 16, 2024 at 11:55

2 Answers 2

0

For you first point i.e. How to generate SHA256 hex string from PEM file

Method 1:

Get public key via terminal command-

Step 1: If you have the pem file with you please use the below openSSL command to get the public key.

openssl rsa -in inputPemFile.pem -pubout -out outputPublicKey.pem

Here, please do make sure your PEM file is in correct format which contains the private key.

Step 2: Now, use below command to extract/read the public key from outputPublicKey.pem file

cat public_key.pem

Method 2:

Direct method

Step 1: Open Qualys SSL Labs

Step 2: Enter your domain hostname from which you want to extract the public key e.g. https://www.google.com/ and press submit button Hostname reference image

Step 3: In the next screen you will get your SHA256 public key, see reference image below sha256 key reference image

===================================================================

For you second point i.e. Implement root certificate public key pinning?

Now, if you are using url session then use URL session delegate method i.e. // User defined variables

private let rsa2048Asn1Header:[UInt8] = [
    0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
    0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
]

private let yourPublicKey: "Your Public Key"

// MARK: URL session delegate:

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    // your code logic
}

Find below the logic which I basically used:

    //MARK:- SSL Pinning with URL Session
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    
    var res = SecTrustResultType.invalid
    guard
        challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
        let serverTrust = challenge.protectionSpace.serverTrust,
        SecTrustEvaluate(serverTrust, &res) == errSecSuccess,
        let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }
    
    if #available(iOS 12.0, *) {
        if let serverPublicKey = SecCertificateCopyKey(serverCert), let serverPublicKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, nil) {
            
            let data: Data = serverPublicKeyData as Data
            let serverHashKey = sha256(data: data)
            print(serverHashKey, serverHashKey.toSHA256())
            //comparing server and local hash keys
            if serverHashKey.toSHA256() == yourPublicKey {
                print("Public Key pinning is successfull")
                completionHandler(.useCredential, URLCredential(trust: serverTrust))
            } else {
                print("Public Key pinning is failed")
                completionHandler(.cancelAuthenticationChallenge, nil)
            }
        }
    } else {
        // Fallback on earlier versions
        if let serverPublicKey = SecCertificateCopyPublicKey(serverCert), let serverPublicKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, nil) {
            
            let data: Data = serverPublicKeyData as Data
            let serverHashKey = sha256(data: data)
            print(serverHashKey, serverHashKey.toSHA256())
            //comparing server and local hash keys
            if serverHashKey.toSHA256() == yourPublicKey {
                print("Public Key pinning is successfull")
                completionHandler(.useCredential, URLCredential(trust: serverTrust))
            } else {
                print("Public Key pinning is failed.")
                completionHandler(.cancelAuthenticationChallenge, nil)
            }
        }
    }
}

Helper function to convert server certificate to SHA256

private func sha256(data : Data) -> String {
    var keyWithHeader = Data(rsa2048Asn1Header)
    keyWithHeader.append(data)
    var hash = [UInt8](repeating: 0,  count: Int(CC_SHA256_DIGEST_LENGTH))
    
    keyWithHeader.withUnsafeBytes {
        _ = CC_SHA256($0.baseAddress, CC_LONG(keyWithHeader.count), &hash)
    }
    return Data(hash).base64EncodedString()
}

If you are using Alamofire, then pass the domain path in the evaluators data in your alamofire session like below

    let evaluators: [String: ServerTrustEvaluating] = [
    "your.domain.com": PublicKeysTrustEvaluator(
        performDefaultValidation: false,
        validateHost: false
    )
]
let serverTrustManager = ServerTrustManager(evaluators: evaluators)
let session = Session(serverTrustManager: serverTrustManager)

Now use this session while calling your alamofire network request.

Hope, I will be able to help you here.

Thanks and regards.

Sign up to request clarification or add additional context in comments.

Comments

0

I usually have a slightly different approach that i will share to see if it helps.

  1. I download the certificate already in CER format

In a terminal i simply run this command:

openssl s_client -connect <URL> -servername <server-name> < /dev/null | openssl x509 -outform DER > <output-file-name>.cer

Since it seems you already have the PEM file, you can run this command:

openssl x509 -inform PEM -outform DER -in <your-file-name>.crt -out <output-file-name>.cer
  1. You have to include the certificate file with extension CER in your project.

  2. Implementation of certificate pinning

Initialise your URLSession with delegate

URLSession(
    configuration: sessionConfiguration,
    delegate: self,
    delegateQueue: nil
)

Then you simply implement this function:

func urlSession(
    _ session: URLSession,
    didReceive challenge: URLAuthenticationChallenge,
    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {

    guard
        let serverTrust = challenge.protectionSpace.serverTrust,
        let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
    else {
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    let certificatesURLs = Bundle.main.urls(forResourcesWithExtension: "cer", subdirectory: nil)
    let certificatesData = certificatesURLs?.compactMap { try? Data(contentsOf: $0) }

    // Compare the server's certificate with the pinned ones
    let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data

    if certificatesData?.contains(serverCertificateData) == true {
        // Found a certificate that matches, allow the connection
        let credential = URLCredential(trust: serverTrust)
        completionHandler(.useCredential, credential)
    } else {
        // Didn't found a certificate that matches, cancel the connection
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

This will allow you to have multiple certificates installed, it can be usefully when one is about to expire but you already have the newer one available, so when one expires, the other one will still be validated.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.