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