Windows 10 Mobile, build 14283: HttpClient and TLS validation

Yesterday, new build of Windows 10 Mobile - 14283 - was released to Insiders. SDK, however, is not out yet. That did not stop Martin (thanks!) from analyzing APIs that came with the build using his ApiPeek, discovering some hidden secrets in the process. I was lucky enough to notice that MS added custom validation of TLS certificates. Wanna know more?

Why custom certificate validation?

It has been a very requested feature for quite some time now. Usually, when your HttpClient connects to secure (HTTPS) websites, the certificate is signed by a trusted certification authority. You might, however, want to provide an extra level of security with the connection. That’s because a rogue MITM proxy server might be in the way. In some companies (to make a good case), these proxies are used to decrypt HTTPS connections - and then encrypt them again using a self-signed company CA.

If the end-user’s device is provisioned - that is, if the company CA is among the trusted roots in the device - nobody will notice a thing. But you can configure your client app to recognize only certain CAs or certificates in the chain, and validate them against what you get from the server. That way, you’ll have a chance to know if the connection is tapped. The process is called certificate pinning and until now, it was practically impossible to do on Windows Universal platforms. Specifically, there were several possible ways to go, but none of them was ideal:

  1. Beginning with Windows 8.1 and Windows 10 Mobile, you can add a CA certificate to the package manifest - and, maybe set it to Exclusive trust. Exclusive means that certificates signed by other CA than the one inserted will be rejected. In most cases, that’s not what you want.
  2. HttpClient in WinRT (Windows.Web.Http) (all the way since Win8.1) only supported verification of basic properties (matching hostname, trusted CA, expiration, right set of possible uses etc.) before sending data. After sending a request, you could get the rest of the information about the server certificate, but that’s too late :-)

That’s why I was delighted to find new options in 14283 APIs.

Prerequisities for the test

Disclaimer: I believe this is still a work in progress. I’ve been doing this just for my own education and entertainment.

So, basically, there’s a new event in HttpBaseProtocolFilter called ServerCustomValidationRequested, which takes an event handler with HttpServerCustomValidationRequestedEventArgs. The API diff above contains more details.

What I needed to test this:

  • the diff of API changes
  • Reactive Extensions (package Rx-WinRT) for subscribing to the unknown event. You’d have a hard time to do it any other way, as it is WinRT event
  • Wireshark and tcpdump for sniffing network communication
  • Fiddler4 for creating a HTTPS decrypting proxy (artificial MITM)
  • Lumia 950 XL with Windows 10 Mobile build 14283

What I tested

  1. connecting to the HTTPS server with valid and trusted server certificate
  2. connecting to the HTTPS server with valid, trusted server certificate, but wrong DNS name
  3. connecting to the HTTPS server with valid but untrusted server certificate

Example code

We’ll be using reflection to get to the necessary event. For some reason, this only works in Win8.1/WP8.1 apps; under Win10, new APIs are not available without the appropriate SDK, not even through reflection.

This is the whole file I used to test this.

using System;
using System.Diagnostics;
using System.Reactive.Linq;
using System.Reflection;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using Windows.Web.Http;
using Windows.Web.Http.Filters;

namespace HttpSslClient81
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += MainPage_Loaded;
        }

        private IDisposable eventHandler;
        private async void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            HttpBaseProtocolFilter hbpf = new HttpBaseProtocolFilter();
            HttpClient hc = new HttpClient(hbpf);

            // Ignore wrong DNS name of certificate - but still process it
            hbpf.IgnorableServerCertificateErrors.Insert(0, ChainValidationResult.InvalidName);
            
            // Subscribe to the event by name
            eventHandler =
                Observable.FromEventPattern<object>(hbpf, "ServerCustomValidationRequested")
                    .Subscribe(p => ServerValidationHandler(p.EventArgs));
            
            try
            {
                HttpResponseMessage hrm = await hc.GetAsync(new Uri("https://paulos.cz"));
                string repsonse = await hrm.Content.ReadAsStringAsync();
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
        }

        public void ServerValidationHandler(object args)
        {
            // args object is HttpServerCustomValidationRequestedEventArgs
            MethodInfo methodInfo = args.GetType().GetRuntimeMethod("Reject", new Type[0]);
            if (methodInfo != null)
            {
                // By invoking this, the request will throw an exception and won't complete.
                methodInfo.Invoke(eventArgs, null);
            }
        }
    }
}

Results

1. Valid and trusted

