Skip to content

Commit c8643b4

Browse files
authored
Merge 2a3f212 into 8392e61
2 parents 8392e61 + 2a3f212 commit c8643b4

File tree

128 files changed

+399
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+399
-35
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
v4.0.0
2+
- Added ability to run post job commands for Management-Add and ODKG jobs.
3+
- Bug Fix: Issue adding certificates without private keys introduced in 3.0.0
4+
15
v3.0.0
26
- Added support for post quantum ML-DSA certificates for store types RFPEM, RFJKS, RFPkcs12, and RFDER
37
- Added support for On Device Key Generation (ODKG)

README.md

Lines changed: 134 additions & 2 deletions
Large diffs are not rendered by default.

RemoteFile/ApplicationSettings.cs

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.Extensions.Logging;
1414
using Keyfactor.Logging;
1515
using System.Reflection;
16+
using Microsoft.PowerShell;
1617

1718

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

27-
private static Dictionary<string,string> configuration;
28-
29-
public static bool UseSudo { get { return configuration.ContainsKey("UseSudo") ? configuration["UseSudo"]?.ToUpper() == "Y" : false; } }
30-
public static bool CreateStoreIfMissing { get { return configuration.ContainsKey("CreateStoreIfMissing") ? configuration["CreateStoreIfMissing"]?.ToUpper() == "Y" : false; } }
31-
public static bool UseNegotiate { get { return configuration.ContainsKey("UseNegotiate") ? configuration["UseNegotiate"]?.ToUpper() == "Y" : false; } }
32-
public static string SeparateUploadFilePath { get { return configuration.ContainsKey("SeparateUploadFilePath") ? AddTrailingSlash(configuration["SeparateUploadFilePath"]) : string.Empty; } }
33-
public static string DefaultLinuxPermissionsOnStoreCreation { get { return configuration.ContainsKey("DefaultLinuxPermissionsOnStoreCreation") ? configuration["DefaultLinuxPermissionsOnStoreCreation"] : DEFAULT_LINUX_PERMISSION_SETTING; } }
34-
public static string DefaultOwnerOnStoreCreation { get { return configuration.ContainsKey("DefaultOwnerOnStoreCreation") ? configuration["DefaultOwnerOnStoreCreation"] : DEFAULT_OWNER_SETTING; } }
35-
public static string DefaultSudoImpersonatedUser { get { return configuration.ContainsKey("DefaultSudoImpersonatedUser") ? configuration["DefaultSudoImpersonatedUser"] : DEFAULT_SUDO_IMPERSONATION_SETTING; } }
36-
public static string TempFilePathForODKG { get { return configuration.ContainsKey("TempFilePathForODKG") ? configuration["TempFilePathForODKG"] : string.Empty; } }
37-
public static bool UseShellCommands { get { return configuration.ContainsKey("UseShellCommands") ? configuration["UseShellCommands"]?.ToUpper() == "Y" : true; } }
28+
private static Dictionary<string,object> configuration;
29+
30+
public static bool UseSudo { get { return configuration.ContainsKey("UseSudo") ? configuration["UseSudo"]?.ToString().ToUpper() == "Y" : false; } }
31+
public static bool CreateStoreIfMissing { get { return configuration.ContainsKey("CreateStoreIfMissing") ? configuration["CreateStoreIfMissing"]?.ToString().ToUpper() == "Y" : false; } }
32+
public static bool UseNegotiate { get { return configuration.ContainsKey("UseNegotiate") ? configuration["UseNegotiate"]?.ToString().ToUpper() == "Y" : false; } }
33+
public static string SeparateUploadFilePath { get { return configuration.ContainsKey("SeparateUploadFilePath") ? AddTrailingSlash(configuration["SeparateUploadFilePath"].ToString()) : string.Empty; } }
34+
public static string DefaultLinuxPermissionsOnStoreCreation { get { return configuration.ContainsKey("DefaultLinuxPermissionsOnStoreCreation") ? configuration["DefaultLinuxPermissionsOnStoreCreation"].ToString() : DEFAULT_LINUX_PERMISSION_SETTING; } }
35+
public static string DefaultOwnerOnStoreCreation { get { return configuration.ContainsKey("DefaultOwnerOnStoreCreation") ? configuration["DefaultOwnerOnStoreCreation"].ToString() : DEFAULT_OWNER_SETTING; } }
36+
public static string DefaultSudoImpersonatedUser { get { return configuration.ContainsKey("DefaultSudoImpersonatedUser") ? configuration["DefaultSudoImpersonatedUser"].ToString() : DEFAULT_SUDO_IMPERSONATION_SETTING; } }
37+
public static string TempFilePathForODKG { get { return configuration.ContainsKey("TempFilePathForODKG") ? configuration["TempFilePathForODKG"].ToString() : string.Empty; } }
38+
public static bool UseShellCommands { get { return configuration.ContainsKey("UseShellCommands") ? configuration["UseShellCommands"]?.ToString().ToUpper() == "Y" : true; } }
3839
public static int SSHPort
3940
{
4041
get
4142
{
42-
if (configuration.ContainsKey("SSHPort") && !string.IsNullOrEmpty(configuration["SSHPort"]))
43+
if (configuration.ContainsKey("SSHPort") && !string.IsNullOrEmpty(configuration["SSHPort"]?.ToString()))
4344
{
4445
int sshPort;
45-
if (int.TryParse(configuration["SSHPort"], out sshPort))
46+
if (int.TryParse(configuration["SSHPort"]?.ToString(), out sshPort))
4647
return sshPort;
4748
else
4849
throw new RemoteFileException($"Invalid optional config.json SSHPort value of {configuration["SSHPort"]}. If present, this must be an integer value.");
@@ -53,13 +54,27 @@ public static int SSHPort
5354
}
5455
}
5556
}
57+
public static List<PostJobCommand> PostJobCommands
58+
{
59+
get
60+
{
61+
if (configuration.ContainsKey("PostJobCommands") && configuration["PostJobCommands"] != null)
62+
{
63+
return JsonConvert.DeserializeObject<List<PostJobCommand>>(configuration["PostJobCommands"]?.ToString());
64+
}
65+
else
66+
{
67+
return new List<PostJobCommand>();
68+
}
69+
}
70+
}
5671

