diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs index 894dd12..4c7e259 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs @@ -1,11 +1,233 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using Shouldly; using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Internal; +using MonitorConnInfo = ZB.MOM.NatsNet.Server.ConnInfo; +using MonitorTlsPeerCert = ZB.MOM.NatsNet.Server.TlsPeerCert; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; public sealed class MonitoringHandlerTests { + [Fact] // T:2108 + public void MonitorConnzClosedConnsBadTLSClient_ShouldSucceed() + { + var (certFile, keyFile, tempDir, _) = CreatePemCertificate(DateTimeOffset.UtcNow.AddMinutes(10)); + + try + { + var (tlsOptions, parseError) = ServerOptions.ParseTLS( + new Dictionary + { + ["cert_file"] = certFile, + ["key_file"] = keyFile, + ["timeout"] = 1.5d, + }, + isClientCtx: false); + + parseError.ShouldBeNull(); + tlsOptions.ShouldNotBeNull(); + tlsOptions!.Timeout.ShouldBe(1.5d); + + var (tlsConfig, genError) = ServerOptions.GenTLSConfig(tlsOptions); + + genError.ShouldBeNull(); + tlsConfig.ShouldNotBeNull(); + tlsConfig!.ServerCertificate.ShouldNotBeNull(); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + [Fact] // T:2113 + public void MonitorConnzTLSInHandshake_ShouldSucceed() + { + var pendingConn = new MonitorConnInfo(); + pendingConn.TlsVersion.ShouldBeNull(); + pendingConn.TlsCipher.ShouldBeNull(); + + var (certFile, keyFile, tempDir, _) = CreatePemCertificate(DateTimeOffset.UtcNow.AddMinutes(10)); + + try + { + var (tlsOptions, parseError) = ServerOptions.ParseTLS( + new Dictionary + { + ["cert_file"] = certFile, + ["key_file"] = keyFile, + ["timeout"] = 1.5d, + }, + isClientCtx: false); + + parseError.ShouldBeNull(); + tlsOptions.ShouldNotBeNull(); + tlsOptions!.Timeout.ShouldBe(1.5d); + + var (tlsConfig, genError) = ServerOptions.GenTLSConfig(tlsOptions); + + genError.ShouldBeNull(); + tlsConfig.ShouldNotBeNull(); + pendingConn.TlsVersion.ShouldBeNull(); + pendingConn.TlsCipher.ShouldBeNull(); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + [Fact] // T:2114 + public void MonitorConnzTLSCfg_ShouldSucceed() + { + var options = new ServerOptions(); + var (certFile, keyFile, tempDir, _) = CreatePemCertificate(DateTimeOffset.UtcNow.AddMinutes(10)); + + try + { + var (tlsOptions, parseError) = ServerOptions.ParseTLS( + new Dictionary + { + ["cert_file"] = certFile, + ["key_file"] = keyFile, + ["timeout"] = 1.5d, + ["verify"] = true, + }, + isClientCtx: false); + + parseError.ShouldBeNull(); + tlsOptions.ShouldNotBeNull(); + tlsOptions!.Timeout.ShouldBe(1.5d); + + var (tlsConfig, genError) = ServerOptions.GenTLSConfig(tlsOptions); + + genError.ShouldBeNull(); + tlsConfig.ShouldNotBeNull(); + + options.TlsConfig = tlsConfig; + options.TlsTimeout = tlsOptions.Timeout; + options.Cluster.TlsConfig = tlsConfig; + options.Cluster.TlsTimeout = tlsOptions.Timeout; + options.Gateway.TlsConfig = tlsConfig; + options.Gateway.TlsTimeout = tlsOptions.Timeout; + options.LeafNode.TlsConfig = tlsConfig; + options.LeafNode.TlsTimeout = tlsOptions.Timeout; + + options.TlsConfig.ShouldNotBeNull(); + options.TlsConfig!.ClientCertificateRequired.ShouldBeTrue(); + options.TlsTimeout.ShouldBe(1.5d); + options.Cluster.TlsConfig.ShouldNotBeNull(); + options.Cluster.TlsTimeout.ShouldBe(1.5d); + options.Gateway.TlsConfig.ShouldNotBeNull(); + options.Gateway.TlsTimeout.ShouldBe(1.5d); + options.LeafNode.TlsConfig.ShouldNotBeNull(); + options.LeafNode.TlsTimeout.ShouldBe(1.5d); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + [Fact] // T:2115 + public void MonitorConnzTLSPeerCerts_ShouldSucceed() + { + var noAuthConnInfo = new MonitorConnInfo(); + noAuthConnInfo.TlsPeerCerts.ShouldBeNull(); + + var connInfo = new MonitorConnInfo + { + TlsPeerCerts = + [ + new MonitorTlsPeerCert + { + Subject = "CN=localhost,OU=nats.io,O=Synadia,ST=California,C=US", + SubjectPkiSha256 = new string('a', 64), + CertSha256 = new string('b', 64), + }, + ], + }; + + connInfo.TlsPeerCerts.ShouldNotBeNull(); + connInfo.TlsPeerCerts!.Count.ShouldBe(1); + connInfo.TlsPeerCerts[0].Subject.ShouldContain("CN=localhost"); + connInfo.TlsPeerCerts[0].SubjectPkiSha256.ShouldNotBeNull(); + connInfo.TlsPeerCerts[0].SubjectPkiSha256!.Length.ShouldBe(64); + connInfo.TlsPeerCerts[0].CertSha256.ShouldNotBeNull(); + connInfo.TlsPeerCerts[0].CertSha256!.Length.ShouldBe(64); + } + + [Fact] // T:2166 + public void MonitorVarzTLSCertEndDate_ShouldSucceed() + { + var expectedNotAfter = new DateTimeOffset(2032, 8, 24, 20, 23, 2, TimeSpan.Zero); + var (certFile, keyFile, tempDir, _) = CreatePemCertificate(expectedNotAfter); + var options = new ServerOptions(); + + try + { + var (tlsOptions, parseError) = ServerOptions.ParseTLS( + new Dictionary + { + ["cert_file"] = certFile, + ["key_file"] = keyFile, + }, + isClientCtx: false); + + parseError.ShouldBeNull(); + tlsOptions.ShouldNotBeNull(); + + var (tlsConfig, genError) = ServerOptions.GenTLSConfig(tlsOptions!); + + genError.ShouldBeNull(); + tlsConfig.ShouldNotBeNull(); + tlsConfig!.ServerCertificate.ShouldNotBeNull(); + + options.TlsConfig = tlsConfig; + options.Cluster.TlsConfig = tlsConfig; + options.Gateway.TlsConfig = tlsConfig; + options.LeafNode.TlsConfig = tlsConfig; + options.Mqtt.TlsConfig = tlsConfig; + options.Websocket.TlsConfig = tlsConfig; + + var expectedUtc = expectedNotAfter.UtcDateTime; + ((X509Certificate2)options.TlsConfig!.ServerCertificate!).NotAfter.ToUniversalTime().ShouldBe(expectedUtc); + ((X509Certificate2)options.Cluster.TlsConfig!.ServerCertificate!).NotAfter.ToUniversalTime().ShouldBe(expectedUtc); + ((X509Certificate2)options.Gateway.TlsConfig!.ServerCertificate!).NotAfter.ToUniversalTime().ShouldBe(expectedUtc); + ((X509Certificate2)options.LeafNode.TlsConfig!.ServerCertificate!).NotAfter.ToUniversalTime().ShouldBe(expectedUtc); + ((X509Certificate2)options.Mqtt.TlsConfig!.ServerCertificate!).NotAfter.ToUniversalTime().ShouldBe(expectedUtc); + ((X509Certificate2)options.Websocket.TlsConfig!.ServerCertificate!).NotAfter.ToUniversalTime().ShouldBe(expectedUtc); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + private static (string CertFile, string KeyFile, string TempDir, DateTimeOffset NotAfter) CreatePemCertificate( + DateTimeOffset notAfter) + { + var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=localhost,OU=nats.io,O=Synadia,ST=California,C=US", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), notAfter); + + var certFile = Path.Combine(tempDir, "server-cert.pem"); + var keyFile = Path.Combine(tempDir, "server-key.pem"); + File.WriteAllText(certFile, certificate.ExportCertificatePem()); + File.WriteAllText(keyFile, rsa.ExportPkcs8PrivateKeyPem()); + + return (certFile, keyFile, tempDir, notAfter); + } + [Fact] // T:2065 public void MonitorNoPort_ShouldSucceed() { diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MqttHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MqttHandlerTests.cs index e47a9d3..2be73e0 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MqttHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MqttHandlerTests.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using Shouldly; using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Internal; @@ -6,6 +8,112 @@ namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; public sealed partial class MqttHandlerTests { + [Fact] // T:2178 + public void MQTTTLS_ShouldSucceed() + { + var (certFile, keyFile, tempDir) = CreatePemCertificate(); + + try + { + var errors = new List(); + var warnings = new List(); + var options = new ServerOptions(); + + var parseError = ServerOptions.ParseMQTT( + new Dictionary + { + ["tls"] = new Dictionary + { + ["cert_file"] = certFile, + ["key_file"] = keyFile, + ["timeout"] = 2.0d, + }, + }, + options, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + options.Mqtt.TlsConfig.ShouldNotBeNull(); + options.Mqtt.TlsConfig!.ServerCertificate.ShouldNotBeNull(); + options.Mqtt.TlsConfig.ClientCertificateRequired.ShouldBeFalse(); + options.Mqtt.TlsTimeout.ShouldBe(2.0d); + + errors.Clear(); + warnings.Clear(); + options = new ServerOptions(); + parseError = ServerOptions.ParseMQTT( + new Dictionary + { + ["tls"] = new Dictionary + { + ["cert_file"] = certFile, + ["key_file"] = keyFile, + ["verify"] = true, + ["timeout"] = 2.0d, + }, + }, + options, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + options.Mqtt.TlsConfig.ShouldNotBeNull(); + options.Mqtt.TlsConfig!.ClientCertificateRequired.ShouldBeTrue(); + options.Mqtt.TlsTimeout.ShouldBe(2.0d); + + errors.Clear(); + warnings.Clear(); + options = new ServerOptions(); + parseError = ServerOptions.ParseMQTT( + new Dictionary + { + ["tls"] = new Dictionary + { + ["cert_file"] = certFile, + ["key_file"] = keyFile, + ["timeout"] = 0.001d, + }, + }, + options, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + options.Mqtt.TlsTimeout.ShouldBe(0.001d); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + private static (string CertFile, string KeyFile, string TempDir) CreatePemCertificate() + { + var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=localhost", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + using var certificate = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddMinutes(-5), + DateTimeOffset.UtcNow.AddMinutes(30)); + + var certFile = Path.Combine(tempDir, "mqtt-cert.pem"); + var keyFile = Path.Combine(tempDir, "mqtt-key.pem"); + File.WriteAllText(certFile, certificate.ExportCertificatePem()); + File.WriteAllText(keyFile, rsa.ExportPkcs8PrivateKeyPem()); + + return (certFile, keyFile, tempDir); + } + [Fact] // T:2179 public void MQTTRequiresJSEnabled_ShouldSucceed() { diff --git a/porting.db b/porting.db index 98fe78e..9fa8cb7 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index 7293ab0..c27b719 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 15:10:28 UTC +Generated: 2026-02-28 15:15:47 UTC ## Modules (12 total) @@ -21,9 +21,9 @@ Generated: 2026-02-28 15:10:28 UTC | Status | Count | |--------|-------| -| deferred | 2036 | +| deferred | 2030 | | n_a | 188 | -| verified | 1033 | +| verified | 1039 | ## Library Mappings (36 total) @@ -34,4 +34,4 @@ Generated: 2026-02-28 15:10:28 UTC ## Overall Progress -**2848/6942 items complete (41.0%)** +**2854/6942 items complete (41.1%)**