Expose certificate selection in MQTTnet options

This commit is contained in:
christian
2024-04-30 19:14:57 +02:00
parent fe9c239734
commit f7d098a119
6 changed files with 120 additions and 31 deletions
@@ -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; }
}
}
@@ -10,11 +10,20 @@ namespace MQTTnet.Client
{ {
public sealed class MqttClientCertificateValidationEventArgs : EventArgs 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 #if NET452 || NET461 || NET48
/// <summary> /// <summary>
/// Can be a host string name or an object derived from WebRequest. /// Can be a host string name or an object derived from WebRequest.
@@ -22,6 +31,6 @@ namespace MQTTnet.Client
public object Sender { get; set; } public object Sender { get; set; }
#endif #endif
public SslPolicyErrors SslPolicyErrors { get; set; } public SslPolicyErrors SslPolicyErrors { get; }
} }
} }
@@ -14,6 +14,8 @@ namespace MQTTnet.Client
{ {
public Func<MqttClientCertificateValidationEventArgs, bool> CertificateValidationHandler { get; set; } public Func<MqttClientCertificateValidationEventArgs, bool> CertificateValidationHandler { get; set; }
public Func<MqttClientCertificateSelectionEventArgs, X509Certificate> CertificateSelectionHandler { get; set; }
public bool UseTls { get; set; } public bool UseTls { get; set; }
public bool IgnoreCertificateRevocationErrors { get; set; } public bool IgnoreCertificateRevocationErrors { get; set; }
@@ -49,6 +49,17 @@ namespace MQTTnet.Client
return this; 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) public MqttClientTlsOptionsBuilder WithClientCertificates(IEnumerable<X509Certificate2> certificates)
{ {
if (certificates == null) if (certificates == null)
@@ -106,10 +106,29 @@ namespace MQTTnet.Implementations
{ {
if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns) 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 try
{ {
#if NETCOREAPP3_1_OR_GREATER #if NETCOREAPP3_1_OR_GREATER
@@ -134,9 +153,7 @@ namespace MQTTnet.Implementations
{ {
TrustMode = X509ChainTrustMode.CustomRootTrust, TrustMode = X509ChainTrustMode.CustomRootTrust,
VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown, VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown,
RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode
? X509RevocationMode.NoCheck
: _tcpOptions.TlsOptions.RevocationMode
}; };
sslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(_tcpOptions.TlsOptions.TrustChain); 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) bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{ {
var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler; var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null) if (certificateValidationHandler != null)
{ {
var eventArgs = new MqttClientCertificateValidationEventArgs var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions);
{
Certificate = x509Certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _tcpOptions
};
return certificateValidationHandler(eventArgs); return certificateValidationHandler(eventArgs);
} }
@@ -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]. // 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); await _webSocket.SendAsync(buffer, WebSocketMessageType.Binary, isEndOfPacket, cancellationToken).ConfigureAwait(false);
#else #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 // called from multiple threads at the same time. But this issue only happens with several
// framework versions. // framework versions.
if (_sendLock == null) if (_sendLock == null)
@@ -156,7 +156,7 @@ namespace MQTTnet.Implementations
#else #else
var proxyUri = new Uri(_options.ProxyOptions.Address); var proxyUri = new Uri(_options.ProxyOptions.Address);
WebProxy webProxy; WebProxy webProxy;
if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password)) if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password))
{ {
var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain); var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain);
@@ -164,9 +164,9 @@ namespace MQTTnet.Implementations
} }
else 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) if (_options.ProxyOptions.UseDefaultCredentials)
{ {
// Only update the property if required because setting it to false will alter // 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; clientWebSocket.Options.Credentials = _options.Credentials;
} }
var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler; var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null) if (certificateValidationHandler != null)
{ {
@@ -245,7 +245,7 @@ namespace MQTTnet.Implementations
#elif WINDOWS_UWP #elif WINDOWS_UWP
throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'."); throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'.");
#elif NET452 || NET461 || NET48 #elif NET452 || NET461 || NET48
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
{ {
var context = new MqttClientCertificateValidationEventArgs var context = new MqttClientCertificateValidationEventArgs
{ {
@@ -262,17 +262,16 @@ namespace MQTTnet.Implementations
clientWebSocket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => 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. // TODO: Find a way to add client options to same callback. Problem is that they have a different type.
var context = new MqttClientCertificateValidationEventArgs var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options);
{
Certificate = certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _options
};
return certificateValidationHandler(context); return certificateValidationHandler(context);
}; };
#endif #endif
var certificateSelectionHandler = _options.TlsOptions?.CertificateSelectionHandler;
if (certificateSelectionHandler != null)
{
throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections.");
}
} }
} }
} }