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 <Jeff.Scherrer@vw.com>
This commit is contained in:
Jeffery Michael
2023-01-28 12:18:31 +01:00
committed by GitHub
co-authored by Jeff Scherrer
parent 0234918f30
commit 73f118fe1c
4 changed files with 238 additions and 14 deletions
+198
View File
@@ -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<IMqttClient> ConnectClientAsync(TestEnvironment testEnvironment, Func<MqttClientCertificateValidationEventArgs, bool> 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<X509EnhancedKeyUsageExtension>().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<X509EnhancedKeyUsageExtension>().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
@@ -14,6 +14,7 @@ using System.Security.Cryptography.X509Certificates;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Certificates;
namespace MQTTnet.Implementations namespace MQTTnet.Implementations
{ {
@@ -38,7 +39,7 @@ namespace MQTTnet.Implementations
if (options.DefaultEndpointOptions.IsEnabled) if (options.DefaultEndpointOptions.IsEnabled)
{ {
RegisterListeners(options.DefaultEndpointOptions, null, logger, _cancellationTokenSource.Token); RegisterListeners(options.DefaultEndpointOptions, logger, _cancellationTokenSource.Token);
} }
if (options.TlsEndpointOptions?.IsEnabled == true) if (options.TlsEndpointOptions?.IsEnabled == true)
@@ -48,13 +49,7 @@ namespace MQTTnet.Implementations
throw new ArgumentException("TLS certificate is not set."); throw new ArgumentException("TLS certificate is not set.");
} }
var tlsCertificate = options.TlsEndpointOptions.CertificateProvider.GetCertificate(); RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token);
if (!tlsCertificate.HasPrivateKey)
{
throw new InvalidOperationException("The certificate for TLS encryption must contain the private key.");
}
RegisterListeners(options.TlsEndpointOptions, tlsCertificate, logger, _cancellationTokenSource.Token);
} }
return CompletedTask.Instance; 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)) 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 ClientHandler = OnClientAcceptedAsync
}; };
@@ -108,7 +104,7 @@ namespace MQTTnet.Implementations
if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None)) 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 ClientHandler = OnClientAcceptedAsync
}; };
@@ -4,6 +4,7 @@
#if !WINDOWS_UWP #if !WINDOWS_UWP
using MQTTnet.Adapter; using MQTTnet.Adapter;
using MQTTnet.Certificates;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
@@ -27,7 +28,7 @@ namespace MQTTnet.Implementations
readonly MqttServerOptions _serverOptions; readonly MqttServerOptions _serverOptions;
readonly MqttServerTcpEndpointBaseOptions _options; readonly MqttServerTcpEndpointBaseOptions _options;
readonly MqttServerTlsTcpEndpointOptions _tlsOptions; readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
readonly X509Certificate2 _tlsCertificate; X509Certificate2 _tlsCertificate;
CrossPlatformSocket _socket; CrossPlatformSocket _socket;
IPEndPoint _localEndPoint; IPEndPoint _localEndPoint;
@@ -36,19 +37,18 @@ namespace MQTTnet.Implementations
AddressFamily addressFamily, AddressFamily addressFamily,
MqttServerOptions serverOptions, MqttServerOptions serverOptions,
MqttServerTcpEndpointBaseOptions tcpEndpointOptions, MqttServerTcpEndpointBaseOptions tcpEndpointOptions,
X509Certificate2 tlsCertificate,
IMqttNetLogger logger) IMqttNetLogger logger)
{ {
_addressFamily = addressFamily; _addressFamily = addressFamily;
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
_options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions)); _options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions));
_tlsCertificate = tlsCertificate;
_rootLogger = logger; _rootLogger = logger;
_logger = logger.WithSource(nameof(MqttTcpServerListener)); _logger = logger.WithSource(nameof(MqttTcpServerListener));
if (_options is MqttServerTlsTcpEndpointOptions tlsOptions) if (_options is MqttServerTlsTcpEndpointOptions tlsOptions)
{ {
_tlsOptions = tlsOptions; _tlsOptions = tlsOptions;
_tlsCertificate = _tlsOptions.CertificateProvider.GetCertificate();
} }
} }
@@ -194,8 +194,26 @@ namespace MQTTnet.Implementations
stream = clientSocket.GetStream(); stream = clientSocket.GetStream();
X509Certificate2 clientCertificate = null; 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 != 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); var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback);
#if NETCOREAPP3_1_OR_GREATER #if NETCOREAPP3_1_OR_GREATER
@@ -198,6 +198,18 @@ namespace MQTTnet.Server
_options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate); _options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate);
return this; return this;
} }
public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider)
{
if (certificateProvider == null)
{
throw new ArgumentNullException(nameof(certificateProvider));
}
_options.TlsEndpointOptions.CertificateProvider = certificateProvider;
return this;
}
#endif #endif
} }
} }