Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

ClientWebSocket supports client certificates in UWP#21908

Merged
Diego-Perez-Botero merged 7 commits into
dotnet:masterfrom
Diego-Perez-Botero:clientwebsocket-client-certs
Jul 7, 2017
Merged

ClientWebSocket supports client certificates in UWP#21908
Diego-Perez-Botero merged 7 commits into
dotnet:masterfrom
Diego-Perez-Botero:clientwebsocket-client-certs

Conversation

@Diego-Perez-Botero
Copy link
Copy Markdown
Member

@Diego-Perez-Botero Diego-Perez-Botero commented Jul 5, 2017

With these changes, the UWP implementation of ClientWebSocket.ConnectAsync takes the ClientCertificates member of the app-provided ClientWebSocketOptions into account. The first client cert that can be successfully converted into a WinRT client cert (if any) is configured on the underlying WinRT MessageWebSocket instance, leading to the cert being used for mutual TLS authentication when establishing connections with WSS endpoints.

This fix leverages WinRT APIs that are only present since Windows 10 Insider Preview Build 16215, so API presence checks are in place.

Fixes #21393

CC: @mconnew

@Diego-Perez-Botero Diego-Perez-Botero added * NO MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) area-System.Net os-windows-uwp labels Jul 5, 2017
@Diego-Perez-Botero Diego-Perez-Botero added this to the UWP6.0 milestone Jul 5, 2017
@Diego-Perez-Botero Diego-Perez-Botero self-assigned this Jul 5, 2017
@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

Diego-Perez-Botero commented Jul 5, 2017

Marking as NO MERGE while we wait for the UWP test runner to be updated with the Windows Insider SDK flight 16225 metadata (#21786).

CC: @joperezr

@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

@dotnet-bot test NETFX x86 release build
@dotnet-bot test outerloop netcoreapp OSX10.12 Debug
@dotnet-bot test outerloop netcoreapp Ubuntu16.10 Debug
@dotnet-bot test outerloop netcoreapp Windows_NT Debug
@dotnet-bot test outerloop netcoreapp Windows_NT Release

Copy link
Copy Markdown

@CIPop CIPop left a comment

Choose a reason for hiding this comment

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

We could make the UWP requirement more discoverable by throwing exceptions with clear messages.

websocketControl.SupportedProtocols.Add(subProtocol);
}

if (MessageWebSocketClientCertificateSupported)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think this should be discoverable by the app-developer as soon as possible - maybe when setting the ClientWebSocketOptions?

Copy link
Copy Markdown
Member Author

@Diego-Perez-Botero Diego-Perez-Botero Jul 5, 2017

Choose a reason for hiding this comment

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

Is your suggestion to throw an exception if MessageWebSocketClientCertificateSupported is false? If so, that sounds like an AppCompat issue (ClientWebSocket.ConnectAsync would stop working outside of Insider Preview builds).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should not throw exceptions for this kind of platform difference. See prior HttpClient UWP PRs. In general, we need to avoid throwing PNSE because it breaks developers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I was recommending throwing an exception when trying to get or set ClientCertificates on ClientWebSocketOptions.
I think it's better to be able to discover this immediately and root-cause it instead of trying to figure out why, after setting all properties, the app doesn't use client certificate authentication.

Copy link
Copy Markdown
Member Author

@Diego-Perez-Botero Diego-Perez-Botero Jul 6, 2017

Choose a reason for hiding this comment

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

As far as Windows build-dependent feature support goes, not throwing (and instead going with a no-op approach) is consistent with the decisions that were made in HttpClient PRs #21403 and #21511

Copy link
Copy Markdown
Member Author

@Diego-Perez-Botero Diego-Perez-Botero Jul 6, 2017

Choose a reason for hiding this comment

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

A more recent example of light-up logic using no-op as fallback is #21905

UPDATE: That PR is now going with the "throw an exception" approach.

Copy link
Copy Markdown
Member Author

@Diego-Perez-Botero Diego-Perez-Botero Jul 7, 2017

Choose a reason for hiding this comment

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

