diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed6e38..ea7fd92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +2.4.0 +* Changed the way certificates are added to cert stores. CertUtil is now used to import the PFX certificate into the associated store. The CSP is now considered when maintaining certificates, empty CSP values will result in using the machines default CSP. +* Added the Crypto Service Provider and SAN Entry Parameters to be used on Inventory queries, Adding and ReEnrollments for the WinCert, WinSQL and IISU extensions. +* Changed how Client Machine Names are handled when a 'localhost' connection is desiered. The new naming convention is: {machineName}|localmachine. This will eliminate the issue of unqiue naming conflicts. +* Updated the manifest.json to now include WinSQL ReEnrollment. +* Updated the integration-manifest.json file for new fields in cert store types. + +2.3.2 +* Changed the Open Cert Store access level from a '5' to 'MaxAllowed' + 2.3.1 * Added additional error trapping for WinRM connections to allow actual error on failure. diff --git a/IISU/Certificate.cs b/IISU/Certificate.cs index 88f3612..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 { @@ -22,5 +24,35 @@ public class Certificate public byte[] RawData { get; set; } public bool HasPrivateKey { get; set; } public string CertificateData => Convert.ToBase64String(RawData); + public string CryptoServiceProvider { get; set; } + public string SAN { get; set; } + + public class Utilities + { + public static string FormatSAN(string san) + { + // Use regular expression to extract key-value pairs + var regex = new Regex(@"(?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/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/ClientPSCertStoreInventory.cs b/IISU/ClientPSCertStoreInventory.cs index 25875ab..82a6365 100644 --- a/IISU/ClientPSCertStoreInventory.cs +++ b/IISU/ClientPSCertStoreInventory.cs @@ -46,8 +46,20 @@ 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 + san = $_.Extensions | Where-Object {{ $_.Oid.FriendlyName -eq ""Subject Alternative Name"" }} | ForEach-Object {{ $_.Format($false) }} + }} + + if ($_.HasPrivateKey) {{ + $certDetails.CSP = $_.PrivateKey.CspKeyContainerInfo.ProviderName + }} + + New-Object PSObject -Property $certDetails }}"; ps.AddScript(certStoreScript); @@ -55,12 +67,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 = Certificate.Utilities.FormatSAN($"{c.Properties["san"]?.Value}") }); + } return myCertificates; } diff --git a/IISU/ClientPSCertStoreManager.cs b/IISU/ClientPSCertStoreManager.cs index 35d0cb8..1a37864 100644 --- a/IISU/ClientPSCertStoreManager.cs +++ b/IISU/ClientPSCertStoreManager.cs @@ -14,9 +14,10 @@ using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using System; -using System.Linq; +using System.IO; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Security.Cryptography.X509Certificates; @@ -45,99 +46,174 @@ public ClientPSCertStoreManager(ILogger logger, Runspace runSpace, long jobNumbe _jobNumber = jobNumber; } - public JobResult AddCertificate(string certificateContents, string privateKeyPassword, string storePath) + public string CreatePFXFile(string certificateContents, string privateKeyPassword) { try { - using var ps = PowerShell.Create(); - - _logger.MethodEntry(); - - ps.Runspace = _runspace; - - _logger.LogTrace($"Creating X509 Cert from: {certificateContents}"); + // Create the x509 certificate x509Cert = new X509Certificate2 ( Convert.FromBase64String(certificateContents), privateKeyPassword, - X509KeyStorageFlags.MachineKeySet | - X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable ); - _logger.LogDebug($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); - _logger.LogDebug($"Begin Add for Cert Store {$@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}"}"); + using (PowerShell ps = PowerShell.Create()) + { + ps.Runspace = _runspace; + + // Add script to write certificate contents to a temporary file + string script = @" + param($certificateContents) + $filePath = [System.IO.Path]::GetTempFileName() + '.pfx' + [System.IO.File]::WriteAllBytes($filePath, [System.Convert]::FromBase64String($certificateContents)) + $filePath + "; - // Add Certificate - var funcScript = @" - $ErrorActionPreference = ""Stop"" + ps.AddScript(script); + ps.AddParameter("certificateContents", certificateContents); // Convert.ToBase64String(x509Cert.Export(X509ContentType.Pkcs12))); - function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$storeName) { - $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, ""LocalMachine"" - $certStore.Open(5) - $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $password, 18 <# Persist, Machine #> - $certStore.Add($cert) + // Invoke the script on the remote computer + var results = ps.Invoke(); - $certStore.Close(); - }"; + // Get the result (temporary file path) returned by the script + return results[0].ToString(); + } + } + catch (Exception) + { + throw new Exception("An error occurred while attempting to create and write the X509 contents."); + } + } - ps.AddScript(funcScript).AddStatement(); - _logger.LogDebug("InstallPfxToMachineStore Statement Added..."); + public void DeletePFXFile(string filePath, string fileName) + { + using (PowerShell ps = PowerShell.Create()) + { + ps.Runspace = _runspace; - ps.AddCommand("InstallPfxToMachineStore") - .AddParameter("bytes", Convert.FromBase64String(certificateContents)) - .AddParameter("password", privateKeyPassword) - .AddParameter("storeName", $@"\\{_runspace.ConnectionInfo.ComputerName}\{storePath}"); - - _logger.LogTrace("InstallPfxToMachineStore Command Added..."); + // Add script to delete the temporary file + string deleteScript = @" + param($filePath) + Remove-Item -Path $filePath -Force + "; - foreach (var cmd in ps.Commands.Commands) - { - _logger.LogTrace("Logging PowerShell Command"); - _logger.LogTrace(cmd.CommandText); - } + ps.AddScript(deleteScript); + ps.AddParameter("filePath", Path.Combine(filePath, fileName) + "*"); - _logger.LogTrace("Invoking ps..."); - ps.Invoke(); - _logger.LogTrace("ps Invoked..."); + // Invoke the script to delete the file + var results = ps.Invoke(); + } + } - if (ps.HadErrors) + public JobResult ImportPFXFile(string filePath, string privateKeyPassword, string cryptoProviderName) + { + try + { + using (PowerShell ps = PowerShell.Create()) { - _logger.LogTrace("ps Has Errors"); - var psError = ps.Streams.Error.ReadAll() - .Aggregate(string.Empty, (current, error) => current + error?.ErrorDetails.Message); + ps.Runspace = _runspace; + + if (cryptoProviderName == null) + { + string script = @" + param($pfxFilePath, $privateKeyPassword, $cspName) + $output = certutil -importpfx -p $privateKeyPassword $pfxFilePath 2>&1 + $c = $LASTEXITCODE + $output + "; + + ps.AddScript(script); + ps.AddParameter("pfxFilePath", filePath); + ps.AddParameter("privateKeyPassword", privateKeyPassword); + } + else + { + string script = @" + param($pfxFilePath, $privateKeyPassword, $cspName) + $output = certutil -importpfx -csp $cspName -p $privateKeyPassword $pfxFilePath 2>&1 + $c = $LASTEXITCODE + $output + "; + + ps.AddScript(script); + ps.AddParameter("pfxFilePath", filePath); + ps.AddParameter("privateKeyPassword", privateKeyPassword); + ps.AddParameter("cspName", cryptoProviderName); + } + + // Invoke the script + var results = ps.Invoke(); + + // Get the last exist code returned from the script + // This statement is in a try/catch block because PSVariable.GetValue() is not a valid method on a remote PS Session and throws an exception. + // Due to security reasons and Windows architecture, retreiving values from a remote system is not supported. + int lastExitCode = 0; + try + { + lastExitCode = (int)ps.Runspace.SessionStateProxy.PSVariable.GetValue("c"); + } + catch (Exception) + { + } + + + bool isError = false; + if (lastExitCode != 0) + { + isError = true; + string outputMsg = ""; + + foreach (var result in results) + { + string outputLine = result.ToString(); + if (!string.IsNullOrEmpty(outputLine)) + { + outputMsg += "\n" + outputLine; + } + } + _logger.LogError(outputMsg); + } + else + { + // Check for errors in the output + foreach (var result in results) + { + string outputLine = result.ToString(); + if (!string.IsNullOrEmpty(outputLine) && outputLine.Contains("Error")) + { + isError = true; + _logger.LogError(outputLine); + } + } + } + + if (isError) + { + throw new Exception("Error occurred while attempting to import the pfx file."); + } + else { return new JobResult { - Result = OrchestratorJobStatusJobResult.Failure, + Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = _jobNumber, - FailureMessage = - $"Site {storePath} on server {_runspace.ConnectionInfo.ComputerName}: {psError}" + FailureMessage = "" }; } } - - _logger.LogTrace("Clearing Commands..."); - ps.Commands.Clear(); - _logger.LogTrace("Commands Cleared.."); - _logger.LogInformation($"Certificate was successfully added to cert store: {storePath}"); - - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Success, - JobHistoryId = _jobNumber, - FailureMessage = "" - }; } catch (Exception e) { - _logger.LogError($"Error Occurred in ClientPSCertStoreManager.AddCertificate(): {e.Message}"); + _logger.LogError($"Error Occurred in ClientPSCertStoreManager.ImportPFXFile(): {e.Message}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = _jobNumber, - FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" + FailureMessage = $"Error Occurred in ImportPFXFile {LogHandler.FlattenException(e)}" }; } } @@ -150,10 +226,11 @@ public void RemoveCertificate(string thumbprint, string storePath) ps.Runspace = _runspace; + // Open with value of 5 means: Open existing only (4) + Open ReadWrite (1) var removeScript = $@" $ErrorActionPreference = 'Stop' $certStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('{storePath}','LocalMachine') - $certStore.Open('MaxAllowed') + $certStore.Open(5) $certToRemove = $certStore.Certificates.Find(0,'{thumbprint}',$false) if($certToRemove.Count -gt 0) {{ $certStore.Remove($certToRemove[0]) 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..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,7 +94,38 @@ 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); } } @@ -182,6 +216,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/Win/Management.cs b/IISU/ImplementedStoreTypes/Win/Management.cs index dbd6ed6..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; @@ -23,6 +25,7 @@ using System.Management.Automation; using System.Net; using Keyfactor.Logging; +using System.IO; namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore.WinCert { @@ -114,26 +117,45 @@ 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 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) { - _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); - return manager.AddCertificate(certificateContents, privateKeyPassword, storePath); + + // 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); + + // 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; } else { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = "Store Path is empty or null." - }; + throw new Exception($"The store path is empty or null."); } } catch (Exception e) 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 0e4542c..6332e1a 100644 --- a/IISU/ImplementedStoreTypes/Win/WinInventory.cs +++ b/IISU/ImplementedStoreTypes/Win/WinInventory.cs @@ -33,6 +33,12 @@ 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 +46,7 @@ public List GetInventoryItems(Runspace runSpace, string st PrivateKeyEntry = cert.HasPrivateKey, UseChainLevel = false, ItemStatus = OrchestratorInventoryItemStatus.Unknown, - Parameters = null + Parameters = entryParms }); } 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..c4e178f --- /dev/null +++ b/IISU/ImplementedStoreTypes/WinSQL/ReEnrollment.cs @@ -0,0 +1,43 @@ +// 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; + + 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/PSHelper.cs b/IISU/PSHelper.cs index 7a8464a..7d2990e 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; @@ -30,8 +32,27 @@ 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) + { + return RunspaceFactory.CreateRunspace(); + } + else { var connInfo = new WSManConnectionInfo(new Uri($"{winRmProtocol}://{clientMachineName}:{winRmPort}/wsman")); connInfo.IncludePortInSPN = includePortInSpn; @@ -46,16 +67,53 @@ public static Runspace GetClientPsRunspace(string winRmProtocol, string clientMa } return RunspaceFactory.CreateRunspace(connInfo); } + } + + public static IEnumerable GetCSPList(Runspace myRunspace) + { + _logger.LogTrace("Getting the list of Crypto Service Providers"); + + using var ps = PowerShell.Create(); + + ps.Runspace = myRunspace; - // 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 + var certStoreScript = $@" + $certUtilOutput = certutil -csplist - PowerShellProcessInstance instance = new PowerShellProcessInstance(new Version(5, 1), null, null, false); - Runspace rs = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(Array.Empty()), instance); + $cspInfoList = @() + foreach ($line in $certUtilOutput) {{ + if ($line -match ""Provider Name:"") {{ + $cspName = ($line -split "":"")[1].Trim() + $cspInfoList += $cspName + }} + }} - return rs; + $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/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/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/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" } } } diff --git a/README.md b/README.md index 11df329..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|✓ | | @@ -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. @@ -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,9 +234,9 @@ 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 | 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,8 @@ 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. +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) @@ -361,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.
@@ -373,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.) @@ -425,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.) @@ -470,15 +474,18 @@ 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)
+When creating cert store type manually, that store property names and entry parameter names are case sensitive + + 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 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": { diff --git a/readme_source.md b/readme_source.md index 50c296f..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. @@ -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,9 +134,9 @@ 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 | 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,8 @@ 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. +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) @@ -261,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.
@@ -273,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.) @@ -325,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.) @@ -370,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)