624 lines
30 KiB
C#
624 lines
30 KiB
C#
// 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.
|
|
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Duplicati.Library.Logging;
|
|
|
|
namespace Duplicati.Library.Certificates;
|
|
|
|
/// <summary>
|
|
/// Result of a CA trust installation operation.
|
|
/// </summary>
|
|
public enum CATrustInstallationStatus
|
|
{
|
|
/// <summary>Installation was successful.</summary>
|
|
Success,
|
|
/// <summary>Certificate was already installed.</summary>
|
|
AlreadyInstalled,
|
|
/// <summary>Installation requires elevated privileges.</summary>
|
|
RequiresElevation,
|
|
/// <summary>Installation failed.</summary>
|
|
Failed,
|
|
/// <summary>No trust installer available for this platform.</summary>
|
|
NotSupported
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of a CA trust installation attempt.
|
|
/// </summary>
|
|
public class CATrustInstallationResult
|
|
{
|
|
/// <summary>
|
|
/// The status of the installation.
|
|
/// </summary>
|
|
public CATrustInstallationStatus Status { get; set; }
|
|
|
|
/// <summary>
|
|
/// An optional error message if installation failed.
|
|
/// </summary>
|
|
public string? ErrorMessage { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Status information about the current certificate configuration.
|
|
/// </summary>
|
|
public class CertificateStatusInfo
|
|
{
|
|
/// <summary>
|
|
/// The CA certificate, if available.
|
|
/// </summary>
|
|
public X509Certificate2? CACert { get; set; }
|
|
|
|
/// <summary>
|
|
/// The server certificate (with private key), if available.
|
|
/// </summary>
|
|
public X509Certificate2? ServerCert { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the certificate was autogenerated.
|
|
/// </summary>
|
|
public bool IsAutogenerated { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the CA certificate is installed in the system trust store.
|
|
/// </summary>
|
|
public bool IsCATrusted { get; set; }
|
|
|
|
/// <summary>
|
|
/// Days until the CA certificate expires, or null if no CA certificate.
|
|
/// </summary>
|
|
public double? CaDaysUntilExpiry { get; set; }
|
|
|
|
/// <summary>
|
|
/// Days until the server certificate expires, or null if no server certificate.
|
|
/// </summary>
|
|
public double? ServerDaysUntilExpiry { get; set; }
|
|
|
|
/// <summary>
|
|
/// DNS names from the server certificate's Subject Alternative Names.
|
|
/// </summary>
|
|
public List<string> DnsNames { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// IP addresses from the server certificate's Subject Alternative Names.
|
|
/// </summary>
|
|
public List<string> IpAddresses { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Whether database field encryption is enabled.
|
|
/// </summary>
|
|
public bool IsEncryptingFields { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class for certificate configuration operations.
|
|
/// Provides high-level operations for generating, renewing, and managing HTTPS certificates.
|
|
/// </summary>
|
|
public static class CertificateConfigurationHelper
|
|
{
|
|
/// <summary>
|
|
/// The log tag for messages from this class.
|
|
/// </summary>
|
|
private static readonly string LOGTAG = Log.LogTagFromType(typeof(CertificateConfigurationHelper));
|
|
|
|
/// <summary>
|
|
/// Installs a CA certificate in the system trust store.
|
|
/// </summary>
|
|
/// <param name="caCert">The CA certificate to install.</param>
|
|
/// <param name="storeLocation">The store location to use (Windows only). If null, auto-detects based on privileges.</param>
|
|
/// <param name="linuxCertDirectory">The certificate directory to use (Linux only). If null, uses the default system location.</param>
|
|
/// <param name="macOSKeychainPath">The keychain path to use (macOS only). If null, uses the default login keychain.</param>
|
|
/// <returns>The result of the installation attempt.</returns>
|
|
public static CATrustInstallationResult InstallCATrust(X509Certificate2 caCert, StoreLocation? storeLocation, string? linuxCertDirectory, string? macOSKeychainPath)
|
|
{
|
|
var installer = CATrustInstallerFactory.CreateInstaller(storeLocation, linuxCertDirectory, macOSKeychainPath);
|
|
if (installer == null)
|
|
{
|
|
return new CATrustInstallationResult
|
|
{
|
|
Status = CATrustInstallationStatus.NotSupported,
|
|
ErrorMessage = "No trust installer available for this platform."
|
|
};
|
|
}
|
|
|
|
var result = installer.Install(caCert);
|
|
return new CATrustInstallationResult
|
|
{
|
|
Status = result switch
|
|
{
|
|
TrustInstallationResult.Success => CATrustInstallationStatus.Success,
|
|
TrustInstallationResult.AlreadyInstalled => CATrustInstallationStatus.AlreadyInstalled,
|
|
TrustInstallationResult.RequiresElevation => CATrustInstallationStatus.RequiresElevation,
|
|
_ => CATrustInstallationStatus.Failed
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a CA certificate is installed in the system trust store.
|
|
/// </summary>
|
|
/// <param name="caCert">The CA certificate to check.</param>
|
|
/// <param name="storeLocation">The store location to use (Windows only). If null, auto-detects based on privileges.</param>
|
|
/// <param name="linuxCertDirectory">The certificate directory to use (Linux only). If null, uses the default system location.</param>
|
|
/// <param name="macOSKeychainPath">The keychain path to use (macOS only). If null, uses the default login keychain.</param>
|
|
/// <returns>True if the certificate is installed; false otherwise.</returns>
|
|
public static bool IsCATrustInstalled(X509Certificate2 caCert, StoreLocation? storeLocation, string? linuxCertDirectory, string? macOSKeychainPath)
|
|
{
|
|
var installer = CATrustInstallerFactory.CreateInstaller(storeLocation, linuxCertDirectory, macOSKeychainPath);
|
|
return installer != null && installer.IsInstalled(caCert);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a CA certificate from the system trust store.
|
|
/// </summary>
|
|
/// <param name="caCert">The CA certificate to remove.</param>
|
|
/// <param name="storeLocation">The store location to use (Windows only). If null, auto-detects based on privileges.</param>
|
|
/// <param name="linuxCertDirectory">The certificate directory to use (Linux only). If null, uses the default system location.</param>
|
|
/// <param name="macOSKeychainPath">The keychain path to use (macOS only). If null, uses the default login keychain.</param>
|
|
/// <returns>True if removal was successful or certificate was not installed; false otherwise.</returns>
|
|
public static bool RemoveCATrust(X509Certificate2 caCert, StoreLocation? storeLocation, string? linuxCertDirectory, string? macOSKeychainPath)
|
|
{
|
|
var installer = CATrustInstallerFactory.CreateInstaller(storeLocation, linuxCertDirectory, macOSKeychainPath);
|
|
if (installer == null)
|
|
return false;
|
|
|
|
var result = installer.Uninstall(caCert);
|
|
return result is TrustUninstallationResult.Success or TrustUninstallationResult.NotInstalled;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures HTTPS certificates are available and valid.
|
|
/// If valid certificates already exist, returns success without changes.
|
|
/// If a CA exists but the server certificate is expiring, renews the server certificate using the existing CA.
|
|
/// If no CA exists, generates new CA and server certificates.
|
|
/// </summary>
|
|
/// <param name="existingIsAutogenerated">Whether existing certificate was autogenerated.</param>
|
|
/// <param name="existingServerCert">The existing server certificate collection.</param>
|
|
/// <param name="existingCaData">The existing CA certificate data.</param>
|
|
/// <param name="hostnames">Optional comma-separated list of hostnames. If null, auto-detected hostnames are used.</param>
|
|
/// <param name="skipTrustInstallation">If true, skips installing the CA in the system trust store.</param>
|
|
/// <param name="storeLocation">The store location to use for trust installation (Windows only).</param>
|
|
/// <param name="linuxCertDirectory">The certificate directory to use for trust installation (Linux only).</param>
|
|
/// <param name="macOSKeychainPath">The keychain path to use for trust installation (macOS only).</param>
|
|
/// <returns>A result object containing success status, certificate data, and trust installation status.</returns>
|
|
public static CertificateGenerationResult EnsureCertificates(
|
|
bool existingIsAutogenerated,
|
|
X509Certificate2Collection? existingServerCert,
|
|
CACertificateData? existingCaData,
|
|
string? hostnames = null,
|
|
bool skipTrustInstallation = false,
|
|
StoreLocation? storeLocation = null,
|
|
string? linuxCertDirectory = null,
|
|
string? macOSKeychainPath = null)
|
|
{
|
|
var result = new CertificateGenerationResult();
|
|
|
|
try
|
|
{
|
|
// Check if valid certificates already exist
|
|
if (CertificateRenewalChecker.HasValidCertificates(existingIsAutogenerated, existingServerCert, existingCaData))
|
|
{
|
|
var serverCertWithKey = existingServerCert!.Cast<X509Certificate2>().FirstOrDefault(c => c.HasPrivateKey);
|
|
var daysUntilExpiry = (serverCertWithKey!.NotAfter - DateTime.UtcNow).TotalDays;
|
|
Log.WriteInformationMessage(LOGTAG, "CertificatesExist", $"Valid certificates already exist (expires in {daysUntilExpiry:F0} days).");
|
|
result.Success = true;
|
|
return result;
|
|
}
|
|
|
|
// If a CA already exists, try to renew the server certificate using the existing CA
|
|
if (existingCaData != null)
|
|
{
|
|
Log.WriteInformationMessage(LOGTAG, "ExistingCAFound", "Existing CA found, attempting to renew server certificate...");
|
|
var renewResult = RenewServerCertificate(existingCaData, hostnames);
|
|
|
|
if (renewResult.Renewed && renewResult.RenewedCertificate != null)
|
|
{
|
|
Log.WriteInformationMessage(LOGTAG, "ServerCertRenewed", "Server certificate renewed using existing CA.");
|
|
result.Success = true;
|
|
result.ServerCertificate = renewResult.RenewedCertificate;
|
|
return result;
|
|
}
|
|
|
|
// If renewal failed (e.g., CA expired), fall through to generate new CA + server cert
|
|
Log.WriteWarningMessage(LOGTAG, "RenewalFailed", null, $"Could not renew with existing CA: {renewResult.RenewalFailedReason}. Generating new CA and server certificate...");
|
|
}
|
|
|
|
// No valid CA exists or renewal failed — generate new CA and server certificates
|
|
return GenerateCertificatesCore(hostnames, skipTrustInstallation, storeLocation, linuxCertDirectory, macOSKeychainPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteErrorMessage(LOGTAG, "EnsureCertificatesFailed", ex, $"Failed to ensure HTTPS certificates: {ex.Message}");
|
|
result.Success = false;
|
|
result.ErrorMessage = ex.Message;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates new CA and server certificates for HTTPS.
|
|
/// If valid certificates already exist, returns success without generating new ones.
|
|
/// </summary>
|
|
/// <param name="existingIsAutogenerated">Whether existing certificate was autogenerated.</param>
|
|
/// <param name="existingServerCert">The existing server certificate collection.</param>
|
|
/// <param name="existingCaData">The existing CA certificate data.</param>
|
|
/// <param name="hostnames">Optional comma-separated list of hostnames. If null, auto-detected hostnames are used.</param>
|
|
/// <param name="skipTrustInstallation">If true, skips installing the CA in the system trust store.</param>
|
|
/// <param name="storeLocation">The store location to use for trust installation (Windows only).</param>
|
|
/// <param name="linuxCertDirectory">The certificate directory to use for trust installation (Linux only).</param>
|
|
/// <param name="macOSKeychainPath">The keychain path to use for trust installation (macOS only).</param>
|
|
/// <returns>A result object containing success status, certificate data, and trust installation status.</returns>
|
|
public static CertificateGenerationResult GenerateCertificates(
|
|
bool existingIsAutogenerated,
|
|
X509Certificate2Collection? existingServerCert,
|
|
CACertificateData? existingCaData,
|
|
string? hostnames = null,
|
|
bool skipTrustInstallation = false,
|
|
StoreLocation? storeLocation = null,
|
|
string? linuxCertDirectory = null,
|
|
string? macOSKeychainPath = null)
|
|
{
|
|
var result = new CertificateGenerationResult();
|
|
|
|
try
|
|
{
|
|
// Check if valid certificates already exist
|
|
if (CertificateRenewalChecker.HasValidCertificates(existingIsAutogenerated, existingServerCert, existingCaData))
|
|
{
|
|
var serverCertWithKey = existingServerCert!.Cast<X509Certificate2>().FirstOrDefault(c => c.HasPrivateKey);
|
|
var daysUntilExpiry = (serverCertWithKey!.NotAfter - DateTime.UtcNow).TotalDays;
|
|
Log.WriteInformationMessage(LOGTAG, "CertificatesExist", $"Valid certificates already exist (expires in {daysUntilExpiry:F0} days).");
|
|
result.Success = true;
|
|
return result;
|
|
}
|
|
|
|
// Generate new CA and server certificates
|
|
return GenerateCertificatesCore(hostnames, skipTrustInstallation, storeLocation, linuxCertDirectory, macOSKeychainPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteErrorMessage(LOGTAG, "CertificateGenerationFailed", ex, $"Failed to generate HTTPS certificates: {ex.Message}");
|
|
result.Success = false;
|
|
result.ErrorMessage = ex.Message;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regenerates the CA and server certificates, optionally removing the old CA from the trust store first.
|
|
/// Unlike <see cref="GenerateCertificates"/>, this always generates new certificates regardless of existing validity.
|
|
/// </summary>
|
|
/// <param name="existingCaCertBase64">The existing CA certificate in Base64 format, for removal from trust store. Can be null.</param>
|
|
/// <param name="hostnames">Optional comma-separated list of hostnames. If null, auto-detected hostnames are used.</param>
|
|
/// <param name="skipTrustInstallation">If true, skips installing the CA in the system trust store.</param>
|
|
/// <param name="storeLocation">The store location to use for trust installation (Windows only).</param>
|
|
/// <param name="linuxCertDirectory">The certificate directory to use for trust installation (Linux only).</param>
|
|
/// <param name="macOSKeychainPath">The keychain path to use for trust installation (macOS only).</param>
|
|
/// <returns>A result object containing success status, certificate data, trust installation status, and old CA removal status.</returns>
|
|
public static CertificateGenerationResult RegenerateCACertificates(
|
|
string? existingCaCertBase64,
|
|
string? hostnames = null,
|
|
bool skipTrustInstallation = false,
|
|
StoreLocation? storeLocation = null,
|
|
string? linuxCertDirectory = null,
|
|
string? macOSKeychainPath = null)
|
|
{
|
|
var result = new CertificateGenerationResult();
|
|
|
|
try
|
|
{
|
|
// Remove old CA from trust store if it exists
|
|
if (!string.IsNullOrWhiteSpace(existingCaCertBase64) && !skipTrustInstallation)
|
|
{
|
|
try
|
|
{
|
|
var existingCaCert = CertificateStorageHelper.DeserializeCertificate(existingCaCertBase64);
|
|
Log.WriteInformationMessage(LOGTAG, "RemovingOldCA", "Removing old CA certificate from trust store...");
|
|
|
|
if (IsCATrustInstalled(existingCaCert, storeLocation, linuxCertDirectory, macOSKeychainPath))
|
|
{
|
|
if (RemoveCATrust(existingCaCert, storeLocation, linuxCertDirectory, macOSKeychainPath))
|
|
Log.WriteInformationMessage(LOGTAG, "OldCARemoved", "Old CA certificate removed from trust store.");
|
|
else
|
|
Log.WriteWarningMessage(LOGTAG, "OldCARemovalFailed", null, "Failed to remove old CA certificate from trust store.");
|
|
}
|
|
else
|
|
{
|
|
Log.WriteInformationMessage(LOGTAG, "OldCANotInTrustStore", "Old CA certificate was not in trust store.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "OldCARemovalError", ex, $"Could not remove old CA from trust store: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Generate new CA and server certificates (always, no validity check)
|
|
return GenerateCertificatesCore(hostnames, skipTrustInstallation, storeLocation, linuxCertDirectory, macOSKeychainPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteErrorMessage(LOGTAG, "CertificateRegenerationFailed", ex, $"Failed to regenerate HTTPS certificates: {ex.Message}");
|
|
result.Success = false;
|
|
result.ErrorMessage = ex.Message;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renews the server certificate using an existing CA certificate.
|
|
/// This always generates a new server certificate regardless of the current certificate's validity.
|
|
/// </summary>
|
|
/// <param name="caData">The CA certificate data to use for signing the new server certificate.</param>
|
|
/// <param name="hostnames">Optional comma-separated list of hostnames. If null, auto-detected hostnames are used.</param>
|
|
/// <returns>A result object containing renewal status and new certificate data if renewed.</returns>
|
|
public static CertificateRenewalResult RenewServerCertificate(CACertificateData caData, string? hostnames = null)
|
|
{
|
|
var result = new CertificateRenewalResult();
|
|
|
|
try
|
|
{
|
|
// Deserialize CA certificate pair
|
|
CACertificatePair caPair;
|
|
try
|
|
{
|
|
caPair = CertificateStorageHelper.DeserializeCACertificatePair(
|
|
caData.CACertificate,
|
|
caData.CAKey,
|
|
caData.CAPassword);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteErrorMessage(LOGTAG, "CADeserializationFailed", ex, "Failed to deserialize CA certificate for renewal.");
|
|
result.RenewalFailedReason = $"Failed to deserialize CA certificate: {ex.Message}";
|
|
return result;
|
|
}
|
|
|
|
try
|
|
{
|
|
// 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();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Dispose CA certificate key material promptly
|
|
caPair.PrivateKey.Dispose();
|
|
caPair.Certificate.Dispose();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteErrorMessage(LOGTAG, "RenewalFailed", ex, $"Failed to renew SSL certificate: {ex.Message}");
|
|
result.RenewalFailedReason = ex.Message;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current certificate status information.
|
|
/// </summary>
|
|
/// <param name="caCertBase64">The CA certificate in Base64 format.</param>
|
|
/// <param name="serverCertCollection">The server certificate collection.</param>
|
|
/// <param name="isAutogenerated">Whether the certificate was autogenerated.</param>
|
|
/// <param name="storeLocation">The store location to check (Windows only).</param>
|
|
/// <param name="linuxCertDirectory">The certificate directory to check (Linux only).</param>
|
|
/// <param name="macOSKeychainPath">The keychain path to check (macOS only).</param>
|
|
/// <returns>A status info object with certificate details.</returns>
|
|
public static CertificateStatusInfo GetCertificateStatus(
|
|
string? caCertBase64,
|
|
X509Certificate2Collection? serverCertCollection,
|
|
bool isAutogenerated,
|
|
StoreLocation? storeLocation = null,
|
|
string? linuxCertDirectory = null,
|
|
string? macOSKeychainPath = null)
|
|
{
|
|
var status = new CertificateStatusInfo
|
|
{
|
|
IsAutogenerated = isAutogenerated
|
|
};
|
|
|
|
// Parse CA certificate
|
|
if (!string.IsNullOrWhiteSpace(caCertBase64))
|
|
{
|
|
try
|
|
{
|
|
var caCert = CertificateStorageHelper.DeserializeCertificate(caCertBase64);
|
|
status.CACert = caCert;
|
|
status.CaDaysUntilExpiry = (caCert.NotAfter - DateTime.Now).TotalDays;
|
|
status.IsCATrusted = IsCATrustInstalled(caCert, storeLocation, linuxCertDirectory, macOSKeychainPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "CAStatusError", ex, $"Error reading CA certificate: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Parse server certificate
|
|
if (serverCertCollection != null)
|
|
{
|
|
try
|
|
{
|
|
var serverCert = serverCertCollection.Cast<X509Certificate2>().FirstOrDefault(c => c.HasPrivateKey);
|
|
if (serverCert != null)
|
|
{
|
|
status.ServerCert = serverCert;
|
|
status.ServerDaysUntilExpiry = (serverCert.NotAfter - DateTime.Now).TotalDays;
|
|
|
|
// Extract SANs
|
|
var sanExtension = serverCert.Extensions["2.5.29.17"];
|
|
if (sanExtension is X509SubjectAlternativeNameExtension sanExt)
|
|
{
|
|
status.DnsNames = sanExt.EnumerateDnsNames().ToList();
|
|
status.IpAddresses = sanExt.EnumerateIPAddresses().Select(ip => ip.ToString()).ToList();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "ServerCertStatusError", ex, $"Error reading server certificate: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Core certificate generation logic shared by <see cref="GenerateCertificates"/> and <see cref="RegenerateCACertificates"/>.
|
|
/// </summary>
|
|
private static CertificateGenerationResult GenerateCertificatesCore(
|
|
string? hostnames,
|
|
bool skipTrustInstallation,
|
|
StoreLocation? storeLocation,
|
|
string? linuxCertDirectory,
|
|
string? macOSKeychainPath)
|
|
{
|
|
var result = new CertificateGenerationResult();
|
|
|
|
// Parse hostnames
|
|
var hostnameList = CertificateRenewalChecker.ParseHostnames(hostnames);
|
|
Log.WriteInformationMessage(LOGTAG, "GeneratingCertificates", $"Generating HTTPS certificates with hostnames: {string.Join(", ", hostnameList)}");
|
|
|
|
// Generate CA certificate
|
|
Log.WriteInformationMessage(LOGTAG, "GeneratingCA", "Generating CA certificate...");
|
|
var caPair = CertificateGenerator.GenerateCACertificate();
|
|
|
|
try
|
|
{
|
|
// Generate server certificate
|
|
Log.WriteInformationMessage(LOGTAG, "GeneratingServerCert", "Generating server certificate...");
|
|
var serverPair = CertificateGenerator.GenerateServerCertificate(caPair.Certificate, caPair.PrivateKey, hostnameList);
|
|
|
|
try
|
|
{
|
|
// 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();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Dispose CA certificate key material promptly
|
|
caPair.PrivateKey.Dispose();
|
|
caPair.Certificate.Dispose();
|
|
}
|
|
}
|
|
}
|