In the new iteration, I'm throwing a NotSupportedException if an app has specified some client certs and the necessary WinRT API surface isn't available. The exception is being thrown as part of WinRTWebSocket.ConnectAsync for three reasons:

  1. Throwing while setting the ClientWebSocketOptions would require a large amount of changes in the PAL, since we should only throw if a client cert is actually added to the ClientWebSocketOptions.ClientCertificates collection. At a minimum, an OnInsert handler would need to be subscribed with the X509CertificateCollection and the necessary "is supported" information would need to be piped through the ClientWebSocketOptions and WebSocketHandle classes.
  2. WebSocketHandle.ConnectAsyncCore (see WebSocketHandle.WinRT.cs) calls into WinRTWebSocket.ConnectAsync (this method) and catches all exceptions. Those exceptions are then used as the inner exception for a new WebSocketException. Thus, app developers would not need to add a new "catch" statement just to handle this scenario (they should already be catching WebSocketException).
  3. A descriptive exception message (which I'm adding) still allows a developer to figure out what went wrong.

foreach (X509Certificate dotNetClientCert in options.ClientCertificates)
{
RTCertificate winRtClientCert = ConvertDotNetClientCertToWinRtClientCert(dotNetClientCert);
if (winRtClientCert != null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should throw for this case as well (maybe when setting the ClientWebSocketOptions) with a good text indicating that the developer should install the certificate in the My store for this to work.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

.NET Framework doesn't have an equivalent exception type/message. Are we OK with adding a UWP-specific exception here? I'm all for it, but just want to confirm that there's a precedent for that type of divergence in behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:Are we OK with adding a UWP-specific exception here?

No. We do not add new UWP-specific exceptions like that since it will break devs depending on NETStandard behaviors.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At most, we should fallback gracefully but log the problem, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We throw a lot of exceptions and sometimes differently for different implementations. In this case I would expect that the call fails as we cannot honor the given configuration. Something on the lines of "the client certificate will not be used as we cannot access the private key". We don't need to invent a new exception - we can easily reuse exceptions thrown by ConnectAsync such as WebSocketException, ArgumentException, InvalidOperationException, etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Throwing an exception here seems less controversial to me. Unlike the other comment, this exception would be related to the app's certificate usage (fixable by the app developer) instead of the Windows build number (nothing the app developer can do about it).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In the new iteration, I'm throwing a NotSupportedException if an app has specified some client certs and first cert cannot be converted into a WinRT Certificate. The exception is being thrown as part of WinRTWebSocket.ConnectAsync following the same rationale explained in the other comment thread.

@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

@dotnet-bot help

@dotnet-bot
Copy link
Copy Markdown

Welcome to the dotnet/corefx Repository

The following is a list of valid commands on this PR. To invoke a command, comment the indicated phrase on the PR

The following commands are valid for all PRs and repositories.

Click to expand
Comment Phrase Action
@dotnet-bot test this please Re-run all legs. Use sparingly
@dotnet-bot test ci please Generates (but does not run) jobs based on changes to the groovy job definitions in this branch
@dotnet-bot help Print this help message

The following jobs are launched by default for each PR against dotnet/corefx:master.

Click to expand
Comment Phrase Job Launched
@dotnet-bot test Linux x64 Release Build Linux x64 Release Build
@dotnet-bot test OSX x64 Debug Build OSX x64 Debug Build
@dotnet-bot test Packaging All Configurations x64 Debug Build Packaging All Configurations x64 Debug Build
@dotnet-bot test Windows x64 Debug Build Windows x64 Debug Build
@dotnet-bot test Windows x86 Release Build Windows x86 Release Build
@dotnet-bot test NETFX x86 Release Build NETFX x86 Release Build
@dotnet-bot test UWP CoreCLR x64 Debug Build UWP CoreCLR x64 Debug Build
@dotnet-bot test UWP NETNative x86 Release Build UWP NETNative x86 Release Build

The following optional jobs are available in PRs against dotnet/corefx:master.

Click to expand
Comment Phrase Job Launched
@dotnet-bot test Linux x64 Debug Build Queues Linux x64 Debug Build
@dotnet-bot test Outerloop Linux x64 Debug Build Queues Outerloop Linux x64 Debug Build
@dotnet-bot test Outerloop Linux x64 Release Build Queues Outerloop Linux x64 Release Build
@dotnet-bot test Outerloop OSX x64 Debug Build Queues Outerloop OSX x64 Debug Build
@dotnet-bot test OSX x64 Release Build Queues OSX x64 Release Build
@dotnet-bot test Outerloop OSX x64 Release Build Queues Outerloop OSX x64 Release Build
@dotnet-bot test Outerloop Packaging All Configurations x64 Debug Build Queues Outerloop Packaging All Configurations x64 Debug Build
@dotnet-bot test Packaging All Configurations x86 Debug Build Queues Packaging All Configurations x86 Debug Build
@dotnet-bot test Outerloop Packaging All Configurations x86 Debug Build Queues Outerloop Packaging All Configurations x86 Debug Build
@dotnet-bot test Packaging All Configurations x64 Release Build Queues Packaging All Configurations x64 Release Build
@dotnet-bot test Outerloop Packaging All Configurations x64 Release Build Queues Outerloop Packaging All Configurations x64 Release Build
@dotnet-bot test Packaging All Configurations x86 Release Build Queues Packaging All Configurations x86 Release Build
@dotnet-bot test Outerloop Packaging All Configurations x86 Release Build Queues Outerloop Packaging All Configurations x86 Release Build
@dotnet-bot test Outerloop Windows x64 Debug Build Queues Outerloop Windows x64 Debug Build
@dotnet-bot test Windows x86 Debug Build Queues Windows x86 Debug Build
@dotnet-bot test Outerloop Windows x86 Debug Build Queues Outerloop Windows x86 Debug Build
@dotnet-bot test Windows x64 Release Build Queues Windows x64 Release Build
@dotnet-bot test Outerloop Windows x64 Release Build Queues Outerloop Windows x64 Release Build
@dotnet-bot test Outerloop Windows x86 Release Build Queues Outerloop Windows x86 Release Build
@dotnet-bot test NETFX x64 Debug Build Queues NETFX x64 Debug Build
@dotnet-bot test Outerloop NETFX x64 Debug Build Queues Outerloop NETFX x64 Debug Build
@dotnet-bot test NETFX x86 Debug Build Queues NETFX x86 Debug Build
@dotnet-bot test Outerloop NETFX x86 Debug Build Queues Outerloop NETFX x86 Debug Build
@dotnet-bot test NETFX x64 Release Build Queues NETFX x64 Release Build
@dotnet-bot test Outerloop NETFX x64 Release Build Queues Outerloop NETFX x64 Release Build
@dotnet-bot test Outerloop NETFX x86 Release Build Queues Outerloop NETFX x86 Release Build
@dotnet-bot test Outerloop UWP CoreCLR x64 Debug Build Queues Outerloop UWP CoreCLR x64 Debug Build
@dotnet-bot test UWP CoreCLR x86 Debug Build Queues UWP CoreCLR x86 Debug Build
@dotnet-bot test Outerloop UWP CoreCLR x86 Debug Build Queues Outerloop UWP CoreCLR x86 Debug Build
@dotnet-bot test UWP CoreCLR x64 Release Build Queues UWP CoreCLR x64 Release Build
@dotnet-bot test Outerloop UWP CoreCLR x64 Release Build Queues Outerloop UWP CoreCLR x64 Release Build
@dotnet-bot test UWP CoreCLR x86 Release Build Queues UWP CoreCLR x86 Release Build
@dotnet-bot test Outerloop UWP CoreCLR x86 Release Build Queues Outerloop UWP CoreCLR x86 Release Build
@dotnet-bot test UWP NETNative x64 Debug Build Queues UWP NETNative x64 Debug Build
@dotnet-bot test Outerloop UWP NETNative x64 Debug Build Queues Outerloop UWP NETNative x64 Debug Build
@dotnet-bot test UWP NETNative x86 Debug Build Queues UWP NETNative x86 Debug Build
@dotnet-bot test Outerloop UWP NETNative x86 Debug Build Queues Outerloop UWP NETNative x86 Debug Build
@dotnet-bot test UWP NETNative x64 Release Build Queues UWP NETNative x64 Release Build
@dotnet-bot test Outerloop UWP NETNative x64 Release Build Queues Outerloop UWP NETNative x64 Release Build
@dotnet-bot test Outerloop UWP NETNative x86 Release Build Queues Outerloop UWP NETNative x86 Release Build

Have a nice day!

@dotnet-bot
Copy link
Copy Markdown

Welcome to the dotnet/corefx Repository

The following is a list of valid commands on this PR. To invoke a command, comment the indicated phrase on the PR

The following commands are valid for all PRs and repositories.

Click to expand
Comment Phrase Action
@dotnet-bot test this please Re-run all legs. Use sparingly
@dotnet-bot test ci please Generates (but does not run) jobs based on changes to the groovy job definitions in this branch
@dotnet-bot help Print this help message

The following jobs are launched by default for each PR against dotnet/corefx:master.

Click to expand
Comment Phrase Job Launched
@dotnet-bot test Linux arm Release Build Linux arm Release Build
@dotnet-bot test Tizen armel Debug Build Tizen armel Debug Build

The following optional jobs are available in PRs against dotnet/corefx:master.

Click to expand
Comment Phrase Job Launched
@dotnet-bot test innerloop CentOS7.1 Debug Queues Innerloop CentOS7.1 Debug x64 Build and Test
@dotnet-bot test innerloop CentOS7.1 Release Queues Innerloop CentOS7.1 Release x64 Build and Test
@dotnet-bot test code coverage Queues Code Coverage Windows Debug
@dotnet-bot test innerloop Debian8.4 Debug Queues Innerloop Debian8.4 Debug x64 Build and Test
@dotnet-bot test innerloop Debian8.4 Release Queues Innerloop Debian8.4 Release x64 Build and Test
@dotnet-bot test innerloop Fedora24 Debug Queues Innerloop Fedora24 Debug x64 Build and Test
@dotnet-bot test innerloop Fedora24 Release Queues Innerloop Fedora24 Release x64 Build and Test
@dotnet-bot test Linux arm Debug Queues Linux arm Debug Build
@dotnet-bot test code formatter check Queues Code Formatter Check
@dotnet-bot test innerloop OpenSUSE13.2 Debug Queues Innerloop OpenSUSE13.2 Debug x64 Build and Test
@dotnet-bot test innerloop OpenSUSE13.2 Release Queues Innerloop OpenSUSE13.2 Release x64 Build and Test
@dotnet-bot test innerloop OpenSUSE42.1 Debug Queues Innerloop OpenSUSE42.1 Debug x64 Build and Test
@dotnet-bot test innerloop OpenSUSE42.1 Release Queues Innerloop OpenSUSE42.1 Release x64 Build and Test
@dotnet-bot test innerloop OSX10.12 Debug Queues Innerloop OSX10.12 Debug x64 Build and Test
@dotnet-bot test innerloop OSX10.12 Release Queues Innerloop OSX10.12 Release x64 Build and Test
@dotnet-bot test outerloop netcoreapp CentOS7.1 Debug Queues OuterLoop netcoreapp CentOS7.1 Debug x64
@dotnet-bot test outerloop netcoreapp CentOS7.1 Release Queues OuterLoop netcoreapp CentOS7.1 Release x64
@dotnet-bot test outerloop netcoreapp Debian8.4 Debug Queues OuterLoop netcoreapp Debian8.4 Debug x64
@dotnet-bot test outerloop netcoreapp Debian8.4 Release Queues OuterLoop netcoreapp Debian8.4 Release x64
@dotnet-bot test outerloop netcoreapp Fedora24 Debug Queues OuterLoop netcoreapp Fedora24 Debug x64
@dotnet-bot test outerloop netcoreapp Fedora24 Release Queues OuterLoop netcoreapp Fedora24 Release x64
@dotnet-bot test outerloop netcoreapp OpenSUSE13.2 Debug Queues OuterLoop netcoreapp OpenSUSE13.2 Debug x64
@dotnet-bot test outerloop netcoreapp OpenSUSE13.2 Release Queues OuterLoop netcoreapp OpenSUSE13.2 Release x64
@dotnet-bot test outerloop netcoreapp OpenSUSE42.1 Debug Queues OuterLoop netcoreapp OpenSUSE42.1 Debug x64
@dotnet-bot test outerloop netcoreapp OpenSUSE42.1 Release Queues OuterLoop netcoreapp OpenSUSE42.1 Release x64
@dotnet-bot test outerloop netcoreapp OSX10.12 Debug Queues OuterLoop netcoreapp OSX10.12 Debug x64
@dotnet-bot test outerloop netcoreapp OSX10.12 Release Queues OuterLoop netcoreapp OSX10.12 Release x64
@dotnet-bot test outerloop netcoreapp PortableLinux Debug Queues OuterLoop netcoreapp PortableLinux Debug x64
@dotnet-bot test outerloop netcoreapp PortableLinux Release Queues OuterLoop netcoreapp PortableLinux Release x64
@dotnet-bot test outerloop netcoreapp RHEL7.2 Debug Queues OuterLoop netcoreapp RHEL7.2 Debug x64
@dotnet-bot test outerloop netcoreapp RHEL7.2 Release Queues OuterLoop netcoreapp RHEL7.2 Release x64
@dotnet-bot test outerloop netcoreapp Ubuntu14.04 Debug Queues OuterLoop netcoreapp Ubuntu14.04 Debug x64
@dotnet-bot test outerloop netcoreapp Ubuntu14.04 Release Queues OuterLoop netcoreapp Ubuntu14.04 Release x64
@dotnet-bot test outerloop netcoreapp Ubuntu16.04 Debug Queues OuterLoop netcoreapp Ubuntu16.04 Debug x64
@dotnet-bot test outerloop netcoreapp Ubuntu16.04 Release Queues OuterLoop netcoreapp Ubuntu16.04 Release x64
@dotnet-bot test outerloop netcoreapp Ubuntu16.10 Debug Queues OuterLoop netcoreapp Ubuntu16.10 Debug x64
@dotnet-bot test outerloop netcoreapp Ubuntu16.10 Release Queues OuterLoop netcoreapp Ubuntu16.10 Release x64
@dotnet-bot test outerloop netcoreapp Windows 7 Debug Queues OuterLoop netcoreapp Windows 7 Debug x64
@dotnet-bot test outerloop netcoreapp Windows 7 Release Queues OuterLoop netcoreapp Windows 7 Release x64
@dotnet-bot test outerloop netcoreapp Windows_NT Debug Queues OuterLoop netcoreapp Windows_NT Debug x64
@dotnet-bot test outerloop netcoreapp Windows_NT Release Queues OuterLoop netcoreapp Windows_NT Release x64
@dotnet-bot test innerloop PortableLinux Debug Queues Innerloop PortableLinux Debug x64 Build and Test
@dotnet-bot test innerloop PortableLinux Release Queues Innerloop PortableLinux Release x64 Build and Test
@dotnet-bot test innerloop RHEL7.2 Debug Queues Innerloop RHEL7.2 Debug x64 Build and Test
@dotnet-bot test innerloop RHEL7.2 Release Queues Innerloop RHEL7.2 Release x64 Build and Test
@dotnet-bot test Tizen armel Release Queues Tizen armel Release Build
@dotnet-bot test innerloop Ubuntu14.04 Debug Queues Innerloop Ubuntu14.04 Debug x64 Build and Test
@dotnet-bot test innerloop Ubuntu14.04 Release Queues Innerloop Ubuntu14.04 Release x64 Build and Test
@dotnet-bot test innerloop Ubuntu16.04 Debug Queues Innerloop Ubuntu16.04 Debug x64 Build and Test
@dotnet-bot test innerloop Ubuntu16.04 Release Queues Innerloop Ubuntu16.04 Release x64 Build and Test
@dotnet-bot test innerloop Ubuntu16.10 Debug Queues Innerloop Ubuntu16.10 Debug x64 Build and Test
@dotnet-bot test innerloop Ubuntu16.10 Release Queues Innerloop Ubuntu16.10 Release x64 Build and Test
@dotnet-bot test innerloop Windows_NT Debug Queues Innerloop Windows_NT Debug x86 Build and Test
@dotnet-bot test innerloop Windows_NT Release Queues Innerloop Windows_NT Release x64 Build and Test

Have a nice day!

@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

@dotnet-bot Test Outerloop Windows x86 Release Build
@dotnet-bot Test Outerloop Linux x64 Release Build
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build

{
if (!MessageWebSocketClientCertificateSupported)
{
throw new NotSupportedException(string.Format(
Copy link
Copy Markdown
Contributor

@davidsh davidsh Jul 7, 2017

Choose a reason for hiding this comment

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

We usually throw PlatformNotSupportedException for things like this. See recent HttpClient PR #21905.

The NotSupportedException is ok for the other case about certain client certificates not working. But for this exception, it should be PlatformNotSupportedException because the user can solve this by upgrading the platform to a more recent Windows 10 OS.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed

SR.net_WebSockets_UWPClientCertSupportRequiresWindows10InsiderPreviewBuild16215OrGreater));
}

RTCertificate winRtClientCert = ConvertDotNetClientCertToWinRtClientCert(options.ClientCertificates[0]);
Copy link
Copy Markdown
Contributor

@davidsh davidsh Jul 7, 2017

Choose a reason for hiding this comment

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

One thing to mention is that I see you are only taking the first certificate from the array of certs. Did you ever wonder why there is an array of certs and not a parameter taking a single cert?

Historically, System.Net always took a collection of client certificates because some of the client certs could have different issuers. And the old networking stack would pass the trusted issuers list from the server to the client-side network stack during the network i/o. So, System.Net would use that information and help filter down the collection to find client certs that matched the issuers. And then it would take the first one from that.

By default, now, the Windows Server OS doesn't send a trusted issuer list for client cert negotiation. And since we're using WinRT APIs, we don't have visibility into that trusted issuers list anymore.

So, in general, we ended up always just using the first certificate.

But see this:
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpCertificateHelper.cs#L84

I do a little filtering on the list of certs to make sure they are client certs, etc. and then take the first one from that.

Copy link
Copy Markdown
Member Author

@Diego-Perez-Botero Diego-Perez-Botero Jul 7, 2017

Choose a reason for hiding this comment

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

Thanks for the context on why this is taking a collection of certs. It wasn't clear at all from the MSDN documentation. I'll enhance this to do similar filtering, although I'll need to figure out how to handle the case where none of the certs satisfy those additional constraints.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the context on why this is taking a collection of certs. It wasn't clear at all from the MSDN documentation.

The ClientWebSocketOptions object and ClientWebSocket itself came originally from .NET Framework. And it was based on HttpWebRequest originally for the HTTP part and then a separate set of code to handle the WebSocket protocol. HttpWebRequest HTTP stack takes a collection of client certs. So, that is why the API uses a collection .... since it matches the convention of HttpWebRequest.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed

"ClientCertificate");
}

// There are currently only two ways to convert a .NET X509Certificate object into a WinRT Certificate without
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is X509Certificate being used instead of the X509Certificate2 object? This comments applies generally to this file. In reality, they are probably always X509Certificate2 objects.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ClientWebSocketOptions.ClientCertificates is of type X509CertificateCollection and all of its methods deal with X509Certificate objects. If it were an X509Certificate2Collection, we'd be dealing with X509Certificate2 objects.

Copy link
Copy Markdown
Contributor

@davidsh davidsh Jul 7, 2017

Choose a reason for hiding this comment

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

Ok. In reality X509Certificate2 is a derived class of X509Certificate. So, any X509Certificate2 is already an X509Certificate object.

As long as you don't need to call X509Certificate2 methods directly, it doesn't matter. If you did need the benefit of calling the extra X509Certificate2 methods, you could test the up-casting to X509Certificate2.

Copy link
Copy Markdown
Member Author

@Diego-Perez-Botero Diego-Perez-Botero Jul 7, 2017

Choose a reason for hiding this comment

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

So far, the X509Certificate contract has been sufficient :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually, the new cert filtering logic forced me to switch over to using X509Certificate2 objects.

@davidsh davidsh requested a review from bartonjs July 7, 2017 03:25
@davidsh
Copy link
Copy Markdown
Contributor

davidsh commented Jul 7, 2017

Adding @bartonjs for review.

@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

@dotnet-bot Test Outerloop Windows x86 Release Build
@dotnet-bot Test Outerloop Linux x64 Release Build
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build

<value>The requested security protocol is not supported.</value>
</data>
<data name="net_WebSockets_UWPClientCertSupportRequiresWindows10InsiderPreviewBuild16215OrGreater" xml:space="preserve">
<value>Support for client certificates in UWP requires Windows 10 Insider Preview Build 16215 or greater.</value>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems weird to bake in the pre-release name and number here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We do this for now since this is the official pre-release name and version. When the Windows 10 Fall Creators Update ships, we'll update this again with the official RTM Windows version name. This is similar to what we do with "Windows 10 Version 1607" which means build 14393.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed. Although literally true, we should make this more generic. Something like, "... unsupported in Windows 10 version 1703 and earlier". As of yesterday, the insider preview build is version 16232, so there's quickly diminishing relevance of baking in the RS3 version.


In reply to: 126160835 [](ancestors = 126160835)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Then again, we can keep this as-is for now and update it later when RS3 RTMs. That may or may not be within the 2.1.0 milestone. We should open an issue to track it.


In reply to: 126186269 [](ancestors = 126186269,126160835)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"unsupported in Windows 10 version 1703 and earlier"

I like that although I would suggest making it clearer by adding more text.

"This feature is unsupported in Windows 10 version 1703 and earlier versions. Please upgrade Windows 10 to a later release."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed.

"ClientCertificate");
}