5772
static ApplicationSettings()
5873
{
5974
ILogger logger = LogHandler.GetClassLogger<ApplicationSettings>();
6075
logger.MethodEntry(LogLevel.Debug);
6176

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

@@ -81,11 +96,18 @@ static ApplicationSettings()
8196
return;
8297
}
8398

84-
configuration = JsonConvert.DeserializeObject<Dictionary<string, string>>(configContents);
99+
try
100+
{
101+
configuration = JsonConvert.DeserializeObject<Dictionary<string, object>>(configContents);
102+
}
103+
catch (Exception ex)
104+
{
105+
throw new RemoteFileException(RemoteFileException.FlattenExceptionMessages(ex, "Error attempting to serialize config.json file. Please review your config.json file for proper formatting."));
106+
}
85107
ValidateConfiguration(logger);
86108

87109
logger.LogDebug("Configuration Settings:");
88-
foreach(KeyValuePair<string,string> keyValue in configuration)
110+
foreach(KeyValuePair<string,object> keyValue in configuration)
89111
{
90112
logger.LogDebug($" {keyValue.Key}: {keyValue.Value}");
91113
}
@@ -95,11 +117,11 @@ static ApplicationSettings()
95117

96118
private static void ValidateConfiguration(ILogger logger)
97119
{
98-
if (!configuration.ContainsKey("UseSudo") || (configuration["UseSudo"].ToUpper() != "Y" && configuration["UseSudo"].ToUpper() != "N"))
120+
if (!configuration.ContainsKey("UseSudo") || (configuration["UseSudo"]?.ToString().ToUpper() != "Y" && configuration["UseSudo"]?.ToString().ToUpper() != "N"))
99121
logger.LogDebug($"Missing or invalid configuration parameter - UseSudo. Will set to default value of 'False'");
100-
if (!configuration.ContainsKey("CreateStoreIfMissing") || (configuration["CreateStoreIfMissing"].ToUpper() != "Y" && configuration["CreateStoreIfMissing"].ToUpper() != "N"))
122+
if (!configuration.ContainsKey("CreateStoreIfMissing") || (configuration["CreateStoreIfMissing"]?.ToString().ToUpper() != "Y" && configuration["CreateStoreIfMissing"]?.ToString().ToUpper() != "N"))
101123
logger.LogDebug($"Missing or invalid configuration parameter - CreateStoreIfMissing. Will set to default value of 'False'");
102-
if (!configuration.ContainsKey("UseNegotiate") || (configuration["UseNegotiate"].ToUpper() != "Y" && configuration["UseNegotiate"].ToUpper() != "N"))
124+
if (!configuration.ContainsKey("UseNegotiate") || (configuration["UseNegotiate"]?.ToString().ToUpper() != "Y" && configuration["UseNegotiate"]?.ToString().ToUpper() != "N"))
103125
logger.LogDebug($"Missing or invalid configuration parameter - UseNegotiate. Will set to default value of 'False'");
104126
if (!configuration.ContainsKey("SeparateUploadFilePath"))
105127
logger.LogDebug($"Missing configuration parameter - SeparateUploadFilePath. Will set to default value of ''");
@@ -113,5 +135,12 @@ private static string AddTrailingSlash(string path)
113135
{
114136
return string.IsNullOrEmpty(path) ? path : path.Substring(path.Length - 1, 1) == @"/" ? path : path += @"/";
115137
}
138+
139+
public class PostJobCommand
140+
{
141+
public string Name { get; set; }
142+
public string Environment { get; set; }
143+
public string Command { get; set; }
144+
}
116145
}
117146
}

