Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
v4.0.1
- Bug Fix: Error running ODKG jobs found in v4.0.0

v4.0.0
- Added ability to run post job commands for Management-Add and ODKG jobs.
- Added "+" as an allowed character for store paths and file names
- Bug Fix: Issue adding certificates without private keys introduced in 3.0.0
- Bug Fix: Issue creating stores on a Linux UO in agent mode (client machine value ending in |LocalMachine)
- Bug Fix: Logging issue with RFORA

v3.0.0
- Added support for post quantum ML-DSA certificates for store types RFPEM, RFJKS, RFPkcs12, and RFDER
- Added support for On Device Key Generation (ODKG)
Expand Down
136 changes: 134 additions & 2 deletions README.md

Large diffs are not rendered by default.

67 changes: 48 additions & 19 deletions RemoteFile/ApplicationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Extensions.Logging;
using Keyfactor.Logging;
using System.Reflection;
using Microsoft.PowerShell;
Comment thread
leefine02 marked this conversation as resolved.


namespace Keyfactor.Extensions.Orchestrator.RemoteFile
Expand All @@ -24,25 +25,25 @@ public class ApplicationSettings
private const string DEFAULT_SUDO_IMPERSONATION_SETTING = "";
private const int DEFAULT_SSH_PORT = 22;

private static Dictionary<string,string> configuration;

public static bool UseSudo { get { return configuration.ContainsKey("UseSudo") ? configuration["UseSudo"]?.ToUpper() == "Y" : false; } }
public static bool CreateStoreIfMissing { get { return configuration.ContainsKey("CreateStoreIfMissing") ? configuration["CreateStoreIfMissing"]?.ToUpper() == "Y" : false; } }
public static bool UseNegotiate { get { return configuration.ContainsKey("UseNegotiate") ? configuration["UseNegotiate"]?.ToUpper() == "Y" : false; } }
public static string SeparateUploadFilePath { get { return configuration.ContainsKey("SeparateUploadFilePath") ? AddTrailingSlash(configuration["SeparateUploadFilePath"]) : string.Empty; } }
public static string DefaultLinuxPermissionsOnStoreCreation { get { return configuration.ContainsKey("DefaultLinuxPermissionsOnStoreCreation") ? configuration["DefaultLinuxPermissionsOnStoreCreation"] : DEFAULT_LINUX_PERMISSION_SETTING; } }
public static string DefaultOwnerOnStoreCreation { get { return configuration.ContainsKey("DefaultOwnerOnStoreCreation") ? configuration["DefaultOwnerOnStoreCreation"] : DEFAULT_OWNER_SETTING; } }
public static string DefaultSudoImpersonatedUser { get { return configuration.ContainsKey("DefaultSudoImpersonatedUser") ? configuration["DefaultSudoImpersonatedUser"] : DEFAULT_SUDO_IMPERSONATION_SETTING; } }
public static string TempFilePathForODKG { get { return configuration.ContainsKey("TempFilePathForODKG") ? configuration["TempFilePathForODKG"] : string.Empty; } }
public static bool UseShellCommands { get { return configuration.ContainsKey("UseShellCommands") ? configuration["UseShellCommands"]?.ToUpper() == "Y" : true; } }
private static Dictionary<string,object> configuration;

