2026-03-01 11:33:05 +01:00
// 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.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 ;
2026-03-02 16:39:19 +01:00
using Duplicati.Library.Logging ;
2026-03-01 11:33:05 +01:00
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" ),
2026-03-02 16:39:19 +01:00
new Option < bool >( "--auto-create-database" , "Create the database if it does not exist" ),
2026-03-01 11:33:05 +01:00
};
AddDatabaseOptions ( cmd );
AddPlatformSpecificCAOptions ( cmd );
2026-03-02 16:39:19 +01:00
cmd . Handler = CommandHandler . Create < string? , bool , string? , string? , bool , string? , string? , string? >( HandleGenerate );
2026-03-01 11:33:05 +01:00
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>
2026-03-02 16:39:19 +01:00
private static Connection OpenDatabase ( string dataFolder , string? settingsEncryptionKey , bool autoCreateDatabase )
2026-03-01 11:33:05 +01:00
{
var databasePath = GetDatabasePath ( dataFolder );
2026-03-02 16:39:19 +01:00
if (! File . Exists ( databasePath ) && ! autoCreateDatabase )
2026-03-01 11:33:05 +01:00
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." );
2026-03-02 16:39:19 +01:00
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." );
2026-03-01 11:33:05 +01:00
}
}
2026-03-02 16:39:19 +01:00
/// <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 );
2026-03-01 11:33:05 +01:00
/// <summary>
/// Handles the 'generate' command.
/// Delegates certificate generation to <see cref="CertificateConfigurationHelper.GenerateCertificates"/>.
/// </summary>
2026-03-02 16:39:19 +01:00
private static int HandleGenerate ( string? hostnames , bool noTrust , string? dataFolder , string? settingsEncryptionKey , bool autoCreateDatabase , string? store , string? certDir , string? keychain )
2026-03-01 11:33:05 +01:00
{
var storeLocation = ParseStoreLocation ( store );
var dataFolderPath = GetDataFolder ( dataFolder );
2026-03-02 16:39:19 +01:00
using var _ = StartConsoleLogScope ();
2026-03-01 11:33:05 +01:00
Console . WriteLine ( $"Using data folder: {dataFolderPath}" );
2026-03-02 16:39:19 +01:00
using var connection = OpenDatabase ( dataFolderPath , settingsEncryptionKey , autoCreateDatabase );
2026-03-01 11:33:05 +01:00
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}" );
2026-03-02 16:39:19 +01:00
using var _ = StartConsoleLogScope ();
2026-03-01 11:33:05 +01:00
2026-03-02 16:39:19 +01:00
using var connection = OpenDatabase ( dataFolderPath , settingsEncryptionKey , false );
2026-03-01 11:33:05 +01:00
// 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}" );
2026-03-02 16:39:19 +01:00
using var _ = StartConsoleLogScope ();
2026-03-01 11:33:05 +01:00
2026-03-02 16:39:19 +01:00
using var connection = OpenDatabase ( dataFolderPath , settingsEncryptionKey , false );
2026-03-01 11:33:05 +01:00
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}" );
2026-03-02 16:39:19 +01:00
using var _ = StartConsoleLogScope ();
2026-03-01 11:33:05 +01:00
2026-03-02 16:39:19 +01:00
using var connection = OpenDatabase ( dataFolderPath , settingsEncryptionKey , false );
2026-03-01 11:33:05 +01:00
// Get existing CA certificate and remove from trust store
var caCertBase64 = connection . ApplicationSettings . ServerCACertificate ;
if (! string . IsNullOrWhiteSpace ( caCertBase64 ))
{
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 ();
2026-03-02 16:39:19 +01:00
using var _ = StartConsoleLogScope ();
2026-03-01 11:33:05 +01:00
2026-03-02 16:39:19 +01:00
using var connection = OpenDatabase ( dataFolderPath , settingsEncryptionKey , false );
2026-03-01 11:33:05 +01:00
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)}" );
2026-03-02 16:39:19 +01:00
using var _ = StartConsoleLogScope ();
2026-03-01 11:33:05 +01:00
2026-03-02 16:39:19 +01:00
using var connection = OpenDatabase ( dataFolderPath , settingsEncryptionKey , false );
2026-03-01 11:33:05 +01:00
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)}" );
2026-03-02 16:39:19 +01:00
using var _ = StartConsoleLogScope ();
2026-03-01 11:33:05 +01:00
2026-03-02 16:39:19 +01:00
using var connection = OpenDatabase ( dataFolderPath , settingsEncryptionKey , false );
2026-03-01 11:33:05 +01:00
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 ;
}
}
}