-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Fix HttpClientHandler ServerCertificateCustomValidationCallback for UWP #21905
Conversation
|
||
// Create .NET X509Chain from the WinRT information. We need to rebuild | ||
// the chain since WinRT only gives us an array of intermediate certificates. | ||
var serverChain = new X509Chain(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can/should we dispose of the serverCert, serverChain, etc. when done with them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably. I was afraid to do it because I didn't remember the behavior contract in terms of .NET Framework. I.e., what happens if a user-provided callback wants to save those values for later inspection, i.e. cert-pinning scenarios?
I did notice that we dispose of them in WinHttpHandler.
finally
{
if (chain != null)
{
chain.Dispose();
}
serverCertificate.Dispose();
}
But that might not be correct. We might need to stop disposing of them in WinHttpHandler and *Nix. Need to research what the behavior is on .NET Framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked .NET Framework. It does indeed dispose of the chain and server certificate after the callback is done. So, I will revise this code to do the same.
For reference, here is a summary of the .NET Framework code:
https://github.com/Microsoft/referencesource/blob/master/System/net/System/Net/_SecureChannel.cs#L1281
if (remoteCertValidationCallback != null)
{
success = remoteCertValidationCallback(m_HostName, remoteCertificateEx, chain, sslPolicyErrors);
}
// ...
finally
{
// At least on Win2k server the chain is found to have dependencies on the original cert context.
// So it should be closed first.
if (chain != null)
{
chain.Reset();
}
if (remoteCertificateEx != null)
{
remoteCertificateEx.Reset();
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bartonjs What does the .Reset() do in X509Chain and X509Certificate2 How is compared to doing .Dispose()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dispose just calls Reset. Any X509Certificate2 objects extracted from chain.ChainElements are still valid, it just releases the native resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. I added .Dispose() calls.
|
||
private X509Certificate2 ConvertCertificate(RTCertificate cert) | ||
{ | ||
// Conver Windows X509v2 cert to .NET X509v2 cert. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Conver => Convert
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
{ | ||
rtResponse = await _next.SendRequestAsync(rtRequest).AsTask(cancel).ConfigureAwait(false); | ||
} | ||
catch (TaskCanceledException) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just TaskCanceledException, or any OperationCanceledException?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied this pattern from here:
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L100
If it's wrong there, we can fix both in later PR.
Exception savedEx = (Exception)rtRequest.Properties[SavedExceptionLookupKey]; | ||
if (savedEx != null) | ||
{ | ||
throw savedEx; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does something higher up the call stack wrap this? It'd be nice to maintain the original call stack if possible, so if we do need to throw it here, using ExceptionDispatchInfo.Throw(savedEx)
instead, or capturing the ExceptionDispatchInfo at the original catch site and then throwing that here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrapped in an HttpRequestException here:
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/netcore50/System/Net/HttpClientHandler.cs#L577
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into seeing if the ExceptionDispatchInfo
is useful here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub So, I could change the throw savedEx
here to ExceptionDispatchInfo.Throw(savedEx)
.
But, since that exception is saved earlier in WinRTCertificateCallback, how can I save the exception w/ the right call stacks there and then later throw it here? I don't understand ExceptionDispatchInfo object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the MSDN, it seems I should save an ExceptionDispatchInfo object obtained by doing ExceptionDispatchInfo.Capture(exception)
instead of saving an Exception like I'm doing now. Then I can throw it later with savedExceptionDispatchInfo.Throw()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Use ExceptionDispatchInfo
cc: @bartonjs to help review the RTServerCertificateCallback method |
@@ -177,6 +201,7 @@ public static IEnumerable<object[]> UseCallback_ValidCertificate_ExpectedValuesD | |||
} | |||
} | |||
|
|||
[ActiveIssue(7812, TestPlatforms.Windows)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last comment in this bug is saying this is an OS problem. Can we just run the test for OS versions that have the fix?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since a new OS which fixes the problem can be either client or server side, yes, I can re-enable the test on newer Windows client builds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. I changed the ActiveIssue to a ConditionalFact/ConditionalTheory.
{ | ||
serverChain.ChainPolicy.ExtraStore.Add(ConvertCertificate(cert)); | ||
} | ||
serverChain.Build(serverCert); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't building the chain be identical with the code in SslStream and WinHttpHandler (include OID checks, etc)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
{ | ||
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; | ||
} | ||
foreach (RTChainValidationResult result in args.ServerCertificateErrors) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this be enough to identify the chain errors? If no, is there any reason for not removing / augmenting this check with code we already have for other components (see above comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like @bartonjs to review the "chain building" code here. I didn't use the code in SslStream and WinHttpHandler because that code also uses X509Chain.Build to build the chain. The only reason it then uses the low-level SSPI calls is to find out if the name mismatch occurs.
The current .NET X509Chain.Build doesn't take any DNS name to compare against. And that is why the low-level SSPI calls are necessary.
On the other hand, this information is already available in the WinRT ChainValidationResult. So, that means, I don't need to call the SSPI stuff since I already have the potential name-mismatch determination.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that's missing is the OID check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chain building part here should match SslStream. Just like in SslStream the hostname error would come from a second source (in this case, the RTChainValidationResult value seems legit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chain building part here should match SslStream.
@bartonjs What does this mean exactly? Does it mean that using Chain.Build() as I am doing here sufficient? Or we need to call the low-level SSPI stuff as is doing in SslStream and WinHttpHandler. And by the way, that code also calls Chain.Build(). The only time it uses SSPI stuff is to figure out the "name mismatch" determination. And we already know that because the WinRT APIs return that information already (a feature missing in .NET X509Chain.Build because it doesn't take a DNS server name parameter).
So, what should I do regarding my PR and how I am "converting" from WinRT certificate information to .NET X509Chain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. Thx. I'll update the chain.Build() calls to match the WinHttpHandler code to include OID check and RevocationMode flags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will you be able to unify the code instead of creating a copy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an existing TODO, #2165
// TODO: Issue #2165. Merge with similar code used in System.Net.Security move to Common/src//System/Net.
public static void BuildChain(
I will update the current issue to reflect that we have more places to consolidate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correction: It seems that issue is now closed. :(
I will open up a new issue and mark this code with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, there is a new issue, #14542 for it. I will amend that issue with this information.
{ | ||
throw; | ||
} | ||
catch (Exception) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if there are two separate kinds of Exception types (one saved and a completely different one here)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, I changed the "savedException" of type Exception
to be a "savedExceptionDispatchInfo" of type ExceptionDispatchInfo
as recommended by @stephentoub.
If there is a saved ExceptionDispatchInfo, we will use it because at the point it happened, the user callback (.NET callback) threw an unhandled exception. We need to report that. It also stops all further processing of the HTTP request, so there are no other exceptions we need to worry about.
When such an unhandled exception is thrown by the user provided .NET callback, our WinRTCertificateCallback wrapper traps that then calls Reject() on the WinRT callback layer. That causes the WinRT layer to throw an exception. That is really the exception we are trapping here in that case. And so, we need to see if we saved the ExceptionDispatchInfo object because that is really the root cause of why the WinRT layer threw an exception. And so, we need to propagate that.
// considered "extra" validation. We need to explicitly ignore errors so that the callback | ||
// will get called. | ||
// | ||
// In addition, the WinRT layer restricts some errors so that they cannot be ignored, such |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From your first link:
Default OS validation of the server certificate is performed before raising this event. If the certificate fails this validation, the connection is terminated and your event handler is not called.
In order to skip parts of the OS validation (not recommended for production scenarios), use the IgnorableServerCertificateErrors property to specify the errors that you want to ignore. Then as long as the certificate does not have any other errors, the OS validation will be considered successful and your event handler will be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or are you asking specifically about the ChainValidationResult values that the app is allowed to ignore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree the documentation is not complete in all WinRT areas.
For example, trying to do this:
var filter = new Windows.Web.Http.Filters.HttpBaseProtocolFilter();
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Revoked);
results in this exception:
System.ArgumentException: The parameter is incorrect.
'item': Provided value is not an ignorable ChainValidationResult value.
// ChainValidationResult.InvalidCertificateAuthorityPolicy | ||
// ChainValidationResult.InvalidSignature | ||
// ChainValidationResult.OtherErrors | ||
// ChainValidationResult.Revoked |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ChainValidationResult.Revoked can be ignored, but not ChainValidationResult.IncompleteChain.
Update: This was based on the Wininet flags that are currently available in the OS (see WINHTTP_OPTION_SECURITY_FLAGS in MSDN), but it turns out that WinRT has its own list of ignorable conditions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be more future-proof to just add ALL ChainValidationResult values to the IgnorableServerCertificateErrors, and just comment that some of them will not effectively be ignored?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Diego-Perez-Botero See my comment above. Passing in ChainValidation.Revoked to the filter.IgnorableServerCertificateErrors.Add returns an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be more future-proof to just add ALL ChainValidationResult values to the IgnorableServerCertificateErrors, and just comment that some of them will not effectively be ignored?
Yes, but passing in ones that can't be ignored throws exceptions from the Add() method. And I didn't want to add exception throwing noise all the time even if we catch it. It adds noise to debugging sessions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. I didn't know that exceptions were thrown for "unignorable" values :)
} | ||
} | ||
|
||
private X509Certificate2 ConvertCertificate(RTCertificate cert) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider renaming this to "ConvertPublicKeyCertificate" or something to that effect. The conversion being carried out here will drop any private keys from the WinRT certificate, which is fine for our needs (server's identity certificates don't contain private keys), but we don't want future developers to assume that this method's conversion approach can be used for all purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
_rtFilter.IgnorableServerCertificateErrors.Add(RTChainValidationResult.RevocationInformationMissing); | ||
_rtFilter.IgnorableServerCertificateErrors.Add(RTChainValidationResult.Untrusted); | ||
_rtFilter.IgnorableServerCertificateErrors.Add(RTChainValidationResult.WrongUsage); | ||
_rtFilter.ServerCustomValidationRequested += RTServerCertificateCallback; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this need to be done in "light up" fashion (checking that the custom validation callback API surface is available)? This property was added in 14393, so I'm not sure if "light up" logic is in order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I added light-up for this. The fallback is to ignore any callback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback is to ignore any callback.
I think the fallback has to be to throw. Not all callbacks are ignoring errors, some of them are imposing restrictions. You can't let a connection transmit data if a custom callback didn't get to have its say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't let a connection transmit data if a custom callback didn't get to have its say.
This is a good point. I think this demonstrates one example where we have to throw an exception at runtime if the light-up detection fails and we are trying to use that feature.
We can either throw an exception during the property setter of the callback. That would be a PNSE (PlatformNotSupportedException). Otherwise, if we validate everything during .SendAsync, we'll need to throw an HttpRequestException with perhaps a PNSE inside that.
I'm leaning toward throwing at the property setter since it is can be calculated right away that the feature won't work since we're running on a Windows 10 OS version before that callback was implemented in WinRT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can either throw an exception during the property setter or the callback
For reference, we have a similar issue with CurlHandler, and we throw the PNSE during the request/callback. While in some situations we could detect this based purely on inspecting the libcurl version/build being used, in general we don't know whether a callback is supported until we try to register it with libcurl and it gives us an answer about whether it could hook it up.
I pushed a new commit which should address all feedback. Please PTAL. After approvals, I still need to rebase (to fix merge conflicts) and run final tests. |
This PR implements the ServerCertificateCustomValidationCallback for UWP. While implementing this, I discovered that the behavior of the current .NET Core code is incorrect regarding how to propagate exceptions from the user provided callback. I opened up a new issue #21904 and revised the test as well. Fixes #21627
@dotnet-bot Test Outerloop Windows x86 Release Build |
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build |
|
||
if (!success) | ||
{ | ||
args.Reject(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like that the API requires call to Reject instead of one to Accept. Not yours, I know.
But to make sure that no early returns get added to this function to make it dangerous I think it would be useful to make the work be done in a helper method, then this is a simple
if (!HandleCustomCallback(args))
{
args.Reject();
}
(It'd possibly be better if each piece that needed data from args was fed the data without args leaving this function, so it's clear that no one else alters it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like that the API requires call to Reject instead of one to Accept. Not yours, I know.
Actually, my team implemented that. :) We had long discussions about Accept vs. Reject. The architects concluded that since we were adding this callback and it was "extra" validation, the default behavior for an empty callback function was to accept. This was because one still has to manually turn on the flags to ignore errors. So, by default, adding a WinRT callback adds checks and doesn't allow errors to occur.
That aside, I will see about refactoring this code to make it more maintainable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SslPolicyErrors building in custom callbacks looks good to me. But requesting a custom callback and not being able to service it needs to throw (fail closed) and I'd be happier with the refactoring to make early returns not break the security model (also fail closed behavior).
[OuterLoop] // TODO: Issue #11345 | ||
[Theory] | ||
[ConditionalTheory(nameof(ClientSupportsDHECipherSuites))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a negative behavior that we should be validating? Like a kind of exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please refer to #7812. This is to prevent CI failures. We don't want these tests running on Windows OS versions that don't have the fixes to the DHE handshake bugs. It is not a feature we want to test, per se.
[OuterLoop] // TODO: Issue #11345 | ||
[Theory] | ||
[ConditionalTheory(nameof(ClientSupportsDHECipherSuites))] | ||
[MemberData(nameof(CertificateValidationServers))] | ||
public async Task NoCallback_BadCertificate_ThrowsException(string url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabling this test seems weird. If no cipher suite was negotiated, wouldn't an exception be thrown? So wouldn't this test just work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please refer to #7812. This is to prevent CI failures. We don't want these tests running on Windows OS versions that don't have the fixes to the DHE handshake bugs. It is not a feature we want to test, per se.
@@ -321,6 +354,7 @@ public static IEnumerable<object[]> UseCallback_ValidCertificate_ExpectedValuesD | |||
} | |||
} | |||
|
|||
[SkipOnTargetFramework(TargetFrameworkMonikers.Uap, "UAP doesn't expose channel binding information")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there should be a test that shows that it throws?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more about a missing feature in UAP. Channel binding information is used but the WinRT API doesn't expose it up-stack. Hence, we can't expose it in the .NET layer either. So, we simply don't run the test on UAP. If we were to write a test to "pin" this behavior, the test would simply be validating a null byte array for channel binding information is returned. Is that what you are suggesting I add?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I mean, the function does something. Whether that be return null, throw, give a fixed value of the integer digits of pi... whatever it does we should validate that it's doing it so that a) we know if it changes (may have doc impact, a bunch of tests that need to be enabled now, etc) and b) if a customer opens an issue it's much easier to say "by design" if we can point at an existing test.
Based on @bartonjs feedback, I pushed another commit. Key changes:
|
…WP (dotnet/corefx#21905) * Fix HttpClientHandler ServerCertificateCustomValidationCallback for UWP This PR implements the ServerCertificateCustomValidationCallback for UWP. While implementing this, I discovered that the behavior of the current .NET Core code is incorrect regarding how to propagate exceptions from the user provided callback. I opened up a new issue dotnet/corefx#21904 and revised the test as well. Fixes dotnet/corefx#21627 * Address more PR feedback Commit migrated from dotnet/corefx@e30fdc3
This PR implements the ServerCertificateCustomValidationCallback for UWP. While implementing this, I discovered that the behavior of the current .NET Core code is incorrect regarding how to propagate exceptions from the user provided callback. I opened up a new issue #21904 and revised the test as well.
Fixes #21627