Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
187209c
Update generated docs
Dec 10, 2025
7872659
ab#79304
Dec 12, 2025
f8e64f4
Update generated docs
Dec 12, 2025
eefbc05
ab#79304
Dec 29, 2025
47c0989
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Dec 29, 2025
5d42cc4
Update generated docs
Dec 29, 2025
138016c
ab#79304
Dec 31, 2025
39300c8
Update generated docs
Dec 31, 2025
ec4386f
ab#79304
Jan 12, 2026
670d302
ab#79304
Jan 12, 2026
5336f5f
Update generated docs
Jan 12, 2026
07dcd3b
ab#79304
Jan 12, 2026
98ae891
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 12, 2026
6744cfe
ab#79304
Jan 12, 2026
8fc6561
ab#79304
Jan 12, 2026
27e08cf
ab#79304
Jan 12, 2026
68d36d2
ab#79304
Jan 12, 2026
93b11b9
Update generated docs
Jan 12, 2026
eb17629
ab#79304
Jan 12, 2026
6674058
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 12, 2026
17d1c2c
ab#79304
Jan 12, 2026
4772c6e
Update generated docs
Jan 12, 2026
18e6133
ab#79304
Jan 13, 2026
ffd6063
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 13, 2026
d518e81
Update generated docs
Jan 13, 2026
7d542ad
ab#79304
Jan 13, 2026
11b347c
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 13, 2026
6a84b45
ab#79304
Jan 15, 2026
1bebe2a
Update generated docs
Jan 15, 2026
a2a28c6
ab#79304
Jan 15, 2026
f489178
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 15, 2026
e9c6dd1
Update generated docs
Jan 15, 2026
34cd7c9
Update README.md
leefine02 Jan 15, 2026
66b6a97
Update README.md
leefine02 Jan 15, 2026
eaacf66
ab#79304
Jan 15, 2026
4147308
Update generated docs
Jan 15, 2026
30080b4
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 15, 2026
ab5c6a5
Update generated docs
Jan 15, 2026
fdd1961
ab#79304
Jan 22, 2026
3aee96d
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 22, 2026
45ff5e3
Update generated docs
Jan 22, 2026
eba1370
ab#79304
Jan 23, 2026
a54fed7
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 23, 2026
88167cf
Update generated docs
Jan 23, 2026
30e47d3
ab#79304
Jan 23, 2026
2a3f212
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 23, 2026
2e8ef20
Update generated docs
Jan 23, 2026
6ab98b3
ab#79304
Jan 23, 2026
904f4f5
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 23, 2026
f9f7050
Update generated docs
Jan 23, 2026
6c5e421
ab#79304
Jan 26, 2026
6089a99
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 26, 2026
340ce68
Update generated docs
Jan 26, 2026
f1b1484
ab#79304
Jan 26, 2026
c0a22d6
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Jan 26, 2026
12e257f
Update generated docs
Jan 26, 2026
ff83aef
ab#79304
Feb 2, 2026
d16fa9e
Merge branch 'ab#79304' of https://github.com/Keyfactor/remote-file-o…
Feb 2, 2026
99eb1bf
ab#79304
Feb 2, 2026
2631456
ab#79304
Feb 2, 2026
fb867c9
ab#79304
Feb 5, 2026
e8ed467
ab#79304
Feb 17, 2026
fdf8a26
Update generated docs
Feb 17, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
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;


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;
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();
}
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
15 changes: 14 additions & 1 deletion RemoteFile/ReenrollmentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,20 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
certificateStore.AddCertificate(config.Alias ?? cert.Thumbprint, Convert.ToBase64String(cert.Export(X509ContentType.Pfx)), config.Overwrite, null, 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}.");
}
}

catch (Exception ex)
Expand Down
Loading
Loading