Skip to content

Commit

Permalink
Expose certificate selection in MQTTnet options
Browse files Browse the repository at this point in the history
  • Loading branch information
chkr1011 committed Apr 30, 2024
1 parent fe9c239 commit f7d098a
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,27 @@ 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
/// <summary>
/// Can be a host string name or an object derived from WebRequest.
/// </summary>
public object Sender { get; set; }
#endif

public SslPolicyErrors SslPolicyErrors { get; set; }
public SslPolicyErrors SslPolicyErrors { get; }
}
}
2 changes: 2 additions & 0 deletions Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public sealed class MqttClientTlsOptions
{
public Func<MqttClientCertificateValidationEventArgs, bool> CertificateValidationHandler { get; set; }

public Func<MqttClientCertificateSelectionEventArgs, X509Certificate> CertificateSelectionHandler { get; set; }

public bool UseTls { get; set; }

public bool IgnoreCertificateRevocationErrors { get; set; }
Expand Down
11 changes: 11 additions & 0 deletions Source/MQTTnet/Client/Options/MqttClientTlsOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ public MqttClientTlsOptionsBuilder WithCertificateValidationHandler(Func<MqttCli
return this;
}

public MqttClientTlsOptionsBuilder WithCertificateSelectionHandler(Func<MqttClientCertificateSelectionEventArgs, X509Certificate> certificateSelectionHandler)
{
if (certificateSelectionHandler == null)
{
throw new ArgumentNullException(nameof(certificateSelectionHandler));
}

_tlsOptions.CertificateSelectionHandler = certificateSelectionHandler;
return this;
}

public MqttClientTlsOptionsBuilder WithClientCertificates(IEnumerable<X509Certificate2> certificates)
{
if (certificates == null)
Expand Down
58 changes: 45 additions & 13 deletions Source/MQTTnet/Implementations/MqttTcpChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -292,19 +309,34 @@ public async Task WriteAsync(ArraySegment<byte> 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);
}

Expand Down
27 changes: 13 additions & 14 deletions Source/MQTTnet/Implementations/MqttWebSocketChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public async Task WriteAsync(ArraySegment<byte> 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)
Expand Down Expand Up @@ -156,17 +156,17 @@ 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);
webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList, credentials);
}
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
Expand Down Expand Up @@ -234,7 +234,7 @@ void SetupClientWebSocket(ClientWebSocket clientWebSocket)
{
clientWebSocket.Options.Credentials = _options.Credentials;
}

var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null)
{
Expand All @@ -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

Check failure on line 250 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

There is no argument given that corresponds to the required parameter 'certificate' of 'MqttClientCertificateValidationEventArgs.MqttClientCertificateValidationEventArgs(X509Certificate, X509Chain, SslPolicyErrors, IMqttClientChannelOptions)'

Check failure on line 250 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

There is no argument given that corresponds to the required parameter 'certificate' of 'MqttClientCertificateValidationEventArgs.MqttClientCertificateValidationEventArgs(X509Certificate, X509Chain, SslPolicyErrors, IMqttClientChannelOptions)'

Check failure on line 250 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

There is no argument given that corresponds to the required parameter 'certificate' of 'MqttClientCertificateValidationEventArgs.MqttClientCertificateValidationEventArgs(X509Certificate, X509Chain, SslPolicyErrors, IMqttClientChannelOptions)'

Check failure on line 250 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

There is no argument given that corresponds to the required parameter 'certificate' of 'MqttClientCertificateValidationEventArgs.MqttClientCertificateValidationEventArgs(X509Certificate, X509Chain, SslPolicyErrors, IMqttClientChannelOptions)'
{
Expand All @@ -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;

Check warning on line 270 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected

Check warning on line 270 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected

Check warning on line 270 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected

Check warning on line 270 in Source/MQTTnet/Implementations/MqttWebSocketChannel.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected
if (certificateSelectionHandler != null)
{
throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections.");
}
}
}
}
Expand Down

0 comments on commit f7d098a

Please sign in to comment.