public static bool UseSudo { get { return configuration.ContainsKey("UseSudo") ? configuration["UseSudo"]?.ToString().ToUpper() == "Y" : false; } }
public static bool CreateStoreIfMissing { get { return configuration.ContainsKey("CreateStoreIfMissing") ? configuration["CreateStoreIfMissing"]?.ToString().ToUpper() == "Y" : false; } }
public static bool UseNegotiate { get { return configuration.ContainsKey("UseNegotiate") ? configuration["UseNegotiate"]?.ToString().ToUpper() == "Y" : false; } }
public static string SeparateUploadFilePath { get { return configuration.ContainsKey("SeparateUploadFilePath") ? AddTrailingSlash(configuration["SeparateUploadFilePath"].ToString()) : string.Empty; } }
public static string DefaultLinuxPermissionsOnStoreCreation { get { return configuration.ContainsKey("DefaultLinuxPermissionsOnStoreCreation") ? configuration["DefaultLinuxPermissionsOnStoreCreation"].ToString() : DEFAULT_LINUX_PERMISSION_SETTING; } }
public static string DefaultOwnerOnStoreCreation { get { return configuration.ContainsKey("DefaultOwnerOnStoreCreation") ? configuration["DefaultOwnerOnStoreCreation"].ToString() : DEFAULT_OWNER_SETTING; } }
public static string DefaultSudoImpersonatedUser { get { return configuration.ContainsKey("DefaultSudoImpersonatedUser") ? configuration["DefaultSudoImpersonatedUser"].ToString() : DEFAULT_SUDO_IMPERSONATION_SETTING; } }
public static string TempFilePathForODKG { get { return configuration.ContainsKey("TempFilePathForODKG") ? configuration["TempFilePathForODKG"].ToString() : string.Empty; } }
public static bool UseShellCommands { get { return configuration.ContainsKey("UseShellCommands") ? configuration["UseShellCommands"]?.ToString().ToUpper() == "Y" : true; } }
public static int SSHPort
{
get
{
if (configuration.ContainsKey("SSHPort") && !string.IsNullOrEmpty(configuration["SSHPort"]))
if (configuration.ContainsKey("SSHPort") && !string.IsNullOrEmpty(configuration["SSHPort"]?.ToString()))
{
int sshPort;
if (int.TryParse(configuration["SSHPort"], out sshPort))
if (int.TryParse(configuration["SSHPort"]?.ToString(), out sshPort))
return sshPort;
else
throw new RemoteFileException($"Invalid optional config.json SSHPort value of {configuration["SSHPort"]}. If present, this must be an integer value.");
Expand All @@ -53,13 +54,27 @@ public static int SSHPort
}
}
}
public static List<PostJobCommand> PostJobCommands
{
get
{
if (configuration.ContainsKey("PostJobCommands") && configuration["PostJobCommands"] != null)
{
return JsonConvert.DeserializeObject<List<PostJobCommand>>(configuration["PostJobCommands"]?.ToString());
}
else
{
return new List<PostJobCommand>();
}
}
}

static ApplicationSettings()
{
ILogger logger = LogHandler.GetClassLogger<ApplicationSettings>();
logger.MethodEntry(LogLevel.Debug);

configuration = new Dictionary<string, string>();
configuration = new Dictionary<string, object>();
string configLocation = $"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}{Path.DirectorySeparatorChar}config.json";
string configContents = string.Empty;

Expand All @@ -81,11 +96,18 @@ static ApplicationSettings()
return;
}

configuration = JsonConvert.DeserializeObject<Dictionary<string, string>>(configContents);
try
{
configuration = JsonConvert.DeserializeObject<Dictionary<string, object>>(configContents);
}
catch (Exception ex)
{
throw new RemoteFileException(RemoteFileException.FlattenExceptionMessages(ex, "Error attempting to serialize config.json file. Please review your config.json file for proper formatting."));
}
ValidateConfiguration(logger);

logger.LogDebug("Configuration Settings:");
foreach(KeyValuePair<string,string> keyValue in configuration)
foreach(KeyValuePair<string,object> keyValue in configuration)
{
logger.LogDebug($" {keyValue.Key}: {keyValue.Value}");
}
Expand All @@ -95,11 +117,11 @@ static ApplicationSettings()

