Search code examples
iosswifthttpsalamofirealamofire5

Alamofire 5 alternative to sessionDidReceiveChallenge


I have just shifted to Alamofire 5.

Earlier I used URLSession and Certificate Pinner and to handle auth challenge I used delegate method of URLSessionDelegate with hash values

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
                completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    print("being challanged! for \(challenge.protectionSpace.host)")
    guard let trust = challenge.protectionSpace.serverTrust else {
        print("invalid trust!")
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    let credential = URLCredential(trust: trust)

    let pinner = setupCertificatePinner(host: challenge.protectionSpace.host)

    if (!pinner.validateCertificateTrustChain(trust)) {
        print("failed: invalid certificate chain!")
        challenge.sender?.cancel(challenge)
    }

    if (pinner.validateTrustPublicKeys(trust)) {
        completionHandler(.useCredential, credential)
    } else {
        didPinningFailed = true
        print("couldn't validate trust for \(challenge.protectionSpace.host)")
        completionHandler(.cancelAuthenticationChallenge, nil)
    }

}

Having moved to Alamofire 5, there is no method sessionDidReceiveChallenge which was available in earlier version.

I tried:

private let session: Session = {
    let manager = ServerTrustManager(allHostsMustBeEvaluated: true, evaluators:
        ["devDomain.com": DisabledTrustEvaluator(),
         "prodDomain.com": PublicKeysTrustEvaluator()])
    let configuration = URLSessionConfiguration.af.default

    return Session(configuration: configuration, serverTrustManager: manager)
}()

But I get error:

Error Domain=Alamofire.AFError Code=11 "Server trust evaluation failed due to reason: No public keys were found or provided for evaluation."

Update: I'd still prefer a way to parse it using 256 fingerprint only, as we get domains and its hashes in first api call.


Solution

  • First you need a ServerTrustEvaluating that handle the certificate pinning a simple implement would be something similar to

    public final class CertificatePinnerTrustEvaluator: ServerTrustEvaluating {
    
        public init() {}
    
        func setupCertificatePinner(host: String) -> CertificatePinner {
    
            //get the CertificatePinner
        }
    
        public func evaluate(_ trust: SecTrust, forHost host: String) throws {
    
            let pinner = setupCertificatePinner(host: host)
    
            if (!pinner.validateCertificateTrustChain(trust)) {
                print("failed: invalid certificate chain!")
                throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
            }
    
            if (!pinner.validateTrustPublicKeys(trust)) {
                print ("couldn't validate trust for \(host)")
    
                throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
            }
        }
    }
    

    To be able to use the same evaluator I would suggest to subclass ServerTrustManager to return the same evaluator I did it like this:

    class CertificatePinnerServerTrustManager: ServerTrustManager {
    
        let evaluator = CertificatePinnerTrustEvaluator()
    
        init() {
            super.init(allHostsMustBeEvaluated: true, evaluators: [:])
        }
    
        open override func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
    
            return evaluator
        }
    }
    

    after that you should be ready to go by creating the session and passing the manager to it

    private let session: Session = {
    
        let trustManager = CertificatePinnerServerTrustManager()
    
        return Session(serverTrustManager: trustManager)
    }()
    

    My reference was the method urlSession(_:task:didReceive:completionHandler:) in Alamofire source in SessionDelegate.swift at line 86 (Alamofire V5.2.1)