private static X509Certificate2 GetEligibleClientCertificate(X509Certificate2Collection candidateCerts)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks like a copy/paste of the one from WInHttpCertificateHelper with an if 0 => null changed to an assert. Can they share?

They both seem to lack the if Count == 1 => use it optimization.

Copy link
Copy Markdown
Contributor

@davidsh davidsh Jul 7, 2017

Choose a reason for hiding this comment

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

We already have an issue #14542 to merge multiple copies of this code.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a TODO comment.

}
}

if (eligibleCerts.Count > 0)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Depending on your caring levels this function is anywhere from "meh" to "eep!" because Find returns a collection of clones instead of a filtered collection.

So let's say that someone adds in their entire CU\My store, and that it happens to be 45 certificates. 5 are public-key only (one of which has no EKU extension). All 45 have DigitalSignature. And 5 have either the client EKU or no EKU extension.

  • 45 certs come in, which were the user's responsibility (and unless the user's me, they probably won't ever dispose them)
  • Find (DigitalSignature) returns 45 new certificates.
  • Find (ClientAuthOid) returns 5 new certificates.
  • HasPrivateKey filtering reduces that to 4 eligible
  • Why does web socket not have a server trusted issuers list filter? Oh well.
  • One cert, guaranteed different than an input cert, is returned.

