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

[Foundation] Implement the server certificate custom validation callback usage in NSUrlSessionHandler #15117

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b0dbe59
WIP invoke server certificate custom validation callback
simonrozsival May 4, 2022
916b931
WIP add tests
simonrozsival May 4, 2022
6bd110a
Fix sec trust param type
simonrozsival May 4, 2022
6e20ea4
WIP improve chain building
simonrozsival May 4, 2022
f0b8dd0
Make it build
akoeplinger May 4, 2022
c6ae804
Merge branch 'main' of https://github.com/xamarin/xamarin-macios into…
simonrozsival Aug 30, 2022
4326052
Update tests
simonrozsival Aug 30, 2022
78509b2
Fix build and update implementation
simonrozsival Aug 30, 2022
9a6deda
Run the new tests only in .NET builds
simonrozsival Sep 2, 2022
09ec0ce
Fix default certificate evaluation
simonrozsival Sep 2, 2022
eeb3495
Fix tests
simonrozsival Sep 2, 2022
f9a46f4
Merge branch 'main' into nsurlsessionhandler-server-certificate-custo…
simonrozsival Sep 2, 2022
c9d822d
Fix using GetCertificateChain on systems which don't support it
simonrozsival Sep 5, 2022
06d972d
Merge branch 'nsurlsessionhandler-server-certificate-custom-validatio…
simonrozsival Sep 5, 2022
65a2df3
Minor code touchup
simonrozsival Sep 5, 2022
cee43a2
Merge branch 'main' into nsurlsessionhandler-server-certificate-custo…
rolfbjarne Sep 9, 2022
9227f2b
Fix code formatting
simonrozsival Sep 12, 2022
799ac3c
Update X509Chain building
simonrozsival Sep 12, 2022
4307863
Merge branch 'nsurlsessionhandler-server-certificate-custom-validatio…
simonrozsival Sep 12, 2022
f772124
Merge branch 'main' into nsurlsessionhandler-server-certificate-custo…
simonrozsival Sep 30, 2022
27b368f
Use helper class to fix trimming issues
simonrozsival Oct 12, 2022
40da74f
Fixes
simonrozsival Oct 12, 2022
db36a5d
Add missing setter
simonrozsival Oct 12, 2022
d691f6d
Merge remote-tracking branch 'origin/main' into nsurlsessionhandler-s…
rolfbjarne Oct 13, 2022
7fb65da
Improve code to make the linker able to remove more code.
rolfbjarne Oct 13, 2022
df170fc
Fix build.
rolfbjarne Oct 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 125 additions & 18 deletions src/Foundation/NSUrlSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,13 +639,114 @@ public IWebProxy? Proxy {
[UnsupportedOSPlatform ("macos")]
public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls13;

// We're ignoring this property, just like Xamarin.Android does:
// https://github.com/xamarin/xamarin-android/blob/09e8cb5c07ea6c39383185a3f90e53186749b802/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs#L160
[UnsupportedOSPlatform ("ios")]
[UnsupportedOSPlatform ("maccatalyst")]
[UnsupportedOSPlatform ("tvos")]
[UnsupportedOSPlatform ("macos")]
public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool>? ServerCertificateCustomValidationCallback { get; set; }
private ServerCertificateCustomValidationCallbackHelper? _serverCertificateCustomValidationCallbackHelper;
public Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool>? ServerCertificateCustomValidationCallback {
get => _serverCertificateCustomValidationCallbackHelper?.Callback;
set {
if (value is null) {
_serverCertificateCustomValidationCallbackHelper = null;
} else {
_serverCertificateCustomValidationCallbackHelper = new ServerCertificateCustomValidationCallbackHelper (value);
}
}
}

// returns false if there's no callback
internal bool TryInvokeServerCertificateCustomValidationCallback (HttpRequestMessage request, SecTrust secTrust, out bool trusted)
{
trusted = false;
var helper = _serverCertificateCustomValidationCallbackHelper;
if (helper is null)
return false;
trusted = helper.Invoke (request, secTrust);
return true;
}

sealed class ServerCertificateCustomValidationCallbackHelper
{
public Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> Callback { get; private set; }

public ServerCertificateCustomValidationCallbackHelper (Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> callback)
{
Callback = callback;
}

public bool Invoke (HttpRequestMessage request, SecTrust secTrust)
{
X509Certificate2[] certificates = ConvertCertificates (secTrust);
X509Certificate2? certificate = certificates.Length > 0 ? certificates [0] : null;
using X509Chain chain = CreateChain (certificates);
SslPolicyErrors sslPolicyErrors = EvaluateSslPolicyErrors (certificate, chain, secTrust);

return Callback (request, certificate, chain, sslPolicyErrors);
}

X509Certificate2[] ConvertCertificates (SecTrust secTrust)
{
var certificates = new X509Certificate2 [secTrust.Count];

if (IsSecTrustGetCertificateChainSupported) {
var originalChain = secTrust.GetCertificateChain ();
for (int i = 0; i < originalChain.Length; i++)
certificates [i] = originalChain [i].ToX509Certificate2 ();
} else {
for (int i = 0; i < secTrust.Count; i++)
certificates [i] = secTrust [i].ToX509Certificate2 ();
}

return certificates;
}

static bool? isSecTrustGetCertificateChainSupported = null;
static bool IsSecTrustGetCertificateChainSupported {
get {
if (!isSecTrustGetCertificateChainSupported.HasValue) {
#if MONOMAC
isSecTrustGetCertificateChainSupported = ObjCRuntime.SystemVersion.CheckmacOS (12, 0);
#elif WATCH
isSecTrustGetCertificateChainSupported = ObjCRuntime.SystemVersion.CheckWatchOS (8, 0);
#elif IOS || TVOS || MACCATALYST
isSecTrustGetCertificateChainSupported = ObjCRuntime.SystemVersion.CheckiOS (15, 0);
#else
#error Unknown platform
#endif
}

return isSecTrustGetCertificateChainSupported.Value;
}
}

X509Chain CreateChain (X509Certificate2[] certificates)
{
// inspired by https://github.com/dotnet/runtime/blob/99d21b9276ebe8f7bea7fb3ba74dca9fca625fe2/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignerInfo.cs#L691-L696
var chain = new X509Chain ();
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.ExtraStore.AddRange (certificates);
return chain;
}

SslPolicyErrors EvaluateSslPolicyErrors (X509Certificate2? certificate, X509Chain chain, SecTrust secTrust)
{
var sslPolicyErrors = SslPolicyErrors.None;

try {
if (certificate is null) {
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable;
} else if (!chain.Build (certificate)) {
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
}
} catch (ArgumentException) {
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
}

if (!secTrust.Evaluate (out _)) {
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
}

return sslPolicyErrors;
}
}

// There's no way to turn off automatic decompression, so yes, we support it
public bool SupportsAutomaticDecompression {
Expand Down Expand Up @@ -879,26 +980,32 @@ public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask
return;

// ToCToU for the callback
var trustCallbackForUrl = sessionHandler.TrustOverrideForUrl;
var trustSec = false;
var usedCallback = false;
#if !NET
var trustCallback = sessionHandler.TrustOverride;
#endif
var trustCallbackForUrl = sessionHandler.TrustOverrideForUrl;
#if NET
var hasCallBack = trustCallbackForUrl is not null;
#else
var hasCallBack = trustCallback is not null || trustCallbackForUrl is not null;
#endif
if (hasCallBack && challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust) {
#if NET
// if the trust delegate allows to ignore the cert, do it. Since we are using nullables, if the delegate is not present, by default is false
var trustSec = (trustCallbackForUrl?.Invoke (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false);
#else
// if one of the delegates allows to ignore the cert, do it. We check first the one that takes the url because is more precisse, later the
// more general one. Since we are using nullables, if the delegate is not present, by default is false
var trustSec = (trustCallbackForUrl?.Invoke (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false) ||
trustSec = (trustCallbackForUrl?.Invoke (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false) ||
(trustCallback?.Invoke (sessionHandler, challenge.ProtectionSpace.ServerSecTrust) ?? false);
usedCallback = true;
}
#else
if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust) {
// if the trust delegate allows to ignore the cert, do it. Since we are using nullables, if the delegate is not present, by default is false
if (trustCallbackForUrl is not null) {
trustSec = trustCallbackForUrl (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust);
usedCallback = true;
} else if (sessionHandler.TryInvokeServerCertificateCustomValidationCallback (inflight.Request, challenge.ProtectionSpace.ServerSecTrust, out trustSec)) {
usedCallback = true;
}
}
#endif

if (usedCallback) {
if (trustSec) {
var credential = new NSUrlCredential (challenge.ProtectionSpace.ServerSecTrust);
completionHandler (NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
Expand Down
93 changes: 93 additions & 0 deletions tests/monotouch-test/System.Net.Http/MessageHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Net.Http;
#if NET
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
#endif
using System.Linq;
using System.IO;
Expand Down Expand Up @@ -546,6 +547,98 @@ public void AcceptSslCertificatesServicePointManager (Type handlerType)
}
}

#if NET
[TestCase ("https://self-signed.badssl.com/")]
[TestCase ("https://wrong.host.badssl.com/")]
public void AcceptSslCertificatesWithCustomValidationCallbackNSUrlSessionHandler (string url)
{
TestRuntime.AssertSystemVersion (ApplePlatform.MacOSX, 10, 9, throwIfOtherPlatform: false);
TestRuntime.AssertSystemVersion (ApplePlatform.iOS, 7, 0, throwIfOtherPlatform: false);

bool callbackWasExecuted = false;
bool done = false;
Exception ex = null;
HttpResponseMessage result = null;
X509Certificate2 serverCertificate = null;
SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None;

var handler = new NSUrlSessionHandler {
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => {
callbackWasExecuted = true;
serverCertificate = certificate;
sslPolicyErrors = errors;
return true;
}
};

TestRuntime.RunAsync (DateTime.Now.AddSeconds (30), async () =>
{
try {
var client = new HttpClient (handler);
result = await client.GetAsync (url);
} catch (Exception e) {
ex = e;
} finally {
done = true;
}
}, () => done);

if (!done) { // timeouts happen in the bots due to dns issues, connection issues etc.. we do not want to fail
Assert.Inconclusive ("Request timedout.");
} else {
Assert.True (callbackWasExecuted, "Validation Callback called");
Assert.AreNotEqual (SslPolicyErrors.None, sslPolicyErrors, "Callback was called with unexpected SslPolicyErrors");
Assert.IsNotNull (serverCertificate, "Server certificate is null");
Assert.IsNull (ex, "Exception wasn't expected.");
Assert.IsNotNull (result, "Result was null");
Assert.IsTrue (result.IsSuccessStatusCode, "Status code was not success");
}
}

[TestCase ("https://www.microsoft.com/")]
public void RejectSslCertificatesWithCustomValidationCallbackNSUrlSessionHandler (string url)
{
TestRuntime.AssertSystemVersion (ApplePlatform.MacOSX, 10, 9, throwIfOtherPlatform: false);
TestRuntime.AssertSystemVersion (ApplePlatform.iOS, 7, 0, throwIfOtherPlatform: false);

bool callbackWasExecuted = false;
bool done = false;
Exception ex = null;
HttpResponseMessage result = null;

var handler = new NSUrlSessionHandler {
ServerCertificateCustomValidationCallback = (sender, certificate, chain, errors) => {
callbackWasExecuted = true;
Assert.IsNotNull (certificate);
Assert.AreEqual (SslPolicyErrors.None, errors);
return false;
}
};

TestRuntime.RunAsync (DateTime.Now.AddSeconds (30), async () =>
{
try {
var client = new HttpClient (handler);
result = await client.GetAsync (url);
} catch (Exception e) {
ex = e;
} finally {
done = true;
}
}, () => done);

if (!done) { // timeouts happen in the bots due to dns issues, connection issues etc.. we do not want to fail
Assert.Inconclusive ("Request timedout.");
} else {
Assert.True (callbackWasExecuted, "Validation Callback called.");
Assert.IsNotNull (ex, result == null ? "Expected exception is missing and got no result." : $"Expected exception but got {result.Content.ReadAsStringAsync ().Result}.");
Assert.IsInstanceOf (typeof (HttpRequestException), ex, "Exception type");
Assert.IsNotNull (ex.InnerException, "InnerException");
Assert.IsInstanceOf (typeof (WebException), ex.InnerException, "InnerException type");
}
}
#endif

[Test]
public void AssertDefaultValuesNSUrlSessionHandler ()
{
Expand Down