747 lines
31 KiB
C#
747 lines
31 KiB
C#
// Copyright (C) 2026, 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.CommandLine;
|
|
using System.CommandLine.NamingConventionBinder;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Duplicati.Library.AutoUpdater;
|
|
using Duplicati.Library.Certificates;
|
|
using Duplicati.Library.Certificates.Platform;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Library.Logging;
|
|
using Duplicati.Server.Database;
|
|
using Duplicati.WebserverCore.Abstractions;
|
|
using ServerSettings = Duplicati.Server.Database.ServerSettings;
|
|
|
|
namespace Duplicati.CommandLine.ConfigureTool.Commands;
|
|
|
|
/// <summary>
|
|
/// Commands for managing HTTPS certificates.
|
|
/// </summary>
|
|
public static class HttpsCommand
|
|
{
|
|
/// <summary>
|
|
/// Adds platform-specific CA options to the command.
|
|
/// </summary>
|
|
/// <param name="cmd">The command to add options to.</param>
|
|
/// <returns>The command with added options.</returns>
|
|
private static Command AddPlatformSpecificCAOptions(Command cmd)
|
|
{
|
|
// Add platform-specific options
|
|
if (OperatingSystem.IsWindows())
|
|
cmd.AddOption(new Option<string>("--store", getDefaultValue: () => OperatingSystem.IsWindows() ? CATrustInstallerFactory.GetDefaultWindowsStoreLocation() == StoreLocation.LocalMachine ? "local" : "user" : "", description: "Certificate store location (local|user). Defaults to 'local' if admin, otherwise 'user')"));
|
|
else if (OperatingSystem.IsLinux())
|
|
cmd.AddOption(new Option<string>("--cert-dir", getDefaultValue: () => OperatingSystem.IsLinux() ? LinuxCATrustInstaller.DEFAULT_CERT_DIR : "", description: "Custom certificate directory for installing CA certificate"));
|
|
else if (OperatingSystem.IsMacOS())
|
|
cmd.AddOption(new Option<string>("--keychain", getDefaultValue: () => OperatingSystem.IsMacOS() ? MacOSCATrustInstaller.DEFAULT_KEYCHAIN_PATH : "", description: "Custom keychain path for installing CA certificate"));
|
|
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds common database options to the command.
|
|
/// </summary>
|
|
/// <param name="cmd">The command to add options to.</param>
|
|
/// <returns>The command</returns>
|
|
private static Command AddDatabaseOptions(Command cmd)
|
|
{
|
|
cmd.AddOption(new Option<string>("--data-folder", "Path to the Duplicati data folder (defaults to standard location)"));
|
|
cmd.AddOption(new Option<string>("--settings-encryption-key", "Settings encryption key for the database (if settings are encrypted)"));
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the 'generate' command.
|
|
/// </summary>
|
|
public static Command CreateGenerateCommand()
|
|
{
|
|
var cmd = new Command("generate", "Generate a new CA and server certificate for HTTPS")
|
|
{
|
|
new Option<string>("--hostnames", "Comma-separated list of hostnames to include in the certificate (defaults to auto-detected hostnames)"),
|
|
new Option<bool>("--no-trust", "Skip installing the CA certificate in the system trust store"),
|
|
new Option<bool>("--auto-create-database", "Create the database if it does not exist"),
|
|
};
|
|
|
|
AddDatabaseOptions(cmd);
|
|
AddPlatformSpecificCAOptions(cmd);
|
|
cmd.Handler = CommandHandler.Create<string?, bool, string?, string?, bool, string?, string?, string?>(HandleGenerate);
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the 'renew' command.
|
|
/// </summary>
|
|
public static Command CreateRenewCommand()
|
|
{
|
|
var cmd = new Command("renew", "Renew the server certificate using the existing CA");
|
|
|
|
AddDatabaseOptions(cmd);
|
|
cmd.Handler = CommandHandler.Create<string?, string?>(HandleRenew);
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the 'regenerate-ca' command.
|
|
/// </summary>
|
|
public static Command CreateRegenerateCaCommand()
|
|
{
|
|
var cmd = new Command("regenerate-ca", "Regenerate the CA and server certificate (removes old CA from trust store)")
|
|
{
|
|
new Option<string>("--hostnames", "Comma-separated list of hostnames to include in the certificate (defaults to auto-detected hostnames)"),
|
|
new Option<bool>("--no-trust", "Skip installing the CA certificate in the system trust store"),
|
|
};
|
|
|
|
AddDatabaseOptions(cmd);
|
|
AddPlatformSpecificCAOptions(cmd);
|
|
cmd.Handler = CommandHandler.Create<string?, bool, string?, string?, string?, string?, string?>(HandleRegenerateCa);
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the 'remove' command.
|
|
/// </summary>
|
|
public static Command CreateRemoveCommand()
|
|
{
|
|
var cmd = new Command("remove", "Remove the CA from trust store and delete certificates from database");
|
|
|
|
AddDatabaseOptions(cmd);
|
|
AddPlatformSpecificCAOptions(cmd);
|
|
cmd.Handler = CommandHandler.Create<string?, string?, string?, string?, string?>(HandleRemove);
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the 'show' command.
|
|
/// </summary>
|
|
public static Command CreateShowCommand()
|
|
{
|
|
var cmd = new Command("show", "Display current certificate status");
|
|
|
|
AddDatabaseOptions(cmd);
|
|
AddPlatformSpecificCAOptions(cmd);
|
|
cmd.Handler = CommandHandler.Create<string?, string?, string?, string?, string?>(HandleShow);
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the 'export' command.
|
|
/// </summary>
|
|
public static Command CreateExportCommand()
|
|
{
|
|
var cmd = new Command("export", "Export the server certificate (public key only) to a file")
|
|
{
|
|
new Option<string>("--file", "Output file path (defaults to duplicati-server.crt in current directory)"),
|
|
};
|
|
|
|
AddDatabaseOptions(cmd);
|
|
cmd.Handler = CommandHandler.Create<string?, string?, string?>(HandleExport);
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the 'export-ca' command.
|
|
/// </summary>
|
|
public static Command CreateExportCaCommand()
|
|
{
|
|
var cmd = new Command("export-ca", "Export the CA certificate (public key only) to a file")
|
|
{
|
|
new Option<string>("--file", "Output file path (defaults to duplicati-ca.crt in current directory)"),
|
|
};
|
|
|
|
AddDatabaseOptions(cmd);
|
|
cmd.Handler = CommandHandler.Create<string?, string?, string?>(HandleExportCa);
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the data folder path, either from the option or using the default.
|
|
/// </summary>
|
|
private static string GetDataFolder(string? dataFolderOption)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(dataFolderOption))
|
|
return Path.GetFullPath(dataFolderOption);
|
|
|
|
return DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ProbeOnly);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the database path for the given data folder.
|
|
/// </summary>
|
|
private static string GetDatabasePath(string dataFolder)
|
|
=> Path.Combine(dataFolder, DataFolderManager.SERVER_DATABASE_FILENAME);
|
|
|
|
/// <summary>
|
|
/// Opens a connection to the server database.
|
|
/// </summary>
|
|
private static Connection OpenDatabase(string dataFolder, string? settingsEncryptionKey, bool autoCreateDatabase)
|
|
{
|
|
var databasePath = GetDatabasePath(dataFolder);
|
|
|
|
if (!File.Exists(databasePath) && !autoCreateDatabase)
|
|
throw new UserInformationException($"Database not found: {databasePath}", "DatabaseNotFound");
|
|
|
|
var opts = new Dictionary<string, string>();
|
|
|
|
// Add settings encryption key if provided
|
|
if (!string.IsNullOrWhiteSpace(settingsEncryptionKey))
|
|
opts["settings-encryption-key"] = settingsEncryptionKey;
|
|
|
|
// Create application settings with the specified data folder
|
|
var appSettings = new DataFolderApplicationSettings(dataFolder);
|
|
|
|
return Server.Program.GetDatabaseConnection(appSettings, opts, true, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Application settings implementation that uses a specific data folder.
|
|
/// </summary>
|
|
private class DataFolderApplicationSettings : IApplicationSettings
|
|
{
|
|
private readonly CancellationTokenSource _applicationExitEvent = new();
|
|
|
|
public DataFolderApplicationSettings(string dataFolder)
|
|
{
|
|
DataFolder = dataFolder;
|
|
}
|
|
|
|
public bool SettingsEncryptionKeyProvidedExternally { get; set; }
|
|
public Action? StartOrStopUsageReporter { get; set; }
|
|
public string DataFolder { get; }
|
|
public string Origin { get; set; } = "ConfigureTool";
|
|
public CancellationToken ApplicationExit => _applicationExitEvent.Token;
|
|
public ISecretProvider? SecretProvider { get; set; }
|
|
|
|
public void SignalApplicationExit() => _applicationExitEvent.Cancel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the store location option and returns the appropriate StoreLocation value.
|
|
/// </summary>
|
|
/// <param name="storeOption">The store option string ("local", "user", or null).</param>
|
|
/// <returns>The StoreLocation value, or null to use auto-detection.</returns>
|
|
private static StoreLocation? ParseStoreLocation(string? storeOption)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(storeOption))
|
|
return null; // Auto-detect
|
|
|
|
return storeOption.ToLowerInvariant() switch
|
|
{
|
|
"local" or "machine" or "localmachine" => StoreLocation.LocalMachine,
|
|
"user" or "currentuser" => StoreLocation.CurrentUser,
|
|
_ => null // Invalid value, will use auto-detection
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads CA certificate data from the database connection.
|
|
/// </summary>
|
|
/// <param name="connection">The database connection.</param>
|
|
/// <returns>The CA certificate data, or null if not available.</returns>
|
|
private static CACertificateData? ReadCaData(Connection connection)
|
|
{
|
|
var caCert = connection.ApplicationSettings.ServerCACertificate;
|
|
var caKey = connection.ApplicationSettings.ServerCACertificateKey;
|
|
var caPassword = connection.ApplicationSettings.ServerCACertificatePassword;
|
|
|
|
if (string.IsNullOrWhiteSpace(caCert) || string.IsNullOrWhiteSpace(caKey) || string.IsNullOrWhiteSpace(caPassword))
|
|
return null;
|
|
|
|
return new CACertificateData
|
|
{
|
|
CACertificate = caCert,
|
|
CAKey = caKey,
|
|
CAPassword = caPassword
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores generated CA and server certificates in the database.
|
|
/// </summary>
|
|
/// <param name="connection">The database connection.</param>
|
|
/// <param name="result">The certificate generation result.</param>
|
|
private static void StoreGeneratedCertificates(Connection connection, CertificateGenerationResult result)
|
|
{
|
|
var settings = new Dictionary<string, string?>
|
|
{
|
|
[ServerSettings.CONST.SERVER_CA_CERTIFICATE] = result.CACertificate!.CACertificate,
|
|
[ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = result.CACertificate.CAKey,
|
|
[ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = result.CACertificate.CAPassword,
|
|
[ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = result.ServerCertificate!.ServerCertificate,
|
|
[ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = result.ServerCertificate.Password,
|
|
[ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true"
|
|
};
|
|
|
|
connection.ApplicationSettings.UpdateSettings(settings, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores a renewed server certificate in the database.
|
|
/// </summary>
|
|
/// <param name="connection">The database connection.</param>
|
|
/// <param name="result">The certificate renewal result.</param>
|
|
private static void StoreRenewedCertificate(Connection connection, CertificateRenewalResult result)
|
|
{
|
|
var settings = new Dictionary<string, string?>
|
|
{
|
|
[ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = result.RenewedCertificate!.ServerCertificate,
|
|
[ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = result.RenewedCertificate.Password,
|
|
[ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true"
|
|
};
|
|
|
|
connection.ApplicationSettings.UpdateSettings(settings, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prints the trust installation status to the console.
|
|
/// </summary>
|
|
/// <param name="status">The trust installation status.</param>
|
|
/// <returns>True if the status indicates a fatal error that should abort the operation.</returns>
|
|
private static bool PrintTrustInstallationStatus(CATrustInstallationStatus? status)
|
|
{
|
|
switch (status)
|
|
{
|
|
case CATrustInstallationStatus.Success:
|
|
Console.WriteLine("CA certificate installed successfully.");
|
|
return false;
|
|
case CATrustInstallationStatus.AlreadyInstalled:
|
|
Console.WriteLine("CA certificate was already installed.");
|
|
return false;
|
|
case CATrustInstallationStatus.NotSupported:
|
|
Console.WriteLine("Warning: No trust installer available for this platform.");
|
|
return false;
|
|
case CATrustInstallationStatus.RequiresElevation:
|
|
Console.WriteLine("Error: Administrator/root privileges required to install CA certificate.");
|
|
Console.WriteLine("Run with elevated permissions or use --no-trust to skip CA installation.");
|
|
return true;
|
|
case CATrustInstallationStatus.Failed:
|
|
Console.WriteLine("Error: Failed to install CA certificate.");
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prints a database encryption warning if encryption is not enabled.
|
|
/// </summary>
|
|
/// <param name="connection">The database connection.</param>
|
|
private static void PrintEncryptionWarning(Connection connection)
|
|
{
|
|
if (!connection.IsEncryptingFields)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("WARNING: Database field encryption is not enabled.");
|
|
Console.WriteLine("Since a generated CA is stored in the database this can enable an attacker to issues certificates and enable a man-in-the-middle attack on all HTTPS connections.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Captures log messages generated in the library and forwards them to the console
|
|
/// </summary>
|
|
/// <returns>A disposable log scope</returns>
|
|
private static IDisposable StartConsoleLogScope()
|
|
=> Log.StartScope(entry =>
|
|
{
|
|
if (entry.Level == LogMessageType.Information)
|
|
Console.WriteLine(entry.FormattedMessage);
|
|
else
|
|
Console.WriteLine($"{entry.Level}: {entry.FormattedMessage}");
|
|
}, entry => entry.Level >= LogMessageType.Information);
|
|
|
|
/// <summary>
|
|
/// Handles the 'generate' command.
|
|
/// Delegates certificate generation to <see cref="CertificateConfigurationHelper.GenerateCertificates"/>.
|
|
/// </summary>
|
|
private static int HandleGenerate(string? hostnames, bool noTrust, string? dataFolder, string? settingsEncryptionKey, bool autoCreateDatabase, string? store, string? certDir, string? keychain)
|
|
{
|
|
var storeLocation = ParseStoreLocation(store);
|
|
var dataFolderPath = GetDataFolder(dataFolder);
|
|
|
|
using var _ = StartConsoleLogScope();
|
|
Console.WriteLine($"Using data folder: {dataFolderPath}");
|
|
|
|
using var connection = OpenDatabase(dataFolderPath, settingsEncryptionKey, autoCreateDatabase);
|
|
|
|
var result = CertificateConfigurationHelper.GenerateCertificates(
|
|
connection.ApplicationSettings.ServerSSLCertificateAutogenerated,
|
|
connection.ApplicationSettings.ServerSSLCertificate,
|
|
ReadCaData(connection),
|
|
hostnames,
|
|
noTrust,
|
|
storeLocation,
|
|
certDir,
|
|
keychain);
|
|
|
|
if (!result.Success)
|
|
{
|
|
Console.WriteLine($"Error: Failed to generate HTTPS certificates.");
|
|
if (!string.IsNullOrWhiteSpace(result.ErrorMessage))
|
|
Console.WriteLine($"Reason: {result.ErrorMessage}");
|
|
return 1;
|
|
}
|
|
|
|
// Check if certificates were already valid (no new certs generated)
|
|
if (result.CACertificate == null || result.ServerCertificate == null)
|
|
{
|
|
Console.WriteLine("Valid certificates already exist.");
|
|
Console.WriteLine("Use 'regenerate-ca' to force regeneration or 'renew' to renew the server certificate.");
|
|
return 0;
|
|
}
|
|
|
|
// Check trust installation status
|
|
if (PrintTrustInstallationStatus(result.TrustInstallationStatus))
|
|
return 1;
|
|
|
|
// Store in database
|
|
Console.WriteLine("Storing certificates in database...");
|
|
StoreGeneratedCertificates(connection, result);
|
|
|
|
PrintEncryptionWarning(connection);
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("HTTPS certificates generated and stored successfully.");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'renew' command.
|
|
/// Delegates certificate renewal to <see cref="CertificateConfigurationHelper.RenewServerCertificate"/>.
|
|
/// </summary>
|
|
private static int HandleRenew(string? dataFolder, string? settingsEncryptionKey)
|
|
{
|
|
var dataFolderPath = GetDataFolder(dataFolder);
|
|
|
|
Console.WriteLine($"Using data folder: {dataFolderPath}");
|
|
using var _ = StartConsoleLogScope();
|
|
|
|
using var connection = OpenDatabase(dataFolderPath, settingsEncryptionKey, false);
|
|
|
|
// Read existing CA data
|
|
var caData = ReadCaData(connection);
|
|
if (caData == null)
|
|
throw new UserInformationException("No existing CA certificate found in database. Use 'generate' to create new certificates.", "CANotFound");
|
|
|
|
Console.WriteLine("Renewing server certificate...");
|
|
|
|
var result = CertificateConfigurationHelper.RenewServerCertificate(caData);
|
|
|
|
if (!result.Renewed)
|
|
{
|
|
Console.WriteLine($"Error: Failed to renew server certificate.");
|
|
if (!string.IsNullOrWhiteSpace(result.RenewalFailedReason))
|
|
Console.WriteLine($"Reason: {result.RenewalFailedReason}");
|
|
return 1;
|
|
}
|
|
|
|
// Store new server certificate in database
|
|
Console.WriteLine("Storing new server certificate in database...");
|
|
StoreRenewedCertificate(connection, result);
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("Server certificate renewed successfully.");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'regenerate-ca' command.
|
|
/// Delegates to <see cref="CertificateConfigurationHelper.RegenerateCACertificates"/>.
|
|
/// </summary>
|
|
private static int HandleRegenerateCa(string? hostnames, bool noTrust, string? dataFolder, string? settingsEncryptionKey, string? store, string? certDir, string? keychain)
|
|
{
|
|
var storeLocation = ParseStoreLocation(store);
|
|
var dataFolderPath = GetDataFolder(dataFolder);
|
|
|
|
Console.WriteLine($"Using data folder: {dataFolderPath}");
|
|
using var _ = StartConsoleLogScope();
|
|
|
|
using var connection = OpenDatabase(dataFolderPath, settingsEncryptionKey, false);
|
|
|
|
var existingCaCertBase64 = connection.ApplicationSettings.ServerCACertificate;
|
|
|
|
var result = CertificateConfigurationHelper.RegenerateCACertificates(
|
|
existingCaCertBase64,
|
|
hostnames,
|
|
noTrust,
|
|
storeLocation,
|
|
certDir,
|
|
keychain);
|
|
|
|
if (!result.Success)
|
|
{
|
|
Console.WriteLine($"Error: Failed to regenerate HTTPS certificates.");
|
|
if (!string.IsNullOrWhiteSpace(result.ErrorMessage))
|
|
Console.WriteLine($"Reason: {result.ErrorMessage}");
|
|
return 1;
|
|
}
|
|
|
|
// Check trust installation status
|
|
if (PrintTrustInstallationStatus(result.TrustInstallationStatus))
|
|
return 1;
|
|
|
|
// Store in database
|
|
Console.WriteLine("Storing certificates in database...");
|
|
StoreGeneratedCertificates(connection, result);
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("CA and server certificates regenerated successfully.");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'remove' command.
|
|
/// </summary>
|
|
private static int HandleRemove(string? dataFolder, string? settingsEncryptionKey, string? store, string? certDir, string? keychain)
|
|
{
|
|
var storeLocation = ParseStoreLocation(store);
|
|
var dataFolderPath = GetDataFolder(dataFolder);
|
|
|
|
Console.WriteLine($"Using data folder: {dataFolderPath}");
|
|
using var _ = StartConsoleLogScope();
|
|
|
|
using var connection = OpenDatabase(dataFolderPath, settingsEncryptionKey, false);
|
|
|
|
// Get existing CA certificate and remove from trust store
|
|
var caCertBase64 = connection.ApplicationSettings.ServerCACertificate;
|
|
|
|
if (string.IsNullOrWhiteSpace(caCertBase64))
|
|
{
|
|
Console.WriteLine("No CA certificate found in database, not attempting to remove from trust store.");
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
var caCert = CertificateStorageHelper.DeserializeCertificate(caCertBase64);
|
|
Console.WriteLine("Removing CA certificate from system trust store...");
|
|
|
|
if (CertificateConfigurationHelper.IsCATrustInstalled(caCert, storeLocation, certDir, keychain))
|
|
{
|
|
if (CertificateConfigurationHelper.RemoveCATrust(caCert, storeLocation, certDir, keychain))
|
|
Console.WriteLine("CA certificate removed from trust store.");
|
|
else
|
|
Console.WriteLine("Warning: Failed to remove CA certificate from trust store.");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("CA certificate was not found in trust store.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Warning: Could not remove CA from trust store: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Remove all certificate data from database
|
|
Console.WriteLine("Removing certificate data from database...");
|
|
var settings = new Dictionary<string, string?>
|
|
{
|
|
[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(settings, false);
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("HTTPS certificates removed successfully.");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'show' command.
|
|
/// Delegates status retrieval to <see cref="CertificateConfigurationHelper.GetCertificateStatus"/>.
|
|
/// </summary>
|
|
private static int HandleShow(string? dataFolder, string? settingsEncryptionKey, string? store, string? certDir, string? keychain)
|
|
{
|
|
var storeLocation = ParseStoreLocation(store);
|
|
var dataFolderPath = GetDataFolder(dataFolder);
|
|
|
|
Console.WriteLine($"Using data folder: {dataFolderPath}");
|
|
Console.WriteLine();
|
|
using var _ = StartConsoleLogScope();
|
|
|
|
using var connection = OpenDatabase(dataFolderPath, settingsEncryptionKey, false);
|
|
|
|
var caCertBase64 = connection.ApplicationSettings.ServerCACertificate;
|
|
var serverCertCollection = connection.ApplicationSettings.ServerSSLCertificate;
|
|
var isAutogenerated = connection.ApplicationSettings.ServerSSLCertificateAutogenerated;
|
|
|
|
// Check if certificates exist
|
|
if (string.IsNullOrWhiteSpace(caCertBase64) && serverCertCollection == null)
|
|
{
|
|
Console.WriteLine("No HTTPS certificates configured.");
|
|
Console.WriteLine("Use 'generate' command to create certificates.");
|
|
return 0;
|
|
}
|
|
|
|
var status = CertificateConfigurationHelper.GetCertificateStatus(
|
|
caCertBase64,
|
|
serverCertCollection,
|
|
isAutogenerated,
|
|
storeLocation,
|
|
certDir,
|
|
keychain);
|
|
|
|
// Display CA certificate info
|
|
Console.WriteLine("=== CA Certificate ===");
|
|
if (status.CACert == null)
|
|
{
|
|
Console.WriteLine("No CA certificate stored.");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Subject: {status.CACert.Subject}");
|
|
Console.WriteLine($"Issuer: {status.CACert.Issuer}");
|
|
Console.WriteLine($"Valid from: {status.CACert.NotBefore:yyyy-MM-dd}");
|
|
Console.WriteLine($"Valid until: {status.CACert.NotAfter:yyyy-MM-dd}");
|
|
|
|
var caStatus = status.CaDaysUntilExpiry <= 0 ? "EXPIRED"
|
|
: status.CaDaysUntilExpiry <= CertificateRenewalChecker.RENEWAL_THRESHOLD_DAYS ? "EXPIRING SOON"
|
|
: "Valid";
|
|
Console.WriteLine($"Status: {caStatus}");
|
|
Console.WriteLine($"Trust store: {(status.IsCATrusted ? "Installed" : "Not installed")}");
|
|
}
|
|
|
|
Console.WriteLine();
|
|
|
|
// Display server certificate info
|
|
Console.WriteLine("=== Server Certificate ===");
|
|
if (status.ServerCert == null)
|
|
{
|
|
if (serverCertCollection != null)
|
|
Console.WriteLine("Server certificate collection exists but no certificate with private key found.");
|
|
else
|
|
Console.WriteLine("No server certificate stored.");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Subject: {status.ServerCert.Subject}");
|
|
Console.WriteLine($"Issuer: {status.ServerCert.Issuer}");
|
|
Console.WriteLine($"Valid from: {status.ServerCert.NotBefore:yyyy-MM-dd}");
|
|
Console.WriteLine($"Valid until: {status.ServerCert.NotAfter:yyyy-MM-dd}");
|
|
|
|
var serverStatus = status.ServerDaysUntilExpiry <= 0 ? "EXPIRED"
|
|
: status.ServerDaysUntilExpiry <= CertificateRenewalChecker.RENEWAL_THRESHOLD_DAYS ? "EXPIRING SOON"
|
|
: "Valid";
|
|
Console.WriteLine($"Status: {serverStatus}");
|
|
|
|
if (status.DnsNames.Any())
|
|
Console.WriteLine($"DNS names: {string.Join(", ", status.DnsNames)}");
|
|
if (status.IpAddresses.Any())
|
|
Console.WriteLine($"IP addresses: {string.Join(", ", status.IpAddresses)}");
|
|
|
|
Console.WriteLine($"Autogenerated: {status.IsAutogenerated}");
|
|
}
|
|
|
|
Console.WriteLine();
|
|
|
|
// Display database encryption status
|
|
Console.WriteLine("=== Security ===");
|
|
Console.WriteLine($"Database field encryption: {(connection.IsEncryptingFields ? "Enabled" : "Disabled")}");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'export' command.
|
|
/// </summary>
|
|
private static int HandleExport(string? file, string? dataFolder, string? settingsEncryptionKey)
|
|
{
|
|
var dataFolderPath = GetDataFolder(dataFolder);
|
|
var outputFile = string.IsNullOrWhiteSpace(file) ? "duplicati-server.crt" : file;
|
|
|
|
Console.WriteLine($"Using data folder: {dataFolderPath}");
|
|
Console.WriteLine($"Exporting server certificate to: {Path.GetFullPath(outputFile)}");
|
|
using var _ = StartConsoleLogScope();
|
|
|
|
using var connection = OpenDatabase(dataFolderPath, settingsEncryptionKey, false);
|
|
|
|
var serverCertCollection = connection.ApplicationSettings.ServerSSLCertificate;
|
|
if (serverCertCollection == null || serverCertCollection.Count == 0)
|
|
{
|
|
Console.WriteLine("Error: No server certificate found in database.");
|
|
return 1;
|
|
}
|
|
|
|
try
|
|
{
|
|
var serverCert = serverCertCollection.Cast<X509Certificate2>().FirstOrDefault(c => c.HasPrivateKey) ?? serverCertCollection[0];
|
|
var pem = CertificateStorageHelper.ExportToPem(serverCert);
|
|
File.WriteAllText(outputFile, pem);
|
|
Console.WriteLine($"Server certificate exported successfully.");
|
|
Console.WriteLine($"Subject: {serverCert.Subject}");
|
|
Console.WriteLine($"Valid until: {serverCert.NotAfter:yyyy-MM-dd}");
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error exporting certificate: {ex.Message}");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'export-ca' command.
|
|
/// </summary>
|
|
private static int HandleExportCa(string? file, string? dataFolder, string? settingsEncryptionKey)
|
|
{
|
|
var dataFolderPath = GetDataFolder(dataFolder);
|
|
var outputFile = string.IsNullOrWhiteSpace(file) ? "duplicati-ca.crt" : file;
|
|
|
|
Console.WriteLine($"Using data folder: {dataFolderPath}");
|
|
Console.WriteLine($"Exporting CA certificate to: {Path.GetFullPath(outputFile)}");
|
|
using var _ = StartConsoleLogScope();
|
|
|
|
using var connection = OpenDatabase(dataFolderPath, settingsEncryptionKey, false);
|
|
|
|
var caCertBase64 = connection.ApplicationSettings.ServerCACertificate;
|
|
if (string.IsNullOrWhiteSpace(caCertBase64))
|
|
{
|
|
Console.WriteLine("Error: No CA certificate found in database.");
|
|
return 1;
|
|
}
|
|
|
|
try
|
|
{
|
|
var caCert = CertificateStorageHelper.DeserializeCertificate(caCertBase64);
|
|
var pem = CertificateStorageHelper.ExportToPem(caCert);
|
|
File.WriteAllText(outputFile, pem);
|
|
Console.WriteLine($"CA certificate exported successfully.");
|
|
Console.WriteLine($"Subject: {caCert.Subject}");
|
|
Console.WriteLine($"Valid until: {caCert.NotAfter:yyyy-MM-dd}");
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error exporting certificate: {ex.Message}");
|
|
return 1;
|
|
}
|
|
}
|
|
}
|