So this function created 50 new finalizable objects, returned one of them, and let 49 of them go to the finalizer queue.

Best practices for using Find:

  • Don't call it if you don't need to. (if count == 1 => just return it)
  • Make sure that you're reducing the candidate space as fast as possible.
    • In this case, client EKU is probably more rare than the DigitalSignature KU.
    • Though your input set it small, so maybe it doesn't matter.
  • Once you've called Find you own the returned objects. Dispose them as able.

Using my made up numbers from above, if you filtered out the !HasPrivateKey certs first you'd be down to 40 certs into the first call to Find. If that was the client EKU test you'd now be down to 4 new certs, and 4 more new certs verifying the KU, one gets returned. 7 finalizations instead of 49. (not to mention fewer allocations)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for the detailed explanation and the great suggestions, Jeremy. I've updated this method in the next iteration taking all of the above into account.

return certificates[0];
}

throw new NotSupportedException(string.Format(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is NotSupportedException appropriate here? I thought that was more of "there's nothing you can do to fix this without rebooting into a different OS/OS-version or changing from one .NET implementation to another". Not "please add your cert to the CU\My store"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we change this to PNSE (PlatformNotSupportedException) similar to others we have.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Copy Markdown

@DavidGoll DavidGoll left a comment

Choose a reason for hiding this comment

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

Aside from switching the order of the Find calls, signing off.

@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

@dotnet-bot Test Outerloop Windows x86 Release Build
@dotnet-bot Test Outerloop Linux x64 Release Build
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build

@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

Diego-Perez-Botero commented Jul 7, 2017

I've confirmed that UAP mode tests pass with this PR's changes on a recent Windows Insider flight, now that the test runner's metadata has been updated (#21985). I'm validating these changes on UAP mode in a Windows 10 Creator's Update machine to be 100% certain that it's now safe to merge this PR :)

UPDATE: All tests are passing in UAP mode on Windows 10 Creator's Update. It looks like #21985 did the trick.

@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

@dotnet-bot test OSX x64 Debug Build

@Diego-Perez-Botero Diego-Perez-Botero removed the * NO MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) label Jul 7, 2017
@Diego-Perez-Botero
Copy link
Copy Markdown
Member Author

