// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Duplicati.Library.AutoUpdater; using Duplicati.Library.Certificates; using Duplicati.Library.RestAPI.Database; using Duplicati.Library.SQLiteHelper; using Duplicati.Server.Database; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; namespace Duplicati.UnitTest; /// /// Integration tests for HTTPS certificate configuration. /// Tests the end-to-end flow of certificate generation, storage, renewal, and server integration. /// [TestFixture] [Category("HTTPSConfiguration")] public class HttpsConfigurationTests { private string _tempDataFolder = null!; private string _databasePath = null!; private Connection _connection = null!; [SetUp] public async Task SetUp() { // Create a temporary folder for test data _tempDataFolder = Path.Combine(Path.GetTempPath(), $"duplicati-test-{Guid.NewGuid()}"); Directory.CreateDirectory(_tempDataFolder); _databasePath = Path.Combine(_tempDataFolder, DataFolderManager.SERVER_DATABASE_FILENAME); // Create a new test database await CreateTestDatabaseAsync(_databasePath); // Open the database connection var dbConnection = await SQLiteLoader.LoadConnectionAsync(_databasePath); _connection = new Connection(dbConnection, true, null, _tempDataFolder, () => { }); } [TearDown] public void TearDown() { _connection?.Dispose(); // Clean up temp folder if (Directory.Exists(_tempDataFolder)) { try { Directory.Delete(_tempDataFolder, true); } catch { // Best effort cleanup } } } /// /// Creates a minimal test database for integration testing. /// private static async Task CreateTestDatabaseAsync(string databasePath) { var dbConnection = await SQLiteLoader.LoadConnectionAsync(databasePath); // Use DatabaseUpgrader to create the schema from embedded resources DatabaseUpgrader.UpgradeDatabase(dbConnection, databasePath, typeof(DatabaseSchemaMarker)); dbConnection.Dispose(); } #region Certificate Generation and Storage Tests [Test] public void GenerateAndStoreCertificates_StoresValidCertificatesInDatabase() { // Act var hostnames = new[] { "localhost", "127.0.0.1" }; var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, hostnames); var pfxPassword = "test-password-123"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var pfxBase64 = Convert.ToBase64String(pfxBytes); var (caCertBase64, caKeyEncrypted) = CertificateStorageHelper.SerializeCACertificatePair(caPair, pfxPassword); // Store in database var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCertBase64, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKeyEncrypted, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = pfxBase64, [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Assert - Verify stored values Assert.IsNotNull(_connection.ApplicationSettings.ServerCACertificate); Assert.IsNotNull(_connection.ApplicationSettings.ServerCACertificateKey); Assert.IsNotNull(_connection.ApplicationSettings.ServerCACertificatePassword); Assert.IsNotNull(_connection.ApplicationSettings.ServerSSLCertificate); Assert.IsTrue(_connection.ApplicationSettings.ServerSSLCertificateAutogenerated); // Verify certificate can be loaded and is valid var loadedCertCollection = _connection.ApplicationSettings.ServerSSLCertificate; Assert.IsNotNull(loadedCertCollection); Assert.That(loadedCertCollection!.Count, Is.GreaterThan(0)); var serverCert = loadedCertCollection.Cast().FirstOrDefault(c => c.HasPrivateKey); Assert.IsNotNull(serverCert); Assert.IsTrue(serverCert!.HasPrivateKey); Assert.That(serverCert.NotAfter, Is.GreaterThan(DateTime.Now)); } [Test] public void StoreAndRetrieveCAData_RoundTripsCorrectly() { // Arrange var caPair = CertificateGenerator.GenerateCACertificate(); var password = "test-password-456"; // Act var (caCertBase64, caKeyEncrypted) = CertificateStorageHelper.SerializeCACertificatePair(caPair, password); var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCertBase64, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKeyEncrypted, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = password }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Retrieve from settings (API changed - RetrieveCAData removed) var retrievedCert = _connection.ApplicationSettings.ServerCACertificate; var retrievedKey = _connection.ApplicationSettings.ServerCACertificateKey; var retrievedPassword = _connection.ApplicationSettings.ServerCACertificatePassword; // Assert Assert.IsNotNull(retrievedCert); Assert.IsNotNull(retrievedKey); Assert.IsNotNull(retrievedPassword); // Verify can be deserialized var deserializedCa = CertificateStorageHelper.DeserializeCACertificatePair( retrievedCert!, retrievedKey!, retrievedPassword!); Assert.AreEqual(caPair.Certificate.Thumbprint, deserializedCa.Certificate.Thumbprint); } [Test] public void UseHTTPS_WithValidCertificates_ReturnsTrue() { // Arrange - Store valid certificates var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "test-password-789"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_DISABLE_HTTPS] = "false" }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Act & Assert Assert.IsTrue(_connection.ApplicationSettings.UseHTTPS); } [Test] public void UseHTTPS_WithDisableHttpsFlag_ReturnsFalse() { // Arrange - Store valid certificates but disable HTTPS var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "test-password-abc"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_DISABLE_HTTPS] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Act & Assert Assert.IsFalse(_connection.ApplicationSettings.UseHTTPS); } [Test] public void UseHTTPS_WithoutCertificates_ReturnsFalse() { // Arrange - No certificates stored Assert.IsNull(_connection.ApplicationSettings.ServerSSLCertificate); // Act & Assert Assert.IsFalse(_connection.ApplicationSettings.UseHTTPS); } #endregion #region Auto-Renewal Tests [Test] public void CertificateRenewalChecker_HasValidCertificates_WithValidCert_ReturnsTrue() { // Arrange - Store valid certificates var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "renewal-test-1"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var (caCertBase64, caKeyEncrypted) = CertificateStorageHelper.SerializeCACertificatePair(caPair, pfxPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCertBase64, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKeyEncrypted, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Act - New API signature: pass parameters directly instead of Connection var caData = new CACertificateData { CACertificate = caCertBase64, CAKey = caKeyEncrypted, CAPassword = pfxPassword }; var serverCert = _connection.ApplicationSettings.ServerSSLCertificate; var isAutogenerated = _connection.ApplicationSettings.ServerSSLCertificateAutogenerated; var hasValidCerts = CertificateRenewalChecker.HasValidCertificates(isAutogenerated, serverCert, caData); // Assert Assert.IsTrue(hasValidCerts); } [Test] public void CertificateRenewalChecker_HasValidCertificates_WithExpiringCert_ReturnsFalse() { // Arrange - Create an expired certificate by manipulating validity dates is not possible // with the current generator, so we use reflection to create a mock expired certificate // or we test the IsExpiringSoon method directly var caPair = CertificateGenerator.GenerateCACertificate(); // Create a certificate with short validity to simulate near-expiry var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); // The certificate should not be expiring soon (it's newly created) Assert.IsFalse(CertificateGenerator.IsExpiringSoon(serverPair.Certificate, 30)); } [Test] public void CertificateRenewalChecker_HasValidCertificates_NonAutogenerated_ReturnsFalse() { // Arrange - Store certificates but mark as not autogenerated var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "renewal-test-2"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var (caCertBase64, caKeyEncrypted) = CertificateStorageHelper.SerializeCACertificatePair(caPair, pfxPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCertBase64, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKeyEncrypted, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "false" // Not autogenerated }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Act - New API signature: pass parameters directly instead of Connection var caData = new CACertificateData { CACertificate = caCertBase64, CAKey = caKeyEncrypted, CAPassword = pfxPassword }; var serverCert = _connection.ApplicationSettings.ServerSSLCertificate; var isAutogenerated = _connection.ApplicationSettings.ServerSSLCertificateAutogenerated; var hasValidCerts = CertificateRenewalChecker.HasValidCertificates(isAutogenerated, serverCert, caData); // Assert - Should return false for non-autogenerated certs Assert.IsFalse(hasValidCerts); } [Test] public void CheckAndRenewIfNeeded_WithExpiringCertificate_PerformsRenewal() { // Arrange - Store valid certificates var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "renewal-test-3"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var (caCertBase64, caKeyEncrypted) = CertificateStorageHelper.SerializeCACertificatePair(caPair, pfxPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCertBase64, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKeyEncrypted, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); var originalThumbprint = serverPair.Certificate.Thumbprint; // Act - Try renewal (won't actually renew since cert is not expiring) // New API signature: pass parameters directly instead of Connection var caData = new CACertificateData { CACertificate = caCertBase64, CAKey = caKeyEncrypted, CAPassword = pfxPassword }; var serverCert = _connection.ApplicationSettings.ServerSSLCertificate; var isAutogenerated = _connection.ApplicationSettings.ServerSSLCertificateAutogenerated; var result = CertificateRenewalChecker.CheckAndRenewIfNeeded(isAutogenerated, serverCert, caData); // Assert - Should not renew since certificate is new (return type is now CertificateRenewalResult) Assert.IsFalse(result.Renewed); Assert.IsNull(result.RenewedCertificate); // Verify original certificate is still in place var currentCert = _connection.ApplicationSettings.ServerSSLCertificate; var currentThumbprint = currentCert!.Cast().First(c => c.HasPrivateKey).Thumbprint; Assert.AreEqual(originalThumbprint, currentThumbprint); } #endregion #region CA Regeneration Tests [Test] public void RegenerateCA_StoresNewCertificatesAndPreservesCAData() { // Arrange - First generation var originalCaPair = CertificateGenerator.GenerateCACertificate(); var originalServerPair = CertificateGenerator.GenerateServerCertificate( originalCaPair.Certificate, originalCaPair.PrivateKey, new[] { "localhost" }); var originalPassword = "original-password"; var originalPfxBytes = CertificateGenerator.CreatePfxBundle(originalServerPair, originalCaPair, originalPassword); var (originalCaCert, originalCaKey) = CertificateStorageHelper.SerializeCACertificatePair(originalCaPair, originalPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = originalCaCert, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = originalCaKey, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = originalPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(originalPfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = originalPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); var originalThumbprint = originalCaPair.Certificate.Thumbprint; // Act - Simulate CA regeneration by generating new certificates var newCaPair = CertificateGenerator.GenerateCACertificate(); var newServerPair = CertificateGenerator.GenerateServerCertificate( newCaPair.Certificate, newCaPair.PrivateKey, new[] { "localhost", "example.com" }); var newPassword = "new-password"; var newPfxBytes = CertificateGenerator.CreatePfxBundle(newServerPair, newCaPair, newPassword); var (newCaCert, newCaKey) = CertificateStorageHelper.SerializeCACertificatePair(newCaPair, newPassword); var newSettings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = newCaCert, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = newCaKey, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = newPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(newPfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = newPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(newSettings, false); // Assert - Verify new CA is different var retrievedCa = _connection.ApplicationSettings.ServerCACertificate; Assert.IsNotNull(retrievedCa); var loadedCa = CertificateStorageHelper.DeserializeCertificate(retrievedCa!); Assert.AreNotEqual(originalThumbprint, loadedCa.Thumbprint); // Verify new server cert has additional hostname var serverCert = _connection.ApplicationSettings.ServerSSLCertificate; Assert.IsNotNull(serverCert); var cert = serverCert!.Cast().First(c => c.HasPrivateKey); var sanExt = cert.Extensions["2.5.29.17"] as X509SubjectAlternativeNameExtension; Assert.IsNotNull(sanExt); Assert.That(sanExt!.EnumerateDnsNames(), Contains.Item("example.com")); } #endregion #region Database Integration Tests [Test] public void RemoveCertificates_ClearsAllCertificateData() { // Arrange - Store certificates var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "remove-test"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var (caCert, caKey) = CertificateStorageHelper.SerializeCACertificatePair(caPair, pfxPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCert, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKey, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Verify certificates exist Assert.IsNotNull(_connection.ApplicationSettings.ServerCACertificate); Assert.IsNotNull(_connection.ApplicationSettings.ServerSSLCertificate); // Act - Remove all certificate data var removeSettings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = null, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = null, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = null, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = null, [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = null, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = null }; _connection.ApplicationSettings.UpdateSettings(removeSettings, false); // Assert - All certificate data should be cleared Assert.IsNull(_connection.ApplicationSettings.ServerCACertificate); Assert.IsNull(_connection.ApplicationSettings.ServerCACertificateKey); Assert.IsNull(_connection.ApplicationSettings.ServerCACertificatePassword); Assert.IsNull(_connection.ApplicationSettings.ServerSSLCertificate); Assert.IsFalse(_connection.ApplicationSettings.ServerSSLCertificateAutogenerated); } [Test] public void CertificateStorage_WithEncryptedFieldsEnabled_StoresSecurely() { // Note: This test verifies the database structure supports encrypted fields. // Full encryption testing requires a database initialized with encryption key. // Arrange & Act - Store certificates var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "encryption-test"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var (caCert, caKey) = CertificateStorageHelper.SerializeCACertificatePair(caPair, pfxPassword); var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCert, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKey, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Assert - Verify encrypted key is actually encrypted (not plaintext) var storedKey = _connection.ApplicationSettings.ServerCACertificateKey; Assert.IsNotNull(storedKey); // The key should be encrypted and not directly parseable as a key // (This is a basic sanity check - the actual encryption is tested in CertificateTests) Assert.DoesNotThrow(() => Convert.FromBase64String(storedKey!)); } #endregion #region Autogenerated Flag Tests [Test] public void StoreCAAndServerCertificate_MarksAsAutogenerated() { // Arrange var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "autogen-test"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); var (caCert, caKey) = CertificateStorageHelper.SerializeCACertificatePair(caPair, pfxPassword); // Act - Store directly (StoreCAAndServerCertificate method removed) var settings = new Dictionary { [ServerSettings.CONST.SERVER_CA_CERTIFICATE] = caCert, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = caKey, [ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, [ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true" }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Assert Assert.IsTrue(_connection.ApplicationSettings.ServerSSLCertificateAutogenerated); } #endregion #region Kestrel HTTPS Configuration Tests [Test] public void ServerCertificate_HasPrivateKey_ForKestrelUsage() { // Arrange var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "kestrel-test"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); // Store in database var settings = new Dictionary { [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Act var certCollection = _connection.ApplicationSettings.ServerSSLCertificate; // Assert - Kestrel requires a certificate with private key Assert.IsNotNull(certCollection); var certWithKey = certCollection!.Cast().FirstOrDefault(c => c.HasPrivateKey); Assert.IsNotNull(certWithKey); Assert.IsTrue(certWithKey!.HasPrivateKey); } [Test] public void ServerCertificate_ContainsIntermediateCA_ForChainValidation() { // Arrange var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); var pfxPassword = "chain-test"; var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); // Store in database var settings = new Dictionary { [ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(pfxBytes), [ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = pfxPassword, }; _connection.ApplicationSettings.UpdateSettings(settings, false); // Act var certCollection = _connection.ApplicationSettings.ServerSSLCertificate; // Assert - PFX bundle should contain both server cert and CA Assert.IsNotNull(certCollection); Assert.That(certCollection!.Count, Is.GreaterThanOrEqualTo(1)); // Verify chain can be built using var chain = new X509Chain(); var serverCert = certCollection.Cast().First(c => c.HasPrivateKey); chain.ChainPolicy.ExtraStore.AddRange(certCollection); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; var chainBuilt = chain.Build(serverCert); Assert.IsTrue(chainBuilt); } #endregion #region Certificate Validation Tests [Test] public void GeneratedCertificate_HasCorrectExtendedKeyUsage() { // Arrange & Act var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); // Assert var ekuExtension = serverPair.Certificate.Extensions["2.5.29.37"] as X509EnhancedKeyUsageExtension; Assert.IsNotNull(ekuExtension); Assert.That(ekuExtension!.EnhancedKeyUsages, Has.Some.Matches(oid => oid.Value == "1.3.6.1.5.5.7.3.1")); // Server Authentication } [Test] public void GeneratedCertificate_HasSubjectAlternativeNames() { // Arrange & Act var hostnames = new[] { "localhost", "test.example.com", "192.168.1.1" }; var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, hostnames); // Assert var sanExtension = serverPair.Certificate.Extensions["2.5.29.17"] as X509SubjectAlternativeNameExtension; Assert.IsNotNull(sanExtension); var dnsNames = sanExtension!.EnumerateDnsNames().ToList(); var ipAddresses = sanExtension.EnumerateIPAddresses().Select(ip => ip.ToString()).ToList(); Assert.That(dnsNames, Contains.Item("localhost")); Assert.That(dnsNames, Contains.Item("test.example.com")); Assert.That(ipAddresses, Has.Some.EqualTo("192.168.1.1")); } [Test] public void GeneratedCA_HasCorrectBasicConstraints() { // Arrange & Act var caPair = CertificateGenerator.GenerateCACertificate(); // Assert var basicConstraints = caPair.Certificate.Extensions["2.5.29.19"] as X509BasicConstraintsExtension; Assert.IsNotNull(basicConstraints); Assert.IsTrue(basicConstraints!.CertificateAuthority); Assert.IsTrue(basicConstraints.HasPathLengthConstraint); Assert.AreEqual(0, basicConstraints.PathLengthConstraint); // Cannot sign sub-CAs } #endregion #region Certificate Expiration Tests [Test] public void IsExpiringSoon_WithValidCertificate_ReturnsFalse() { // Arrange var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); // Act & Assert - New certificate should not be expiring Assert.IsFalse(CertificateGenerator.IsExpiringSoon(serverPair.Certificate, 30)); } [Test] public void CertificateValidity_ServerCert_Approximately90Days() { // Arrange & Act var caPair = CertificateGenerator.GenerateCACertificate(); var serverPair = CertificateGenerator.GenerateServerCertificate( caPair.Certificate, caPair.PrivateKey, new[] { "localhost" }); // Assert var validity = serverPair.Certificate.NotAfter - serverPair.Certificate.NotBefore; Assert.That(validity.TotalDays, Is.GreaterThan(80)); Assert.That(validity.TotalDays, Is.LessThan(100)); } [Test] public void CertificateValidity_CACert_Approximately10Years() { // Arrange & Act var caPair = CertificateGenerator.GenerateCACertificate(); // Assert var validity = caPair.Certificate.NotAfter - caPair.Certificate.NotBefore; Assert.That(validity.TotalDays, Is.GreaterThan(365 * 9)); Assert.That(validity.TotalDays, Is.LessThan(365 * 11)); } #endregion }