xamarin-macios icon indicating copy to clipboard operation
xamarin-macios copied to clipboard

Extending NSUrlSessionHandler with client certificates

Open MitchellFreeman opened this issue 3 years ago • 11 comments

I'm working on a Xamarin iOS project that requires the use of mutual TLS. I tried multiple different ways to get this working, but most used the HttpClientHandler and I couldn't get any of them to work. This was when I found the DidReceiveChallenge method of NSUrlSessionHandler. I made my own copy of this class and added

if (credentials != null && challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodClientCertificate)
{
    completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credentials);
}

to this method along with some code to store the client certificate. This works, but I'm wondering why this this simple change isn't already supported in some way.

Expected Behavior

The NSUrlSessionHandler class supports client certificates

Actual Behavior

The DidReceiveChallenge method of its delegate needs a small modification so that it supports client certificates

MitchellFreeman avatar Jan 23 '22 21:01 MitchellFreeman

Hello,

There is indeed a way for you to accept client certificates in the NSUrlSessionHandler without the need to copy the class. As you can see in the class we provided two delegates:

	public delegate bool NSUrlSessionHandlerTrustOverrideCallback (NSUrlSessionHandler sender, SecTrust trust);
	public delegate bool NSUrlSessionHandlerTrustOverrideForUrlCallback (NSUrlSessionHandler sender, string url, SecTrust trust);

Which you can set via the properties:

public NSUrlSessionHandlerTrustOverrideCallback TrustOverride;
public NSUrlSessionHandlerTrustOverrideForUrlCallback TrustOverrideForUrl;

The delegate NSUrlSessionHandlerTrustOverrideCallback has been obsoleted and you should use NSUrlSessionHandlerTrustOverrideForUrlCallback since it allows to filter per url.

You can see in the code that those delegates are executed as part of the handler management of the certs: https://github.com/xamarin/xamarin-macios/blob/main/src/Foundation/NSUrlSessionHandler.cs#L823

I am closing the issue but feel free to re-open if needed.

mandel-macaque avatar Jan 24 '22 01:01 mandel-macaque

@mandel-macaque While I understand that this can be used to evaluate self signed certificates, I don't know how this could be used to add client certificates. I see that there is a credential created here but I don't see how this this is derived in a way from SecTrust that allows the addition of a client certificate.

MitchellFreeman avatar Jan 24 '22 03:01 MitchellFreeman

@MitchellFreeman sorry, I misunderstood the question. Can you install the client certificate in the device? Or is this an issue. We might need to add an extra callback for this :/

mandel-macaque avatar Jan 24 '22 16:01 mandel-macaque

@mandel-macaque Sorry that may be partially my fault. Based on how iOS installs certificates I don't believe this is something I will be able to do. But thanks for pointing me towards those existing callbacks as validating self signed certificates is something I also need to do.

MitchellFreeman avatar Jan 24 '22 19:01 MitchellFreeman

@MitchellFreeman ok, so since you have no way to install the client certificate we have to find a way to do so, I'll do some research on who is the secure way to do this, since we are bypassing the OS and we don't want to introduce an attack vector. Does that sound like a plan? Ideally I'll get back with a objc sample and will do the appropriate in c#

mandel-macaque avatar Jan 25 '22 16:01 mandel-macaque

@mandel-macaque Sounds good. Although I don't see how this is bypassing the OS as we are simply replying to NSUrlProtectionSpace.AuthenticationMethodClientCertificate with our client certificate.

MitchellFreeman avatar Jan 25 '22 19:01 MitchellFreeman

Yes, I meant to be able not to install them and pass them to the OS, I have another issue in which we want to add some extra properties for the handler, this probably will be included there. I might merge both issues: https://github.com/xamarin/xamarin-macios/issues/13579

mandel-macaque avatar Jan 31 '22 16:01 mandel-macaque

I found this: https://github.com/xamarin/xamarin-macios/blob/a66950d2de86eefaa52c51d5034fd817b8dc7d21/src/Foundation/NSUrlSessionHandler.cs#L1021

I think what is missing is also a check for if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodClientCertificate)

dotMorten avatar Jun 20 '23 19:06 dotMorten

Did a bit more hacking. At this line I inserted the following code. That is of course just for testing, but proves it can be done:

if (sessionHandler.ClientCertificates is not null && sessionHandler.ClientCertificates.Count > 0 &&
    challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodClientCertificate)
{
    var cert = new SecCertificate(sessionHandler.ClientCertificates[0]);
    var identity = SecIdentity.Import(sessionHandler.ClientCertificates[0] as X509Certificate2);
    var trust = new SecTrust(sessionHandler.ClientCertificates, SecPolicy.CreateBasicX509Policy());
    var credential = new NSUrlCredential(identity, new SecCertificate[] { cert }, NSUrlCredentialPersistence.ForSession);
    completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
    return;
}

This (wrongly) assumes there's only one certificate and it has a private key available, but the request succeeded with this code added, so it is definitely on the right track. Testcode:

X509Certificate2 certificate = await LoadCertificateFromFile();
var handler = new NSUrlSessionHandler();
handler.ClientCertificates = new X509Certificate2Collection(certificate); //Note: Also made handler.ClientCertificates settable to match other APIs
using var client = new HttpClient(handler);
var response = await client.GetAsync(infoUri);
response = response.EnsureSuccessStatusCode();

This is with file-based certificates - I haven't tried using certificates that are installed on the device.

One simple solution that we could use for now, is to provide a public callback similar to the server validation callback, and requiring you to return a NSUrlCredential, and let the user implement the certificate selection. Then at a later stage add support for just picking certificates from the certificate collection is the callback wasn't used.

dotMorten avatar Mar 16 '24 19:03 dotMorten

Here's that alternative solution with a callback. I inserted the code below here: https://github.com/xamarin/xamarin-macios/blob/87953eda87eabdcbf487579ac7d5b441db872c0f/src/Foundation/NSUrlSessionHandler.cs#L1036

else if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodClientCertificate)
{
    if(sessionHandler.TryInvokeClientCertificateChallengeCallback(inflight.Request, challenge, out NSUrlCredential? credential))
    {
        completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
        return;
     }
}

And added this to NSUrlSessionHandler:

    public Func<HttpRequestMessage, NSUrlAuthenticationChallenge, NSUrlCredential?>? ClientCertificateChallengeCallback
    {
        get; set;
    }

     private bool TryInvokeClientCertificateChallengeCallback(HttpRequestMessage request, NSUrlAuthenticationChallenge challenge, [NotNullWhen(true)] out NSUrlCredential? credential)
     {			
        var callback = ClientCertificateChallengeCallback;
        credential = null;
        if (callback is null)
            return false;
        credential = callback(request, challenge);
        return credential != null;
     }

dotMorten avatar Mar 16 '24 20:03 dotMorten

@MitchellFreeman I put up a PR that adds certificate support linked above

dotMorten avatar Apr 11 '24 22:04 dotMorten

Fixed in #21284.

rolfbjarne avatar Sep 24 '24 21:09 rolfbjarne

Just wanted to confirm that things works beautifully in RC2 ! Thank you all for helping this getting in

dotMorten avatar Oct 09 '24 16:10 dotMorten