Diego-Perez-Botero commented Jul 7, 2017

Outerloop Windows x86 Release Build and Outerloop Linux x64 Release Build test failures are due to known ManagedHandler_HttpClientHandler_SslProtocols_Test issues (#21923 #21924 #21925 #21927 #21917).

OSX x64 Debug Build build failures are unrelated to this PR.

@Diego-Perez-Botero Diego-Perez-Botero merged commit 0c47830 into dotnet:master Jul 7, 2017
@Diego-Perez-Botero Diego-Perez-Botero deleted the clientwebsocket-client-certs branch July 7, 2017 23:25
davidsh added a commit that referenced this pull request Jul 10, 2017
* Implement client certificates for HttpClient on UAP

Added code for HttpClientHandler.ClientCertificates on UAP.

Moved code duplicated in several places into a common helper class, CertificateHelper.
Then I discovered a bug introduced into GetEligibleCertificates (from PR #21908) that
doesn't properly filter by EKU or KeyUsage if there is only one cert (this was failing
the WinHttpHandler Unit tests). So, I removed that optimization.

Used the new Azure test endpoint for client certificates. I did not use the loopback
server because there is a problem using it with HttpClient in UAP. There is a crash
in the SslStream.AuthenticateAsServer that I was not able to diagnose yet. It will
require a lot of time debugging and I wanted to get this HttpClientHandler feature
done now. Tracking that investigation with #22021.

Fixes #21628
Contributes to #14542

* Address PR feedback

* Moved method to Windows-only Common file
* Fixed some line spacing

Other potential refactoring/optimizations will be looked into in #22045
@karelz karelz modified the milestones: UWP6.0, 2.1.0 Jul 14, 2017
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
)

