WithClientCertificates Fails with Disposed X509Certificate2 Object (Possible Reference Retention)
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.
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?