From f7d098a119665d14070502f4b9a87264a0cb6f14 Mon Sep 17 00:00:00 2001 From: christian <6939810+chkr1011@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:14:57 +0200 Subject: [PATCH] Expose certificate selection in MQTTnet options --- ...MqttClientCertificateSelectionEventArgs.cs | 36 ++++++++++++ ...qttClientCertificateValidationEventArgs.cs | 17 ++++-- .../Client/Options/MqttClientTlsOptions.cs | 2 + .../Options/MqttClientTlsOptionsBuilder.cs | 11 ++++ .../MQTTnet/Implementations/MqttTcpChannel.cs | 58 ++++++++++++++----- .../Implementations/MqttWebSocketChannel.cs | 27 +++++---- 6 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 Source/MQTTnet/Client/Options/MqttClientCertificateSelectionEventArgs.cs diff --git a/Source/MQTTnet/Client/Options/MqttClientCertificateSelectionEventArgs.cs b/Source/MQTTnet/Client/Options/MqttClientCertificateSelectionEventArgs.cs new file mode 100644 index 000000000..f1be293a9 --- /dev/null +++ b/Source/MQTTnet/Client/Options/MqttClientCertificateSelectionEventArgs.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Client +{ + public sealed class MqttClientCertificateSelectionEventArgs : EventArgs + { + public MqttClientCertificateSelectionEventArgs( + string targetHost, + X509CertificateCollection localCertificates, + X509Certificate remoteCertificate, + string[] acceptableIssuers, + MqttClientTcpOptions tcpOptions) + { + TargetHost = targetHost; + LocalCertificates = localCertificates; + RemoveCertificate = remoteCertificate; + AcceptableIssuers = acceptableIssuers; + TcpOptions = tcpOptions ?? throw new ArgumentNullException(nameof(tcpOptions)); + } + + public string[] AcceptableIssuers { get; } + + public X509CertificateCollection LocalCertificates { get; } + + public X509Certificate RemoveCertificate { get; } + + public string TargetHost { get; } + + public MqttClientTcpOptions TcpOptions { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs b/Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs index ff4825612..75284c570 100644 --- a/Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs +++ b/Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs @@ -10,11 +10,20 @@ namespace MQTTnet.Client { public sealed class MqttClientCertificateValidationEventArgs : EventArgs { - public X509Certificate Certificate { get; set; } + public MqttClientCertificateValidationEventArgs(X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors, IMqttClientChannelOptions clientOptions) + { + Certificate = certificate; + Chain = chain; + ClientOptions = clientOptions; + SslPolicyErrors = sslPolicyErrors; + } - public X509Chain Chain { get; set; } + public X509Certificate Certificate { get; } + + public X509Chain Chain { get; } + + public IMqttClientChannelOptions ClientOptions { get; } - public IMqttClientChannelOptions ClientOptions { get; set; } #if NET452 || NET461 || NET48 /// /// Can be a host string name or an object derived from WebRequest. @@ -22,6 +31,6 @@ public sealed class MqttClientCertificateValidationEventArgs : EventArgs public object Sender { get; set; } #endif - public SslPolicyErrors SslPolicyErrors { get; set; } + public SslPolicyErrors SslPolicyErrors { get; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs b/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs index d50b2aa05..d4c2ef18c 100644 --- a/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs @@ -14,6 +14,8 @@ public sealed class MqttClientTlsOptions { public Func CertificateValidationHandler { get; set; } + public Func CertificateSelectionHandler { get; set; } + public bool UseTls { get; set; } public bool IgnoreCertificateRevocationErrors { get; set; } diff --git a/Source/MQTTnet/Client/Options/MqttClientTlsOptionsBuilder.cs b/Source/MQTTnet/Client/Options/MqttClientTlsOptionsBuilder.cs index 72b0b24ea..babd40f9e 100644 --- a/Source/MQTTnet/Client/Options/MqttClientTlsOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTlsOptionsBuilder.cs @@ -49,6 +49,17 @@ public MqttClientTlsOptionsBuilder WithCertificateValidationHandler(Func certificateSelectionHandler) + { + if (certificateSelectionHandler == null) + { + throw new ArgumentNullException(nameof(certificateSelectionHandler)); + } + + _tlsOptions.CertificateSelectionHandler = certificateSelectionHandler; + return this; + } + public MqttClientTlsOptionsBuilder WithClientCertificates(IEnumerable certificates) { if (certificates == null) diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs index 6ed97ccae..c70f39032 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -106,10 +106,29 @@ public async Task ConnectAsync(CancellationToken cancellationToken) { if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns) { - targetHost = dns.Host; + targetHost = dns.Host; } } - var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback); + + SslStream sslStream; + if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null) + { + sslStream = new SslStream( + networkStream, + false, + InternalUserCertificateValidationCallback, + InternalUserCertificateSelectionCallback); + } + else + { + // Use a different constructor depending on the options for MQTTnet so that we do not have + // to copy the exact same behavior of the selection handler. + sslStream = new SslStream( + networkStream, + false, + InternalUserCertificateValidationCallback); + } + try { #if NETCOREAPP3_1_OR_GREATER @@ -134,9 +153,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken) { TrustMode = X509ChainTrustMode.CustomRootTrust, VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown, - RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors - ? X509RevocationMode.NoCheck - : _tcpOptions.TlsOptions.RevocationMode + RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode }; sslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(_tcpOptions.TlsOptions.TrustChain); @@ -292,19 +309,34 @@ public async Task WriteAsync(ArraySegment buffer, bool isEndOfPacket, Canc } } + X509Certificate InternalUserCertificateSelectionCallback( + object sender, + string targetHost, + X509CertificateCollection localCertificates, + X509Certificate remoteCertificate, + string[] acceptableIssuers) + { + var certificateSelectionHandler = _tcpOptions?.TlsOptions?.CertificateSelectionHandler; + if (certificateSelectionHandler != null) + { + var eventArgs = new MqttClientCertificateSelectionEventArgs(targetHost, localCertificates, remoteCertificate, acceptableIssuers, _tcpOptions); + return certificateSelectionHandler(eventArgs); + } + + if (localCertificates?.Count > 0) + { + return localCertificates[0]; + } + + return null; + } + bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler; if (certificateValidationHandler != null) { - var eventArgs = new MqttClientCertificateValidationEventArgs - { - Certificate = x509Certificate, - Chain = chain, - SslPolicyErrors = sslPolicyErrors, - ClientOptions = _tcpOptions - }; - + var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions); return certificateValidationHandler(eventArgs); } diff --git a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index a9f2b92ec..9d4ba32c2 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -109,7 +109,7 @@ public async Task WriteAsync(ArraySegment buffer, bool isEndOfPacket, Canc // A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2]. await _webSocket.SendAsync(buffer, WebSocketMessageType.Binary, isEndOfPacket, cancellationToken).ConfigureAwait(false); #else - // The lock is required because the client will throw an exception if _SendAsync_ is + // The lock is required because the client will throw an exception if _SendAsync_ is // called from multiple threads at the same time. But this issue only happens with several // framework versions. if (_sendLock == null) @@ -156,7 +156,7 @@ IWebProxy CreateProxy() #else var proxyUri = new Uri(_options.ProxyOptions.Address); WebProxy webProxy; - + if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password)) { var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain); @@ -164,9 +164,9 @@ IWebProxy CreateProxy() } else { - webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList); + webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList); } - + if (_options.ProxyOptions.UseDefaultCredentials) { // Only update the property if required because setting it to false will alter @@ -234,7 +234,7 @@ void SetupClientWebSocket(ClientWebSocket clientWebSocket) { clientWebSocket.Options.Credentials = _options.Credentials; } - + var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler; if (certificateValidationHandler != null) { @@ -245,7 +245,7 @@ void SetupClientWebSocket(ClientWebSocket clientWebSocket) #elif WINDOWS_UWP throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'."); #elif NET452 || NET461 || NET48 - ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => + ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => { var context = new MqttClientCertificateValidationEventArgs { @@ -262,17 +262,16 @@ void SetupClientWebSocket(ClientWebSocket clientWebSocket) clientWebSocket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { // TODO: Find a way to add client options to same callback. Problem is that they have a different type. - var context = new MqttClientCertificateValidationEventArgs - { - Certificate = certificate, - Chain = chain, - SslPolicyErrors = sslPolicyErrors, - ClientOptions = _options - }; - + var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options); return certificateValidationHandler(context); }; #endif + + var certificateSelectionHandler = _options.TlsOptions?.CertificateSelectionHandler; + if (certificateSelectionHandler != null) + { + throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections."); + } } } }