From 4f9e54ba1db5470631787a49397a76698d5f291e Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Thu, 2 May 2024 22:05:33 +0200 Subject: [PATCH] Expose certificate selection in client options (#1984) * Expose certificate selection in MQTTnet options * Fix .NET Framework builds * Update release notes --- .github/workflows/ReleaseNotes.md | 3 +- ...MqttClientCertificateSelectionEventArgs.cs | 36 ++++++++++++ ...qttClientCertificateValidationEventArgs.cs | 17 ++++-- .../Client/Options/MqttClientTlsOptions.cs | 2 + .../Options/MqttClientTlsOptionsBuilder.cs | 11 ++++ .../MQTTnet/Implementations/MqttTcpChannel.cs | 58 ++++++++++++++----- .../Implementations/MqttWebSocketChannel.cs | 35 +++++------ 7 files changed, 124 insertions(+), 38 deletions(-) create mode 100644 Source/MQTTnet/Client/Options/MqttClientCertificateSelectionEventArgs.cs diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 239b39cf..e9ae9cd1 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,7 +1,8 @@ * [Core] Optimized packet serialization of PUBACK and PUBREC packets for protocol version 5.0.0 (#1939, thanks to @Y-Sindo). * [Core] The package inspector is now fully async (#1941). * [Client] Added a dedicated exception when the client is not connected (#1954, thanks to @marcpiulachs). -* [Server] The server will no longer send _NoMatchingSubscribers_ when the actual subscription was non success (#1965, BREAKING CHANGE!). +* [Client] Exposed the certificate selection event handler in client options (#1984). +* [Server] The server will no longer send _NoMatchingSubscribers_ when the actual subscription was non success (#1965, BREAKING CHANGE!). * [Server] Fixed broken support for _null_ in _AddServer_ method in ASP.NET integration (#1981). * [ManagedClient] Added a new event (SubscriptionsChangedAsync) which is fired when a subscription or unsubscription was made (#1894, thanks to @pdufrene). * [TopicTemplate] Added new extension which provides a template engine for topics (#1932, thanks to @simonthum). diff --git a/Source/MQTTnet/Client/Options/MqttClientCertificateSelectionEventArgs.cs b/Source/MQTTnet/Client/Options/MqttClientCertificateSelectionEventArgs.cs new file mode 100644 index 00000000..f1be293a --- /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 ff482561..1fe22a67 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; + SslPolicyErrors = sslPolicyErrors; + ClientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); + } - 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 @@ namespace MQTTnet.Client 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 d50b2aa0..d4c2ef18 100644 --- a/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs @@ -14,6 +14,8 @@ namespace MQTTnet.Client { 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 72b0b24e..babd40f9 100644 --- a/Source/MQTTnet/Client/Options/MqttClientTlsOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTlsOptionsBuilder.cs @@ -49,6 +49,17 @@ namespace MQTTnet.Client return this; } + public MqttClientTlsOptionsBuilder WithCertificateSelectionHandler(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 6ed97cca..c70f3903 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -106,10 +106,29 @@ namespace MQTTnet.Implementations { 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 @@ namespace MQTTnet.Implementations { 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 @@ namespace MQTTnet.Implementations } } + 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 a9f2b92e..d7c2acf9 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -109,7 +109,7 @@ namespace MQTTnet.Implementations // 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 @@ namespace MQTTnet.Implementations #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 @@ namespace MQTTnet.Implementations } 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 @@ namespace MQTTnet.Implementations { clientWebSocket.Options.Credentials = _options.Credentials; } - + var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler; if (certificateValidationHandler != null) { @@ -245,15 +245,11 @@ namespace MQTTnet.Implementations #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 + var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options) { - Sender = sender, - Certificate = certificate, - Chain = chain, - SslPolicyErrors = sslPolicyErrors, - ClientOptions = _options + Sender = sender }; return certificateValidationHandler(context); @@ -262,17 +258,16 @@ namespace MQTTnet.Implementations 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."); + } } } }