diff --git a/Duplicati/Library/Certificates/CertificateConfigurationHelper.cs b/Duplicati/Library/Certificates/CertificateConfigurationHelper.cs index e4c49a7b7..ac961ba46 100644 --- a/Duplicati/Library/Certificates/CertificateConfigurationHelper.cs +++ b/Duplicati/Library/Certificates/CertificateConfigurationHelper.cs @@ -392,38 +392,56 @@ public static class CertificateConfigurationHelper return result; } - // Check if CA itself is expired - if (caPair.Certificate.NotAfter < DateTime.UtcNow) + try { - Log.WriteErrorMessage(LOGTAG, "CAExpired", null, "Cannot renew certificate: CA certificate has expired."); - result.RenewalFailedReason = "CA certificate has expired."; - return result; + // Check if CA itself is expired + if (caPair.Certificate.NotAfter < DateTime.UtcNow) + { + Log.WriteErrorMessage(LOGTAG, "CAExpired", null, "Cannot renew certificate: CA certificate has expired."); + result.RenewalFailedReason = "CA certificate has expired."; + return result; + } + + // Parse hostnames + var hostnameList = CertificateRenewalChecker.ParseHostnames(hostnames); + Log.WriteInformationMessage(LOGTAG, "GeneratingCertificate", $"Generating new server certificate with hostnames: {string.Join(", ", hostnameList)}"); + + var newServerPair = CertificateGenerator.GenerateServerCertificate(caPair.Certificate, caPair.PrivateKey, hostnameList); + + try + { + // Generate new password for PFX + var pfxPassword = CertificateStorageHelper.GeneratePfxPassword(); + + // Create PFX bundle + var pfxBytes = CertificateGenerator.CreatePfxBundle(newServerPair, caPair, pfxPassword); + var pfxBase64 = Convert.ToBase64String(pfxBytes); + + Log.WriteInformationMessage(LOGTAG, "CertificateRenewed", $"Server certificate renewed successfully. New certificate expires: {newServerPair.Certificate.NotAfter:yyyy-MM-dd}"); + + result.Renewed = true; + result.RenewedCertificate = new ServerCertificateData + { + ServerCertificate = pfxBase64, + Password = pfxPassword, + Autogenerated = true + }; + + return result; + } + finally + { + // Dispose server certificate key material promptly + newServerPair.PrivateKey.Dispose(); + newServerPair.Certificate.Dispose(); + } } - - // Parse hostnames - var hostnameList = CertificateRenewalChecker.ParseHostnames(hostnames); - Log.WriteInformationMessage(LOGTAG, "GeneratingCertificate", $"Generating new server certificate with hostnames: {string.Join(", ", hostnameList)}"); - - var newServerPair = CertificateGenerator.GenerateServerCertificate(caPair.Certificate, caPair.PrivateKey, hostnameList); - - // Generate new password for PFX - var pfxPassword = CertificateStorageHelper.GeneratePfxPassword(); - - // Create PFX bundle - var pfxBytes = CertificateGenerator.CreatePfxBundle(newServerPair, caPair, pfxPassword); - var pfxBase64 = Convert.ToBase64String(pfxBytes); - - Log.WriteInformationMessage(LOGTAG, "CertificateRenewed", $"Server certificate renewed successfully. New certificate expires: {newServerPair.Certificate.NotAfter:yyyy-MM-dd}"); - - result.Renewed = true; - result.RenewedCertificate = new ServerCertificateData + finally { - ServerCertificate = pfxBase64, - Password = pfxPassword, - Autogenerated = true - }; - - return result; + // Dispose CA certificate key material promptly + caPair.PrivateKey.Dispose(); + caPair.Certificate.Dispose(); + } } catch (Exception ex) { @@ -521,67 +539,85 @@ public static class CertificateConfigurationHelper Log.WriteInformationMessage(LOGTAG, "GeneratingCA", "Generating CA certificate..."); var caPair = CertificateGenerator.GenerateCACertificate(); - // Generate server certificate - Log.WriteInformationMessage(LOGTAG, "GeneratingServerCert", "Generating server certificate..."); - var serverPair = CertificateGenerator.GenerateServerCertificate(caPair.Certificate, caPair.PrivateKey, hostnameList); - - // Generate separate passwords for CA key encryption and server PFX - var caPassword = CertificateStorageHelper.GeneratePfxPassword(); - var pfxPassword = CertificateStorageHelper.GeneratePfxPassword(); - - // Install CA in trust store if not skipped - if (!skipTrustInstallation) + try { - Log.WriteInformationMessage(LOGTAG, "InstallingCA", "Installing CA certificate in system trust store..."); - var installResult = InstallCATrust(caPair.Certificate, storeLocation, linuxCertDirectory, macOSKeychainPath); - result.TrustInstallationStatus = installResult.Status; + // Generate server certificate + Log.WriteInformationMessage(LOGTAG, "GeneratingServerCert", "Generating server certificate..."); + var serverPair = CertificateGenerator.GenerateServerCertificate(caPair.Certificate, caPair.PrivateKey, hostnameList); - switch (installResult.Status) + try { - case CATrustInstallationStatus.Success: - Log.WriteInformationMessage(LOGTAG, "CAInstalled", "CA certificate installed successfully."); - break; - case CATrustInstallationStatus.AlreadyInstalled: - Log.WriteInformationMessage(LOGTAG, "CAAlreadyInstalled", "CA certificate was already installed."); - break; - case CATrustInstallationStatus.NotSupported: - Log.WriteWarningMessage(LOGTAG, "NoTrustInstaller", null, "No trust installer available for this platform."); - break; - case CATrustInstallationStatus.RequiresElevation: - Log.WriteWarningMessage(LOGTAG, "CARequiresElevation", null, "Administrator/root privileges required to install CA certificate."); - break; - case CATrustInstallationStatus.Failed: - Log.WriteWarningMessage(LOGTAG, "CAInstallFailed", null, "Failed to install CA certificate."); - break; + // Generate separate passwords for CA key encryption and server PFX + var caPassword = CertificateStorageHelper.GeneratePfxPassword(); + var pfxPassword = CertificateStorageHelper.GeneratePfxPassword(); + + // Install CA in trust store if not skipped + if (!skipTrustInstallation) + { + Log.WriteInformationMessage(LOGTAG, "InstallingCA", "Installing CA certificate in system trust store..."); + var installResult = InstallCATrust(caPair.Certificate, storeLocation, linuxCertDirectory, macOSKeychainPath); + result.TrustInstallationStatus = installResult.Status; + + switch (installResult.Status) + { + case CATrustInstallationStatus.Success: + Log.WriteInformationMessage(LOGTAG, "CAInstalled", "CA certificate installed successfully."); + break; + case CATrustInstallationStatus.AlreadyInstalled: + Log.WriteInformationMessage(LOGTAG, "CAAlreadyInstalled", "CA certificate was already installed."); + break; + case CATrustInstallationStatus.NotSupported: + Log.WriteWarningMessage(LOGTAG, "NoTrustInstaller", null, "No trust installer available for this platform."); + break; + case CATrustInstallationStatus.RequiresElevation: + Log.WriteWarningMessage(LOGTAG, "CARequiresElevation", null, "Administrator/root privileges required to install CA certificate."); + break; + case CATrustInstallationStatus.Failed: + Log.WriteWarningMessage(LOGTAG, "CAInstallFailed", null, "Failed to install CA certificate."); + break; + } + } + else + { + Log.WriteInformationMessage(LOGTAG, "SkippingCATrust", "Skipping CA trust installation."); + } + + // Serialize certificates + Log.WriteInformationMessage(LOGTAG, "StoringCertificates", "Serializing certificates..."); + var (caCertBase64, caKeyEncrypted) = CertificateStorageHelper.SerializeCACertificatePair(caPair, caPassword); + var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); + var pfxBase64 = Convert.ToBase64String(pfxBytes); + + Log.WriteInformationMessage(LOGTAG, "CertificatesGenerated", "HTTPS certificates generated successfully."); + + result.Success = true; + result.CACertificate = new CACertificateData + { + CACertificate = caCertBase64, + CAKey = caKeyEncrypted, + CAPassword = caPassword + }; + result.ServerCertificate = new ServerCertificateData + { + ServerCertificate = pfxBase64, + Password = pfxPassword, + Autogenerated = true + }; + + return result; + } + finally + { + // Dispose server certificate key material promptly + serverPair.PrivateKey.Dispose(); + serverPair.Certificate.Dispose(); } } - else + finally { - Log.WriteInformationMessage(LOGTAG, "SkippingCATrust", "Skipping CA trust installation."); + // Dispose CA certificate key material promptly + caPair.PrivateKey.Dispose(); + caPair.Certificate.Dispose(); } - - // Serialize certificates - Log.WriteInformationMessage(LOGTAG, "StoringCertificates", "Serializing certificates..."); - var (caCertBase64, caKeyEncrypted) = CertificateStorageHelper.SerializeCACertificatePair(caPair, caPassword); - var pfxBytes = CertificateGenerator.CreatePfxBundle(serverPair, caPair, pfxPassword); - var pfxBase64 = Convert.ToBase64String(pfxBytes); - - Log.WriteInformationMessage(LOGTAG, "CertificatesGenerated", "HTTPS certificates generated successfully."); - - result.Success = true; - result.CACertificate = new CACertificateData - { - CACertificate = caCertBase64, - CAKey = caKeyEncrypted, - CAPassword = caPassword - }; - result.ServerCertificate = new ServerCertificateData - { - ServerCertificate = pfxBase64, - Password = pfxPassword, - Autogenerated = true - }; - - return result; } } diff --git a/Duplicati/Library/Certificates/Platform/LinuxCATrustInstaller.cs b/Duplicati/Library/Certificates/Platform/LinuxCATrustInstaller.cs index 580c51350..93b7aad59 100644 --- a/Duplicati/Library/Certificates/Platform/LinuxCATrustInstaller.cs +++ b/Duplicati/Library/Certificates/Platform/LinuxCATrustInstaller.cs @@ -222,9 +222,24 @@ public class LinuxCATrustInstaller : ICATrustInstaller if (process == null) return (-1, string.Empty, $"Failed to start process: {fileName}"); + // Read stdout/stderr to completion before waiting for exit to avoid + // deadlocks when the process output fills the OS pipe buffer. var outputTask = process.StandardOutput.ReadToEndAsync(); var errorTask = process.StandardError.ReadToEndAsync(); + if (!Task.WaitAll([outputTask, errorTask], PROCESS_TIMEOUT)) + { + try + { + process.Kill(); + } + catch + { + // Ignore kill errors + } + return (-1, string.Empty, $"Process timed out after {PROCESS_TIMEOUT}"); + } + process.WaitForExit(PROCESS_TIMEOUT); if (!process.HasExited) { @@ -239,9 +254,6 @@ public class LinuxCATrustInstaller : ICATrustInstaller return (-1, string.Empty, $"Process timed out after {PROCESS_TIMEOUT}"); } - var output = outputTask.Result; - var error = errorTask.Result; - - return (process.ExitCode, output, error); + return (process.ExitCode, outputTask.Result, errorTask.Result); } } diff --git a/Duplicati/Library/Certificates/Platform/MacOSCATrustInstaller.cs b/Duplicati/Library/Certificates/Platform/MacOSCATrustInstaller.cs index e58202125..7ce0cfc19 100644 --- a/Duplicati/Library/Certificates/Platform/MacOSCATrustInstaller.cs +++ b/Duplicati/Library/Certificates/Platform/MacOSCATrustInstaller.cs @@ -142,8 +142,10 @@ public class MacOSCATrustInstaller : ICATrustInstaller try { - // Find and delete certificate - var result = RunSecurityCommand("delete-certificate", "-c", caCertificate.Subject, KeychainPath); + // Delete certificate by SHA-1 hash to avoid accidentally removing + // other certificates that share the same subject name. + var hash = caCertificate.GetCertHashString(); + var result = RunSecurityCommand("delete-certificate", "-Z", hash, KeychainPath); if (result.ExitCode == 0) return TrustUninstallationResult.Success; @@ -181,9 +183,24 @@ public class MacOSCATrustInstaller : ICATrustInstaller if (process == null) return (-1, string.Empty, "Failed to start security process"); + // Read stdout/stderr to completion before waiting for exit to avoid + // deadlocks when the process output fills the OS pipe buffer. var outputTask = process.StandardOutput.ReadToEndAsync(); var errorTask = process.StandardError.ReadToEndAsync(); + if (!Task.WaitAll([outputTask, errorTask], PROCESS_TIMEOUT)) + { + try + { + process.Kill(); + } + catch + { + // Ignore kill errors + } + return (-1, string.Empty, $"Security process timed out after {PROCESS_TIMEOUT}"); + } + process.WaitForExit(PROCESS_TIMEOUT); if (!process.HasExited) { @@ -198,9 +215,6 @@ public class MacOSCATrustInstaller : ICATrustInstaller return (-1, string.Empty, $"Security process timed out after {PROCESS_TIMEOUT}"); } - var output = outputTask.Result; - var error = errorTask.Result; - - return (process.ExitCode, output, error); + return (process.ExitCode, outputTask.Result, errorTask.Result); } }