xamarin-macios
xamarin-macios copied to clipboard
Extending NSUrlSessionHandler with client certificates
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
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 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 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 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 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 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.
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
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)
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.
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;
}
@MitchellFreeman I put up a PR that adds certificate support linked above
Fixed in #21284.
Just wanted to confirm that things works beautifully in RC2 ! Thank you all for helping this getting in