I wanted to know if the server validation handler would get executed if all the certificate properties were OK. This is the basic prerequisite for certificate pinning to work. And I was glad to see that the method indeed gets executed before any data is sent.

All the necessary arguments are contained in the EventArgs or just args, as you can see below:

{Windows.Web.Http.Filters.HttpServerCustomValidationRequestedEventArgs}
    RequestMessage: {Method: GET, RequestUri: 'https://paulos.cz/', Content: <null>, TransportInformation:
ServerCertificate: 'paulos.cz', ServerCertificateErrorSeverity: None, ServerCertificateErrors:
{
}, ServerIntermediateCertificates:
{
Let's Encrypt Authority X1
DST Root CA X3
}, Headers:
{
  Accept-Encoding: gzip, deflate
}}
    ServerCertificate: {Windows.Security.Cryptography.Certificates.Certificate}
    ServerCertificateErrorSeverity: None
    ServerCertificateErrors: {System.__ComObject}
    ServerIntermediateCertificates: {System.__ComObject}
    Native View: To inspect the native object, enable native code debugging.

There, if for any reason you don’t trust the given server certificate, you can reject it using Reject() method, and the original request method (SendRequestAsync()) will throw an exception talking about security.

2. Valid and trusted with wrong DNS name

Next, I wanted to test the most basic case of invalid certificate - wrong DNS name. Without ignoring the error like this:

// Ignore wrong DNS name of certificate - but still process it
hbpf.IgnorableServerCertificateErrors.Insert(0, ChainValidationResult.InvalidName);

the request would go straight to an exception. With that line, however, we’ll get to the ServerValidationHandler() and we can reject the certificate again, if necessary.

This was the content of args:

{Windows.Web.Http.Filters.HttpServerCustomValidationRequestedEventArgs}
    RequestMessage: {Method: GET, RequestUri: 'https://37.205.10.235/', Content: <null>, TransportInformation:
ServerCertificate: 'paulos.cz', ServerCertificateErrorSeverity: Ignorable, ServerCertificateErrors:
{
  InvalidName
}, ServerIntermediateCertificates:
{
Let's Encrypt Authority X1
DST Root CA X3
}, Headers:
{
  Accept-Encoding: gzip, deflate
}}
    ServerCertificate: {Windows.Security.Cryptography.Certificates.Certificate}
    ServerCertificateErrorSeverity: Ignorable
    ServerCertificateErrors: {System.__ComObject}
    ServerIntermediateCertificates: {System.__ComObject}
    Native View: To inspect the native object, enable native code debugging.

3. Valid but not trusted

This was the tricky part. I essentially used a HTTPS decrypting proxy created by Fiddler4, but I did not install the CA into the phone. I set Fiddler to decrypt all HTTPS traffic, disabled its local listening part and allowed listening to remote connections; then, I set the phone to use my proxy when on my Wifi network:

Editing Wifi settings

Then I repeated the second test. (The proxy did not really care if the hostname was correct or not, it just generated a new certificate with the called hostname.)

This was the content of args:

{Windows.Web.Http.Filters.HttpServerCustomValidationRequestedEventArgs}
    RequestMessage: {Method: GET, RequestUri: 'https://37.205.10.235/', Content: <null>, TransportInformation:
ServerCertificate: '37.205.10.235', ServerCertificateErrorSeverity: Ignorable, ServerCertificateErrors:
{
  Untrusted
}, ServerIntermediateCertificates:
{
}, Headers:
{
  Accept-Encoding: gzip, deflate
}}
    ServerCertificate: {Windows.Security.Cryptography.Certificates.Certificate}
    ServerCertificateErrorSeverity: Ignorable
    ServerCertificateErrors: {System.__ComObject}
    ServerIntermediateCertificates: {System.__ComObject}
    Native View: To inspect the native object, enable native code debugging.

As you can see, the certificate is untrusted. There lies the most funky behaviour, though. After ignoring the error:

hbpf.IgnorableServerCertificateErrors.Insert(0, ChainValidationResult.Untrusted);

the connection was closed anyway with an exception (talking about unknown authority), but not before sending and receiving a packet of application data! Rejection in the ServerValidationHandler() closes the connection sooner. I’m still not completely sure what the problem was, maybe just some work in progress?

Summary

So, that was one nice Saturday post :-) It took lot of trials and errors to get it right, but I’m kinda excited to see it working in the wild in a few months. It looks like the event is usable very well! Let me know what you think about it in the comments!

Comments