This is a continuation to the previous part on HSTS. This time we will go through HPKP, or HTTP Public Key Pinning.

So what is HTTP Public Key Pinning?

You can skip this if you are familiar with the concept of HPKP.

HPKP allows you to pin a certificate (or multiple) in your app's certificate chain. This tells a browser that if it ever receives a certificate that is not on this list, assume someone is man-in-the-middling the connection with an otherwise valid (!) certificate. This kind of situation could occur if a Certificate Authority is compromised, or a Certificate Authority works against the way they are supposed to and creates a certificate for a domain they don't own.

Now this is not a threat most web applications need to be worried about. More critical applications such as online banks should definitely implement HPKP. Others should consider, because HPKP does have some drawbacks.

Actually, HPKP is awesome, until you run into problems. One of your certificates expired so you renewed it. Oh, but the new certificates thumbprint is not in the pins! This means all of your users will see a massive red warning and the browser will block them from accessing it. HPKP rules are hard to remove from the browser's cache, like HSTS.

That's why you can have multiple pins in an HPKP header. One for the main certificate in use, another for a backup, and yet another for a Certificate Signing Request's thumbprint. This way you can swap in your backup certificate if needed, or get a new certificate from any CA with the pre-created CSR. They will all work.

So, what does an HPKP header actually contain? Here's a couple examples:

Public-Key-Pins: pin-sha256="base64=="; max-age=604800; report-uri="/hpkp-violation-report"

Registers a single certificate thumbprint with a max age of one week. Violations will be reported to a relative URL. This rule is enforced, browsers will block access if the certificate is not there.

Public-Key-Pins-Report-Only: pin-sha256="base64=="; pin-sha256="base64=="; max-age=604800; includeSubDomains; report-uri="/hpkp-violation-report"

Registers two certificate thumbprints with a max age of one week. Violations will be reported to the same URL. If there is a violation though, it will only be reported, access is not blocked. And also, this rule is to be applied on every subdomain of the current domain. Be really, really careful with that setting. But setting the header to Report-Only can be a very good idea when testing.

Getting your site's pins

So how do you get those SHA-256 hashes from your certificates? For the lazy there is a really awesome tool made by Scott Helme, which you can find here: https://report-uri.io/home/pkp_hash. What his tool allows you to do is point it at your site, and it'll give you the hashes in Base64 format, ready to go :)

Implementing HPKP in ASP.NET Core

An easy way to implement HPKP is to use my library which you can get on NuGet: Joonasw.AspNetCore.SecurityHeaders.

Simply install it to your ASP.NET Core project, and then you can add HPKP headers to your app with a single function call.

Example Configure method in Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug(LogLevel.Debug);

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseHttpsEnforcement();
        app.UseHsts(new HstsOptions
        {
            Seconds = 30 * 24 * 60 * 60,
            IncludeSubDomains = false,
            Preload = false
        });

        app.UseHpkp(hpkp =>
        {
            hpkp.UseMaxAgeSeconds(7 * 24 * 60 * 60)
                .AddSha256Pin("nrmpk4ZI3wbRBmUZIT5aKAgP0LlKHRgfA2Snjzeg9iY=")
                .SetReportOnly()
                .ReportViolationsTo("/hpkp-report");
        });
    }

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "hpkp-report",
            template: "hpkp-report",
            defaults: new { controller = "Report", action = "Hpkp" });

        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Here we use UseHpkp() to add HPKP headers outside the development environment. We only add a single hash, and set the expiry to one week. We also set it to Report-Only mode, so it doesn't block anyone yet, and set it to send violation reports to /hpkp-report.

Summary

HTTP Public Key Pinning can be used to prevent browsers from accepting certificates for your domain that it is not supposed to. But you must be very careful when implementing it so you do not fall on your face.

Also check out the previous parts on HTTP Strict Transport Security, and Enforcing HTTPS.