From 73f118fe1c4d70bf8d1d6ffcaea33ef5e93845a1 Mon Sep 17 00:00:00 2001 From: Jeffery Michael <37258233+YAJeff@users.noreply.github.com> Date: Sat, 28 Jan 2023 03:18:31 -0800 Subject: [PATCH] Moved the usage of certificate provider into the server listener (#1652) * Moved the usage of certificate provider into the server listener so that each connection has the posibility of receiving a new SSL cert. * Removed warning from unused field. * Reduced changeds on original codebase. --------- Co-authored-by: Jeff Scherrer --- Source/MQTTnet.Tests/Server/Tls_Tests.cs | 198 ++++++++++++++++++ .../Implementations/MqttTcpServerAdapter.cs | 18 +- .../Implementations/MqttTcpServerListener.cs | 24 ++- .../Options/MqttServerOptionsBuilder.cs | 12 ++ 4 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 Source/MQTTnet.Tests/Server/Tls_Tests.cs diff --git a/Source/MQTTnet.Tests/Server/Tls_Tests.cs b/Source/MQTTnet.Tests/Server/Tls_Tests.cs new file mode 100644 index 00000000..1c9effee --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Tls_Tests.cs @@ -0,0 +1,198 @@ + +#if !WINDOWS_UWP && (NET48_OR_GREATER || NET5_0_OR_GREATER) +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Certificates; +using MQTTnet.Client; +using MQTTnet.Server; +using MQTTnet.Tests.Mockups; +using System; +using System.Linq; +using System.Net; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Tests.Server +{ + + [TestClass] + + public class Tls_Tests : BaseTestClass + { + + public X509Certificate2 CreateCertificate(string oid) + { + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); + sanBuilder.AddDnsName("localhost"); + var rsa = RSA.Create(); + + var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); + + certRequest.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); + + certRequest.CertificateExtensions.Add( + new X509EnhancedKeyUsageExtension( + new OidCollection { new Oid(oid) }, false)); + + certRequest.CertificateExtensions.Add(sanBuilder.Build()); + + var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10)); + var pfxCertificate = new X509Certificate2(certificate.Export(X509ContentType.Pfx), (string)null, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); + + return pfxCertificate; + } + + private async Task ConnectClientAsync(TestEnvironment testEnvironment, Func certValidator) + { + var clientOptionsBuilder = testEnvironment.Factory.CreateClientOptionsBuilder(); + clientOptionsBuilder + .WithClientId(Guid.NewGuid().ToString()) + .WithTcpServer("localhost", 8883) + .WithTls(tls => + { + tls.UseTls = true; + tls.SslProtocol = SslProtocols.Tls12; + tls.CertificateValidationHandler = certValidator; + }); + + var clientOptions = clientOptionsBuilder.Build(); + return await testEnvironment.ConnectClient(clientOptions); + } + + [TestMethod] + public async Task Tls_Swap_Test() + { + var testEnvironment = CreateTestEnvironment(MQTTnet.Formatter.MqttProtocolVersion.V500); + var serverOptionsBuilder = testEnvironment.Factory.CreateServerOptionsBuilder(); + + var firstOid = "1.3.6.1.5.5.7.3.1"; + var secondOid = "1.3.6.1.5.5.7.3.2"; + + var certificateProvider = new CertificateProvider + { + CurrentCertificate = CreateCertificate(firstOid) + }; + + serverOptionsBuilder + .WithoutDefaultEndpoint() + .WithEncryptedEndpoint() + .WithEncryptionSslProtocol(SslProtocols.Tls12) + .WithEncryptionCertificate(certificateProvider); + + var serverOptions = serverOptionsBuilder.Build(); + + var server = testEnvironment.CreateServer(serverOptions); + + var publishedCount = 0; + server.InterceptingPublishAsync += args => + { + Interlocked.Increment(ref publishedCount); + + return Task.CompletedTask; + }; + + await server.StartAsync(); + + var firstClient = await ConnectClientAsync(testEnvironment, args => + { + Assert.AreEqual(firstOid, ((X509Certificate2)args.Certificate).Extensions.OfType().First().EnhancedKeyUsages[0].Value); + return true; + }); + + var firstClientReceivedCount = 0; + firstClient.ApplicationMessageReceivedAsync += args => + { + Interlocked.Increment(ref firstClientReceivedCount); + + return Task.CompletedTask; + }; + + await firstClient.SubscribeAsync("TestTopic1"); + + await firstClient.PublishAsync(new MqttApplicationMessage + { + Topic = "TestTopic1", + Payload = new byte[] { 1, 2, 3, 4} + }); + + await testEnvironment.Server.InjectApplicationMessage(new InjectedMqttApplicationMessage( + new MqttApplicationMessage + { + Topic = "TestTopic1", + Payload = new byte[] { 1, 2, 3, 4 } + })); + + certificateProvider.CurrentCertificate = CreateCertificate(secondOid); + + // Validate that the certificate was switched + var secondClient = await ConnectClientAsync(testEnvironment, args => + { + Assert.AreEqual(secondOid, ((X509Certificate2)args.Certificate).Extensions.OfType().First().EnhancedKeyUsages[0].Value); + return true; + }); + + var secondClientReceivedCount = 0; + secondClient.ApplicationMessageReceivedAsync += args => + { + Interlocked.Increment(ref secondClientReceivedCount); + + return Task.CompletedTask; + }; + + await secondClient.SubscribeAsync("TestTopic2"); + + await firstClient.PublishAsync(new MqttApplicationMessage + { + Topic = "TestTopic2", + Payload = new byte[] { 1, 2, 3, 4 } + }); + + await testEnvironment.Server.InjectApplicationMessage(new InjectedMqttApplicationMessage( + new MqttApplicationMessage + { + Topic = "TestTopic2", + Payload = new byte[] { 1, 2, 3, 4 } + })); + + // Ensure first client still works + await firstClient.PublishAsync(new MqttApplicationMessage + { + Topic = "TestTopic1", + Payload = new byte[] { 1, 2, 3, 4 } + }); + + await testEnvironment.Server.InjectApplicationMessage(new InjectedMqttApplicationMessage( + new MqttApplicationMessage + { + Topic = "TestTopic1", + Payload = new byte[] { 1, 2, 3, 4 } + })); + + await Task.Delay(1000); + + Assert.AreEqual(6, publishedCount); + Assert.AreEqual(4, firstClientReceivedCount); + Assert.AreEqual(2, secondClientReceivedCount); + + await server.StopAsync(); + } + + + private class CertificateProvider : ICertificateProvider + { + + public X509Certificate2 CurrentCertificate { get; set; } + + public X509Certificate2 GetCertificate() + => CurrentCertificate; + + } + + } +} +#endif \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs index ac94261d..7a9c2229 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs @@ -14,6 +14,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using MQTTnet.Internal; +using MQTTnet.Certificates; namespace MQTTnet.Implementations { @@ -38,7 +39,7 @@ namespace MQTTnet.Implementations if (options.DefaultEndpointOptions.IsEnabled) { - RegisterListeners(options.DefaultEndpointOptions, null, logger, _cancellationTokenSource.Token); + RegisterListeners(options.DefaultEndpointOptions, logger, _cancellationTokenSource.Token); } if (options.TlsEndpointOptions?.IsEnabled == true) @@ -48,13 +49,7 @@ namespace MQTTnet.Implementations throw new ArgumentException("TLS certificate is not set."); } - var tlsCertificate = options.TlsEndpointOptions.CertificateProvider.GetCertificate(); - if (!tlsCertificate.HasPrivateKey) - { - throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); - } - - RegisterListeners(options.TlsEndpointOptions, tlsCertificate, logger, _cancellationTokenSource.Token); + RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token); } return CompletedTask.Instance; @@ -91,11 +86,12 @@ namespace MQTTnet.Implementations } } - void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, X509Certificate2 tlsCertificate, IMqttNetLogger logger, CancellationToken cancellationToken) + void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken) { + if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None)) { - var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, tlsCertificate, logger) + var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger) { ClientHandler = OnClientAcceptedAsync }; @@ -108,7 +104,7 @@ namespace MQTTnet.Implementations if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None)) { - var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, tlsCertificate, logger) + var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, logger) { ClientHandler = OnClientAcceptedAsync }; diff --git a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs index 02b954ba..0fc4caab 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs @@ -4,6 +4,7 @@ #if !WINDOWS_UWP using MQTTnet.Adapter; +using MQTTnet.Certificates; using MQTTnet.Diagnostics; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -27,7 +28,7 @@ namespace MQTTnet.Implementations readonly MqttServerOptions _serverOptions; readonly MqttServerTcpEndpointBaseOptions _options; readonly MqttServerTlsTcpEndpointOptions _tlsOptions; - readonly X509Certificate2 _tlsCertificate; + X509Certificate2 _tlsCertificate; CrossPlatformSocket _socket; IPEndPoint _localEndPoint; @@ -36,19 +37,18 @@ namespace MQTTnet.Implementations AddressFamily addressFamily, MqttServerOptions serverOptions, MqttServerTcpEndpointBaseOptions tcpEndpointOptions, - X509Certificate2 tlsCertificate, IMqttNetLogger logger) { _addressFamily = addressFamily; _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); _options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions)); - _tlsCertificate = tlsCertificate; _rootLogger = logger; _logger = logger.WithSource(nameof(MqttTcpServerListener)); if (_options is MqttServerTlsTcpEndpointOptions tlsOptions) { _tlsOptions = tlsOptions; + _tlsCertificate = _tlsOptions.CertificateProvider.GetCertificate(); } } @@ -194,8 +194,26 @@ namespace MQTTnet.Implementations stream = clientSocket.GetStream(); X509Certificate2 clientCertificate = null; + if (_tlsOptions.CertificateProvider != null) + { + var tlsCertificate = _tlsOptions.CertificateProvider.GetCertificate(); + if (tlsCertificate != _tlsCertificate) + { + var oldTlsCertificate = _tlsCertificate; + _tlsCertificate = tlsCertificate; +#if !NET452 + oldTlsCertificate.Dispose(); +#endif + } + } + if (_tlsCertificate != null) { + if (!_tlsCertificate.HasPrivateKey) + { + throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); + } + var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback); #if NETCOREAPP3_1_OR_GREATER diff --git a/Source/MQTTnet/Server/Options/MqttServerOptionsBuilder.cs b/Source/MQTTnet/Server/Options/MqttServerOptionsBuilder.cs index 3b359203..e966ae6c 100644 --- a/Source/MQTTnet/Server/Options/MqttServerOptionsBuilder.cs +++ b/Source/MQTTnet/Server/Options/MqttServerOptionsBuilder.cs @@ -198,6 +198,18 @@ namespace MQTTnet.Server _options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate); return this; } + + public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider) + { + if (certificateProvider == null) + { + throw new ArgumentNullException(nameof(certificateProvider)); + } + + _options.TlsEndpointOptions.CertificateProvider = certificateProvider; + + return this; + } #endif } } \ No newline at end of file