Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

oliviermartin
Copy link

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:

  • Extend URLCredential to also pass client private key and certificate
  • Pass the URLCredential through URLSessionConfiguration. Note: on MacOS/iOS this approach could also be used as the URLSessionDelegate is often a copy/paste to pass URLCredential back to URLSession.
  • Configure libCurl to use these client private key and certificate

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:

  • The private key cannot be stored in a secure vault. There is no such API in Swift "Foundation" to handle this generic support.
  • As mentioned before, it is a different approach as the one used for MacOS/iOS that is based on URLSessionDelegate. But for using the same approach, we would either need to fork libcurl in swift-corelibs-foundation to handle the authentication challenge in Swift or we would need to a different library "backend" to handle URLSession

@parkera
Copy link
Member

parkera commented Apr 24, 2024

@swift-ci test

Copy link
Contributor

@jrflat jrflat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work to add this! I believe adding new public API like this would need to go through an API review process, but we might also be able to accomplish this by implementing existing Darwin URLCredential API and adding smalls stubs for SecIdentity/SecCertificate. I'll need to give that a bit more thought, but in the meantime, I've left a few comments that should hopefully help address the build issues/other minor feedback :)

@travarin
Copy link

travarin commented Apr 25, 2024

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 oliviermartin force-pushed the added-support-for-client-certificate-authentication branch from e4ff2fa to bf338c8 Compare May 9, 2024 07:33
@oliviermartin
Copy link
Author

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:

  • With the current implementation (using curl on non-Darwin system), we would need to fork+change+embed curl code in Swift Foundation library to hook URLSession(Task)Delegate into libcurl code whether we wanted to follow the same approach as Darwin system (ie: using func urlSession(URLSession, task: URLSessionTask, didReceive: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) for client authentication).
  • An alternative would be to add a new hook into URLSessionTaskDelegate (and URLSessionDelegate()) to pass URLCredential to URLSession backend before any connection - something like: func urlSession(URLSession, task: URLSessionTask, getCredential: (URLCredential) -> Void)

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

@oliviermartin oliviermartin force-pushed the added-support-for-client-certificate-authentication branch from bf338c8 to 7141710 Compare May 9, 2024 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants