0

Aim:

Test SSL pinning by using URLProtocol.

Problem:

Cannot subclass URLProtectionSpace in the expected manner. The server trust property is never called and the alamofire auth callback only receives a URLProtectionSpace class type instead of my class even though the initializer of my custom class gets called.

Configuration: [using Alamofire]

let sessionConfiguration: URLSessionConfiguration = .default
    sessionConfiguration.protocolClasses?.insert(BaseURLProtocol.self, at: 0)
    let sessionManager = AlamofireSessionBuilderImpl(configuration: sessionConfiguration).default
    // overriding the auth challenge in Alamofire in order to test what is being called
    sessionManager.delegate.sessionDidReceiveChallengeWithCompletion = { session, challenge, completionHandler in
        let protectionSpace = challenge.protectionSpace

        guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
            protectionSpace.host.contains("myDummyURL.com") else {
                // otherwise it means a different challenge is encountered and we are only interested in certificate validation
                completionHandler(.performDefaultHandling, nil)
                return
        }

        guard let serverTrust = protectionSpace.serverTrust else {
            completionHandler(.performDefaultHandling, nil)
            return
        }

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

        let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data

        // cannot find the local certificate
        guard let localCertPath = Bundle.main.path(forResource: "cert", ofType: "der"),
            let localCertificateData = NSData(contentsOfFile: localCertPath) else {
                return completionHandler(.cancelAuthenticationChallenge, nil)
        }

        guard localCertificateData.isEqual(to: serverCertificateData) else {
            // the certificate received from the server is invalid
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        let credential = URLCredential(trust: serverTrust)

        completionHandler(.useCredential, credential)
    }

BaseURLProtocol definition:

class BaseURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
    return true
}

override class func canonicalRequest(for request: URLRequest) -> URLRequest {
    return request
}

override func startLoading() {
    debugPrint("--- request loading \(request)")

    guard request.url?.host?.contains("myDummyURL.com") ?? false else {
        debugPrint("--- caught untargetted request --- skipping ---host is \(request.url?.host)")
        return
    }

    let protectionSpace = CertificatePinningMockURLProtectionSpace(host: "https://myDummyURL.com", port: 443, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust)

    let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: self)

    client?.urlProtocol(self, didReceive: challenge)
}

}

CertificatePinningMockURLProtectionSpace: [using Alamofire ServerTrustPolicy for getting the certs]

-- The serverTrust property never gets called. I've also overridden all other properties of URLProtectionSpace and nothing except for the init gets called.

class CertificatePinningMockURLProtectionSpace: URLProtectionSpace {
private static let expectedHost = "myDummyURL.com"

override init(host: String, port: Int, protocol: String?, realm: String?, authenticationMethod: String?) {
    debugPrint("--- super init will be called")
    super.init(host: host, port: port, protocol: `protocol`, realm: realm, authenticationMethod: authenticationMethod)
}

override var serverTrust: SecTrust? {
    guard let certificate = ServerTrustPolicy.certificates(in: .main).first else {
        return nil
    }

    let policy: SecPolicy = SecPolicyCreateSSL(true, CertificatePinningMockURLProtectionSpace.expectedHost as CFString)

    var serverTrust: SecTrust?

    SecTrustCreateWithCertificates(certificate, policy, &serverTrust)

    return serverTrust
}

}

Test statement:

sessionManager.request("https://myDummyURL.com").responseString(completionHandler: { response in
                        debugPrint("--- response is \(response)")
                        done()
                    })

Can the URLProtectionSpace successfully be overridden and provided as a mock to the URLProtocolClient inside the URLProtocol?

Fawkes
  • 3,831
  • 3
  • 22
  • 37
  • `URLProtectionSpace` implements `NSCopying`. If the system frameworks makes a copy you need to override `copy(with:)` and copy the server trust to the new instance. – Mats Sep 26 '18 at 13:07
  • @Mats unfortunately none of the copy methods are called... My only guess at the moment is that the serverTrust property is actually something generated from internals and those internals are always copied property by property in a new URLProtectionSpace. Even though I see no evident reasons for doing it, I don't think I can explain it somehow else. – Fawkes Sep 26 '18 at 13:48
  • I guess it's maybe limited because otherwise there is a small chance you may start brute forcing the certificates from inside closed libraries... idk – Fawkes Sep 26 '18 at 16:38

1 Answers1

0

A lot of those Core-Foundation-derived "classes" are highly resistant to subclassing, so it's no surprise that this one would be, too. It is probably basically just a struct with some magic glue under the hood. :-)

A unit test might be a better approach here than a functional test. Create an arbitrary protection space object, populate it, and pass it to the delegate method directly, and assert that the callback gets called with the expected results.

Or if you really want to do a complete end-to-end test, you could instantiate a local web server, and then your test could tweak its configuration to control the credentials provided to your app on-the-fly.

dgatwood
  • 10,129
  • 1
  • 28
  • 49
  • I already left that project, but I think the cleanest is to have a local web server. It does seem like the foundation classes are quite a convoluted beast for any more complicated testing approach. – Fawkes Jan 12 '20 at 16:57