With these changes, the UWP implementation of ClientWebSocket.ConnectAsync takes the ClientCertificates member of the app-provided ClientWebSocketOptions into account. The first client cert that can be successfully converted into a WinRT client cert (if any) is configured on the underlying WinRT MessageWebSocket instance, leading to the cert being used for mutual TLS authentication when establishing connections with WSS endpoints.

This fix leverages WinRT APIs that are only present since Windows 10 Insider Preview Build 16215, so API presence checks are in place.

Fixes dotnet/corefx#21393

Commit migrated from dotnet/corefx@0c47830
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
)

* Implement client certificates for HttpClient on UAP

Added code for HttpClientHandler.ClientCertificates on UAP.

Moved code duplicated in several places into a common helper class, CertificateHelper.
Then I discovered a bug introduced into GetEligibleCertificates (from PR dotnet/corefx#21908) that
doesn't properly filter by EKU or KeyUsage if there is only one cert (this was failing
the WinHttpHandler Unit tests). So, I removed that optimization.

Used the new Azure test endpoint for client certificates. I did not use the loopback
server because there is a problem using it with HttpClient in UAP. There is a crash
in the SslStream.AuthenticateAsServer that I was not able to diagnose yet. It will
require a lot of time debugging and I wanted to get this HttpClientHandler feature
done now. Tracking that investigation with dotnet/corefx#22021.

Fixes dotnet/corefx#21628
Contributes to dotnet/corefx#14542

* Address PR feedback

* Moved method to Windows-only Common file
* Fixed some line spacing

Other potential refactoring/optimizations will be looked into in dotnet/corefx#22045


Commit migrated from dotnet/corefx@472b8d8
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClientWebSocket doesn't support client certificates in UWP

8 participants