skip to Main Content

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?

2

Answers


  1. 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.

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search