Skip to content
Merged
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
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