From abb4dc3e823fdf8cdd41fd3e535c5fb620f47a2b Mon Sep 17 00:00:00 2001 From: Tommy Parnell Date: Sat, 17 Jun 2017 16:18:21 -0400 Subject: [PATCH] add hpkp --- Readme.md | 6 ++- src/HardHat.Example/Startup.cs | 13 +++--- src/HardHat.UnitTests/HpKpBuilderTests.cs | 37 ++++++++++++++++ src/HardHat/Builders/HpKpHeaderBuilder.cs | 42 ++++++++++++++++++ src/HardHat/Constants.cs | 2 + src/HardHat/ContentSecurityPolicy.cs | 4 +- src/HardHat/ContentSecurityPolicyBuilder.cs | 8 +++- src/HardHat/Extensions.cs | 28 ++++++++++++ src/HardHat/Middlewares/Hpkp.cs | 49 +++++++++++++++++++++ src/HardHat/Models/PublicKeyPin.cs | 31 +++++++++++++ 10 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 src/HardHat.UnitTests/HpKpBuilderTests.cs create mode 100644 src/HardHat/Builders/HpKpHeaderBuilder.cs create mode 100644 src/HardHat/Middlewares/Hpkp.cs create mode 100644 src/HardHat/Models/PublicKeyPin.cs diff --git a/Readme.md b/Readme.md index 45a561d..6eef687 100644 --- a/Readme.md +++ b/Readme.md @@ -22,7 +22,7 @@ In short this allows: app.UseIENoOpen(); // don't allow old ie to open files in the context of your site app.UseNoMimeSniff(); // prevent MIME sniffing https://en.wikipedia.org/wiki/Content_sniffing app.UseCrossSiteScriptingFilters(); //add headers to have the browsers auto detect and block some xss attacks - app.UseContentSecurityPolicy( + app.UseContentSecurityPolicy( // Provide a security policy so only content can come from trusted sources new ContentSecurityPolicyBuilder() .WithDefaultSource(CSPConstants.Self) .WithImageSource("http://images.mysite.com") @@ -30,6 +30,10 @@ In short this allows: .WithFrameAncestors(CSPConstants.None) .BuildPolicy() ); + app.UseHpkp(maxAge: 5184000, keys: new List{ // Prevent man in the middle attacks by providing a hash of your public keys + new PublicKeyPin("cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=", HpKpCrypto.sha256), + new PublicKeyPin("M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE=", HpKpCrypto.sha256) + }, includeSubDomains: true, reportUri: "/report", reportOnly: false); ... app.UseMvc(routes => { diff --git a/src/HardHat.Example/Startup.cs b/src/HardHat.Example/Startup.cs index d1d0f1d..63c6420 100644 --- a/src/HardHat.Example/Startup.cs +++ b/src/HardHat.Example/Startup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System.Collections.Generic; namespace HardHat.Example { @@ -58,13 +59,13 @@ namespace HardHat.Example .WithMediaSource(CSPConstants.Schemes.MediaStream) .BuildPolicy() ); - app.UseStaticFiles(); + // use public key pinning + app.UseHpkp(maxAge: 5184000, keys: new List{ + new PublicKeyPin("cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=", HpKpCrypto.sha256), + new PublicKeyPin("M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE=", HpKpCrypto.sha256) + }, includeSubDomains: true, reportUri: "/report", reportOnly: false); - new ContentSecurityPolicyBuilder() - .WithFontSource(CSPConstants.Self) - .WithImageSource("https://example.com") - .WithSandBox(SandboxOption.AllowForms); - + app.UseStaticFiles(); app.UseMvc(routes => { diff --git a/src/HardHat.UnitTests/HpKpBuilderTests.cs b/src/HardHat.UnitTests/HpKpBuilderTests.cs new file mode 100644 index 0000000..ec9ceda --- /dev/null +++ b/src/HardHat.UnitTests/HpKpBuilderTests.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using HardHat.Builders; +using Xunit; + +namespace HardHat.UnitTests +{ + public class HpKpBuilderTests + { + [Fact] + public void ThrowsExceptions() + { + Assert.Throws(() => + { + HpKpHeaderBuilder.Build(0, null); + }); + Assert.Throws(() => + { + HpKpHeaderBuilder.Build(2, null); + }); + Assert.Throws(() => + { + HpKpHeaderBuilder.Build(2, new List()); + }); + + var results = HpKpHeaderBuilder.Build(2, new List() + { + new PublicKeyPin("yo", HpKpCrypto.sha256), + new PublicKeyPin("dawg", HpKpCrypto.sha256) + }, true, "/awesome"); + + Assert.Equal("pin-sha256=\"yo\"; pin-sha256=\"dawg\"; max-age=2; includeSubDomains; report-uri=\"/awesome\"", results); + + } + } +} diff --git a/src/HardHat/Builders/HpKpHeaderBuilder.cs b/src/HardHat/Builders/HpKpHeaderBuilder.cs new file mode 100644 index 0000000..750e26d --- /dev/null +++ b/src/HardHat/Builders/HpKpHeaderBuilder.cs @@ -0,0 +1,42 @@ +using HardHat; +using System; +using System.Collections.Generic; +using System.Text; + +namespace HardHat.Builders +{ + internal static class HpKpHeaderBuilder + { + internal static string Build(ulong maxAge, ICollection keys, bool includeSubDomains = false, string reportUri = "") + { + if (maxAge < 1) + { + throw new ArgumentOutOfRangeException(nameof(maxAge)); + } + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (keys.Count < 2) + { + throw new ArgumentException(" The current specification requires including a second pin for a backup key which isn't yet used in production. This allows for changing the server's public key without breaking accessibility for clients that have already noted the pins. This is important for example when the former key gets compromised.", nameof(keys)); + } + var builder = new StringBuilder(); + foreach(var key in keys) + { + // pin-sha256="base64=="; + builder.Append($"pin-{Enum.GetName(typeof(HpKpCrypto), key.cryptoType)}=\"{key.fingerprint}\"; "); + } + builder.Append($"max-age={maxAge}"); + if(includeSubDomains) + { + builder.Append($"; includeSubDomains"); + } + if(!string.IsNullOrWhiteSpace(reportUri)) + { + builder.Append($"; report-uri=\"{reportUri}\""); + } + return builder.ToString(); + } + } +} diff --git a/src/HardHat/Constants.cs b/src/HardHat/Constants.cs index 2c435f2..d35b003 100644 --- a/src/HardHat/Constants.cs +++ b/src/HardHat/Constants.cs @@ -20,6 +20,8 @@ internal const string UserAgent = "User-Agent"; internal const string ServerHeader = "Server"; internal const string semicolon = ";"; + internal const string HpKpHeader = "Public-Key-Pins"; + internal const string HpKpHeaderReportOnly = "Public-Key-Pins-Report-Only"; internal static class Referrers { internal const string NoReferrer = "no-referrer"; diff --git a/src/HardHat/ContentSecurityPolicy.cs b/src/HardHat/ContentSecurityPolicy.cs index 5629fc3..4b30854 100644 --- a/src/HardHat/ContentSecurityPolicy.cs +++ b/src/HardHat/ContentSecurityPolicy.cs @@ -63,7 +63,9 @@ namespace HardHat public string ReportUri = string.Empty; - + /// + /// Reports violations that would have occurred. Does not actively enforce the Content Policy + /// public bool OnlySendReport { get; set; } = false; } diff --git a/src/HardHat/ContentSecurityPolicyBuilder.cs b/src/HardHat/ContentSecurityPolicyBuilder.cs index bcbfa4f..438e49a 100644 --- a/src/HardHat/ContentSecurityPolicyBuilder.cs +++ b/src/HardHat/ContentSecurityPolicyBuilder.cs @@ -183,7 +183,7 @@ namespace HardHat /// /// Defines valid MIME types for plugins invoked via and . To load an you must specify application/x-java-applet. /// - /// + /// valid mime types /// public ContentSecurityPolicyBuilder WithPluginTypes(params string[] mimeTypes) { @@ -194,7 +194,11 @@ namespace HardHat Policy.PluginTypes.UnionWith(mimeTypes); return this; } - + /// + /// sandbox directive enables a sandbox for the requested resource similar to the