From 13e853340ac8fc45a784b1af21dbc3b54e1ef018 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Tue, 28 May 2024 19:07:46 -0500 Subject: [PATCH 1/5] POC for Macys - do not use for production. --- IISU/ClientPSCertStoreManager.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 1a37864..02f1b74 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -112,6 +112,8 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { try { + _logger.LogTrace("Entering ImportPFX"); + using (PowerShell ps = PowerShell.Create()) { ps.Runspace = _runspace; @@ -119,7 +121,7 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin if (cryptoProviderName == null) { string script = @" - param($pfxFilePath, $privateKeyPassword, $cspName) + param($pfxFilePath, $privateKeyPassword) $output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 $c = $LASTEXITCODE $output @@ -154,9 +156,11 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin try { lastExitCode = (int)ps.Runspace.SessionStateProxy.PSVariable.GetValue("c"); + _logger.LogTrace($"Last exit code: {lastExitCode}"); } catch (Exception) { + _logger.LogTrace("Unable to get the last exit code."); } @@ -182,7 +186,10 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin foreach (var result in results) { string outputLine = result.ToString(); - if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error")) + + _logger.LogTrace(outputLine); + + if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error") || outputLine.Contains("permissions are needed")) { isError = true; _logger.LogError(outputLine); From 3ce27b4f378196b382d7cdd0c8d08fc66c0b95a0 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 29 May 2024 11:51:30 -0500 Subject: [PATCH 2/5] Minor changes --- IISU/ClientPSCertStoreManager.cs | 71 ++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 02f1b74..56dd5d5 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Logging; using System; using System.IO; +using System.Linq.Expressions; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Security.Cryptography.X509Certificates; @@ -48,6 +49,10 @@ public ClientPSCertStoreManager(ILogger logger, Runspace runSpace, long jobNumbe public string CreatePFXFile(string certificateContents, string privateKeyPassword) { + _logger.LogTrace("Entering CreatePFXFile"); + if (!string.IsNullOrEmpty(privateKeyPassword)) { _logger.LogTrace("privateKeyPassword was present"); } + else _logger.LogTrace("No privateKeyPassword Presented"); + try { // Create the x509 certificate @@ -79,11 +84,13 @@ public string CreatePFXFile(string certificateContents, string privateKeyPasswor var results = ps.Invoke(); // Get the result (temporary file path) returned by the script + _logger.LogTrace($"Results after creating PFX File: {results[0].ToString()}"); return results[0].ToString(); } } - catch (Exception) + catch (Exception ex) { + _logger.LogError(ex.ToString()); throw new Exception("An error occurred while attempting to create and write the X509 contents."); } } @@ -120,11 +127,30 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin if (cryptoProviderName == null) { + //string script = @" + //param($pfxFilePath, $privateKeyPassword) + //$output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 + //$c = $LASTEXITCODE 2>&1 + //$output + //"; + string script = @" param($pfxFilePath, $privateKeyPassword) $output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 - $c = $LASTEXITCODE + $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" + $stuff = certutil -dump + + if ($stuff.GetType().Name -eq ""String"") + { + $stuff = @($stuff, $exit_message) + } + else + { + $stuff += $exit_message + } + $output + $stuff "; ps.AddScript(script); @@ -136,8 +162,20 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin string script = @" param($pfxFilePath, $privateKeyPassword, $cspName) $output = certutil -importpfx -csp $cspName -p $privateKeyPassword $pfxFilePath 2>&1 - $c = $LASTEXITCODE + $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" + $stuff = certutil -dump + + if ($stuff.GetType().Name -eq ""String"") + { + $stuff = @($stuff, $exit_message) + } + else + { + $stuff += $exit_message + } + $output + $stuff "; ps.AddScript(script); @@ -147,6 +185,7 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin } // Invoke the script + _logger.LogTrace("Attempting to import the PFX"); var results = ps.Invoke(); // Get the last exist code returned from the script @@ -155,7 +194,7 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin int lastExitCode = 0; try { - lastExitCode = (int)ps.Runspace.SessionStateProxy.PSVariable.GetValue("c"); + lastExitCode = GetLastExitCode(results[^1].ToString()); _logger.LogTrace($"Last exit code: {lastExitCode}"); } catch (Exception) @@ -225,6 +264,30 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin } } + private int GetLastExitCode(string result) + { + // Split the string by colon + string[] parts = result.Split(':'); + + // Ensure the split result has the expected parts + if (parts.Length == 2 && parts[0] == "LASTEXITCODE") + { + // Parse the second part into an integer + if (int.TryParse(parts[1], out int lastExitCode)) + { + return lastExitCode; + } + else + { + throw new Exception("Failed to parse the LASTEXITCODE value."); + } + } + else + { + throw new Exception("The last element does not contain the expected format."); + } + } + public void RemoveCertificate(string thumbprint, string storePath) { using var ps = PowerShell.Create(); From 1a5353d56fca20aba9ef24a767d944d9c2b7203b Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 29 May 2024 11:55:41 -0500 Subject: [PATCH 3/5] Added store path and addstore for certs with no private keys --- IISU/ClientPSCertStoreManager.cs | 78 ++++++++++++++----- IISU/ImplementedStoreTypes/Win/Management.cs | 2 +- .../WinIIS/Management.cs | 2 +- .../WinSQL/Management.cs | 2 +- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 1a37864..a373c20 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -15,6 +15,7 @@ using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FlowAnalysis; using Microsoft.Extensions.Logging; using System; using System.IO; @@ -108,7 +109,7 @@ public void DeletePFXFile(string filePath, string fileName) } } - public JobResult ImportPFXFile(string filePath, string privateKeyPassword, string cryptoProviderName) + public JobResult ImportPFXFile(string filePath, string privateKeyPassword, string cryptoProviderName, string storePath) { try { @@ -118,30 +119,67 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin if (cryptoProviderName == null) { - string script = @" - param($pfxFilePath, $privateKeyPassword, $cspName) - $output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 - $c = $LASTEXITCODE - $output - "; + if (privateKeyPassword == null) + { + // If no private key password is provided, import the pfx file directory to the store using addstore argument + string script = @" + param($pfxFilePath, $storePath) + $output = certutil -addstore Cert:\LocalMachine\$storePath $pfxFilePath 2>&1 + $c = $LASTEXITCODE + $output + "; + + ps.AddScript(script); + ps.AddParameter("pfxFilePath", filePath); + ps.AddParameter("storePath", storePath); + } + else + { + // Use ImportPFX to import the pfx file with private key password to the appropriate cert store + string script = @" + param($pfxFilePath, $privateKeyPassword, $storePath) + $output = certutil -p $privateKeyPassword -importpfx Cert:\LocalMachine\$storePath $pfxFilePath 2>&1 + $c = $LASTEXITCODE + $output + "; - ps.AddScript(script); - ps.AddParameter("pfxFilePath", filePath); - ps.AddParameter("privateKeyPassword", privateKeyPassword); + ps.AddScript(script); + ps.AddParameter("pfxFilePath", filePath); + ps.AddParameter("privateKeyPassword", privateKeyPassword); + ps.AddParameter("storePath", storePath); + } } else { - string script = @" - param($pfxFilePath, $privateKeyPassword, $cspName) - $output = certutil -importpfx -csp $cspName -p $privateKeyPassword $pfxFilePath 2>&1 - $c = $LASTEXITCODE - $output - "; + if (privateKeyPassword == null) + { + string script = @" + param($pfxFilePath, $cspName, $storePath) + $output = certutil -csp $cspName -addstore LocalMachine\$storePath $pfxFilePath 2>&1 + $c = $LASTEXITCODE + $output + "; + + ps.AddScript(script); + ps.AddParameter("pfxFilePath", filePath); + ps.AddParameter("cspName", cryptoProviderName); + ps.AddParameter("storePath", storePath); + } + else + { + string script = @" + param($pfxFilePath, $privateKeyPassword, $cspName, $storePath) + $output = certutil -importpfx -csp $cspName -p $privateKeyPassword LocalMachine\$storePath $pfxFilePath 2>&1 + $c = $LASTEXITCODE + $output + "; - ps.AddScript(script); - ps.AddParameter("pfxFilePath", filePath); - ps.AddParameter("privateKeyPassword", privateKeyPassword); - ps.AddParameter("cspName", cryptoProviderName); + ps.AddScript(script); + ps.AddParameter("pfxFilePath", filePath); + ps.AddParameter("privateKeyPassword", privateKeyPassword); + ps.AddParameter("cspName", cryptoProviderName); + ps.AddParameter("storePath", storePath); + } } // Invoke the script diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index ba624ed..f251c7f 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -145,7 +145,7 @@ private JobResult performAddition(ManagementJobConfiguration config) // Using certutil on the remote computer, import the pfx file using a supplied csp if any. _logger.LogTrace($"Importing temporary PFX File: {filePath}."); - JobResult result = manager.ImportPFXFile(filePath, privateKeyPassword, cryptoProvider); + JobResult result = manager.ImportPFXFile(filePath, privateKeyPassword, cryptoProvider, storePath); // Delete the temporary file _logger.LogTrace($"Deleting temporary PFX File: {filePath}."); diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index b4a3c33..72efe5f 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -148,7 +148,7 @@ private JobResult PerformAddCertificate(ManagementJobConfiguration config, strin _logger.LogTrace($"{filePath} was created."); // Using certutil on the remote computer, import the pfx file using a supplied csp if any. - JobResult result = manager.ImportPFXFile(filePath, privateKeyPassword, cryptoProvider); + JobResult result = manager.ImportPFXFile(filePath, privateKeyPassword, cryptoProvider, storePath); // Delete the temporary file manager.DeletePFXFile(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath)); diff --git a/IISU/ImplementedStoreTypes/WinSQL/Management.cs b/IISU/ImplementedStoreTypes/WinSQL/Management.cs index 028c6a6..e304f1e 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Management.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Management.cs @@ -145,7 +145,7 @@ private JobResult PerformAddCertificate(ManagementJobConfiguration config, strin _logger.LogTrace($"{filePath} was created."); // Using certutil on the remote computer, import the pfx file using a supplied csp if any. - JobResult result = manager.ImportPFXFile(filePath, privateKeyPassword, cryptoProvider); + JobResult result = manager.ImportPFXFile(filePath, privateKeyPassword, cryptoProvider, storePath); // Delete the temporary file manager.DeletePFXFile(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath)); From 5be3649aae40c359098d209edea926dceda89e1a Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 31 May 2024 14:50:41 -0500 Subject: [PATCH 4/5] #ab58570 Added additional error trapping and logging. Also modified the certutil logic to use -addstore when no password was provided when adding a certificate. --- IISU/ClientPSCertStoreManager.cs | 39 ++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 069d071..df91f38 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -133,8 +133,17 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin // If no private key password is provided, import the pfx file directory to the store using addstore argument string script = @" param($pfxFilePath, $storePath) - $output = certutil -addstore Cert:\LocalMachine\$storePath $pfxFilePath 2>&1 - $c = $LASTEXITCODE + $output = certutil -addstore $storePath $pfxFilePath 2>&1 + $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" + + if ($output.GetType().Name -eq ""String"") + { + $output = @($output, $exit_message) + } + else + { + $output += $exit_message + } $output "; @@ -148,10 +157,9 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin string script = @" param($pfxFilePath, $privateKeyPassword) - $output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 + $output = certutil -importpfx -p $privateKeyPassword $storePath $pfxFilePath 2>&1 $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" $stuff = certutil -dump - if ($stuff.GetType().Name -eq ""String"") { $stuff = @($stuff, $exit_message) @@ -160,7 +168,6 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { $stuff += $exit_message } - $output $stuff "; @@ -177,9 +184,20 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { string script = @" param($pfxFilePath, $cspName, $storePath) - $output = certutil -csp $cspName -addstore LocalMachine\$storePath $pfxFilePath 2>&1 - $c = $LASTEXITCODE + $output = certutil -csp $cspName -addstore $storePath $pfxFilePath 2>&1 + $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" + + $stuff = certutil -dump + if ($stuff.GetType().Name -eq ""String"") + { + $stuff = @($stuff, $exit_message) + } + else + { + $stuff += $exit_message + } $output + $stuff "; ps.AddScript(script); @@ -191,10 +209,10 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { string script = @" param($pfxFilePath, $privateKeyPassword, $cspName) - $output = certutil -importpfx -csp $cspName -p $privateKeyPassword LocalMachine\$storePath $pfxFilePath 2>&1 + $output = certutil -importpfx -csp $cspName -p $privateKeyPassword $storePath $pfxFilePath 2>&1 $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" - $stuff = certutil -dump + $stuff = certutil -dump if ($stuff.GetType().Name -eq ""String"") { $stuff = @($stuff, $exit_message) @@ -203,7 +221,6 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { $stuff += $exit_message } - $output $stuff "; @@ -221,8 +238,6 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin var results = ps.Invoke(); // Get the last exist code returned from the script - // This statement is in a try/catch block because PSVariable.GetValue() is not a valid method on a remote PS Session and throws an exception. - // Due to security reasons and Windows architecture, retreiving values from a remote system is not supported. int lastExitCode = 0; try { From 278fa47254151ab3a2eb307f1e89819e9fb52ba7 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 31 May 2024 14:50:41 -0500 Subject: [PATCH 5/5] AB#58570 Added additional error trapping and logging. Also modified the certutil logic to use -addstore when no password was provided when adding a certificate. --- CHANGELOG.md | 4 ++++ IISU/ClientPSCertStoreManager.cs | 39 ++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7fd92..f4bda5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +2.4.1 +* Modified the CertUtil logic to use the -addstore argument when no password is sent with the certificate information. +* Added additional error trapping and trace logs + 2.4.0 * Changed the way certificates are added to cert stores. CertUtil is now used to import the PFX certificate into the associated store. The CSP is now considered when maintaining certificates, empty CSP values will result in using the machines default CSP. * Added the Crypto Service Provider and SAN Entry Parameters to be used on Inventory queries, Adding and ReEnrollments for the WinCert, WinSQL and IISU extensions. diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 069d071..df91f38 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -133,8 +133,17 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin // If no private key password is provided, import the pfx file directory to the store using addstore argument string script = @" param($pfxFilePath, $storePath) - $output = certutil -addstore Cert:\LocalMachine\$storePath $pfxFilePath 2>&1 - $c = $LASTEXITCODE + $output = certutil -addstore $storePath $pfxFilePath 2>&1 + $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" + + if ($output.GetType().Name -eq ""String"") + { + $output = @($output, $exit_message) + } + else + { + $output += $exit_message + } $output "; @@ -148,10 +157,9 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin string script = @" param($pfxFilePath, $privateKeyPassword) - $output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 + $output = certutil -importpfx -p $privateKeyPassword $storePath $pfxFilePath 2>&1 $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" $stuff = certutil -dump - if ($stuff.GetType().Name -eq ""String"") { $stuff = @($stuff, $exit_message) @@ -160,7 +168,6 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { $stuff += $exit_message } - $output $stuff "; @@ -177,9 +184,20 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { string script = @" param($pfxFilePath, $cspName, $storePath) - $output = certutil -csp $cspName -addstore LocalMachine\$storePath $pfxFilePath 2>&1 - $c = $LASTEXITCODE + $output = certutil -csp $cspName -addstore $storePath $pfxFilePath 2>&1 + $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" + + $stuff = certutil -dump + if ($stuff.GetType().Name -eq ""String"") + { + $stuff = @($stuff, $exit_message) + } + else + { + $stuff += $exit_message + } $output + $stuff "; ps.AddScript(script); @@ -191,10 +209,10 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { string script = @" param($pfxFilePath, $privateKeyPassword, $cspName) - $output = certutil -importpfx -csp $cspName -p $privateKeyPassword LocalMachine\$storePath $pfxFilePath 2>&1 + $output = certutil -importpfx -csp $cspName -p $privateKeyPassword $storePath $pfxFilePath 2>&1 $exit_message = ""LASTEXITCODE:$($LASTEXITCODE)"" - $stuff = certutil -dump + $stuff = certutil -dump if ($stuff.GetType().Name -eq ""String"") { $stuff = @($stuff, $exit_message) @@ -203,7 +221,6 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin { $stuff += $exit_message } - $output $stuff "; @@ -221,8 +238,6 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin var results = ps.Invoke(); // Get the last exist code returned from the script - // This statement is in a try/catch block because PSVariable.GetValue() is not a valid method on a remote PS Session and throws an exception. - // Due to security reasons and Windows architecture, retreiving values from a remote system is not supported. int lastExitCode = 0; try {