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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
167 changes: 142 additions & 25 deletions IISU/ClientPSCertStoreManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
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;
using System.Linq.Expressions;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Security.Cryptography.X509Certificates;
Expand Down Expand Up @@ -48,6 +50,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
Expand Down Expand Up @@ -79,11 +85,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.");
}
}
Expand All @@ -108,55 +116,137 @@ 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
{
_logger.LogTrace("Entering ImportPFX");

using (PowerShell ps = PowerShell.Create())
{
ps.Runspace = _runspace;

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 $storePath $pfxFilePath 2>&1
$exit_message = ""LASTEXITCODE:$($LASTEXITCODE)""

if ($output.GetType().Name -eq ""String"")
{
$output = @($output, $exit_message)
}
else
{
$output += $exit_message
}
$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)
$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)
}
else
{
$stuff += $exit_message
}
$output
$stuff
";

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 $storePath $pfxFilePath 2>&1
$exit_message = ""LASTEXITCODE:$($LASTEXITCODE)""

ps.AddScript(script);
ps.AddParameter("pfxFilePath", filePath);
ps.AddParameter("privateKeyPassword", privateKeyPassword);
ps.AddParameter("cspName", cryptoProviderName);
$stuff = certutil -dump
if ($stuff.GetType().Name -eq ""String"")
{
$stuff = @($stuff, $exit_message)
}
else
{
$stuff += $exit_message
}
$output
$stuff
";

ps.AddScript(script);
ps.AddParameter("pfxFilePath", filePath);
ps.AddParameter("cspName", cryptoProviderName);
ps.AddParameter("storePath", storePath);
}
else
{
string script = @"
param($pfxFilePath, $privateKeyPassword, $cspName)
$output = certutil -importpfx -csp $cspName -p $privateKeyPassword $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);
ps.AddParameter("pfxFilePath", filePath);
ps.AddParameter("privateKeyPassword", privateKeyPassword);
ps.AddParameter("cspName", cryptoProviderName);
ps.AddParameter("storePath", storePath);
}
}

// Invoke the script
_logger.LogTrace("Attempting to import the PFX");
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
{
lastExitCode = (int)ps.Runspace.SessionStateProxy.PSVariable.GetValue("c");
lastExitCode = GetLastExitCode(results[^1].ToString());
_logger.LogTrace($"Last exit code: {lastExitCode}");
}
catch (Exception)
{
_logger.LogTrace("Unable to get the last exit code.");
}


Expand All @@ -182,7 +272,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);
Expand Down Expand Up @@ -218,6 +311,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();
Expand Down
2 changes: 1 addition & 1 deletion IISU/ImplementedStoreTypes/Win/Management.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}.");
Expand Down
2 changes: 1 addition & 1 deletion IISU/ImplementedStoreTypes/WinIIS/Management.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion IISU/ImplementedStoreTypes/WinSQL/Management.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down