Skip to content

Commit bfc9fdc

Browse files
authored
Merge pull request #102 from Keyfactor/TestingBranch
ab#54472 - Multiple Updates to WinCert Orchestrator
2 parents 5e12d12 + 268c18b commit bfc9fdc

26 files changed

+653
-165
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
2.4.0
2+
* 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.
3+
* 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.
4+
* 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.
5+
* Updated the manifest.json to now include WinSQL ReEnrollment.
6+
* Updated the integration-manifest.json file for new fields in cert store types.
7+
8+
2.3.2
9+
* Changed the Open Cert Store access level from a '5' to 'MaxAllowed'
10+
111
2.3.1
212
* Added additional error trapping for WinRM connections to allow actual error on failure.
313

IISU/Certificate.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Linq;
17+
using System.Text.RegularExpressions;
1618

1719
namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore
1820
{
@@ -22,5 +24,35 @@ public class Certificate
2224
public byte[] RawData { get; set; }
2325
public bool HasPrivateKey { get; set; }
2426
public string CertificateData => Convert.ToBase64String(RawData);
27+
public string CryptoServiceProvider { get; set; }
28+
public string SAN { get; set; }
29+
30+
public class Utilities
31+
{
32+
public static string FormatSAN(string san)
33+
{
34+
// Use regular expression to extract key-value pairs
35+
var regex = new Regex(@"(?<key>DNS Name|Email|IP Address)=(?<value>[^=,\s]+)");
36+
var matches = regex.Matches(san);
37+
38+
// Format matches into the desired format
39+
string result = string.Join("&", matches.Cast<Match>()
40+
.Select(m => $"{NormalizeKey(m.Groups["key"].Value)}={m.Groups["value"].Value}"));
41+
42+
return result;
43+
}
44+
45+
private static string NormalizeKey(string key)
46+
{
47+
return key.ToLower() switch
48+
{
49+
"dns name" => "dns",
50+
"email" => "email",
51+
"ip address" => "ip",
52+
_ => key.ToLower() // For other types, keep them as-is
53+
};
54+
}
55+
56+
}
2557
}
2658
}