RemoteFile/ManagementBase.cs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Org.BouncyCastle.X509;
1616
using System;
1717
using System.IO;
18+
using System.Linq.Expressions;
1819
using static Org.BouncyCastle.Math.EC.ECCurve;
1920

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

58-
logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
59+
try
60+
{
61+
if (!string.IsNullOrEmpty(PostJobApplicationRestart))
62+
certificateStore.RunPostJobCommand(PostJobApplicationRestart, config.CertificateStoreDetails.StorePath, certificateStoreSerializer.GetPrivateKeyPath());
63+
}
64+
catch (Exception ex)
65+
{
66+
logger.LogError($"Exception for {config.Capability} attempting post job command for {PostJobApplicationRestart}: {RemoteFileException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}");
67+
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: ") };
68+
}
69+
finally
70+
{
71+
logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
72+
}
73+
5974
break;
6075

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

113128
string thumbprint = string.Empty;
114129

115-
using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(jobCertificate.Contents)))
130+
if (string.IsNullOrEmpty(jobCertificate.PrivateKeyPassword))
131+
{
132+
X509Certificate x = new X509Certificate(Convert.FromBase64String(jobCertificate.Contents));
133+
thumbprint = x.Thumbprint();
134+
}
135+
else
116136
{
117-
Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder();
118-
Pkcs12Store store = storeBuilder.Build();
137+
using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(jobCertificate.Contents)))
138+
{
139+
Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder();
140+
Pkcs12Store store = storeBuilder.Build();
119141

120-
store.Load(ms, jobCertificate.PrivateKeyPassword.ToCharArray());
142+
store.Load(ms, jobCertificate.PrivateKeyPassword.ToCharArray());
121143

122-
foreach (string alias in store.Aliases)
123-
{
124-
thumbprint = store.GetCertificate(alias).Certificate.Thumbprint();
125-
break;
144+
foreach (string alias in store.Aliases)
145+
{
146+
thumbprint = store.GetCertificate(alias).Certificate.Thumbprint();
147+
break;
148+
}
126149
}
127150
}
128151

RemoteFile/ReenrollmentBase.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,20 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm
104104
certificateStore.AddCertificate(config.Alias ?? cert.Thumbprint, Convert.ToBase64String(cert.Export(X509ContentType.Pfx)), config.Overwrite, null, RemoveRootCertificate);
105105
certificateStore.SaveCertificateStore(certificateStoreSerializer.SerializeRemoteCertificateStore(certificateStore.GetCertificateStore(), storePathFile.Path, storePathFile.File, StorePassword, certificateStore.RemoteHandler));
106106

107-
logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
107+
try
108+
{
109+
if (!string.IsNullOrEmpty(PostJobApplicationRestart))
110+
certificateStore.RunPostJobCommand(PostJobApplicationRestart, config.CertificateStoreDetails.StorePath, certificateStoreSerializer.GetPrivateKeyPath());
111+
}
112+
catch (Exception ex)
113+
{
114+
logger.LogError($"Exception for {config.Capability} attempting post job command for {PostJobApplicationRestart}: {RemoteFileException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}");
115+
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: ") };
116+
}
117+
finally
118+
{
119+
logger.LogDebug($"END add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}.");
120+
}
108121
}
109122

