Skip to content

Commit

Permalink
Merge pull request #102 from Keyfactor/TestingBranch
Browse files Browse the repository at this point in the history
ab#54472 - Multiple Updates to WinCert Orchestrator
  • Loading branch information
fiddlermikey authored Apr 30, 2024
2 parents 5e12d12 + 268c18b commit bfc9fdc
Show file tree
Hide file tree
Showing 26 changed files with 653 additions and 165 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
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.
* Changed how Client Machine Names are handled when a 'localhost' connection is desiered. The new naming convention is: {machineName}|localmachine. This will eliminate the issue of unqiue naming conflicts.
* Updated the manifest.json to now include WinSQL ReEnrollment.
* Updated the integration-manifest.json file for new fields in cert store types.

2.3.2
* Changed the Open Cert Store access level from a '5' to 'MaxAllowed'

2.3.1
* Added additional error trapping for WinRM connections to allow actual error on failure.

Expand Down
32 changes: 32 additions & 0 deletions IISU/Certificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// limitations under the License.

using System;
using System.Linq;
using System.Text.RegularExpressions;

namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore
{
Expand All @@ -22,5 +24,35 @@ public class Certificate
public byte[] RawData { get; set; }
public bool HasPrivateKey { get; set; }
public string CertificateData => Convert.ToBase64String(RawData);
public string CryptoServiceProvider { get; set; }
public string SAN { get; set; }

public class Utilities
{
public static string FormatSAN(string san)
{
// Use regular expression to extract key-value pairs
var regex = new Regex(@"(?<key>DNS Name|Email|IP Address)=(?<value>[^=,\s]+)");
var matches = regex.Matches(san);

// Format matches into the desired format
string result = string.Join("&", matches.Cast<Match>()
.Select(m => $"{NormalizeKey(m.Groups["key"].Value)}={m.Groups["value"].Value}"));

return result;
}

private static string NormalizeKey(string key)
{
return key.ToLower() switch
{
"dns name" => "dns",
"email" => "email",
"ip address" => "ip",
_ => key.ToLower() // For other types, keep them as-is
};
}

}
}
}
4 changes: 3 additions & 1 deletion IISU/CertificateStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ public void RemoveCertificate(string thumbprint)
{
using var ps = PowerShell.Create();
ps.Runspace = RunSpace;

// Open with value of 5 means: Open existing only (4) + Open ReadWrite (1)
var removeScript = $@"
$ErrorActionPreference = 'Stop'
$certStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('{StorePath}','LocalMachine')
$certStore.Open('MaxAllowed')
$certStore.Open(5)
$certToRemove = $certStore.Certificates.Find(0,'{thumbprint}',$false)
if($certToRemove.Count -gt 0) {{
$certStore.Remove($certToRemove[0])
Expand Down
22 changes: 19 additions & 3 deletions IISU/ClientPSCertStoreInventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,37 @@ public List<Certificate> GetCertificatesFromStore(Runspace runSpace, string stor
$certs = $certStore.Certificates
$certStore.Close()
$certStore.Dispose()
foreach ( $cert in $certs){{
$cert | Select-Object -Property Thumbprint, RawData, HasPrivateKey
$certs | ForEach-Object {{
$certDetails = @{{
Subject = $_.Subject
Thumbprint = $_.Thumbprint
HasPrivateKey = $_.HasPrivateKey
RawData = $_.RawData
san = $_.Extensions | Where-Object {{ $_.Oid.FriendlyName -eq ""Subject Alternative Name"" }} | ForEach-Object {{ $_.Format($false) }}
}}
if ($_.HasPrivateKey) {{
$certDetails.CSP = $_.PrivateKey.CspKeyContainerInfo.ProviderName
}}
New-Object PSObject -Property $certDetails
}}";

ps.AddScript(certStoreScript);

var certs = ps.Invoke();

foreach (var c in certs)
{
myCertificates.Add(new Certificate
{
Thumbprint = $"{c.Properties["Thumbprint"]?.Value}",
HasPrivateKey = bool.Parse($"{c.Properties["HasPrivateKey"]?.Value}"),
RawData = (byte[])c.Properties["RawData"]?.Value
RawData = (byte[])c.Properties["RawData"]?.Value,
CryptoServiceProvider = $"{c.Properties["CSP"]?.Value }",
SAN = Certificate.Utilities.FormatSAN($"{c.Properties["san"]?.Value}")
});
}

return myCertificates;
}
Expand Down
199 changes: 138 additions & 61 deletions IISU/ClientPSCertStoreManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
using Keyfactor.Logging;
using Keyfactor.Orchestrators.Common.Enums;
using Keyfactor.Orchestrators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Security.Cryptography.X509Certificates;
Expand Down Expand Up @@ -45,99 +46,174 @@ public ClientPSCertStoreManager(ILogger logger, Runspace runSpace, long jobNumbe
_jobNumber = jobNumber;
}

public JobResult AddCertificate(string certificateContents, string privateKeyPassword, string storePath)
public string CreatePFXFile(string certificateContents, string privateKeyPassword)
{
try
{
using var ps = PowerShell.Create();

_logger.MethodEntry();

ps.Runspace = _runspace;

_logger.LogTrace($"Creating X509 Cert from: {certificateContents}");
// Create the x509 certificate
x509Cert = new X509Certificate2
(
Convert.FromBase64String(certificateContents),
privateKeyPassword,
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable
);

_logger.LogDebug($"X509 Cert Created With Subject: {x509Cert.SubjectName}");
_logger.LogDebug($"Begin Add for Cert Store {$@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}"}");
using (PowerShell ps = PowerShell.Create())
{
ps.Runspace = _runspace;

// Add script to write certificate contents to a temporary file
string script = @"
param($certificateContents)
$filePath = [System.IO.Path]::GetTempFileName() + '.pfx'
[System.IO.File]::WriteAllBytes($filePath, [System.Convert]::FromBase64String($certificateContents))
$filePath
";

// Add Certificate
var funcScript = @"
$ErrorActionPreference = ""Stop""
ps.AddScript(script);
ps.AddParameter("certificateContents", certificateContents); // Convert.ToBase64String(x509Cert.Export(X509ContentType.Pkcs12)));

function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$storeName) {
$certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, ""LocalMachine""
$certStore.Open(5)
$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $password, 18 <# Persist, Machine #>
$certStore.Add($cert)
// Invoke the script on the remote computer
var results = ps.Invoke();

$certStore.Close();
}";
// Get the result (temporary file path) returned by the script
return results[0].ToString();
}
}
catch (Exception)
{
throw new Exception("An error occurred while attempting to create and write the X509 contents.");
}
}

ps.AddScript(funcScript).AddStatement();
_logger.LogDebug("InstallPfxToMachineStore Statement Added...");
public void DeletePFXFile(string filePath, string fileName)
{
using (PowerShell ps = PowerShell.Create())
{
ps.Runspace = _runspace;

ps.AddCommand("InstallPfxToMachineStore")
.AddParameter("bytes", Convert.FromBase64String(certificateContents))
.AddParameter("password", privateKeyPassword)
.AddParameter("storeName", $@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}");

_logger.LogTrace("InstallPfxToMachineStore Command Added...");
// Add script to delete the temporary file
string deleteScript = @"
param($filePath)
Remove-Item -Path $filePath -Force
";

foreach (var cmd in ps.Commands.Commands)
{
_logger.LogTrace("Logging PowerShell Command");
_logger.LogTrace(cmd.CommandText);
}
ps.AddScript(deleteScript);
ps.AddParameter("filePath", Path.Combine(filePath, fileName) + "*");

_logger.LogTrace("Invoking ps...");
ps.Invoke();
_logger.LogTrace("ps Invoked...");
// Invoke the script to delete the file
var results = ps.Invoke();
}
}

if (ps.HadErrors)
public JobResult ImportPFXFile(string filePath, string privateKeyPassword, string cryptoProviderName)
{
try
{
using (PowerShell ps = PowerShell.Create())
{
_logger.LogTrace("ps Has Errors");
var psError = ps.Streams.Error.ReadAll()
.Aggregate(string.Empty, (current, error) => current + error?.ErrorDetails.Message);
ps.Runspace = _runspace;

if (cryptoProviderName == null)
{
string script = @"
param($pfxFilePath, $privateKeyPassword, $cspName)
$output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1
$c = $LASTEXITCODE
$output
";

ps.AddScript(script);
ps.AddParameter("pfxFilePath", filePath);
ps.AddParameter("privateKeyPassword", privateKeyPassword);
}
else
{
string script = @"
param($pfxFilePath, $privateKeyPassword, $cspName)
$output = certutil -importpfx -csp $cspName -p $privateKeyPassword $pfxFilePath 2>&1
$c = $LASTEXITCODE
$output
";

ps.AddScript(script);
ps.AddParameter("pfxFilePath", filePath);
ps.AddParameter("privateKeyPassword", privateKeyPassword);
ps.AddParameter("cspName", cryptoProviderName);
}

// Invoke the script
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");
}
catch (Exception)
{
}


bool isError = false;
if (lastExitCode != 0)
{
isError = true;
string outputMsg = "";

foreach (var result in results)
{
string outputLine = result.ToString();
if (!string.IsNullOrEmpty(outputLine))
{
outputMsg += "\n" + outputLine;
}
}
_logger.LogError(outputMsg);
}
else
{
// Check for errors in the output
foreach (var result in results)
{
string outputLine = result.ToString();
if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error"))
{
isError = true;
_logger.LogError(outputLine);
}
}
}

if (isError)
{
throw new Exception("Error occurred while attempting to import the pfx file.");
}
else
{
return new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
Result = OrchestratorJobStatusJobResult.Success,
JobHistoryId = _jobNumber,
FailureMessage =
$"Site {storePath} on server {_runspace.ConnectionInfo.ComputerName}: {psError}"
FailureMessage = ""
};
}
}

_logger.LogTrace("Clearing Commands...");
ps.Commands.Clear();
_logger.LogTrace("Commands Cleared..");
_logger.LogInformation($"Certificate was successfully added to cert store: {storePath}");

return new JobResult
{
Result = OrchestratorJobStatusJobResult.Success,
JobHistoryId = _jobNumber,
FailureMessage = ""
};
}
catch (Exception e)
{
_logger.LogError($"Error Occurred in ClientPSCertStoreManager.AddCertificate(): {e.Message}");
_logger.LogError($"Error Occurred in ClientPSCertStoreManager.ImportPFXFile(): {e.Message}");

return new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
JobHistoryId = _jobNumber,
FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}"
FailureMessage = $"Error Occurred in ImportPFXFile {LogHandler.FlattenException(e)}"
};
}
}
Expand All @@ -150,10 +226,11 @@ public void RemoveCertificate(string thumbprint, string storePath)

ps.Runspace = _runspace;

// Open with value of 5 means: Open existing only (4) + Open ReadWrite (1)
var removeScript = $@"
$ErrorActionPreference = 'Stop'
$certStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('{storePath}','LocalMachine')
$certStore.Open('MaxAllowed')
$certStore.Open(5)
$certToRemove = $certStore.Certificates.Find(0,'{thumbprint}',$false)
if($certToRemove.Count -gt 0) {{
$certStore.Remove($certToRemove[0])
Expand Down
Loading

0 comments on commit bfc9fdc

Please sign in to comment.