IISU/CertificateStore.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ public void RemoveCertificate(string thumbprint)
4141
{
4242
using var ps = PowerShell.Create();
4343
ps.Runspace = RunSpace;
44+
45+
// Open with value of 5 means: Open existing only (4) + Open ReadWrite (1)
4446
var removeScript = $@"
4547
$ErrorActionPreference = 'Stop'
4648
$certStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('{StorePath}','LocalMachine')
47-
$certStore.Open('MaxAllowed')
49+
$certStore.Open(5)
4850
$certToRemove = $certStore.Certificates.Find(0,'{thumbprint}',$false)
4951
if($certToRemove.Count -gt 0) {{
5052
$certStore.Remove($certToRemove[0])

IISU/ClientPSCertStoreInventory.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,37 @@ public List<Certificate> GetCertificatesFromStore(Runspace runSpace, string stor
4646
$certs = $certStore.Certificates
4747
$certStore.Close()
4848
$certStore.Dispose()
49-
foreach ( $cert in $certs){{
50-
$cert | Select-Object -Property Thumbprint, RawData, HasPrivateKey
49+
$certs | ForEach-Object {{
50+
$certDetails = @{{
51+
Subject = $_.Subject
52+
Thumbprint = $_.Thumbprint
53+
HasPrivateKey = $_.HasPrivateKey
54+
RawData = $_.RawData
55+
san = $_.Extensions | Where-Object {{ $_.Oid.FriendlyName -eq ""Subject Alternative Name"" }} | ForEach-Object {{ $_.Format($false) }}
56+
}}
57+
58+
if ($_.HasPrivateKey) {{
59+
$certDetails.CSP = $_.PrivateKey.CspKeyContainerInfo.ProviderName
60+
}}
61+
62+
New-Object PSObject -Property $certDetails
5163
}}";
5264

5365
ps.AddScript(certStoreScript);
5466

5567
var certs = ps.Invoke();
5668

5769
foreach (var c in certs)
70+
{
5871
myCertificates.Add(new Certificate
5972
{
6073
Thumbprint = $"{c.Properties["Thumbprint"]?.Value}",
6174
HasPrivateKey = bool.Parse($"{c.Properties["HasPrivateKey"]?.Value}"),
62-
RawData = (byte[])c.Properties["RawData"]?.Value
75+
RawData = (byte[])c.Properties["RawData"]?.Value,
76+
CryptoServiceProvider = $"{c.Properties["CSP"]?.Value }",
77+
SAN = Certificate.Utilities.FormatSAN($"{c.Properties["san"]?.Value}")
6378
});
79+
}
6480

6581
return myCertificates;
6682
}

IISU/ClientPSCertStoreManager.cs

Lines changed: 138 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
using Keyfactor.Logging;
1515
using Keyfactor.Orchestrators.Common.Enums;
1616
using Keyfactor.Orchestrators.Extensions;
17+
using Microsoft.CodeAnalysis;
1718
using Microsoft.Extensions.Logging;
1819
using System;
19-
using System.Linq;
20+
using System.IO;
2021
using System.Management.Automation;
2122
using System.Management.Automation.Runspaces;
2223
using System.Security.Cryptography.X509Certificates;
@@ -45,99 +46,174 @@ public ClientPSCertStoreManager(ILogger logger, Runspace runSpace, long jobNumbe
4546
_jobNumber = jobNumber;
4647
}
4748

48-
public JobResult AddCertificate(string certificateContents, string privateKeyPassword, string storePath)
49+
public string CreatePFXFile(string certificateContents, string privateKeyPassword)
4950
{
5051
try
5152
{
52-
using var ps = PowerShell.Create();
53-
54-
_logger.MethodEntry();
55-
56-
ps.Runspace = _runspace;
57-
58-
_logger.LogTrace($"Creating X509 Cert from: {certificateContents}");
53+
// Create the x509 certificate
5954
x509Cert = new X509Certificate2
6055
(
6156
Convert.FromBase64String(certificateContents),
6257
privateKeyPassword,
63-
X509KeyStorageFlags.MachineKeySet |
64-
X509KeyStorageFlags.PersistKeySet |
58+
X509KeyStorageFlags.MachineKeySet |
59+
X509KeyStorageFlags.PersistKeySet |
6560
X509KeyStorageFlags.Exportable
6661
);
6762

68-
_logger.LogDebug($"X509 Cert Created With Subject: {x509Cert.SubjectName}");
69-
_logger.LogDebug($"Begin Add for Cert Store {$@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}"}");
63+
using (PowerShell ps = PowerShell.Create())
64+
{
65+
ps.Runspace = _runspace;
66+
67+
// Add script to write certificate contents to a temporary file
68+
string script = @"
69+
param($certificateContents)
70+
$filePath = [System.IO.Path]::GetTempFileName() + '.pfx'
71+
[System.IO.File]::WriteAllBytes($filePath, [System.Convert]::FromBase64String($certificateContents))
72+
$filePath
73+
";
7074

71-
// Add Certificate
72-
var funcScript = @"
73-
$ErrorActionPreference = ""Stop""
75+
ps.AddScript(script);
76+
ps.AddParameter("certificateContents", certificateContents); // Convert.ToBase64String(x509Cert.Export(X509ContentType.Pkcs12)));
7477

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

81-
$certStore.Close();
82-
}";
81+
// Get the result (temporary file path) returned by the script
82+
return results[0].ToString();
83+
}
84+
}
85+
catch (Exception)
86+
{
87+
throw new Exception("An error occurred while attempting to create and write the X509 contents.");
88+
}
89+
}
8390

84-
ps.AddScript(funcScript).AddStatement();
85-
_logger.LogDebug("InstallPfxToMachineStore Statement Added...");
91+
public void DeletePFXFile(string filePath, string fileName)
92+
{
93+
using (PowerShell ps = PowerShell.Create())
94+
{
95+
ps.Runspace = _runspace;
8696

87-
ps.AddCommand("InstallPfxToMachineStore")
88-
.AddParameter("bytes", Convert.FromBase64String(certificateContents))
89-
.AddParameter("password", privateKeyPassword)
90-
.AddParameter("storeName", $@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}");
91-
92-
_logger.LogTrace("InstallPfxToMachineStore Command Added...");
97+
// Add script to delete the temporary file
98+
string deleteScript = @"
99+
param($filePath)
100+
Remove-Item -Path $filePath -Force
101+
";
93102

94-
foreach (var cmd in ps.Commands.Commands)
95-
{
96-
_logger.LogTrace("Logging PowerShell Command");
97-
_logger.LogTrace(cmd.CommandText);
98-
}
103+
ps.AddScript(deleteScript);
104+
ps.AddParameter("filePath", Path.Combine(filePath, fileName) + "*");
99105

100-
_logger.LogTrace("Invoking ps...");
101-
ps.Invoke();
102-
_logger.LogTrace("ps Invoked...");
106+
// Invoke the script to delete the file
107+
var results = ps.Invoke();
108+
}
109+
}
103110

104-
if (ps.HadErrors)
111+
public JobResult ImportPFXFile(string filePath, string privateKeyPassword, string cryptoProviderName)
112+
{
113+
try
114+
{
115+
using (PowerShell ps = PowerShell.Create())
105116
{
106-
_logger.LogTrace("ps Has Errors");
107-
var psError = ps.Streams.Error.ReadAll()
108-
.Aggregate(string.Empty, (current, error) => current + error?.ErrorDetails.Message);
117+
ps.Runspace = _runspace;
118+
119+
if (cryptoProviderName == null)
120+
{
121+
string script = @"
122+
param($pfxFilePath, $privateKeyPassword, $cspName)
123+
$output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1
124+
$c = $LASTEXITCODE
125+
$output
126+
";
127+
128+
ps.AddScript(script);
129+
ps.AddParameter("pfxFilePath", filePath);
130+
ps.AddParameter("privateKeyPassword", privateKeyPassword);
131+
}
132+
else
133+
{
134+
string script = @"
135+
param($pfxFilePath, $privateKeyPassword, $cspName)
136+
$output = certutil -importpfx -csp $cspName -p $privateKeyPassword $pfxFilePath 2>&1
137+
$c = $LASTEXITCODE
138+
$output
139+
";
140+
141+
ps.AddScript(script);
142+
ps.AddParameter("pfxFilePath", filePath);
143+
ps.AddParameter("privateKeyPassword", privateKeyPassword);
144+
ps.AddParameter("cspName", cryptoProviderName);
145+
}
146+
147+
// Invoke the script
148+
var results = ps.Invoke();
149+
150+
// Get the last exist code returned from the script
151+
// 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.
152+
// Due to security reasons and Windows architecture, retreiving values from a remote system is not supported.
153+
int lastExitCode = 0;
154+
try
155+
{
156+
lastExitCode = (int)ps.Runspace.SessionStateProxy.PSVariable.GetValue("c");
157+
}
158+
catch (Exception)
159+
{
160+
}
161+
162+
163+
bool isError = false;
164+
if (lastExitCode != 0)
165+
{
166+
isError = true;
167+
string outputMsg = "";
168+
169+
foreach (var result in results)
170+
{
171+
string outputLine = result.ToString();
172+
if (!string.IsNullOrEmpty(outputLine))
173+
{
174+
outputMsg += "\n" + outputLine;
175+
}
176+
}
177+
_logger.LogError(outputMsg);
178+
}
179+
else
180+
{
181+
// Check for errors in the output
182+
foreach (var result in results)
183+
{
184+
string outputLine = result.ToString();
185+
if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error"))
186+
{
187+
isError = true;
188+
_logger.LogError(outputLine);
189+
}
190+
}
191+
}
192+
193+
if (isError)
194+
{
195+
throw new Exception("Error occurred while attempting to import the pfx file.");
196+
}
197+
else
109198
{
110199
return new JobResult
111200
{
112-
Result = OrchestratorJobStatusJobResult.Failure,
201+
Result = OrchestratorJobStatusJobResult.Success,
113202
JobHistoryId = _jobNumber,
114-
FailureMessage =
115-
$"Site {storePath} on server {_runspace.ConnectionInfo.ComputerName}: {psError}"
203+
FailureMessage = ""
116204
};
117205
}
118206
}
119-
120-
_logger.LogTrace("Clearing Commands...");
121-
ps.Commands.Clear();
122-
_logger.LogTrace("Commands Cleared..");
123-
_logger.LogInformation($"Certificate was successfully added to cert store: {storePath}");
124-
125-
return new JobResult
126-
{
127-
Result = OrchestratorJobStatusJobResult.Success,
128-
JobHistoryId = _jobNumber,
129-
FailureMessage = ""
130-
};
131207
}
132208
catch (Exception e)
133209
{
134-
_logger.LogError($"Error Occurred in ClientPSCertStoreManager.AddCertificate(): {e.Message}");
210+
_logger.LogError($"Error Occurred in ClientPSCertStoreManager.ImportPFXFile(): {e.Message}");
135211

136212
return new JobResult
137213
{
138214
Result = OrchestratorJobStatusJobResult.Failure,
139215
JobHistoryId = _jobNumber,
140-
FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}"
216+
FailureMessage = $"Error Occurred in ImportPFXFile {LogHandler.FlattenException(e)}"
141217
};
142218
}
143219
}
@@ -150,10 +226,11 @@ public void RemoveCertificate(string thumbprint, string storePath)
150226

151227
ps.Runspace = _runspace;
152228

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

0 commit comments

Comments
 (0)