110123
catch (Exception ex)

RemoteFile/RemoteCertificateStore.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
using static Keyfactor.Extensions.Orchestrator.RemoteFile.ReenrollmentBase;
2626
using static Keyfactor.PKI.PKIConstants.X509;
2727
using Keyfactor.PKI.PrivateKeys;
28+
using Keyfactor.PKI.CryptographicObjects.Formatters;
29+
using Org.BouncyCastle.X509;
2830

2931
namespace Keyfactor.Extensions.Orchestrator.RemoteFile
3032
{
@@ -33,6 +35,8 @@ internal class RemoteCertificateStore
3335
private const string NO_EXTENSION = "noext";
3436
private const string FULL_SCAN = "fullscan";
3537
private const string LOCAL_MACHINE_SUFFIX = "|localmachine";
38+
private const string POST_JOB_COMMAND_ARG1 = "%StorePath%";
39+
private const string POST_JOB_COMMAND_ARG2 = "%SeparatePrivateKeyFilePath%";
3640

3741
internal enum ServerTypeEnum
3842
{
@@ -263,7 +267,7 @@ internal void AddCertificate(string alias, string certificateEntry, bool overwri
263267
RemoveRootCertificate(Convert.FromBase64String(certificateEntry), pfxPassword) :
264268
Convert.FromBase64String(certificateEntry);
265269

266-
using (MemoryStream ms = new MemoryStream(newCertBytes))
270+
using (MemoryStream ms = new MemoryStream(string.IsNullOrEmpty(pfxPassword) ? ConvertDERToP12(newCertBytes) : newCertBytes))
267271
{
268272
newEntry.Load(ms, string.IsNullOrEmpty(pfxPassword) ? new char[0] : pfxPassword.ToCharArray());
269273
}
@@ -370,6 +374,28 @@ internal string GenerateCSR(string subjectText, bool overwrite, string alias, Su
370374
return csr;
371375
}
372376

377+
internal void RunPostJobCommand(string applicationName, string storePath, string separatePrivateKeyFilePath)
378+
{
379+
string cmd = string.Empty;
380+
try
381+
{
382+
cmd = ApplicationSettings.PostJobCommands.FirstOrDefault(p => p.Name == applicationName && p.Environment == ServerType.ToString())?.Command;
383+
}
384+
catch (Exception ex)
385+
{
386+
string errMessage = RemoteFileException.FlattenExceptionMessages(ex, "Error reading config.json PostJobCommands Setting: ");
387+
logger.LogError(errMessage);
388+
throw new RemoteFileException(errMessage);
389+
}
390+
if (string.IsNullOrEmpty(cmd))
391+
throw new RemoteFileException($"Post job application {applicationName} command mapping not found in config.json.");
392+
393+
if (!string.IsNullOrEmpty(storePath)) cmd = cmd.Replace(POST_JOB_COMMAND_ARG1, storePath);
394+
if (!string.IsNullOrEmpty(separatePrivateKeyFilePath)) cmd = cmd.Replace(POST_JOB_COMMAND_ARG2, separatePrivateKeyFilePath);
395+
396+
RemoteHandler.RunCommand(cmd, null, false, null);
397+
}
398+
373399
internal void Initialize(string sudoImpersonatedUser, bool useShellCommands)
374400
{
375401
logger.MethodEntry(LogLevel.Debug);
@@ -561,6 +587,23 @@ private string FormatPath(string path)
561587

562588
return "'" + path + (path.Substring(path.Length - 1) == @"\" ? string.Empty : @"\") + "'";
563589
}
590+
591+
private byte[] ConvertDERToP12(byte[] cert)
592+
{
593+
X509Certificate x509Cert = new X509CertificateParser().ReadCertificate(cert);
594+
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
595+
store.SetCertificateEntry("temp", new X509CertificateEntry(x509Cert));
596+
597+
using (var ms = new MemoryStream())
598+
{
599+
store.Save(
600+
ms,
601+
new char[] {},
602+
new SecureRandom()
603+
);
604+
return ms.ToArray();
605+
}
606+
}
564607
}
565608

566609
class PathFile

0 commit comments

Comments
 (0)