private static void ValidateConfiguration(ILogger logger)
{
if (!configuration.ContainsKey("UseSudo") || (configuration["UseSudo"].ToUpper() != "Y" && configuration["UseSudo"].ToUpper() != "N"))
if (!configuration.ContainsKey("UseSudo") || (configuration["UseSudo"]?.ToString().ToUpper() != "Y" && configuration["UseSudo"]?.ToString().ToUpper() != "N"))
logger.LogDebug($"Missing or invalid configuration parameter - UseSudo. Will set to default value of 'False'");
if (!configuration.ContainsKey("CreateStoreIfMissing") || (configuration["CreateStoreIfMissing"].ToUpper() != "Y" && configuration["CreateStoreIfMissing"].ToUpper() != "N"))
if (!configuration.ContainsKey("CreateStoreIfMissing") || (configuration["CreateStoreIfMissing"]?.ToString().ToUpper() != "Y" && configuration["CreateStoreIfMissing"]?.ToString().ToUpper() != "N"))
logger.LogDebug($"Missing or invalid configuration parameter - CreateStoreIfMissing. Will set to default value of 'False'");
if (!configuration.ContainsKey("UseNegotiate") || (configuration["UseNegotiate"].ToUpper() != "Y" && configuration["UseNegotiate"].ToUpper() != "N"))
if (!configuration.ContainsKey("UseNegotiate") || (configuration["UseNegotiate"]?.ToString().ToUpper() != "Y" && configuration["UseNegotiate"]?.ToString().ToUpper() != "N"))
logger.LogDebug($"Missing or invalid configuration parameter - UseNegotiate. Will set to default value of 'False'");
if (!configuration.ContainsKey("SeparateUploadFilePath"))
logger.LogDebug($"Missing configuration parameter - SeparateUploadFilePath. Will set to default value of ''");
Expand All @@ -113,5 +135,12 @@ private static string AddTrailingSlash(string path)
{
return string.IsNullOrEmpty(path) ? path : path.Substring(path.Length - 1, 1) == @"/" ? path : path += @"/";
}

public class PostJobCommand
{
public string Name { get; set; }
public string Environment { get; set; }
public string Command { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ public List<SerializedStoreInfo> SerializeRemoteCertificateStore(Pkcs12Store cer
try
{
remoteHandler.UploadCertificateFile($"{WorkFolder}", $"{tempStoreFileJKS}", jksStoreInfo[0].Contents);
remoteHandler.RunCommand(orapkiCommand1, null, ApplicationSettings.UseSudo, null);
remoteHandler.RunCommand(orapkiCommand2, null, ApplicationSettings.UseSudo, null);
remoteHandler.RunCommand(orapkiCommand1, null, ApplicationSettings.UseSudo, [storePassword]);
remoteHandler.RunCommand(orapkiCommand2, null, ApplicationSettings.UseSudo, [storePassword]);

byte[] storeContents = remoteHandler.DownloadCertificateFile($"{WorkFolder}ewallet.p12");

Expand Down
41 changes: 32 additions & 9 deletions RemoteFile/ManagementBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Org.BouncyCastle.X509;
using System;
using System.IO;
using System.Linq.Expressions;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added using System.Linq.Expressions; doesn't appear to be used in this file and will introduce an unnecessary compile-time warning/noise. Remove it unless it's required for upcoming changes.

Suggested change
using System.Linq.Expressions;

Copilot uses AI. Check for mistakes.
using static Org.BouncyCastle.Math.EC.ECCurve;

namespace Keyfactor.Extensions.Orchestrator.RemoteFile
Expand Down Expand Up @@ -55,7 +56,21 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
certificateStore.AddCertificate(config.JobCertificate.Alias ?? GetThumbprint(config.JobCertificate, logger), config.JobCertificate.Contents, config.Overwrite, config.JobCertificate.PrivateKeyPassword, RemoveRootCertificate);
certificateStore.SaveCertificateStore(certificateStoreSerializer.SerializeRemoteCertificateStore(certificateStore.GetCertificateStore(), storePathFile.Path, storePathFile.File, StorePassword, certificateStore.RemoteHandler));

logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
try
{
if (!string.IsNullOrEmpty(PostJobApplicationRestart))
certificateStore.RunPostJobCommand(PostJobApplicationRestart, config.CertificateStoreDetails.StorePath, certificateStoreSerializer.GetPrivateKeyPath());
}
catch (Exception ex)
{
logger.LogError($"Exception for {config.Capability} attempting post job command for {PostJobApplicationRestart}: {RemoteFileException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}");
return new JobResult() { Result = OrchestratorJobStatusJobResult.Warning, JobHistoryId = config.JobHistoryId, FailureMessage = RemoteFileException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Certificate was successfully added to store, but post job command for {PostJobApplicationRestart} failed with: ") };
}
finally
{
logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
}

break;

case CertStoreOperationType.Remove:
Expand Down Expand Up @@ -112,17 +127,25 @@ private string GetThumbprint (ManagementJobCertificate jobCertificate, ILogger l

string thumbprint = string.Empty;

using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(jobCertificate.Contents)))
if (string.IsNullOrEmpty(jobCertificate.PrivateKeyPassword))
{
X509Certificate x = new X509Certificate(Convert.FromBase64String(jobCertificate.Contents));
thumbprint = x.Thumbprint();
}
Comment thread
leefine02 marked this conversation as resolved.
else
{
Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder();
Pkcs12Store store = storeBuilder.Build();
using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(jobCertificate.Contents)))
{
Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder();
Pkcs12Store store = storeBuilder.Build();

store.Load(ms, jobCertificate.PrivateKeyPassword.ToCharArray());
store.Load(ms, jobCertificate.PrivateKeyPassword.ToCharArray());

foreach (string alias in store.Aliases)
{
thumbprint = store.GetCertificate(alias).Certificate.Thumbprint();
break;
foreach (string alias in store.Aliases)
{
thumbprint = store.GetCertificate(alias).Certificate.Thumbprint();
break;
}
}
}

Expand Down
17 changes: 15 additions & 2 deletions RemoteFile/ReenrollmentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,23 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
}

// save certificate
certificateStore.AddCertificate(config.Alias ?? cert.Thumbprint, Convert.ToBase64String(cert.Export(X509ContentType.Pfx)), config.Overwrite, null, RemoveRootCertificate);
certificateStore.AddCertificate(config.Alias ?? cert.Thumbprint, Convert.ToBase64String(cert.Export(X509ContentType.Pfx, "password")), config.Overwrite, "password", RemoveRootCertificate);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exports the certificate to a PFX using a hard-coded password ("password"). Even if used only transiently, hardcoding a known password is insecure and unnecessary; it can also be confusing when troubleshooting. Use an empty password (or a generated per-job random password) and fix AddCertificate to handle passwordless PKCS#12 correctly instead of relying on a non-empty password.

Suggested change
certificateStore.AddCertificate(config.Alias ?? cert.Thumbprint, Convert.ToBase64String(cert.Export(X509ContentType.Pfx, "password")), config.Overwrite, "password", RemoveRootCertificate);
certificateStore.AddCertificate(config.Alias ?? cert.Thumbprint, Convert.ToBase64String(cert.Export(X509ContentType.Pfx)), config.Overwrite, string.Empty, RemoveRootCertificate);

Copilot uses AI. Check for mistakes.
certificateStore.SaveCertificateStore(certificateStoreSerializer.SerializeRemoteCertificateStore(certificateStore.GetCertificateStore(), storePathFile.Path, storePathFile.File, StorePassword, certificateStore.RemoteHandler));

logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
try
{
if (!string.IsNullOrEmpty(PostJobApplicationRestart))
certificateStore.RunPostJobCommand(PostJobApplicationRestart, config.CertificateStoreDetails.StorePath, certificateStoreSerializer.GetPrivateKeyPath());
}
catch (Exception ex)
{
logger.LogError($"Exception for {config.Capability} attempting post job command for {PostJobApplicationRestart}: {RemoteFileException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}");
return new JobResult() { Result = OrchestratorJobStatusJobResult.Warning, JobHistoryId = config.JobHistoryId, FailureMessage = RemoteFileException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Certificate was successfully added to store, but post job command for {PostJobApplicationRestart} failed with: ") };
}
finally
{
logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
}
}

catch (Exception ex)
Expand Down
Loading
Loading