Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Minimum a Distributed System Dev Should Know About HTTPS Negotiation

DZone's Guide to

The Minimum a Distributed System Dev Should Know About HTTPS Negotiation

We take a look at network security from a developer's perspective, and how you how devs can work effectively with HTTPs.

· Security Zone ·
Free Resource

RavenDB vs MongoDB: Which is Better? This White Paper compares the two leading NoSQL Document Databases on 9 features to find out which is the best solution for your next project.  

I mentioned in a previous post that an SSL connection will typically use a Server Name Indication in the initial (unencrypted) packet to let the server know which address it is interested in. This allows the server to do things such as select the appropriate certificate to answer this initial challenge.

A more interesting scenario is when you want to force your users to always use HTTPS. That is pretty trivial. You set up a website to listen on port 80 and port 443 and redirect all HTTP traffic from port 80 to port 443 as HTTPS. Pretty much any web server under the sun already has some sort of easy-to-use configuration for that. Let us see how this will look like if we were writing this using bare bones Kestrel.

/ Kestrel setup
var host = new WebHostBuilder()
  .UseKestrel(options =>
  {
      options.Listen(IPAddress.Any, 80, listenOptions =>
      {
          // any connection on port 80 will be redirected to port 443
          listenOptions.ConnectionAdapters.Add(new UnconditionalRedirectToHttpsConnectionAdapter
          {
              Port = 443
          });
      });
  });

// this connection adapter merely register that this needs to be redirected
public class UnconditionalRedirectToHttpsConnectionAdapter : IConnectionAdapter
{
    public int Port = 443;

    public Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
    {
        context.Features.Set(this);
        return Task.FromResult<IAdaptedConnection>(new AdaptedConnection
        {
            ConnectionStream = context.ConnectionStream
        });
    }
    public bool IsHttps { get; } = false;
}

// handling of the redirect is done at the HTTP layer directly
app.Run(async (context) =>
{
    var redirect = context.Features.Get<UnconditionalRedirectToHttpsConnectionAdapter>();
    if (redirect != null)
    {
        var location = new UriBuilder(
                "https", context.Request.Host.Host,
                redirect.Port, context.Request.Path)
            .Uri.ToString();
        context.Response.Redirect(location, true);
        return;
    }

    await context.Response.WriteAsync("Hello World!");
});

This is pretty easy, right? We set up a connection adapter on port 80, so we can detect that this is using the wrong port and then just redirect it. Notice that there is some magic that we need to apply here. At the connection adapter, we deal with a raw TCP socket, but we don’t want to mess around with that, so we just pass the decision up the chain until we get to the part that deals with HTTP and let it send the redirect.

Pretty easy, right? But what about when a user does something like this?

http://my-awesome-service:443

Note that, in this case, we are using the HTTP protocol and not the HTTPS protocol. At that point, things are a mess. A client will make a request and send a TCP packet containing HTTP request data, but the server is trying to parse that as an SSL client help message. What will usually happen is that the server will look at the incoming packet, decide that this is garbage and just close the connection. That leads to some really hard to figure out errors and a lot of forehead slapping when you figure out what the issue is.

Now, I’m sure that you’ll agree that anyone seeing a URL as listed above will be a bit suspicious. But what about these ones?

  • http://my-awesome-service:8080
  • https://my-awesome-service:8080

Unlike before, where we would probably notice that :443 is the HTTPS port and we are using HTTP, here there is no additional indication of what the problem is. So we need to try both. And if a user is getting a connection dropped error when trying the connection, there is very little chance that they’ll consider switching to HTTPS. It is far more likely that they will start looking at the firewall rules.

So now, we need to do protocol sniffing and figure out what to do from there. Let us see how this will look like in code:

options.Listen(IPAddress.Any, 443, listenOptions =>
{
    // setup HTTPS as usual
    listenOptions.UseHttps(new HttpsConnectionAdapterOptions
    {
        SslProtocols = SslProtocols.Tls12,
        ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(
            @"C:\Users\ayende\Documents\Visual Studio 2017\Projects\ConsoleApp1\ConsoleApp1\cert.pfx"
        )
    });
    // wrap the HTTPS adapter with one that does the packet sniffing and react accordingly
    listenOptions.ConnectionAdapters[0] =
        new RedirectNonSslToHttpsConnectionAdapter(listenOptions.ConnectionAdapters[0]);

});


// this is using StreamExtended from  https://github.com/justcoding121/StreamExtended to try and sniff the SSL client hello message
public class RedirectNonSslToHttpsConnectionAdapter : UnconditionalRedirectToHttpsConnectionAdapter
{
    private readonly IConnectionAdapter _next;

    public RedirectNonSslToHttpsConnectionAdapter(IConnectionAdapter next)
    {
        _next = next;
    }

    public override async Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
    {
        var contextConnectionStream = context.ConnectionStream;

        var yourClientStream = new CustomBufferedStream(contextConnectionStream, 4096);
        var clientSslHelloInfo = await SslTools.PeekClientHello(yourClientStream);
        if (clientSslHelloInfo == null)
        {
            context.Features.Set<UnconditionalRedirectToHttpsConnectionAdapter>(this);
            return new AdaptedConnection
            {
                ConnectionStream = yourClientStream
            };
        }
        // hacky, need to use the buffered stream here, but can't acces the internal ctor
        var ctor = typeof(ConnectionAdapterContext).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
            new[] { typeof(IFeatureCollection), typeof(Stream) }, null);
        context = (ConnectionAdapterContext)ctor.Invoke(new object[] { context.Features, yourClientStream });

        return await _next.OnConnectionAsync(context);
    }
}

We read the first few bytes of the request and see if this is the start of an SSL TCP connection. If it is, we forward the call to the usual Kestrel HTTPS behavior. If it isn’t, we mark the request as must redirect and pass it, as is, to the request parsed and ready for action and then send the redirect back.

In this way, any request on port 80 will be sent to port 443 and an HTTP request on a port that listens to HTTPS will be told that it needs to switch.

One note about the code in this post. This was written at 1:30 am as a proof of concept only. I’m pretty sure that I’m heavily abusing the connection adapter system, especially with regards to the reflection bits there.

Get comfortable using NoSQL in a free, self-directed learning course provided by RavenDB. Learn to create fully-functional real-world programs on NoSQL Databases. Register today.

Topics:
security ,https ,network security

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}