apple / swift-corelibs-foundation

The Foundation Project, providing core utilities, internationalization, and OS independence
swift.org
Apache License 2.0
5.23k stars 1.12k forks source link

URLSession: Added support for client certificate authentication for non MacOS platforms #4937

Open oliviermartin opened 2 months ago

oliviermartin commented 2 months ago

Swift Foundation Networking does not currently support client certificate authentication which is quite a limitation when integrating with a more complex system. For MacOS/iOS based platform, the client certificate authentication is done through URLSessionDelegate that handles authentication challenges.

Swift Foundation Networking relies on libcurl for URLSession. This support does not go through URLSessionDelegate for authentication challenge. The approach used by this pull-request is:

There is no unittest for this new code as it would require Swift to support server with TLS client certificate authentication support. The code can be locally tested with openssl s_server.

Here is an example code to use this API:

    func test_clientCertificateAuthentication() {
        let sem = DispatchSemaphore.init(value: 0)

        let privateClientKey: Data
        let privateClientCertificate: Data
        do {
            // TestPrivateKey.key contains the DER format of the private key
            privateClientKey = try Data(contentsOf: URL(fileURLWithPath: "TestPrivateKey.key"))
            // TestClientCertificate.der contains the DER format of the client certificate
            privateClientCertificate = try Data(contentsOf: URL(fileURLWithPath: "TestClientCertificate.der"))
        } catch {
            print("Failed to load file: \(error)")
            return
        }
        let urlCredential = URLCredential(clientKey: privateClientKey, clientCertificate: privateClientCertificate, persistence: .none)
        var urlSessionConfiguration = URLSessionConfiguration()
        urlSessionConfiguration.clientCredential = urlCredential

        let urlString = "https://127.0.0.1:443"
        let url = URL(string: urlString)!
        let session = URLSession(configuration: urlSessionConfiguration)

        let task = session.dataTask(with: url) { data, response, error in
            guard let response = response as? HTTPURLResponse else {
                print("No response from server:\(error)")
                defer { sem.signal() }
                return
            }
            print("Response from server: \(response.statusCode) data:\(data) error:\(error)")
            defer { sem.signal() }
        }
        task.resume()

        sem.wait()
    }

Limitations of this support:

parkera commented 2 months ago

@swift-ci test

travarin commented 2 months ago

Pass the URLCredential through URLSessionConfiguration

I think it'd be better to set this on the task instead of the session configuration. Generally the client would use the same session + configuration for every request they make, but might not necessarily want to enable client certificate authentication on all of them.

oliviermartin commented 1 month ago

Thanks @jrflat and @travarin for the feedback! I have just updated the PR with your suggestions.

Reading URLSession made me understand a bit better the philosophy behind this design (URLSession vs URLSessionTask). My understanding came initially from the examples of client certificate authentication I found on Internet that often implement URLSessionDelegate instead of URLSessionTaskDelegate for their client certificate authentication - and probably create one URLSession per URL.

Just on few points you mentioned in your comments:

I guess the next step is for me to create a proposal in swift-evolution?