From 4d8e575afc36530bfebce184266451428f292728 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 29 Feb 2024 02:53:37 +0000 Subject: [PATCH 01/14] Initial changes for creating and importing pfx files to accept specific csps. --- IISU/Certificate.cs | 2 + IISU/ClientPSCertStoreInventory.cs | 21 ++- IISU/ClientPSCertStoreManager.cs | 143 +++++++++++++++++- IISU/ImplementedStoreTypes/Win/Management.cs | 37 ++++- .../ImplementedStoreTypes/Win/WinInventory.cs | 9 +- IISU/PSHelper.cs | 51 ++++++- IISU/WindowsCertStore.csproj | 7 +- .../Properties/launchSettings.json | 8 + WinCertTestConsole/WinCertTestConsole.csproj | 1 + WindowsCertStore.sln | 9 ++ 10 files changed, 271 insertions(+), 17 deletions(-) create mode 100644 WinCertTestConsole/Properties/launchSettings.json diff --git a/IISU/Certificate.cs b/IISU/Certificate.cs index 88f3612..e0270be 100644 --- a/IISU/Certificate.cs +++ b/IISU/Certificate.cs @@ -22,5 +22,7 @@ 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; } } } \ No newline at end of file diff --git a/IISU/ClientPSCertStoreInventory.cs b/IISU/ClientPSCertStoreInventory.cs index 25875ab..8b7bcaf 100644 --- a/IISU/ClientPSCertStoreInventory.cs +++ b/IISU/ClientPSCertStoreInventory.cs @@ -46,8 +46,19 @@ public List 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 + }} + + if ($_.HasPrivateKey) {{ + $certDetails.CSP = $_.PrivateKey.CspKeyContainerInfo.ProviderName + }} + + New-Object PSObject -Property $certDetails }}"; ps.AddScript(certStoreScript); @@ -55,12 +66,16 @@ public List GetCertificatesFromStore(Runspace runSpace, string stor 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 = $"{c.Properties["Subject"]?.Value}" }); + } return myCertificates; } diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 35d0cb8..ac45044 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -16,6 +16,7 @@ using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; using System; +using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -45,6 +46,145 @@ public ClientPSCertStoreManager(ILogger logger, Runspace runSpace, long jobNumbe _jobNumber = jobNumber; } + public string CreatePFXFile(string certificateContents, string privateKeyPassword) + { + try + { + // Create the x509 certificate + //x509Cert = new X509Certificate2 + // ( + // Convert.FromBase64String(certificateContents), + // privateKeyPassword, + // X509KeyStorageFlags.MachineKeySet | + // X509KeyStorageFlags.PersistKeySet | + // X509KeyStorageFlags.Exportable + // ); + + 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 + "; + + ps.AddScript(script); + ps.AddParameter("certificateContents", certificateContents); // Convert.ToBase64String(x509Cert.Export(X509ContentType.Pkcs12))); + + // Invoke the script on the remote computer + var results = ps.Invoke(); + + // 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."); + } + } + + public void DeletePFXFile(string filePath, string fileName) + { + using (PowerShell ps = PowerShell.Create()) + { + ps.Runspace = _runspace; + + // Add script to delete the temporary file + string deleteScript = @" + param($filePath) + Remove-Item -Path $filePath -Force + "; + + ps.AddScript(deleteScript); + ps.AddParameter("filePath", Path.Combine(filePath, fileName) + "*"); + + // Invoke the script to delete the file + var results = ps.Invoke(); + } + } + + public JobResult ImportPFXFile(string filePath, string privateKeyPassword, string cryptoProviderName) + { + try + { + 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 + $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 + $output + "; + + ps.AddScript(script); + ps.AddParameter("pfxFilePath", filePath); + ps.AddParameter("privateKeyPassword", privateKeyPassword); + ps.AddParameter("cspName", cryptoProviderName); + } + + // Invoke the script + var results = ps.Invoke(); + + // Check for errors in the output + bool isError = false; + foreach (var result in results) + { + string outputLine = result.ToString(); + if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error")) + { + isError = true; + Console.WriteLine(outputLine); // Print the error message + } + } + + if (isError) + { + throw new Exception("Error occurred while attempting to import the pfx file."); + } + else + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Success, + JobHistoryId = _jobNumber, + FailureMessage = "" + }; + } + } + } + catch (Exception e) + { + _logger.LogError($"Error Occurred in ClientPSCertStoreManager.ImportPFXFile(): {e.Message}"); + + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = _jobNumber, + FailureMessage = $"Error Occurred in ImportPFXFile {LogHandler.FlattenException(e)}" + }; + } + } + public JobResult AddCertificate(string certificateContents, string privateKeyPassword, string storePath) { try @@ -65,9 +205,6 @@ public JobResult AddCertificate(string certificateContents, string privateKeyPas X509KeyStorageFlags.Exportable ); - _logger.LogDebug($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); - _logger.LogDebug($"Begin Add for Cert Store {$@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}"}"); - // Add Certificate var funcScript = @" $ErrorActionPreference = ""Stop"" diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index dbd6ed6..d765792 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -23,6 +23,7 @@ using System.Management.Automation; using System.Net; using Keyfactor.Logging; +using System.IO; namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinCert { @@ -114,26 +115,46 @@ private JobResult performAddition(ManagementJobConfiguration config) { try { +#nullable enable string certificateContents = config.JobCertificate.Contents; string privateKeyPassword = config.JobCertificate.PrivateKeyPassword; string storePath = config.CertificateStoreDetails.StorePath; long jobNumber = config.JobHistoryId; + string? cryptoProvider = config.JobProperties["ProviderName"]?.ToString(); +#nullable disable + + // If a crypto provider was provided, check to see if it exists + if (cryptoProvider != null) + { + _logger.LogInformation($"Checking the server for the crypto provider: {cryptoProvider}"); + if (!PsHelper.IsCSPFound(PsHelper.GetCSPList(myRunspace), cryptoProvider)) + { throw new Exception($"The Crypto Profider: {cryptoProvider} was not found. Please check the Crypto Provider provided."); } + } if (storePath != null) { _logger.LogInformation($"Attempting to add certificate to cert store: {storePath}"); - + ClientPSCertStoreManager manager = new ClientPSCertStoreManager(_logger, myRunspace, jobNumber); - return manager.AddCertificate(certificateContents, privateKeyPassword, storePath); + + // Write the certificate contents to a temporary file on the remote computer, returning the filename. + string filePath = manager.CreatePFXFile(certificateContents, privateKeyPassword); + _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); + + // Delete the temporary file + manager.DeletePFXFile(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath)); + + return result; + + // This method is being retired + // return manager.AddCertificate(certificateContents, privateKeyPassword, storePath); } else { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = "Store Path is empty or null." - }; + throw new Exception($"The store pathis empty or null."); } } catch (Exception e) diff --git a/IISU/ImplementedStoreTypes/Win/WinInventory.cs b/IISU/ImplementedStoreTypes/Win/WinInventory.cs index 0e4542c..33b8d65 100644 --- a/IISU/ImplementedStoreTypes/Win/WinInventory.cs +++ b/IISU/ImplementedStoreTypes/Win/WinInventory.cs @@ -33,6 +33,13 @@ public List GetInventoryItems(Runspace runSpace, string st foreach (Certificate cert in base.GetCertificatesFromStore(runSpace, storePath)) { + var entryParms = new Dictionary + { + { "ProviderName", cert.CryptoServiceProvider }, + { "SAN", cert.SAN } + }; + + inventoryItems.Add(new CurrentInventoryItem { Certificates = new[] { cert.CertificateData }, @@ -40,7 +47,7 @@ public List GetInventoryItems(Runspace runSpace, string st PrivateKeyEntry = cert.HasPrivateKey, UseChainLevel = false, ItemStatus = OrchestratorInventoryItemStatus.Unknown, - Parameters = null + Parameters = entryParms }); } diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 7a8464a..202c38b 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -15,6 +15,8 @@ using Keyfactor.Logging; using Microsoft.Extensions.Logging; using System; +using System.Collections; +using System.Collections.Generic; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Net; @@ -54,8 +56,55 @@ public static Runspace GetClientPsRunspace(string winRmProtocol, string clientMa PowerShellProcessInstance instance = new PowerShellProcessInstance(new Version(5, 1), null, null, false); Runspace rs = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(Array.Empty()), instance); - + return rs; } + + public static IEnumerable GetCSPList(Runspace myRunspace) + { + _logger.LogTrace("Getting the list of Crypto Service Providers"); + + using var ps = PowerShell.Create(); + + ps.Runspace = myRunspace; + + var certStoreScript = $@" + $certUtilOutput = certutil -csplist + + $cspInfoList = @() + foreach ($line in $certUtilOutput) {{ + if ($line -match ""Provider Name:"") {{ + $cspName = ($line -split "":"")[1].Trim() + $cspInfoList += $cspName + }} + }} + + $cspInfoList"; + + ps.AddScript(certStoreScript); + + foreach (var result in ps.Invoke()) + { + var cspName = result?.BaseObject?.ToString(); + if (cspName != null) { yield return cspName; } + } + + _logger.LogInformation("No Crypto Service Providers were found"); + yield return null; + } + + public static bool IsCSPFound(IEnumerable cspList, string userCSP) + { + foreach (var csp in cspList) + { + if (string.Equals(csp, userCSP, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogTrace($"CSP found: {csp}"); + return true; + } + } + _logger.LogTrace($"CSP: {userCSP} was not found"); + return false; + } } } diff --git a/IISU/WindowsCertStore.csproj b/IISU/WindowsCertStore.csproj index fb4cca1..d6d1c51 100644 --- a/IISU/WindowsCertStore.csproj +++ b/IISU/WindowsCertStore.csproj @@ -12,6 +12,11 @@ false + + none + false + + @@ -26,7 +31,7 @@ - + diff --git a/WinCertTestConsole/Properties/launchSettings.json b/WinCertTestConsole/Properties/launchSettings.json new file mode 100644 index 0000000..33504c9 --- /dev/null +++ b/WinCertTestConsole/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/WinCertTestConsole/WinCertTestConsole.csproj b/WinCertTestConsole/WinCertTestConsole.csproj index 319c554..504e735 100644 --- a/WinCertTestConsole/WinCertTestConsole.csproj +++ b/WinCertTestConsole/WinCertTestConsole.csproj @@ -3,6 +3,7 @@ Exe net6.0 + AnyCPU diff --git a/WindowsCertStore.sln b/WindowsCertStore.sln index 28117ce..883ef0b 100644 --- a/WindowsCertStore.sln +++ b/WindowsCertStore.sln @@ -39,17 +39,26 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|x64.ActiveCfg = Debug|Any CPU + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|x64.Build.0 = Debug|Any CPU {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|Any CPU.ActiveCfg = Release|Any CPU {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|Any CPU.Build.0 = Release|Any CPU + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|x64.ActiveCfg = Release|x64 + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|x64.Build.0 = Release|x64 {D0F4A3CC-5236-4393-9C97-AE55ACE319F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0F4A3CC-5236-4393-9C97-AE55ACE319F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0F4A3CC-5236-4393-9C97-AE55ACE319F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0F4A3CC-5236-4393-9C97-AE55ACE319F2}.Debug|x64.Build.0 = Debug|Any CPU {D0F4A3CC-5236-4393-9C97-AE55ACE319F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0F4A3CC-5236-4393-9C97-AE55ACE319F2}.Release|Any CPU.Build.0 = Release|Any CPU + {D0F4A3CC-5236-4393-9C97-AE55ACE319F2}.Release|x64.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9be2ad68fc92d2081aa65e38f508cbbe4e08d9bf Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 14 Mar 2024 01:05:17 +0000 Subject: [PATCH 02/14] Added ReEnrollment jobs to WinSQL --- CHANGELOG.md | 4 + IISU/Certificate.cs | 30 +++++++ IISU/ClientPSCertStoreInventory.cs | 3 +- IISU/ClientPSCertStoreManager.cs | 19 ++--- IISU/ClientPSCertStoreReEnrollment.cs | 46 +++++++---- IISU/ClientPsSqlManager.cs | 5 ++ IISU/ImplementedStoreTypes/Win/Management.cs | 6 +- .../ImplementedStoreTypes/Win/ReEnrollment.cs | 2 +- .../ImplementedStoreTypes/Win/WinInventory.cs | 1 - .../WinIIS/Management.cs | 73 ++++++++++++++--- .../WinIIS/ReEnrollment.cs | 2 +- .../WinIIS/WinIISInventory.cs | 4 +- .../WinSQL/Management.cs | 79 +++++++++++++++---- .../WinSQL/ReEnrollment.cs | 44 +++++++++++ .../WinSQL/SQLServerInventory.cs | 7 +- IISU/WinCertJobTypeBase.cs | 7 ++ readme_source.md | 3 +- 17 files changed, 269 insertions(+), 66 deletions(-) create mode 100644 IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed6e38..d789fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +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 and IISU extensions. The CSP was added for WinSWL for Management Add jobs only. + 2.3.1 * Added additional error trapping for WinRM connections to allow actual error on failure. diff --git a/IISU/Certificate.cs b/IISU/Certificate.cs index e0270be..81b89a4 100644 --- a/IISU/Certificate.cs +++ b/IISU/Certificate.cs @@ -13,6 +13,8 @@ // limitations under the License. using System; +using System.Linq; +using System.Text.RegularExpressions; namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore { @@ -24,5 +26,33 @@ public class Certificate 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(@"(?DNS Name|Email|IP Address)=(?[^=,\s]+)"); + var matches = regex.Matches(san); + + // Format matches into the desired format + string result = string.Join("&", matches.Cast() + .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 + }; + } + + } } } \ No newline at end of file diff --git a/IISU/ClientPSCertStoreInventory.cs b/IISU/ClientPSCertStoreInventory.cs index 8b7bcaf..82a6365 100644 --- a/IISU/ClientPSCertStoreInventory.cs +++ b/IISU/ClientPSCertStoreInventory.cs @@ -52,6 +52,7 @@ public List GetCertificatesFromStore(Runspace runSpace, string stor Thumbprint = $_.Thumbprint HasPrivateKey = $_.HasPrivateKey RawData = $_.RawData + san = $_.Extensions | Where-Object {{ $_.Oid.FriendlyName -eq ""Subject Alternative Name"" }} | ForEach-Object {{ $_.Format($false) }} }} if ($_.HasPrivateKey) {{ @@ -73,7 +74,7 @@ public List GetCertificatesFromStore(Runspace runSpace, string stor HasPrivateKey = bool.Parse($"{c.Properties["HasPrivateKey"]?.Value}"), RawData = (byte[])c.Properties["RawData"]?.Value, CryptoServiceProvider = $"{c.Properties["CSP"]?.Value }", - SAN = $"{c.Properties["Subject"]?.Value}" + SAN = Certificate.Utilities.FormatSAN($"{c.Properties["san"]?.Value}") }); } diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index ac45044..f65d60d 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -51,14 +51,14 @@ public string CreatePFXFile(string certificateContents, string privateKeyPasswor try { // Create the x509 certificate - //x509Cert = new X509Certificate2 - // ( - // Convert.FromBase64String(certificateContents), - // privateKeyPassword, - // X509KeyStorageFlags.MachineKeySet | - // X509KeyStorageFlags.PersistKeySet | - // X509KeyStorageFlags.Exportable - // ); + x509Cert = new X509Certificate2 + ( + Convert.FromBase64String(certificateContents), + privateKeyPassword, + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable + ); using (PowerShell ps = PowerShell.Create()) { @@ -185,6 +185,7 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin } } + [Obsolete("This method is no longer used. Certificates are added by creating and importing PFX files.", false)] public JobResult AddCertificate(string certificateContents, string privateKeyPassword, string storePath) { try @@ -211,7 +212,7 @@ public JobResult AddCertificate(string certificateContents, string privateKeyPas function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$storeName) { $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, ""LocalMachine"" - $certStore.Open(5) + $certStore.Open('MaxAllowed') $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $password, 18 <# Persist, Machine #> $certStore.Add($cert) diff --git a/IISU/ClientPSCertStoreReEnrollment.cs b/IISU/ClientPSCertStoreReEnrollment.cs index d971c55..85ce1ad 100644 --- a/IISU/ClientPSCertStoreReEnrollment.cs +++ b/IISU/ClientPSCertStoreReEnrollment.cs @@ -44,7 +44,7 @@ public ClientPSCertStoreReEnrollment(ILogger logger, IPAMSecretResolver resolver _resolver = resolver; } - public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReenrollment, bool bindCertificate) + public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReenrollment, CertStoreBindingTypeENUM bindingType) { bool hasError = false; @@ -218,21 +218,37 @@ public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submit ps.Commands.Clear(); runSpace.Close(); - JobResult result; + // Default results + JobResult result = new JobResult + { + Result = OrchestratorJobStatusJobResult.Success, + JobHistoryId = config.JobHistoryId, + FailureMessage = "" + }; + + // Do specific bindings + switch (bindingType) + { + case CertStoreBindingTypeENUM.WinIIS: + // Bind the certificate to IIS + ClientPSIIManager iisManager = new ClientPSIIManager(config, serverUserName, serverPassword); + result = iisManager.BindCertificate(myCert); + // Provide logging information + if (result.Result == OrchestratorJobStatusJobResult.Success) { _logger.LogInformation("Certificate was successfully bound to the IIS Server."); } + else { _logger.LogInformation("There was an issue while attempting to bind the certificate to the IIS Server. Check the logs for more information."); } + break; + + case CertStoreBindingTypeENUM.WinSQL: + + // Bind to SQL Server + ClientPsSqlManager sqlManager = new ClientPsSqlManager(config, serverUserName, serverPassword); + result = sqlManager.BindCertificates("", myCert); + + // Provide logging information + if (result.Result == OrchestratorJobStatusJobResult.Success) { _logger.LogInformation("Certificate was successfully bound to the SQL Server."); } + else { _logger.LogInformation("There was an issue while attempting to bind the certificate to the SQL Server. Check the logs for more information."); } + break; - if (bindCertificate) - { - // Bind the certificate to IIS - ClientPSIIManager iisManager = new ClientPSIIManager(config, serverUserName, serverPassword); - result = iisManager.BindCertificate(myCert); - }else - { - result = new JobResult - { - Result = OrchestratorJobStatusJobResult.Success, - JobHistoryId = config.JobHistoryId, - FailureMessage = "" - }; } ps.Commands.Clear(); diff --git a/IISU/ClientPsSqlManager.cs b/IISU/ClientPsSqlManager.cs index 9132ef5..45a485a 100644 --- a/IISU/ClientPsSqlManager.cs +++ b/IISU/ClientPsSqlManager.cs @@ -95,6 +95,11 @@ public ClientPsSqlManager(InventoryJobConfiguration config,Runspace runSpace) } } + public ClientPsSqlManager(ReenrollmentJobConfiguration config, string serverUsername, string serverPassword) + { + _logger = LogHandler.GetClassLogger(); + } + public JobResult UnBindCertificate() { try diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index d765792..5d00dc7 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -128,12 +128,12 @@ private JobResult performAddition(ManagementJobConfiguration config) { _logger.LogInformation($"Checking the server for the crypto provider: {cryptoProvider}"); if (!PsHelper.IsCSPFound(PsHelper.GetCSPList(myRunspace), cryptoProvider)) - { throw new Exception($"The Crypto Profider: {cryptoProvider} was not found. Please check the Crypto Provider provided."); } + { throw new Exception($"The Crypto Profider: {cryptoProvider} was not found. Please check the spelling and accuracy of the Crypto Provider Name provided. If unsure which provider to use, leave the field blank and the default crypto provider will be used."); } } if (storePath != null) { - _logger.LogInformation($"Attempting to add certificate to cert store: {storePath}"); + _logger.LogInformation($"Attempting to add WinCert certificate to cert store: {storePath}"); ClientPSCertStoreManager manager = new ClientPSCertStoreManager(_logger, myRunspace, jobNumber); @@ -149,7 +149,7 @@ private JobResult performAddition(ManagementJobConfiguration config) return result; - // This method is being retired + // This method is retired // return manager.AddCertificate(certificateContents, privateKeyPassword, storePath); } else diff --git a/IISU/ImplementedStoreTypes/Win/ReEnrollment.cs b/IISU/ImplementedStoreTypes/Win/ReEnrollment.cs index a261438..cf9abc6 100644 --- a/IISU/ImplementedStoreTypes/Win/ReEnrollment.cs +++ b/IISU/ImplementedStoreTypes/Win/ReEnrollment.cs @@ -34,7 +34,7 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm _logger = LogHandler.GetClassLogger(typeof(ReEnrollment)); ClientPSCertStoreReEnrollment myReEnrollment = new ClientPSCertStoreReEnrollment(_logger, _resolver); - return myReEnrollment.PerformReEnrollment(config, submitReenrollmentUpdate, false); + return myReEnrollment.PerformReEnrollment(config, submitReenrollmentUpdate, CertStoreBindingTypeENUM.None); } } diff --git a/IISU/ImplementedStoreTypes/Win/WinInventory.cs b/IISU/ImplementedStoreTypes/Win/WinInventory.cs index 33b8d65..6332e1a 100644 --- a/IISU/ImplementedStoreTypes/Win/WinInventory.cs +++ b/IISU/ImplementedStoreTypes/Win/WinInventory.cs @@ -39,7 +39,6 @@ public List GetInventoryItems(Runspace runSpace, string st { "SAN", cert.SAN } }; - inventoryItems.Add(new CurrentInventoryItem { Certificates = new[] { cert.CertificateData }, diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index e9bd38d..b4a3c33 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -114,23 +115,69 @@ public JobResult ProcessJob(ManagementJobConfiguration config) private JobResult PerformAddCertificate(ManagementJobConfiguration config, string serverUsername, string serverPassword) { - _logger.LogTrace("Before PerformAddition..."); + try + { +#nullable enable + string certificateContents = config.JobCertificate.Contents; + string privateKeyPassword = config.JobCertificate.PrivateKeyPassword; + string storePath = config.CertificateStoreDetails.StorePath; + long jobNumber = config.JobHistoryId; + string? cryptoProvider = config.JobProperties["ProviderName"]?.ToString(); +#nullable disable - string certificateContents = config.JobCertificate.Contents; - string privateKeyPassword = config.JobCertificate.PrivateKeyPassword; - string storePath = config.CertificateStoreDetails.StorePath; - long jobNumber = config.JobHistoryId; + // If a crypto provider was provided, check to see if it exists + if (cryptoProvider != null) + { + _logger.LogInformation($"Checking the server for the crypto provider: {cryptoProvider}"); + if (!PsHelper.IsCSPFound(PsHelper.GetCSPList(myRunspace), cryptoProvider)) + { throw new Exception($"The Crypto Profider: {cryptoProvider} was not found. Please check the spelling and accuracy of the Crypto Provider Name provided. If unsure which provider to use, leave the field blank and the default crypto provider will be used."); } + } - ClientPSCertStoreManager manager = new ClientPSCertStoreManager(_logger, myRunspace, jobNumber); - JobResult result = manager.AddCertificate(certificateContents, privateKeyPassword, storePath); + if (storePath != null) + { + _logger.LogInformation($"Attempting to add IISU certificate to cert store: {storePath}"); + } - if (result.Result == OrchestratorJobStatusJobResult.Success) + ClientPSCertStoreManager manager = new ClientPSCertStoreManager(_logger, myRunspace, jobNumber); + + // This method is retired + //JobResult result = manager.AddCertificate(certificateContents, privateKeyPassword, storePath); + + // Write the certificate contents to a temporary file on the remote computer, returning the filename. + string filePath = manager.CreatePFXFile(certificateContents, privateKeyPassword); + _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); + + // Delete the temporary file + manager.DeletePFXFile(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath)); + + if (result.Result == OrchestratorJobStatusJobResult.Success) + { + // Bind to IIS + _logger.LogInformation("Attempting to bind certificate to website."); + ClientPSIIManager iisManager = new ClientPSIIManager(config, serverUsername, serverPassword); + result = iisManager.BindCertificate(manager.X509Cert); + + // Provide logging information + if (result.Result == OrchestratorJobStatusJobResult.Success) { _logger.LogInformation("Certificate was successfully bound to the website."); } + else { _logger.LogInformation("There was an issue while attempting to bind the certificate to the website. Check the logs for more information."); } + + return result; + } + else return result; + } + catch (Exception e) { - // Bind to IIS - ClientPSIIManager iisManager = new ClientPSIIManager(config, serverUsername, serverPassword); - result = iisManager.BindCertificate(manager.X509Cert); - return result; - } else return result; + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = + $"Management/Add {e.Message}" + }; + } } private JobResult PerformRemoveCertificate(ManagementJobConfiguration config, string serverUsername, string serverPassword) diff --git a/IISU/ImplementedStoreTypes/WinIIS/ReEnrollment.cs b/IISU/ImplementedStoreTypes/WinIIS/ReEnrollment.cs index 4307725..8c07b0b 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/ReEnrollment.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/ReEnrollment.cs @@ -37,7 +37,7 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm _logger = LogHandler.GetClassLogger(typeof(ReEnrollment)); ClientPSCertStoreReEnrollment myReEnrollment = new ClientPSCertStoreReEnrollment(_logger, _resolver); - return myReEnrollment.PerformReEnrollment(config, submitReEnrollmentUpdate, true); + return myReEnrollment.PerformReEnrollment(config, submitReEnrollmentUpdate, CertStoreBindingTypeENUM.WinIIS); } } diff --git a/IISU/ImplementedStoreTypes/WinIIS/WinIISInventory.cs b/IISU/ImplementedStoreTypes/WinIIS/WinIISInventory.cs index 457cdcf..f88f046 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/WinIISInventory.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/WinIISInventory.cs @@ -104,7 +104,9 @@ public List GetInventoryItems(Runspace runSpace, string st { "IPAddress", binding.Properties["Bindings"]?.Value.ToString()?.Split(':')[0] }, { "HostName", binding.Properties["Bindings"]?.Value.ToString()?.Split(':')[2] }, { "SniFlag", sniValue }, - { "Protocol", binding.Properties["Protocol"]?.Value } + { "Protocol", binding.Properties["Protocol"]?.Value }, + { "ProviderName", foundCert.CryptoServiceProvider }, + { "SAN", foundCert.SAN } }; myBoundCerts.Add( diff --git a/IISU/ImplementedStoreTypes/WinSQL/Management.cs b/IISU/ImplementedStoreTypes/WinSQL/Management.cs index aaa5d8b..028c6a6 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Management.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Management.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.IO; using System.Management.Automation.Runspaces; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; @@ -111,31 +112,75 @@ private JobResult PerformAddCertificate(ManagementJobConfiguration config, strin { _logger.LogTrace("Before PerformAddition..."); - string certificateContents = config.JobCertificate.Contents; - string privateKeyPassword = config.JobCertificate.PrivateKeyPassword; - string storePath = config.CertificateStoreDetails.StorePath; - long jobNumber = config.JobHistoryId; - - ClientPSCertStoreManager manager = new ClientPSCertStoreManager(_logger, myRunspace, jobNumber); - JobResult result = manager.AddCertificate(certificateContents, privateKeyPassword, storePath); - - if (result.Result == OrchestratorJobStatusJobResult.Success) + try { +#nullable enable + string certificateContents = config.JobCertificate.Contents; + string privateKeyPassword = config.JobCertificate.PrivateKeyPassword; + string storePath = config.CertificateStoreDetails.StorePath; + long jobNumber = config.JobHistoryId; + string? cryptoProvider = config.JobProperties["ProviderName"]?.ToString(); +#nullable disable - if (config.JobProperties.ContainsKey("RenewalThumbprint")) + // If a crypto provider was provided, check to see if it exists + if (cryptoProvider != null) { - RenewalThumbprint = config.JobProperties["RenewalThumbprint"].ToString(); - _logger.LogTrace($"Found Thumbprint Will Renew all Certs with this thumbprint: {RenewalThumbprint}"); + _logger.LogInformation($"Checking the server for the crypto provider: {cryptoProvider}"); + if (!PsHelper.IsCSPFound(PsHelper.GetCSPList(myRunspace), cryptoProvider)) + { throw new Exception($"The Crypto Profider: {cryptoProvider} was not found. Please check the spelling and accuracy of the Crypto Provider Name provided. If unsure which provider to use, leave the field blank and the default crypto provider will be used."); } } - // Bind to SQL Server - ClientPsSqlManager sqlManager = new ClientPsSqlManager(config, serverUsername, serverPassword); - result = sqlManager.BindCertificates(RenewalThumbprint,manager.X509Cert); - return result; + if (storePath != null) + { + _logger.LogInformation($"Attempting to add WinSql certificate to cert store: {storePath}"); + } + + ClientPSCertStoreManager manager = new ClientPSCertStoreManager(_logger, myRunspace, jobNumber); + + // This method is retired + //JobResult result = manager.AddCertificate(certificateContents, privateKeyPassword, storePath); + + // Write the certificate contents to a temporary file on the remote computer, returning the filename. + string filePath = manager.CreatePFXFile(certificateContents, privateKeyPassword); + _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); + // Delete the temporary file + manager.DeletePFXFile(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath)); - } else return result; + if (result.Result == OrchestratorJobStatusJobResult.Success) + { + + if (config.JobProperties.ContainsKey("RenewalThumbprint")) + { + RenewalThumbprint = config.JobProperties["RenewalThumbprint"].ToString(); + _logger.LogTrace($"Found Thumbprint Will Renew all Certs with this thumbprint: {RenewalThumbprint}"); + } + + // Bind to SQL Server + ClientPsSqlManager sqlManager = new ClientPsSqlManager(config, serverUsername, serverPassword); + result = sqlManager.BindCertificates(RenewalThumbprint, manager.X509Cert); + + // Provide logging information + if (result.Result == OrchestratorJobStatusJobResult.Success) { _logger.LogInformation("Certificate was successfully bound to the SQL Server."); } + else { _logger.LogInformation("There was an issue while attempting to bind the certificate to the SQL Server. Check the logs for more information."); } + + return result; + } + else return result; + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = + $"Management/Add {e.Message}" + }; + } } private JobResult PerformRemoveCertificate(ManagementJobConfiguration config, string serverUsername, string serverPassword) diff --git a/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs b/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs new file mode 100644 index 0000000..38540c1 --- /dev/null +++ b/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs @@ -0,0 +1,44 @@ +// Copyright 2022 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinSQL +{ + public class ReEnrollment : WinCertJobTypeBase, IReenrollmentJobExtension + { + private ILogger _logger; + private string RenewalThumbprint; + + public string ExtensionName => string.Empty; + + public ReEnrollment(IPAMSecretResolver resolver) + { + _resolver = resolver; + } + + public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReEnrollmentUpdate) + { + _logger = LogHandler.GetClassLogger(typeof(ReEnrollment)); + + ClientPSCertStoreReEnrollment myReEnrollment = new ClientPSCertStoreReEnrollment(_logger, _resolver); + + // SQL ReEnrollment performs a different type of binding. Set the bindcertificate to false and call SQL Binding + return myReEnrollment.PerformReEnrollment(config, submitReEnrollmentUpdate, CertStoreBindingTypeENUM.WinSQL); + } + } +} diff --git a/IISU/ImplementedStoreTypes/WinSQL/SQLServerInventory.cs b/IISU/ImplementedStoreTypes/WinSQL/SQLServerInventory.cs index 49b2cb3..dc290fa 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/SQLServerInventory.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/SQLServerInventory.cs @@ -86,9 +86,10 @@ public List GetInventoryItems(Runspace runSpace, Inventory if (foundCert == null) continue; var sqlSettingsDict = new Dictionary - { - { "InstanceName", kp.Value.ToString() } - }; + { + { "InstanceName", kp.Value.ToString() }, + { "ProviderName", foundCert.CryptoServiceProvider } + }; myBoundCerts.Add( new CurrentInventoryItem diff --git a/IISU/WinCertJobTypeBase.cs b/IISU/WinCertJobTypeBase.cs index d6bcd17..0baf794 100644 --- a/IISU/WinCertJobTypeBase.cs +++ b/IISU/WinCertJobTypeBase.cs @@ -20,4 +20,11 @@ public abstract class WinCertJobTypeBase { public IPAMSecretResolver _resolver; } + + public enum CertStoreBindingTypeENUM + { + None, + WinIIS, + WinSQL + } } diff --git a/readme_source.md b/readme_source.md index 50c296f..039221d 100644 --- a/readme_source.md +++ b/readme_source.md @@ -136,7 +136,7 @@ Short Name| WinSql | Short display name for the store type Custom Capability | Leave Unchecked | Store type name orchestrator will register with. Check the box to allow entry of value Supported Job Types | Inventory, Add, Remove | Job types the extension supports Needs Server | Checked | Determines if a target server name is required when creating store -Blueprint Allowed | Unchecked | Determines if store type may be included in an Orchestrator blueprint +Blueprint Allowed | Checked | Determines if store type may be included in an Orchestrator blueprint Uses PowerShell | Unchecked | Determines if underlying implementation is PowerShell Requires Store Password | Unchecked | Determines if a store password is required when configuring an individual store. Supports Entry Password | Unchecked | Determines if an individual entry within a store can have a password. @@ -184,6 +184,7 @@ They are typically used to support binding of a certificate to a resource. Name|Display Name| Type|Default Value|Required When|Description ---|---|---|---|---|--- InstanceName | Instance Name|String||Not required | When enrolling leave blank or use MSSQLServer for the Default Instance, Instance Name for an Instance or MSSQLServer,Instance Name if enrolling to multiple instances plus the default instance. +ProviderName | Crypto Provider Name | String ||| Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server. ![](images/SQLServerEntryParams.png) From fc7d525f227d05e5e1045dbfcd8247434bac058f Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Mon, 18 Mar 2024 22:55:12 +0000 Subject: [PATCH 03/14] Update generated README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 11df329..a448e3f 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ Short Name| WinSql | Short display name for the store type Custom Capability | Leave Unchecked | Store type name orchestrator will register with. Check the box to allow entry of value Supported Job Types | Inventory, Add, Remove | Job types the extension supports Needs Server | Checked | Determines if a target server name is required when creating store -Blueprint Allowed | Unchecked | Determines if store type may be included in an Orchestrator blueprint +Blueprint Allowed | Checked | Determines if store type may be included in an Orchestrator blueprint Uses PowerShell | Unchecked | Determines if underlying implementation is PowerShell Requires Store Password | Unchecked | Determines if a store password is required when configuring an individual store. Supports Entry Password | Unchecked | Determines if an individual entry within a store can have a password. @@ -284,6 +284,7 @@ They are typically used to support binding of a certificate to a resource. Name|Display Name| Type|Default Value|Required When|Description ---|---|---|---|---|--- InstanceName | Instance Name|String||Not required | When enrolling leave blank or use MSSQLServer for the Default Instance, Instance Name for an Instance or MSSQLServer,Instance Name if enrolling to multiple instances plus the default instance. +ProviderName | Crypto Provider Name | String ||| Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server. ![](images/SQLServerEntryParams.png) From 5e8ec1ac5396fd2aaed2b8e57bd85c674c7be461 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Tue, 26 Mar 2024 14:00:40 -0500 Subject: [PATCH 04/14] Updated some documentation. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d789fe0..4f27cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ * 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 and IISU extensions. The CSP was added for WinSWL for Management Add jobs only. +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. 2.3.0 From 56396f112268138bb8582ce7fa83a703785c236e Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Tue, 26 Mar 2024 14:01:28 -0500 Subject: [PATCH 05/14] Added additional exception handleing for SQL Management jobs. Updated the manifest for SQL ReEnrollment. --- CHANGELOG.md | 6 ++- IISU/ClientPsSqlManager.cs | 4 ++ .../WinSQL/ReEnrollment.cs | 1 - IISU/PSHelper.cs | 41 +++++++++++++------ IISU/manifest.json | 4 ++ 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f27cfd..ea7fd92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,14 @@ 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 and IISU extensions. The CSP was added for WinSWL for Management Add jobs only. +* 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. 2.3.0 diff --git a/IISU/ClientPsSqlManager.cs b/IISU/ClientPsSqlManager.cs index 45a485a..627116f 100644 --- a/IISU/ClientPsSqlManager.cs +++ b/IISU/ClientPsSqlManager.cs @@ -187,6 +187,10 @@ public string GetSqlInstanceValue(string instanceName,PowerShell ps) } return null; } + catch (ArgumentOutOfRangeException ex) + { + throw new Exception($"There were no SQL instances with the name: {instanceName}. Please check the spelling of the SQL instance."); + } catch (Exception e) { throw new Exception($"Error when initiating getting instance name from registry: {e.Message}", e.InnerException); diff --git a/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs b/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs index 38540c1..fc2e623 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs @@ -22,7 +22,6 @@ namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinSQL public class ReEnrollment : WinCertJobTypeBase, IReenrollmentJobExtension { private ILogger _logger; - private string RenewalThumbprint; public string ExtensionName => string.Empty; diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 202c38b..cc51b29 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -32,8 +32,35 @@ public static Runspace GetClientPsRunspace(string winRmProtocol, string clientMa _logger = LogHandler.GetClassLogger(); _logger.MethodEntry(); - if (clientMachineName.ToLower() != "localhost") - + // 2.4 - Client Machine Name now follows the naming conventions of {clientMachineName}|{localMachine} + // If the clientMachineName is just 'localhost', it will maintain that as locally only (as previosuly) + // If there is no 2nd part to the clientMachineName, a remote PowerShell session will be created + + // Break the clientMachineName into parts + string[] parts = clientMachineName.Split('|'); + + // Extract the client machine name and arguments based upon the number of parts + string machineName = parts.Length > 1 ? parts[0] : clientMachineName; + string argument = parts.Length > 1 ? parts[1] : null; + + // Determine if this is truely a local connection + bool isLocal = (machineName.ToLower() == "localhost") || (argument != null && argument.ToLower() == "localmachine"); + + _logger.LogInformation($"Full clientMachineName={clientMachineName} | machineName={machineName} | argument={argument} | isLocal={isLocal}"); + + if (isLocal) + { + // Create an out of process PowerShell runspace and explictly use version 5.1 + // This is needed when running as a service, which is how the orchestrator extension operates + // Interestingly this is not needd when running as a console application + // TODO: Consider refactoring this so that we properly dispose of these objects instead of waiting on the GC + + PowerShellProcessInstance instance = new PowerShellProcessInstance(new Version(5, 1), null, null, false); + Runspace rs = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(Array.Empty()), instance); + + return rs; + } + else { var connInfo = new WSManConnectionInfo(new Uri($"{winRmProtocol}://{clientMachineName}:{winRmPort}/wsman")); connInfo.IncludePortInSPN = includePortInSpn; @@ -48,16 +75,6 @@ public static Runspace GetClientPsRunspace(string winRmProtocol, string clientMa } return RunspaceFactory.CreateRunspace(connInfo); } - - // Create an out of process PowerShell runspace and explictly use version 5.1 - // This is needed when running as a service, which is how the orchestrator extension operates - // Interestingly this is not needd when running as a console application - // TODO: Consider refactoring this so that we properly dispose of these objects instead of waiting on the GC - - PowerShellProcessInstance instance = new PowerShellProcessInstance(new Version(5, 1), null, null, false); - Runspace rs = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(Array.Empty()), instance); - - return rs; } public static IEnumerable GetCSPList(Runspace myRunspace) diff --git a/IISU/manifest.json b/IISU/manifest.json index d7c7c64..c13cfe7 100644 --- a/IISU/manifest.json +++ b/IISU/manifest.json @@ -32,6 +32,10 @@ "CertStores.WinSql.Management": { "assemblypath": "WindowsCertStore.dll", "TypeFullName": "Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinSql.Management" + }, + "CertStores.WinSql.ReEnrollment": { + "assemblypath": "WindowsCertStore.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinSql.ReEnrollment" } } } From 316e4ec71c6140a413b58c1a69f7d853b89cb1eb Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 27 Mar 2024 16:26:04 -0500 Subject: [PATCH 06/14] Changed 'MaxAllowed' back to '5' and documented what that value represents. Cleaned up some old code. --- IISU/CertificateStore.cs | 4 +- IISU/ClientPSCertStoreManager.cs | 99 +------------------ IISU/ClientPsSqlManager.cs | 31 +++++- .../WinSQL/ReEnrollment.cs | 2 +- integration-manifest.json | 30 +++++- 5 files changed, 65 insertions(+), 101 deletions(-) diff --git a/IISU/CertificateStore.cs b/IISU/CertificateStore.cs index 25c284a..0ea35e6 100644 --- a/IISU/CertificateStore.cs +++ b/IISU/CertificateStore.cs @@ -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]) diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index f65d60d..6809857 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -17,7 +17,6 @@ using Microsoft.Extensions.Logging; using System; using System.IO; -using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Security.Cryptography.X509Certificates; @@ -185,101 +184,6 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin } } - [Obsolete("This method is no longer used. Certificates are added by creating and importing PFX files.", false)] - public JobResult AddCertificate(string certificateContents, string privateKeyPassword, string storePath) - { - try - { - using var ps = PowerShell.Create(); - - _logger.MethodEntry(); - - ps.Runspace = _runspace; - - _logger.LogTrace($"Creating X509 Cert from: {certificateContents}"); - x509Cert = new X509Certificate2 - ( - Convert.FromBase64String(certificateContents), - privateKeyPassword, - X509KeyStorageFlags.MachineKeySet | - X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable - ); - - // Add Certificate - var funcScript = @" - $ErrorActionPreference = ""Stop"" - - function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$storeName) { - $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, ""LocalMachine"" - $certStore.Open('MaxAllowed') - $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $password, 18 <# Persist, Machine #> - $certStore.Add($cert) - - $certStore.Close(); - }"; - - ps.AddScript(funcScript).AddStatement(); - _logger.LogDebug("InstallPfxToMachineStore Statement Added..."); - - ps.AddCommand("InstallPfxToMachineStore") - .AddParameter("bytes", Convert.FromBase64String(certificateContents)) - .AddParameter("password", privateKeyPassword) - .AddParameter("storeName", $@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}"); - - _logger.LogTrace("InstallPfxToMachineStore Command Added..."); - - foreach (var cmd in ps.Commands.Commands) - { - _logger.LogTrace("Logging PowerShell Command"); - _logger.LogTrace(cmd.CommandText); - } - - _logger.LogTrace("Invoking ps..."); - ps.Invoke(); - _logger.LogTrace("ps Invoked..."); - - if (ps.HadErrors) - { - _logger.LogTrace("ps Has Errors"); - var psError = ps.Streams.Error.ReadAll() - .Aggregate(string.Empty, (current, error) => current + error?.ErrorDetails.Message); - { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = _jobNumber, - FailureMessage = - $"Site {storePath} on server {_runspace.ConnectionInfo.ComputerName}: {psError}" - }; - } - } - - _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}"); - - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = _jobNumber, - FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" - }; - } - } - public void RemoveCertificate(string thumbprint, string storePath) { using var ps = PowerShell.Create(); @@ -288,10 +192,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]) diff --git a/IISU/ClientPsSqlManager.cs b/IISU/ClientPsSqlManager.cs index 627116f..b36084f 100644 --- a/IISU/ClientPsSqlManager.cs +++ b/IISU/ClientPsSqlManager.cs @@ -22,7 +22,9 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; +using System.Net; using System.Security.Cryptography.X509Certificates; +using System.Web.Services.Description; namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore { @@ -35,6 +37,7 @@ internal class ClientPsSqlManager private string RenewalThumbprint { get; set; } = ""; private string ClientMachineName { get; set; } private long JobHistoryID { get; set; } + private readonly ILogger _logger; private readonly Runspace _runSpace; @@ -91,13 +94,39 @@ public ClientPsSqlManager(InventoryJobConfiguration config,Runspace runSpace) } catch (Exception e) { - throw new Exception($"Error when initiating a SQL Management Job: {e.Message}", e.InnerException); + throw new Exception($"Error when initiating a SQL Inventory Job: {e.Message}", e.InnerException); } } public ClientPsSqlManager(ReenrollmentJobConfiguration config, string serverUsername, string serverPassword) { _logger = LogHandler.GetClassLogger(); + + try + { + ClientMachineName = config.CertificateStoreDetails.ClientMachine; + JobHistoryID = config.JobHistoryId; + + if (config.JobProperties.ContainsKey("InstanceName")) + { + var instanceRef = config.JobProperties["InstanceName"]?.ToString(); + SqlInstanceName = string.IsNullOrEmpty(instanceRef) ? "MSSQLSERVER" : instanceRef; + } + + // Establish PowerShell Runspace + var jobProperties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + string winRmProtocol = jobProperties.WinRmProtocol; + string winRmPort = jobProperties.WinRmPort; + bool includePortInSPN = jobProperties.SpnPortFlag; + RestartService = jobProperties.RestartService; + + _logger.LogTrace($"Establishing runspace on client machine: {ClientMachineName}"); + _runSpace = PsHelper.GetClientPsRunspace(winRmProtocol, ClientMachineName, winRmPort, includePortInSPN, serverUsername, serverPassword); + } + catch (Exception e) + { + throw new Exception($"Error when initiating a SQL ReEnrollment Job: {e.Message}", e.InnerException); + } } public JobResult UnBindCertificate() diff --git a/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs b/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs index fc2e623..c4e178f 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs @@ -17,7 +17,7 @@ using Keyfactor.Orchestrators.Extensions.Interfaces; using Microsoft.Extensions.Logging; -namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinSQL +namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinSql { public class ReEnrollment : WinCertJobTypeBase, IReenrollmentJobExtension { diff --git a/integration-manifest.json b/integration-manifest.json index 0745fd6..87833f4 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -305,7 +305,7 @@ "HasPrivateKey": false, "OnAdd": false, "OnRemove": false, - "OnReenrollment": false + "OnReenrollment": true }, "DependsOn": "", "DefaultValue": "", @@ -397,6 +397,34 @@ "OnRemove": false, "OnReenrollment": false } + }, + { + "Name": "ProviderName", + "DisplayName": "Crypto Provider Name", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "", + "Options": "" + }, + { + "Name": "SAN", + "DisplayName": "SAN", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": true + }, + "DependsOn": "", + "DefaultValue": "", + "Options": "" } ], "PasswordOptions": { From 5384f6d66cd942a7adcefe25af48efd567bfe962 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 27 Mar 2024 17:35:50 -0500 Subject: [PATCH 07/14] Updated the readme-source documentation --- readme_source.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/readme_source.md b/readme_source.md index 039221d..46da11f 100644 --- a/readme_source.md +++ b/readme_source.md @@ -45,7 +45,7 @@ For customers wishing to use something other than the local administrator accoun - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. ## Creating New Certificate Store Types -Currently this orchestrator handles two extensions: IISU for IIS servers with bound certificates and WinCert for general Windows Certificates. +Currently this orchestrator handles three types of extensions: IISU for IIS servers with bound certificates, WinCert for general Windows Certificates and WinSql for managing certificates for SQL Server. Below describes how each of these certificate store types are created and configured.
IISU Extension @@ -134,7 +134,7 @@ CONFIG ELEMENT | VALUE | DESCRIPTION Name | Windows SQL Server Certificate| Display name for the store type (may be customized) Short Name| WinSql | Short display name for the store type Custom Capability | Leave Unchecked | Store type name orchestrator will register with. Check the box to allow entry of value -Supported Job Types | Inventory, Add, Remove | Job types the extension supports +Supported Job Types | Inventory, Add, Remove, Reenrollment | Job types the extension supports Needs Server | Checked | Determines if a target server name is required when creating store Blueprint Allowed | Checked | Determines if store type may be included in an Orchestrator blueprint Uses PowerShell | Unchecked | Determines if underlying implementation is PowerShell @@ -185,6 +185,7 @@ Name|Display Name| Type|Default Value|Required When|Description ---|---|---|---|---|--- InstanceName | Instance Name|String||Not required | When enrolling leave blank or use MSSQLServer for the Default Instance, Instance Name for an Instance or MSSQLServer,Instance Name if enrolling to multiple instances plus the default instance. ProviderName | Crypto Provider Name | String ||| Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server. +SAN | SAN | String || Reenrolling | Specifies Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Certificate templates generally require a SAN that matches the subject of the certificate (per RFC 2818). Format is a list of = entries separated by ampersands. Examples: 'dns=www.mysite.com' for a single SAN or 'dns=www.mysite.com&dns=www.mysite2.com' for multiple SANs. Can be made optional if RFC 2818 is disabled on the CA. ![](images/SQLServerEntryParams.png) @@ -262,6 +263,8 @@ Click Save to save the Certificate Store Type. ## Creating New Certificate Stores Once the Certificate Store Types have been created, you need to create the Certificate Stores prior to using the extension. + +**Note:** A new naming convention for the Client Machine allows for multiple stores on the same server with different cert store path and cert store types. This convention is \{MachineName\}\|\{[optional]localmachine\}. If the optional value is 'localmachine' (legacy 'localhost' is still supported) is supplied, a local PowerShell runspace executing in the context of the Orchestrator service account will be used to access the certificate store. Here are the settings required for each Store Type previously configured.
@@ -274,7 +277,7 @@ CONFIG ELEMENT |DESCRIPTION ----------------|--------------- Category | Select IIS Bound Certificate or the customized certificate store display name from above. Container | Optional container to associate certificate store with. -Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is 'localhost', a local PowerShell runspace executing in the context of the Orchestrator service account will be used to access the certificate store and perform IIS binding operations. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. +Client Machine | Contains the Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. Store Path | Windows certificate store to manage. Choose "My" for the Personal Store or "WebHosting" for the Web Hosting Store. Orchestrator | Select an approved orchestrator capable of managing IIS Bound Certificates (one that has declared the IISU capability) WinRm Protocol | Protocol to use when establishing the WinRM session. (Listener on Client Machine must be configured for selected protocol.) @@ -326,7 +329,7 @@ CONFIG ELEMENT |DESCRIPTION ----------------|--------------- Category | Select Windows Certificate or the customized certificate store display name from above. Container | Optional container to associate certificate store with. -Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is 'localhost', a local PowerShell runspace executing in the context of the Orchestrator service account will be used to access the certificate store. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. +Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. Store Path | Windows certificate store to manage. Store must exist in the Local Machine store on the target server. Orchestrator | Select an approved orchestrator capable of managing Windows Certificates (one that has declared the WinCert capability) WinRm Protocol | Protocol to use when establishing the WinRM session. (Listener on Client Machine must be configured for selected protocol.) From 8fbc6c7cf7c87d03a022906ebabe903a9c07d2f3 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Wed, 27 Mar 2024 22:36:20 +0000 Subject: [PATCH 08/14] Update generated README --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a448e3f..2da5062 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ For customers wishing to use something other than the local administrator accoun - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. ## Creating New Certificate Store Types -Currently this orchestrator handles two extensions: IISU for IIS servers with bound certificates and WinCert for general Windows Certificates. +Currently this orchestrator handles three types of extensions: IISU for IIS servers with bound certificates, WinCert for general Windows Certificates and WinSql for managing certificates for SQL Server. Below describes how each of these certificate store types are created and configured.
IISU Extension @@ -234,7 +234,7 @@ CONFIG ELEMENT | VALUE | DESCRIPTION Name | Windows SQL Server Certificate| Display name for the store type (may be customized) Short Name| WinSql | Short display name for the store type Custom Capability | Leave Unchecked | Store type name orchestrator will register with. Check the box to allow entry of value -Supported Job Types | Inventory, Add, Remove | Job types the extension supports +Supported Job Types | Inventory, Add, Remove, Reenrollment | Job types the extension supports Needs Server | Checked | Determines if a target server name is required when creating store Blueprint Allowed | Checked | Determines if store type may be included in an Orchestrator blueprint Uses PowerShell | Unchecked | Determines if underlying implementation is PowerShell @@ -285,6 +285,7 @@ Name|Display Name| Type|Default Value|Required When|Description ---|---|---|---|---|--- InstanceName | Instance Name|String||Not required | When enrolling leave blank or use MSSQLServer for the Default Instance, Instance Name for an Instance or MSSQLServer,Instance Name if enrolling to multiple instances plus the default instance. ProviderName | Crypto Provider Name | String ||| Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server. +SAN | SAN | String || Reenrolling | Specifies Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Certificate templates generally require a SAN that matches the subject of the certificate (per RFC 2818). Format is a list of = entries separated by ampersands. Examples: 'dns=www.mysite.com' for a single SAN or 'dns=www.mysite.com&dns=www.mysite2.com' for multiple SANs. Can be made optional if RFC 2818 is disabled on the CA. ![](images/SQLServerEntryParams.png) @@ -362,6 +363,8 @@ Click Save to save the Certificate Store Type. ## Creating New Certificate Stores Once the Certificate Store Types have been created, you need to create the Certificate Stores prior to using the extension. + +**Note:** A new naming convention for the Client Machine allows for multiple stores on the same server with different cert store path and cert store types. This convention is \{MachineName\}\|\{[optional]localmachine\}. If the optional value is 'localmachine' (legacy 'localhost' is still supported) is supplied, a local PowerShell runspace executing in the context of the Orchestrator service account will be used to access the certificate store. Here are the settings required for each Store Type previously configured.
@@ -374,7 +377,7 @@ CONFIG ELEMENT |DESCRIPTION ----------------|--------------- Category | Select IIS Bound Certificate or the customized certificate store display name from above. Container | Optional container to associate certificate store with. -Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is 'localhost', a local PowerShell runspace executing in the context of the Orchestrator service account will be used to access the certificate store and perform IIS binding operations. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. +Client Machine | Contains the Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. Store Path | Windows certificate store to manage. Choose "My" for the Personal Store or "WebHosting" for the Web Hosting Store. Orchestrator | Select an approved orchestrator capable of managing IIS Bound Certificates (one that has declared the IISU capability) WinRm Protocol | Protocol to use when establishing the WinRM session. (Listener on Client Machine must be configured for selected protocol.) @@ -426,7 +429,7 @@ CONFIG ELEMENT |DESCRIPTION ----------------|--------------- Category | Select Windows Certificate or the customized certificate store display name from above. Container | Optional container to associate certificate store with. -Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is 'localhost', a local PowerShell runspace executing in the context of the Orchestrator service account will be used to access the certificate store. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. +Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. Store Path | Windows certificate store to manage. Store must exist in the Local Machine store on the target server. Orchestrator | Select an approved orchestrator capable of managing Windows Certificates (one that has declared the WinCert capability) WinRm Protocol | Protocol to use when establishing the WinRM session. (Listener on Client Machine must be configured for selected protocol.) From d9d4417dae1aebded81d4f54de769c95b6752bc6 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Tue, 16 Apr 2024 12:20:39 -0700 Subject: [PATCH 09/14] Modified how runspaces are created for local machines access . Added code to get the last exit code from certutil to accurrately report errors --- IISU/ClientPSCertStoreManager.cs | 36 ++++++++++++++++++++++++++------ IISU/PSHelper.cs | 10 +-------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 6809857..f462a25 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -14,6 +14,7 @@ using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using System; using System.IO; @@ -120,6 +121,7 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin string script = @" param($pfxFilePath, $privateKeyPassword, $cspName) $output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 + $c = $LASTEXITCODE $output "; @@ -132,6 +134,7 @@ 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 $output "; @@ -144,15 +147,36 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin // Invoke the script var results = ps.Invoke(); - // Check for errors in the output + //Get the last exist code returned from the script + int lastExitCode = (int)(ps.Runspace.SessionStateProxy.PSVariable.GetValue("c")); + bool isError = false; - foreach (var result in results) + 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 { - string outputLine = result.ToString(); - if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error")) + // Check for errors in the output + foreach (var result in results) { - isError = true; - Console.WriteLine(outputLine); // Print the error message + string outputLine = result.ToString(); + if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error")) + { + isError = true; + _logger.LogError(outputLine); + } } } diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index cc51b29..7d2990e 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -50,15 +50,7 @@ public static Runspace GetClientPsRunspace(string winRmProtocol, string clientMa if (isLocal) { - // Create an out of process PowerShell runspace and explictly use version 5.1 - // This is needed when running as a service, which is how the orchestrator extension operates - // Interestingly this is not needd when running as a console application - // TODO: Consider refactoring this so that we properly dispose of these objects instead of waiting on the GC - - PowerShellProcessInstance instance = new PowerShellProcessInstance(new Version(5, 1), null, null, false); - Runspace rs = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(Array.Empty()), instance); - - return rs; + return RunspaceFactory.CreateRunspace(); } else { From 4b7b26e09234665b337fde503eb20e5fc9571dd2 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 17 Apr 2024 06:18:17 -0700 Subject: [PATCH 10/14] Improved some trace log information. --- IISU/ImplementedStoreTypes/Win/Management.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/IISU/ImplementedStoreTypes/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index 5d00dc7..ba624ed 100644 --- a/IISU/ImplementedStoreTypes/Win/Management.cs +++ b/IISU/ImplementedStoreTypes/Win/Management.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License.using Keyfactor.Logging; +// Ignore Spelling: Keyfactor + using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Keyfactor.Orchestrators.Extensions.Interfaces; @@ -128,7 +130,7 @@ private JobResult performAddition(ManagementJobConfiguration config) { _logger.LogInformation($"Checking the server for the crypto provider: {cryptoProvider}"); if (!PsHelper.IsCSPFound(PsHelper.GetCSPList(myRunspace), cryptoProvider)) - { throw new Exception($"The Crypto Profider: {cryptoProvider} was not found. Please check the spelling and accuracy of the Crypto Provider Name provided. If unsure which provider to use, leave the field blank and the default crypto provider will be used."); } + { throw new Exception($"The Crypto Provider: {cryptoProvider} was not found. Please check the spelling and accuracy of the Crypto Provider Name provided. If unsure which provider to use, leave the field blank and the default crypto provider will be used."); } } if (storePath != null) @@ -138,23 +140,22 @@ private JobResult performAddition(ManagementJobConfiguration config) ClientPSCertStoreManager manager = new ClientPSCertStoreManager(_logger, myRunspace, jobNumber); // Write the certificate contents to a temporary file on the remote computer, returning the filename. + _logger.LogTrace($"Creating temporary pfx file."); string filePath = manager.CreatePFXFile(certificateContents, privateKeyPassword); - _logger.LogTrace($"{filePath} was created."); // 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); // Delete the temporary file + _logger.LogTrace($"Deleting temporary PFX File: {filePath}."); manager.DeletePFXFile(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath)); return result; - - // This method is retired - // return manager.AddCertificate(certificateContents, privateKeyPassword, storePath); } else { - throw new Exception($"The store pathis empty or null."); + throw new Exception($"The store path is empty or null."); } } catch (Exception e) From 3eb72af2091d0b84864aec721e12698571e3dfdf Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 17 Apr 2024 18:20:46 -0500 Subject: [PATCH 11/14] Added error trapping when attepting to get the last exit code from a remote machine. Microsoft does not allow you to retrieve information from remote computers due to security and architecture. --- IISU/ClientPSCertStoreManager.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index f462a25..1a37864 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -147,8 +147,18 @@ public JobResult ImportPFXFile(string filePath, string privateKeyPassword, strin // Invoke the script var results = ps.Invoke(); - //Get the last exist code returned from the script - int lastExitCode = (int)(ps.Runspace.SessionStateProxy.PSVariable.GetValue("c")); + // 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) From e6f2f1fbd4262f64c7fa975f865e8cf0fd86f468 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 22 Apr 2024 13:20:31 -0500 Subject: [PATCH 12/14] Corrected some misspelled words in the ReadMe. --- readme_source.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/readme_source.md b/readme_source.md index 46da11f..c5c4a2e 100644 --- a/readme_source.md +++ b/readme_source.md @@ -9,7 +9,7 @@ The returned list will contain the actual certificate store name to be used when By default, most certificates are stored in the “Personal” (My) and “Web Hosting” (WebHosting) stores. -This extension implements four job types: Inventory, Management Add/Remove, and ReEnrollment. +This extension implements four job types: Inventory, Management Add/Remove, and Reenrollment. WinRM is used to remotely manage the certificate stores and IIS bindings. WinRM must be properly configured to allow the orchestrator on the server to manage the certificates. Setting up WinRM is not in the scope of this document. @@ -21,7 +21,7 @@ In version 2.0 of the IIS Orchestrator, the certificate store type has been rena **Note: There is an additional (and deprecated) certificate store type of “IIS” that ships with the Keyfactor platform. Migration of certificate stores from the “IIS” type to either the “IISBin” or “IISU” types is not currently supported.** -**Note: If Looking to use GMSA Accounts to run the Service Kefyactor Command 10.2 or greater is required for No Value checkbox to work** +**Note: If Looking to use GMSA Accounts to run the Service Keyfactor Command 10.2 or greater is required for No Value checkbox to work** ## Security and Permission Considerations From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. @@ -34,7 +34,7 @@ For customers wishing to use something other than the local administrator accoun * WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. -* Even though a given account may be in the administrators group or have administrative privledges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. +* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. * The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. @@ -374,12 +374,12 @@ Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot ----|------------------------|------------------------------------|--------------|----------------|------------------------- -1 |New Cert Enrollment To Default Instance Leave Blank|**Intance Name:** |Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase1.gif) -2 |New Cert Enrollment To Default Instance MSSQLServer|**Intance Name:** MSSQLServer|Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase2.gif) -3 |New Cert Enrollment To Instance1|**Intance Name:** Instance1|Cert will be Installed to Instance1, Service will be restarted for Instance1|True|![](images/SQLTestCase3.gif) -4 |New Cert Enrollment To Instance1 and Default Instance|**Intance Name:** MSSQLServer,Instance1|Cert will be Installed to Default Instance and Instance1, Service will be restarted for Default Instance and Instance1|True|![](images/SQLTestCase4.gif) +1 |New Cert Enrollment To Default Instance Leave Blank|**Instance Name:** |Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase1.gif) +2 |New Cert Enrollment To Default Instance MSSQLServer|**Instance Name:** MSSQLServer|Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase2.gif) +3 |New Cert Enrollment To Instance1|**Instance Name:** Instance1|Cert will be Installed to Instance1, Service will be restarted for Instance1|True|![](images/SQLTestCase3.gif) +4 |New Cert Enrollment To Instance1 and Default Instance|**Instance Name:** MSSQLServer,Instance1|Cert will be Installed to Default Instance and Instance1, Service will be restarted for Default Instance and Instance1|True|![](images/SQLTestCase4.gif) 5 |One Click Renew Cert Enrollment To Instance1 and Default Instance|N/A|Cert will be Renewed/Installed to Default Instance and Instance1, Service will be restarted for Default Instance and Instance1|True|![](images/SQLTestCase5.gif) -6 |Remove Cert From Instance1 and Default Instance|**Intance Name:** |Cert from TC5 will be Removed From Default Instance and Instance1|True|![](images/SQLTestCase6.gif) +6 |Remove Cert From Instance1 and Default Instance|**Instance Name:** |Cert from TC5 will be Removed From Default Instance and Instance1|True|![](images/SQLTestCase6.gif) 7 |Inventory Different Certs Different Instance|N/A|2 Certs will be inventoried and each tied to its Instance|True|![](images/SQLTestCase7.gif) 8 |Inventory Same Cert Different Instance|N/A|2 Certs will be inventoried the cert will have a comma separated list of Instances|True|![](images/SQLTestCase8.gif) 9 |Inventory Against Machine Without SQL Server|N/A|Will fail with error saying it can't find SQL Server|True|![](images/SQLTestCase9.gif) From 67aff5a79ec73c636696a6963d19336399d1f81a Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Mon, 22 Apr 2024 18:21:02 +0000 Subject: [PATCH 13/14] Update generated README --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2da5062..ff4ac71 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ The returned list will contain the actual certificate store name to be used when By default, most certificates are stored in the “Personal” (My) and “Web Hosting” (WebHosting) stores. -This extension implements four job types: Inventory, Management Add/Remove, and ReEnrollment. +This extension implements four job types: Inventory, Management Add/Remove, and Reenrollment. WinRM is used to remotely manage the certificate stores and IIS bindings. WinRM must be properly configured to allow the orchestrator on the server to manage the certificates. Setting up WinRM is not in the scope of this document. @@ -121,7 +121,7 @@ In version 2.0 of the IIS Orchestrator, the certificate store type has been rena **Note: There is an additional (and deprecated) certificate store type of “IIS” that ships with the Keyfactor platform. Migration of certificate stores from the “IIS” type to either the “IISBin” or “IISU” types is not currently supported.** -**Note: If Looking to use GMSA Accounts to run the Service Kefyactor Command 10.2 or greater is required for No Value checkbox to work** +**Note: If Looking to use GMSA Accounts to run the Service Keyfactor Command 10.2 or greater is required for No Value checkbox to work** ## Security and Permission Considerations From an official support point of view, Local Administrator permissions are required on the target server. Some customers have been successful with using other accounts and granting rights to the underlying certificate and private key stores. Due to complexities with the interactions between Group Policy, WinRM, User Account Control, and other unpredictable customer environmental factors, Keyfactor cannot provide assistance with using accounts other than the local administrator account. @@ -134,7 +134,7 @@ For customers wishing to use something other than the local administrator accoun * WinRM needs to be properly set up between the server hosting the UO and the target server. This means that a WinRM client running on the UO server when running in the context of the UO service account needs to be able to create a session on the target server using the configured credentials of the target server and any PowerShell commands running on the remote session need to have appropriate permissions. -* Even though a given account may be in the administrators group or have administrative privledges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. +* Even though a given account may be in the administrators group or have administrative privileges on the target system and may be able to execute certificate and binding operations when running locally, the same account may not work when being used via WinRM. User Account Control (UAC) can get in the way and filter out administrative privledges. UAC / WinRM configuration has a LocalAccountTokenFilterPolicy setting that can be adjusted to not filter out administrative privledges for remote users, but enabling this may have other security ramifications. * The following list may not be exhaustive, but in general the account (when running under a remote WinRM session) needs permissions to: - Instantiate and open a .NET X509Certificates.X509Store object for the target certificate store and be able to read and write both the certificates and related private keys. Note that ACL permissions on the stores and private keys are separate. @@ -474,12 +474,12 @@ Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot ----|------------------------|------------------------------------|--------------|----------------|------------------------- -1 |New Cert Enrollment To Default Instance Leave Blank|**Intance Name:** |Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase1.gif) -2 |New Cert Enrollment To Default Instance MSSQLServer|**Intance Name:** MSSQLServer|Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase2.gif) -3 |New Cert Enrollment To Instance1|**Intance Name:** Instance1|Cert will be Installed to Instance1, Service will be restarted for Instance1|True|![](images/SQLTestCase3.gif) -4 |New Cert Enrollment To Instance1 and Default Instance|**Intance Name:** MSSQLServer,Instance1|Cert will be Installed to Default Instance and Instance1, Service will be restarted for Default Instance and Instance1|True|![](images/SQLTestCase4.gif) +1 |New Cert Enrollment To Default Instance Leave Blank|**Instance Name:** |Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase1.gif) +2 |New Cert Enrollment To Default Instance MSSQLServer|**Instance Name:** MSSQLServer|Cert will be Installed to default Instance, Service will be restarted for default instance|True|![](images/SQLTestCase2.gif) +3 |New Cert Enrollment To Instance1|**Instance Name:** Instance1|Cert will be Installed to Instance1, Service will be restarted for Instance1|True|![](images/SQLTestCase3.gif) +4 |New Cert Enrollment To Instance1 and Default Instance|**Instance Name:** MSSQLServer,Instance1|Cert will be Installed to Default Instance and Instance1, Service will be restarted for Default Instance and Instance1|True|![](images/SQLTestCase4.gif) 5 |One Click Renew Cert Enrollment To Instance1 and Default Instance|N/A|Cert will be Renewed/Installed to Default Instance and Instance1, Service will be restarted for Default Instance and Instance1|True|![](images/SQLTestCase5.gif) -6 |Remove Cert From Instance1 and Default Instance|**Intance Name:** |Cert from TC5 will be Removed From Default Instance and Instance1|True|![](images/SQLTestCase6.gif) +6 |Remove Cert From Instance1 and Default Instance|**Instance Name:** |Cert from TC5 will be Removed From Default Instance and Instance1|True|![](images/SQLTestCase6.gif) 7 |Inventory Different Certs Different Instance|N/A|2 Certs will be inventoried and each tied to its Instance|True|![](images/SQLTestCase7.gif) 8 |Inventory Same Cert Different Instance|N/A|2 Certs will be inventoried the cert will have a comma separated list of Instances|True|![](images/SQLTestCase8.gif) 9 |Inventory Against Machine Without SQL Server|N/A|Will fail with error saying it can't find SQL Server|True|![](images/SQLTestCase9.gif) From 268c18bc447621416935f99cbebf7ea83f60f575 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Thu, 25 Apr 2024 23:38:42 +0000 Subject: [PATCH 14/14] Update generated README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ff4ac71..b2cf34b 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The Keyfactor Universal Orchestrator may be installed on either Windows or Linux |Supports Management Remove|✓ | | |Supports Create Store| | | |Supports Discovery| | | -|Supports Renrollment|✓ | | +|Supports Reenrollment|✓ | | |Supports Inventory|✓ | | @@ -486,3 +486,6 @@ Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot
+When creating cert store type manually, that store property names and entry parameter names are case sensitive + +