MQTTnet icon indicating copy to clipboard operation
MQTTnet copied to clipboard

WithClientCertificates Fails with Disposed X509Certificate2 Object (Possible Reference Retention)

Open uriel-kluk opened this issue 10 months ago • 1 comments

Description

When configuring MQTTnet with mTLS using WithClientCertificates, disposing the X509Certificate2 object before MqttClientOptionsBuilder.Build() causes the TLS handshake to fail with a tlsv13 alert certificate required error. This suggests MQTTnet (or underlying .NET SslStream) might retain a reference to the X509Certificate2 object rather than copying its data immediately, leading to issues when the object is disposed early. This behavior is inconsistent with the expectation that certificate data is copied during configuration, as noted in .NET networking patterns.

Environment:

MQTTnet Version: 5.0.1.1416 .NET Version: .NET 8.0 OS: Ubuntu 22.04, Broker: custom MQTT broker requiring TLS 1.3 mTLS

Steps to Reproduce:

Create an MQTT client with mTLS using a PEM certificate and key and use the keyword using for the X509Certificate2

var mqttClient = new MqttFactory().CreateMqttClient();
var certPem = File.ReadAllText("cert.pem");
var keyPem = File.ReadAllText("key.pem");
using var clientCertificate = X509Certificate2.CreateFromPem(certPem, keyPem);

// Case 1: Dispose before Build() - Fails
using var clientCertificatePfx = new X509Certificate2(clientCertificate.Export(X509ContentType.Pkcs12));
var optionsBuilder = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.example.com", 8883)
    .WithTlsOptions(o => o.WithClientCertificates([clientCertificatePfx]));
var options = optionsBuilder.Build(); // clientCertificatePfx disposed here
await mqttClient.ConnectAsync(options); // Fails

Compare with non-disposed version

var clientCertificatePfx = new X509Certificate2(clientCertificate.Export(X509ContentType.Pkcs12));
var optionsBuilder = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.example.com", 8883)
    .WithTlsOptions(o => o.WithClientCertificates([clientCertificatePfx]));
var options = optionsBuilder.Build(); // clientCertificatePfx still alive
await mqttClient.ConnectAsync(options); // Succeeds

Expected Behavior:

WithClientCertificates should copy the certificate data (public cert and private key) when called, allowing the X509Certificate2 object to be safely disposed before Build() or ConnectAsync(). This aligns with typical .NET networking patterns (e.g., HttpClient with client certificates).

Actual Behavior:

Disposing the X509Certificate2 object before Build() causes the TLS handshake to fail with:

MQTTnet.Adapter.MqttConnectingFailedException: Error while authenticating.
---> Interop+Crypto+OpenSslCryptographicException: error:0A00045C:SSL routines::tlsv13 alert certificate required

Impact:

Forces users to keep X509Certificate2 objects alive longer than necessary, risking memory leaks or requiring manual management. Breaks idiomatic C# patterns like using for disposable resources, leading to unexpected failures in production code.

Suggested Fix:

Ensure WithClientCertificates deep-copies the certificate data (e.g., raw bytes of cert and key) into MqttClientOptions, avoiding reliance on the original X509Certificate2 object post-configuration. Alternatively, document this behavior explicitly, warning users against early disposal.

uriel-kluk avatar Feb 21 '25 20:02 uriel-kluk

This aligns with typical .NET networking patterns (e.g., HttpClient with client certificates).

Do you have some sample code how this is actually done in other components like HTTP Client?

chkr1011 avatar Oct 22 '25 10:10 chkr1011