diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs index 7e8f97ca768..91f360d5075 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Security.Cryptography.X509Certificates; using Akka.Actor; using Akka.Configuration; using Akka.TestKit; @@ -51,6 +52,34 @@ private static Config TestConfig(string certPath, string password) }"); } + private static Config TestThumbprintConfig(string thumbPrint) + { + var config = ConfigurationFactory.ParseString(@" + akka { + loglevel = DEBUG + actor.provider = ""Akka.Remote.RemoteActorRefProvider,Akka.Remote"" + remote { + dot-netty.tcp { + port = 0 + hostname = ""127.0.0.1"" + enable-ssl = ""true"" + log-transport = true + } + } + }"); + return false + ? config + : config.WithFallback(@"akka.remote.dot-netty.tcp.ssl { + suppress-validation = ""true"" + certificate { + use-thumprint-over-file = true + thumbprint = """ + thumbPrint + @""" + store-location = ""current-user"" + store-name = ""My"" + } + }"); + } + private ActorSystem sys2; private Address address1; private Address address2; @@ -69,12 +98,29 @@ private void Setup(string certPath, string password) echoPath = new RootActorPath(address2) / "user" / "echo"; } + private void SetupThumbprint(string certPath, string password) + { + InstallCert(); + sys2 = ActorSystem.Create("sys2", TestThumbprintConfig(Thumbprint)); + InitializeLogger(sys2); + + var echo = sys2.ActorOf(Props.Create(), "echo"); + + address1 = RARP.For(Sys).Provider.DefaultAddress; + address2 = RARP.For(sys2).Provider.DefaultAddress; + echoPath = new RootActorPath(address2) / "user" / "echo"; + } + #endregion // WARNING: YOU NEED TO RUN TEST IN ADMIN MODE IN ORDER TO ADD/REMOVE CERTIFICATES TO CERT STORE! public DotNettySslSupportSpec(ITestOutputHelper output) : base(TestConfig(ValidCertPath, Password), output) { } + + private string Thumbprint { get; set; } + + [Fact] public void Secure_transport_should_be_possible_between_systems_sharing_the_same_certificate() @@ -89,6 +135,25 @@ public void Secure_transport_should_be_possible_between_systems_sharing_the_same probe.ExpectMsg("hello"); } + [Fact] + public void Secure_transport_should_be_possible_between_systems_using_thumbprint() + { + // skip this test due to linux/mono certificate issues + if (IsMono) return; + try + { + SetupThumbprint(ValidCertPath, Password); + + var probe = CreateTestProbe(); + Sys.ActorSelection(echoPath).Tell("hello", probe.Ref); + probe.ExpectMsg("hello"); + } + finally + { + RemoveCert(); + } + } + [Fact] public void Secure_transport_should_NOT_be_possible_between_systems_using_SSL_and_one_not_using_it() { @@ -112,8 +177,55 @@ protected override void Dispose(bool disposing) { Shutdown(sys2, TimeSpan.FromSeconds(3)); } + } + private void InstallCert() + { + var store = new X509Store("My", StoreLocation.CurrentUser); + try + { + store.Open(OpenFlags.ReadWrite); + + + var cert = new X509Certificate2(ValidCertPath, Password); + Thumbprint = cert.Thumbprint; + store.Add(cert); + } + finally + { +#if NET452 //netstandard 1.6 doesn't have close on store + store.Close(); +#else +#endif + } + + } + + + private void RemoveCert() + { + var store = new X509Store("My", StoreLocation.CurrentUser); + try + { + + + + store.Open(OpenFlags.ReadWrite); + var certs = store.Certificates.Find(X509FindType.FindByThumbprint, Thumbprint, false); + if (certs.Count > 0) + { + store.Remove(certs[0]); + } + } + finally + { +#if NET452 //NetStandard1.6 doesn't have close on store. + store.Close(); +#else +#endif + } + } public class Echo : ReceiveActor { public Echo() diff --git a/src/core/Akka.Remote/Configuration/Remote.conf b/src/core/Akka.Remote/Configuration/Remote.conf index b45ac9021db..e8c6255bd62 100644 --- a/src/core/Akka.Remote/Configuration/Remote.conf +++ b/src/core/Akka.Remote/Configuration/Remote.conf @@ -527,6 +527,25 @@ akka { # Available flags include: # default-key-set | exportable | machine-key-set | persist-key-set | user-key-set | user-protected # flags = [ "default-key-set" ] + + # To use a Thumbprint instead of a file, set this to true + # And specify a thumprint and it's storage location below + use-thumprint-over-file = false + + # Valid Thumprint required (if use-thumbprint-over-file is true) + # A typical thumbprint is a format similar to: "45df32e258c92a7abf6c112e54912ab15bbb9eb0" + # On Windows machines, The thumprint for an installed certificate can be located + # By using certlm.msc and opening the certificate under the 'Details' tab. + thumpbrint = "" + + # The Store name. Under windows The most common option is "My", which indicates the personal store. + # See System.Security.Cryptography.X509Certificates.StoreName for other common values. + store-name = "" + + # Valid options : local-machine or current-user + # current-user indicates a certificate stored under the user's account + # local-machine indicates a certificate stored at an operating system level (potentially shared by users) + store-location = "current-user" } suppress-validation = false } diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs index ebac1435d56..3e8420a6f8a 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs @@ -256,14 +256,38 @@ public static SslSettings Create(Config config) { if (config == null) throw new ArgumentNullException(nameof(config), "DotNetty SSL HOCON config was not found (default path: `akka.remote.dot-netty.Ssl`)"); - var flagsRaw = config.GetStringList("certificate.flags"); - var flags = flagsRaw.Aggregate(X509KeyStorageFlags.DefaultKeySet, (flag, str) => flag | ParseKeyStorageFlag(str)); - - return new SslSettings( - certificatePath: config.GetString("certificate.path"), - certificatePassword: config.GetString("certificate.password"), - flags: flags, - suppressValidation: config.GetBoolean("suppress-validation", false)); + + + if (config.GetBoolean("certificate.use-thumprint-over-file", false)) + { + return new SslSettings(config.GetString("certificate.thumbprint"), + config.GetString("certificate.store-name"), + ParseStoreLocationName(config.GetString("certificate.store-location")), + config.GetBoolean("suppress-validation", false)); + + } + else + { + var flagsRaw = config.GetStringList("certificate.flags"); + var flags = flagsRaw.Aggregate(X509KeyStorageFlags.DefaultKeySet, (flag, str) => flag | ParseKeyStorageFlag(str)); + + return new SslSettings( + certificatePath: config.GetString("certificate.path"), + certificatePassword: config.GetString("certificate.password"), + flags: flags, + suppressValidation: config.GetBoolean("suppress-validation", false)); + } + + } + + private static StoreLocation ParseStoreLocationName(string str) + { + switch (str) + { + case "local-machine": return StoreLocation.LocalMachine; + case "current-user": return StoreLocation.CurrentUser; + default: throw new ArgumentException($"Unrecognized flag in X509 certificate config [{str}]. Available flags: local-machine | current-user"); + } } private static X509KeyStorageFlags ParseKeyStorageFlag(string str) @@ -296,6 +320,36 @@ public SslSettings() SuppressValidation = false; } + public SslSettings(string certificateThumbprint, string storeName, StoreLocation storeLocation, bool suppressValidation) + { + + var store = new X509Store(storeName, storeLocation); + try + { + store.Open(OpenFlags.ReadOnly); + + + var find = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, !suppressValidation); + if (find.Count == 0) + { + throw new ArgumentException( + "Could not find Valid certificate for thumbprint (by default it can be found under `akka.remote.dot-netty.tcp.ssl.certificate.thumpbrint`. Also check akka.remote.dot-netty.tcp.ssl.certificate.store-name and akka.remote.dot-netty.tcp.ssl.certificate.store-location)"); + } + + Certificate = find[0]; + SuppressValidation = suppressValidation; + } + finally + { +#if NET45 //netstandard1.6 doesn't have close on store. + store.Close(); +#else +#endif + + } + + } + public SslSettings(string certificatePath, string certificatePassword, X509KeyStorageFlags flags, bool suppressValidation) { if (string.IsNullOrEmpty(certificatePath))