diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index a4649f2..8ec5771 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,10 +11,17 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@3.1.2 + uses: keyfactor/actions/.github/workflows/starter.yml@v4 + with: + command_token_url: ${{ vars.COMMAND_TOKEN_URL }} # Only required for doctool generated screenshots + command_hostname: ${{ vars.COMMAND_HOSTNAME }} # Only required for doctool generated screenshots + command_base_api_path: ${{ vars.COMMAND_API_PATH }} # Only required for doctool generated screenshots secrets: - token: ${{ secrets.V2BUILDTOKEN}} - APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} - gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} - gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} - scan_token: ${{ secrets.SAST_TOKEN }} + token: ${{ secrets.V2BUILDTOKEN}} # REQUIRED + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} # Only required for golang builds + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} # Only required for golang builds + scan_token: ${{ secrets.SAST_TOKEN }} # REQUIRED + entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} # Only required for doctool generated screenshots + entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} # Only required for doctool generated screenshots + command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} # Only required for doctool generated screenshots + command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} # Only required for doctool generated screenshots diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6b284..7469403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +2.6.3 +* Fixed re-enrollment or ODKG job when RDN Components contained escaped commas. +* Updated renewal job for IIS Certs to delete the old cert if not bound or used by other web sites. +* Improved Inventory reporting of CSP when cert uses newer CNG Keys. +* Fixed an issue with complex PFX passwords that contained special characters such as '@' or '$', etc. +* Fixed an issue when adding certificate to store, sometimes the wrong thumbprint was returned, thus breaking web site binding. +* Removed the IIS bindings check. Now bindings are handled similar to IIS - if you bind a cert to a site using the same bindings, you risk the possibility of one of the duplicate sites to stop working and the certificate being bound to either site. Refer to IIS Documentation pertaining to HTTPS binding. +* Fixed an issue with (remote) ODKG jobs that caused an error when the CSP was not specified that included bindings. +* Fixed an issue with (remote) ODKG jobs that caused an error when the CSP was not specified that did not require binding. + 2.6.2 * Fixed error when attempting to connect to remote computer using UO service account * Fixed error when connecting to remote computer using HTTPS; was defaulting to HTTP diff --git a/IISU/ClientPSCertStoreReEnrollment.cs b/IISU/ClientPSCertStoreReEnrollment.cs index d769d9b..d16eef6 100644 --- a/IISU/ClientPSCertStoreReEnrollment.cs +++ b/IISU/ClientPSCertStoreReEnrollment.cs @@ -208,6 +208,17 @@ public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submit FailureMessage = "" }; + break; + + case CertStoreBindingTypeENUM.None: + + jobResult = new JobResult + { + Result = OrchestratorJobStatusJobResult.Success, + JobHistoryId = config.JobHistoryId, + FailureMessage = "" + }; + break; } } diff --git a/IISU/ImplementedStoreTypes/Win/Inventory.cs b/IISU/ImplementedStoreTypes/Win/Inventory.cs index 0884751..546640b 100644 --- a/IISU/ImplementedStoreTypes/Win/Inventory.cs +++ b/IISU/ImplementedStoreTypes/Win/Inventory.cs @@ -93,7 +93,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = jobConfiguration.JobHistoryId, - FailureMessage = "" + FailureMessage = $"Inventory completed returning {inventoryItems.Count} Items." }; } diff --git a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs index af45bdd..65c1b68 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs @@ -95,7 +95,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = jobConfiguration.JobHistoryId, - FailureMessage = "" + FailureMessage = $"Inventory completed returning {inventoryItems.Count} Items." }; } diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index bbd19c1..3a07501 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Management.Automation; using Keyfactor.Extensions.Orchestrator.WindowsCertStore.Models; using Keyfactor.Logging; @@ -89,6 +90,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; + string alias = config.JobCertificate?.Alias?.Split(':').FirstOrDefault() ?? string.Empty; // Thumbprint is first part of the alias _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword); @@ -171,6 +173,14 @@ public JobResult ProcessJob(ManagementJobConfiguration config) psResult = OrchestratorJobStatusJobResult.Unknown; } + // Only is the binding returns successful, check of original cert is still bound to any site, if not remove it from the store + if (psResult == OrchestratorJobStatusJobResult.Success && !string.IsNullOrEmpty(alias)) + { + _logger.LogTrace("Attempting to remove original certificate from store if it is no longer bound to any site."); + RemoveIISCertificate(alias); + _logger.LogTrace("Returned from removing cert if not used."); + } + complete = new JobResult { Result = psResult, diff --git a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs index 7402f97..765cf0a 100644 --- a/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinSQL/Inventory.cs @@ -93,7 +93,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = jobConfiguration.JobHistoryId, - FailureMessage = "" + FailureMessage = $"Inventory completed returning {inventoryItems.Count} Items." }; } diff --git a/IISU/PSHelper.cs b/IISU/PSHelper.cs index 498a9ec..acd9831 100644 --- a/IISU/PSHelper.cs +++ b/IISU/PSHelper.cs @@ -386,7 +386,7 @@ public Collection ExecutePowerShellScript(string script) } // Add Parameters if provided - if (parameters != null) + if (parameters != null && parameters.Count > 0) { if (isLocalMachine || isScript) { @@ -398,13 +398,18 @@ public Collection ExecutePowerShellScript(string script) else { // Remote execution: Use ArgumentList for parameters - var paramBlock = string.Join(", ", parameters.Select(p => $"[{p.Value.GetType().Name}] ${p.Key}")); + var paramBlock = string.Join(", ", parameters.Select(p => + { + string typeName = p.Value?.GetType().Name ?? "object"; + return $"[{typeName}] ${p.Key}"; + })); + var paramUsage = string.Join(" ", parameters.Select(p => $"-{p.Key} ${p.Key}")); string scriptBlockWithParams = $@" - param({paramBlock}) - {commandOrScript} {paramUsage} - "; + param({paramBlock}) + {commandOrScript} {paramUsage} + "; PS.Commands.Clear(); // Clear previous commands PS.AddCommand("Invoke-Command") diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index a77dccf..00f7fdf 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -1,4 +1,18 @@ -# Set preferences globally at the script level +# Version 1.3.0 + +# Summary +# Contains PowerShell functions to execute administration jobs for general Windows certificates, IIS and SQL Server. +# There are additional supporting PowerShell functions to support job specific actions. + +# Update notes: +# 08/12/25 Updated functions to manage IIS bindings and certificates +# Updated script to read CSPs correctly using newer CNG Keys +# Fix an error with complex PFX passwords having irregular characters +# 08/29/25 Fixed the add cert to store function to return the correct thumbprint +# Made changes to the IIS Binding logic, breaking it into manageable pieces to aid in debugging issues +# 09/16/25 Updated the Get CSP function to handle null values when reading hybrid certificates + +# Set preferences globally at the script level $DebugPreference = "Continue" $VerbosePreference = "Continue" $InformationPreference = "Continue" @@ -204,7 +218,19 @@ function Add-KFCertificateToStore{ Write-Information "Entering PowerShell Script Add-KFCertificate" Write-Verbose "Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" - $thumbprint = $null + # Get the thumbprint of the passed in certificate + # Convert password to secure string if provided, otherwise use $null + $bytes = [System.Convert]::FromBase64String($Base64Cert) + $securePassword = if ($PrivateKeyPassword) { ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force } else { $null } + + # Set the storage flags and get the certificate's thumbprint + $keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor ` + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet + + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, $keyStorageFlags) + $thumbprint = $cert.Thumbprint + + if (-not $thumbprint) { throw "Failed to get the certificate thumbprint. The PFX may be invalid or the password is incorrect." } if ($CryptoServiceProvider) { @@ -225,159 +251,83 @@ function Add-KFCertificateToStore{ # Execute certutil based on whether a private key password was supplied try { - # Build certutil command to import the certificate with exportable private key and CSP - $command = "certutil -f -p `"$PrivateKeyPassword`" -csp `"$CryptoServiceProvider`" -importpfx $StoreName `"$tempPfx`"" - $traceCommand = "certutil -f -p `"************`" -csp `"$CryptoServiceProvider`" -importpfx $StoreName `"$tempPfx`"" + # Start building certutil arguments + $arguments = @('-f') - Write-Verbose "Running: $traceCommand" - $output = Invoke-Expression $command - - if ($LASTEXITCODE -ne 0) { - throw "certutil failed with code $LASTEXITCODE. `nOutput: $output `nMake sure there is no cryptographic mismatch and the CSP supports the imported PFX.`n" + if ($PrivateKeyPassword) { + Write-Verbose "Has a private key" + $arguments += '-p' + $arguments += $PrivateKeyPassword } - # Get latest cert with private key in the store - $store = "Cert:\LocalMachine\$StoreName" - $cert = Get-ChildItem -Path $store | Where-Object { $_.HasPrivateKey } | Sort-Object NotBefore -Descending | Select-Object -First 1 - - if ($cert) { - Write-Information "Certificate imported successfully with Thumbprint: $($cert.Thumbprint)" - return $cert.Thumbprint - } else { - throw "Import succeeded, but no certificate with a private key was found in $store" + if ($CryptoServiceProvider) { + Write-Verbose "Has a CryptoServiceProvider: $CryptoServiceProvider" + $arguments += '-csp' + $arguments += $CryptoServiceProvider } - } catch { - Write-Error "ERROR: $_" - } finally { - if (Test-Path $tempPfx) { - #Remove-Item $tempPfx -Force - } - } + $arguments += '-importpfx' + $arguments += $StoreName + $arguments += $tempPfx - } else { - $bytes = [System.Convert]::FromBase64String($Base64Cert) - $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" - Write-Information "Store '$StoreName' is open." - $certStore.Open(5) + # Quote any arguments with spaces + $argLine = ($arguments | ForEach-Object { + if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } + }) -join ' ' - $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $PrivateKeyPassword, 18 <# Persist, Machine #> - $certStore.Add($cert) - $certStore.Close(); - Write-Information "Store '$StoreName' is closed." + write-Verbose "Running certutil with arguments: $argLine" - # Get the thumbprint so it can be returned to the calling function - $thumbprint = $cert.Thumbprint - Write-Information "The thumbprint '$thumbprint' was created." - } + # Setup process execution + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = "certutil.exe" + $processInfo.Arguments = $argLine.Trim() + $processInfo.RedirectStandardOutput = $true + $processInfo.RedirectStandardError = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true - Write-Host "Certificate added successfully to $StoreName." - return $thumbprint - } catch { - Write-Error "An error occurred: $_" - return $null - } -} + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo -function Add-KFCertificateToStoreNEW{ - param ( - [Parameter(Mandatory = $true)] - [string]$Base64Cert, - - [Parameter(Mandatory = $false)] - [string]$PrivateKeyPassword, - - [Parameter(Mandatory = $true)] - [string]$StoreName, - - [Parameter(Mandatory = $false)] - [string]$CryptoServiceProvider - ) + $process.Start() | Out-Null - try { - Write-Information "Entering PowerShell Script Add-KFCertificate" - Write-Verbose "Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" - - $thumbprint = $null + $stdOut = $process.StandardOutput.ReadToEnd() + $stdErr = $process.StandardError.ReadToEnd() - if ($CryptoServiceProvider) - { - # Test to see if CSP exists - if(-not (Test-CryptoServiceProvider -CSPName $CryptoServiceProvider)) - { - Write-Information "INFO: The CSP $CryptoServiceProvider was not found on the system." - Write-Warning "WARN: CSP $CryptoServiceProvider was not found on the system." - return - } + $process.WaitForExit() - Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'" - - # Convert Base64 PFX to bytes and save to temp file - $tempPfxPath = [System.IO.Path]::GetTempFileName() + ".pfx" - [System.IO.File]::WriteAllBytes($tempPfxPath, [Convert]::FromBase64String($Base64Cert)) - - try { - # Load the PFX into a PKCS12 object - $pfx = New-Object -ComObject X509Enrollment.CX509Enrollment - $pfx.InitializeImport(1, [System.IO.File]::ReadAllText($tempPfxPath), $PrivateKeyPassword) - - # Create new private key with desired CSP - $privateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey - $privateKey.ProviderName = $CryptoServiceProvider - $privateKey.Length = [int]2048 - $privateKey.KeySpec = 1 # AT_KEYEXCHANGE - $privateKey.ExportPolicy = 1 # AllowExport - $privateKey.MachineContext = $true - $privateKey.Create() - - # Associate private key with enrollment - $pfx.InstallResponse(2, "", 0, $null) - - Write-Host "Certificate imported successfully using CSP: $CryptoServiceProvider" - - # The most recently added cert (with private key) should be the new one - $latest = $certsBefore | Where-Object { $_.HasPrivateKey } | Sort-Object NotBefore -Descending | Select-Object -First 1 - - if ($latest) { - Write-Information "Certificate imported successfully with thumbprint: $($latest.Thumbprint)" - return $latest.Thumbprint - } else { - throw "Certificate installed but no cert with private key was found in store '$StoreName'." + if ($process.ExitCode -ne 0) { + throw "certutil failed with code $($process.ExitCode). Output:`n$stdOut`nError:`n$stdErr" } - } catch { - # Handle any errors and log the exception message - Write-Error "Error during certificate import: $_" - return "Error: $_" + Write-Error "ERROR: $_" } finally { - # Ensure the temporary file is deleted - if (Test-Path $tempFileName) { - Remove-Item $tempFileName -Force + if (Test-Path $tempPfx) { + Remove-Item $tempPfx -Force } } + } else { - $bytes = [System.Convert]::FromBase64String($Base64Cert) $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" Write-Information "Store '$StoreName' is open." - $certStore.Open(5) - $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $PrivateKeyPassword, 18 <# Persist, Machine #> + # Open store with read/write, and don't create the store if it doesn't exist + $openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite -bor ` + [System.Security.Cryptography.X509Certificates.OpenFlags]::OpenExistingOnly + $certStore.Open($openFlags) $certStore.Add($cert) $certStore.Close(); Write-Information "Store '$StoreName' is closed." - - # Get the thumbprint so it can be returned to the calling function - $thumbprint = $cert.Thumbprint - Write-Information "The thumbprint '$thumbprint' was created." } - Write-Host "Certificate added successfully to $StoreName." + Write-Information "The thumbprint '$thumbprint' was added to store $StoreName." return $thumbprint } catch { Write-Error "An error occurred: $_" return $null } } + function Remove-KFCertificateFromStore { param ( [string]$Thumbprint, @@ -431,302 +381,255 @@ function Remove-KFCertificateFromStore { return $isSuccessful } +# IIS Functions function New-KFIISSiteBinding { [CmdletBinding()] [OutputType([pscustomobject])] param ( [Parameter(Mandatory = $true)] [string]$SiteName, + [string]$IPAddress = "*", + [int]$Port = 443, + + [AllowEmptyString()] [string]$Hostname = "", + [ValidateSet("http", "https")] [string]$Protocol = "https", + + [ValidateScript({ + if ($Protocol -eq 'https' -and [string]::IsNullOrEmpty($_)) { + throw "Thumbprint is required when Protocol is 'https'" + } + $true + })] [string]$Thumbprint, + [string]$StoreName = "My", + [int]$SslFlags = 0 ) Write-Information "Entering PowerShell Script: New-KFIISSiteBinding" -InformationAction SilentlyContinue - Write-Verbose "Entered New-KFIISSiteBinding with values SiteName: '$SiteName', IPAddress: '$IPAddress', Port: $Port, HostName: '$Hostname', Protocol: '$Protocol', Thumbprint: '$Thumbprint', Store Path: '$StoreName', SslFlags: '$SslFlags'" - - $result = $null - - # Check for existing binding conflict - $conflicts = @(CheckExistingBindings -DesiredIP $IPAddress -DesiredPort $Port -DesiredHost $Hostname -TargetSiteName $SiteName) + Write-Verbose "Function: New-KFIISSiteBinding" + Write-Verbose "Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" - if ($conflicts.Count -gt 0) { - $conflictMessage = "Binding conflict detected with the following existing bindings:`n" + ($conflicts | ForEach-Object { " - Site: $($_.SiteName), IP: $($_.BindingIP), Port: $($_.BindingPort), Host: $($_.BindingHost)" }) -join "`n" + try { + # This function mimics IIS Manager behavior: + # - Replaces exact binding matches (same IP:Port:Hostname) + # - Allows multiple bindings with different hostnames (SNI) + # - Lets IIS handle true conflicts rather than pre-checking + + # Step 1: Verify site exists and get management approach + $managementInfo = Get-IISManagementInfo -SiteName $SiteName + if (-not $managementInfo.Success) { + return $managementInfo.Result + } - Write-Warning $conflictMessage -InformationAction SilentlyContinue + # Step 2: Remove existing HTTPS bindings for this exact binding information + # This mimics IIS behavior: replace exact matches, allow different hostnames + $searchBindings = "${IPAddress}:${Port}:${Hostname}" + Write-Verbose "Removing existing HTTPS bindings for: $searchBindings" + + $removalResult = Remove-ExistingIISBinding -SiteName $SiteName -BindingInfo $searchBindings -UseIISDrive $managementInfo.UseIISDrive + if ($removalResult.Status -eq 'Error') { + return $removalResult + } - $result = New-ResultObject -Status Skipped -Code 100 -Step CheckBinding -Message $msg -ErrorMessage $conflictMessage + # Step 3: Add new binding with SSL certificate + Write-Verbose "Adding new binding with SSL certificate" + + $addParams = @{ + SiteName = $SiteName + Protocol = $Protocol + IPAddress = $IPAddress + Port = $Port + Hostname = $Hostname + Thumbprint = $Thumbprint + StoreName = $StoreName + SslFlags = $SslFlags + UseIISDrive = $managementInfo.UseIISDrive + } + + $addResult = Add-IISBindingWithSSL @addParams + return $addResult - return $result } + catch { + $errorMessage = "Unexpected error in New-KFIISSiteBinding: $($_.Exception.Message)" + Write-Error $errorMessage + return New-ResultObject -Status Error -Code 999 -Step UnexpectedError -ErrorMessage $errorMessage + } +} +function Remove-ExistingIISBinding { + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory = $true)] + [string]$SiteName, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$BindingInfo, + + [Parameter(Mandatory = $true)] + [bool]$UseIISDrive + ) - $searchBindings = "${IPAddress}:${Port}:${Hostname}" - $hasIISDrive = Ensure-IISDrive - Write-Verbose "IIS Drive is available: $hasIISDrive" + Write-Verbose "Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" - if ($hasIISDrive) { - Import-Module WebAdministration - $sitePath = "IIS:\Sites\$SiteName" - if (-not (Test-Path $sitePath)) { - $msg = "Site '$SiteName' not found in IIS drive." - Write-Error $msg -InformationAction SilentlyContinue - $result = New-ResultObject -Status Error -Code 201 -Step FindWebSite -Message $msg -Details @{ SiteName = $SiteName; IPAddress = $IPAddress; Port = $Port; HostName = $Hostname } - } else { + try { + if ($UseIISDrive) { + $sitePath = "IIS:\Sites\$SiteName" $site = Get-Item $sitePath $httpsBindings = $site.Bindings.Collection | Where-Object { - $_.bindingInformation -eq $searchBindings -and $_.protocol -eq "https" + $_.bindingInformation -eq $BindingInfo -and $_.protocol -eq "https" } foreach ($binding in $httpsBindings) { - try { - Write-Verbose "Calling Remove-WebBinding -Name $SiteName -BindingInformation $binding.bindingInformation -Protocol $binding.protocol -Confirm:$false" - Remove-WebBinding -Name $SiteName -BindingInformation $binding.bindingInformation -Protocol $binding.protocol -Confirm:$false - } catch { - $msg = "Error removing binding '$($binding.bindingInformation)': $_" - Write-Warning $msg -InformationAction SilentlyContinue - $result = New-ResultObject -Status Error -Code 201 -Step RemoveBinding -ErrorMessage $msg - return $result - } - } + $bindingInfo = $binding.GetAttributeValue("bindingInformation") + $protocol = $binding.protocol - # Site2 then has Test1 cert assigned to it?? - try { - Write-Verbose "Calling New-WebBinding -Name $SiteName -Protocol $Protocol -IPAddress $IPAddress -Port $Port -HostHeader '$Hostname' -SslFlags $SslFlags" - New-WebBinding -Name $SiteName -Protocol $Protocol -IPAddress $IPAddress -Port $Port -HostHeader $Hostname -SslFlags $SslFlags - } catch { - $msg = "Error adding binding: $_" - Write-Warning $msg -InformationAction SilentlyContinue - $result = New-ResultObject -Status Error -Code 202 -Step AddBinding -ErrorMessage $msg - return $result - } - - Write-Verbose "Calling Get-WebBinding -Name $SiteName -Protocol $Protocol, Where BindingInformation equals '$searchBindings'" - $binding = Get-WebBinding -Name $SiteName -Protocol $Protocol | Where-Object { - $_.bindingInformation -eq $searchBindings - } - - if ($binding) { - Write-Verbose "Binding thumbprint $thumbprint to $binding.bindingInformation in store: $StoreName" - $null = $binding.AddSslCertificate($Thumbprint, $StoreName) - $result = New-ResultObject -Status Success -Code 0 -Step BindSSL - } else { - $result = New-ResultObject -Status Error -Code 202 -Step BindSSL -Message "No binding found for: $searchBindings" + Write-Verbose "Removing binding: $bindingInfo ($protocol)" + Remove-WebBinding -Name $SiteName -BindingInformation $bindingInfo -Protocol $protocol -Confirm:$false + Write-Verbose "Successfully removed binding" } } - } else { - # SERVERMANAGER FALLBACK - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager - $site = $iis.Sites[$SiteName] + else { + # ServerManager fallback + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" + $iis = New-Object Microsoft.Web.Administration.ServerManager + $site = $iis.Sites[$SiteName] - if ($null -eq $site) { - $msg = "Site '$SiteName' not found in ServerManager." - Write-Error $msg -InformationAction SilentlyContinue - $result = New-ResultObject -Status Error -Code 201 -Step FindWebSite -Message $msg -Details @{ SiteName = $SiteName; IPAddress = $IPAddress; Port = $Port; HostName = $Hostname } - } else { $httpsBindings = $site.Bindings | Where-Object { - $_.bindingInformation -eq $searchBindings -and $_.protocol -eq "https" + $_.BindingInformation -eq $BindingInfo -and $_.Protocol -eq "https" } foreach ($binding in $httpsBindings) { - try { - $site.Bindings.Remove($binding) - } catch { - $msg = "Error removing binding: $_" - Write-Warning $msg -InformationAction SilentlyContinue - $result = New-ResultObject -Status Error -Code 201 -Step RemoveBinding -ErrorMessage $msg - return $result - } - } - - $cleanThumbprint = $Thumbprint -replace '[^a-fA-F0-9]', '' - $hashBytes = -split $cleanThumbprint -replace '..', '$& ' -split ' ' | Where-Object { $_ -ne '' } | ForEach-Object { [Convert]::ToByte($_, 16) } - - try { - $newBinding = $site.Bindings.Add($searchBindings, $Protocol) - if ($Protocol -eq "https") { - $newBinding.CertificateStoreName = $StoreName - $newBinding.CertificateHash = [byte[]]$hashBytes - $newBinding.SetAttributeValue("sslFlags", $SslFlags) - } - $iis.CommitChanges() - $result = New-ResultObject -Status Success -Code 0 -Step BindSSL -Message "Binding and certificate successfully applied via ServerManager." - } catch { - $msg = "Error adding binding: $_" - Write-Warning $msg -InformationAction SilentlyContinue - $result = New-ResultObject -Status Error -Code 202 -Step BindSSL -ErrorMessage $msg - } - } - } - - return $result -} - -function CheckExistingBindings { - param ( - [string]$DesiredIP, - [string]$DesiredPort, - [string]$DesiredHost, - [string]$TargetSiteName - ) - - $conflicts = @() - - if (Ensure-IISDrive) { - Import-Module WebAdministration - - Get-Website | Where-Object { $_.Name -ne $TargetSiteName } | ForEach-Object { - $siteName = $_.Name - $_.Bindings.Collection | ForEach-Object { - $parts = $_.bindingInformation.Split(':') - $bindingIP = $parts[0] - $bindingPort = $parts[1] - $bindingHost = if ($_.HostHeader) { $_.HostHeader } else { "" } - - if ( - $bindingIP -eq $DesiredIP -and - $bindingPort -eq $DesiredPort -and - $bindingHost -eq $DesiredHost - ) { - $conflicts += [pscustomobject]@{ - SiteName = $siteName - BindingIP = $bindingIP - BindingPort = $bindingPort - BindingHost = $bindingHost - } - } + Write-Verbose "Removing binding: $($binding.BindingInformation)" + $site.Bindings.Remove($binding) } + + $iis.CommitChanges() } - return @($conflicts) + return New-ResultObject -Status Success -Code 0 -Step RemoveBinding -Message "Successfully removed existing bindings" } - else { - # SERVERMANAGER FALLBACK - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager - - foreach ($site in $iis.Sites) { - if ($site.Name -ne $TargetSiteName) { - foreach ($binding in $site.Bindings) { - $bindingInfo = $binding.BindingInformation.Split(':') - $bindingIP = $bindingInfo[0] - $bindingPort = $bindingInfo[1] - $bindingHost = $binding.Host - - if ( - $bindingIP -eq $DesiredIP -and - $bindingPort -eq $DesiredPort -and - ($bindingHost -eq $DesiredHost -or ($bindingHost -eq $null -and $DesiredHost -eq "")) - ) { - $conflicts += [pscustomobject]@{ - SiteName = $site.Name - BindingIP = $bindingIP - BindingPort = $bindingPort - BindingHost = $bindingHost - } - } - } - } - } - - return $conflicts + catch { + $errorMessage = "Error removing existing binding: $($_.Exception.Message)" + Write-Warning $errorMessage + return New-ResultObject -Status Error -Code 201 -Step RemoveBinding -ErrorMessage $errorMessage } } - -function CheckExistingBindingsORIG { +function Add-IISBindingWithSSL { + [CmdletBinding()] + [OutputType([pscustomobject])] param ( - [string]$DesiredIP, - [string]$DesiredPort, - [string]$DesiredHost, - [string]$TargetSiteName + [Parameter(Mandatory = $true)] + [string]$SiteName, + + [Parameter(Mandatory = $true)] + [string]$Protocol, + + [Parameter(Mandatory = $true)] + [string]$IPAddress, + + [Parameter(Mandatory = $true)] + [int]$Port, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$Hostname, + + [string]$Thumbprint, + + [string]$StoreName = "My", + + [int]$SslFlags = 0, + + [Parameter(Mandatory = $true)] + [bool]$UseIISDrive ) - if (Ensure-IISDrive) { - Import-Module WebAdministration - - $conflict = $false - - Get-Website | Where-Object { $_.Name -ne $TargetSiteName } | ForEach-Object { - $siteName = $_.Name - $_.Bindings.Collection | ForEach-Object { - $parts = $_.bindingInformation.Split(':') - $bindingIP = $parts[0] - $bindingPort = $parts[1] - $bindingHost = if ($_.HostHeader) { $_.HostHeader } else { "" } - - if ( - $bindingIP -eq $DesiredIP -and - $bindingPort -eq $DesiredPort -and - $bindingHost -eq $DesiredHost - ) { - Write-Verbose "⚠️ Conflict found in site '$siteName' with binding: $($DesiredIP):$($DesiredPort):$($DesiredHost)" - $conflict = $true - } + Write-Verbose "Adding binding: Protocol=$Protocol, IP=$IPAddress, Port=$Port, Host='$Hostname'" + + try { + if ($UseIISDrive) { + # Add binding using WebAdministration module + $bindingParams = @{ + Name = $SiteName + Protocol = $Protocol + IPAddress = $IPAddress + Port = $Port + SslFlags = $SslFlags } - } - - return $conflict - } - else { - # SERVERMANAGER FALLBACK - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager + + # Only add HostHeader if it's not empty (New-WebBinding doesn't like empty strings) + if (-not [string]::IsNullOrEmpty($Hostname)) { + $bindingParams.HostHeader = $Hostname + } + + Write-Verbose "Creating new web binding with parameters: $(($bindingParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', ')" + New-WebBinding @bindingParams - $conflict = $false + # Bind SSL certificate if HTTPS + if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { + $searchBindings = "${IPAddress}:${Port}:${Hostname}" + Write-Verbose "Searching for binding: $searchBindings" + + $binding = Get-WebBinding -Name $SiteName -Protocol $Protocol | Where-Object { + $_.bindingInformation -eq $searchBindings + } - $site = $iis.Sites[$SiteName] - foreach ($site in $iis.Sites) { - if ($site.Name -ne $TargetSiteName) { - foreach ($binding in $site.Bindings) { - $bindingInfo = $binding.BindingInformation.Split(':') - $bindingIP = $bindingInfo[0] - $bindingPort = $bindingInfo[1] - $bindingHost = $binding.Host - - if ( - $bindingIP -eq $DesiredIP -and - $bindingPort -eq $DesiredPort -and - ($bindingHost -eq $DesiredHost -or ($bindingHost -eq $null -and $DesiredHost -eq "")) - ) { - $conflict = $true - } + if ($binding) { + Write-Verbose "Binding SSL certificate with thumbprint: $Thumbprint" + $null = $binding.AddSslCertificate($Thumbprint, $StoreName) + Write-Verbose "SSL certificate successfully bound" + return New-ResultObject -Status Success -Code 0 -Step BindSSL -Message "Binding and SSL certificate successfully applied" + } else { + return New-ResultObject -Status Error -Code 202 -Step BindSSL -ErrorMessage "No binding found for: $searchBindings" } } + else { + return New-ResultObject -Status Success -Code 0 -Step AddBinding -Message "HTTP binding successfully added" + } } + else { + # ServerManager fallback + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" + $iis = New-Object Microsoft.Web.Administration.ServerManager + $site = $iis.Sites[$SiteName] + + $searchBindings = "${IPAddress}:${Port}:${Hostname}" + $newBinding = $site.Bindings.Add($searchBindings, $Protocol) + + if ($Protocol -eq "https" -and -not [string]::IsNullOrEmpty($Thumbprint)) { + # Clean and convert thumbprint to byte array + $cleanThumbprint = $Thumbprint -replace '[^a-fA-F0-9]', '' + $hashBytes = for ($i = 0; $i -lt $cleanThumbprint.Length; $i += 2) { + [Convert]::ToByte($cleanThumbprint.Substring($i, 2), 16) + } - return $conflict - } -} - -function Ensure-IISDrive { - [CmdletBinding()] - param () + $newBinding.CertificateStoreName = $StoreName + $newBinding.CertificateHash = [byte[]]$hashBytes + $newBinding.SetAttributeValue("sslFlags", $SslFlags) + } - # Try to import the WebAdministration module if not already loaded - if (-not (Get-Module -Name WebAdministration)) { - try { - Import-Module WebAdministration -ErrorAction Stop - } - catch { - Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available." - return $false + $iis.CommitChanges() + return New-ResultObject -Status Success -Code 0 -Step BindSSL -Message "Binding and certificate successfully applied via ServerManager" } } - - # Check if IIS drive is available - if (-not (Get-PSDrive -Name 'IIS' -ErrorAction SilentlyContinue)) { - Write-Warning "IIS:\ drive not available. Ensure IIS is installed and the WebAdministration module is imported." - return $false + catch { + $errorMessage = "Error adding binding with SSL: $($_.Exception.Message)" + Write-Warning $errorMessage + return New-ResultObject -Status Error -Code 202 -Step AddBinding -ErrorMessage $errorMessage } - - return $true } +# +# May want to replace this function with Remove-ExistingIISBinding in future version function Remove-KFIISSiteBinding { [CmdletBinding()] param ( @@ -778,6 +681,7 @@ function Remove-KFIISSiteBinding { } } +# Called on a renewal to remove any certificates if not bound or used function Remove-KFIISCertificateIfUnused { param ( [Parameter(Mandatory = $true)] @@ -818,7 +722,7 @@ function Remove-KFIISCertificateIfUnused { if ($bindings.Count -gt 0) { Write-Warning "The certificate with thumbprint $thumbprint is still used by the following bindings:" - $bindings | Format-Table -AutoSize + $bindings | Format-Table -AutoSize | Out-String | Write-Warning return } @@ -1166,6 +1070,9 @@ function New-CSREnrollment { # Validate the Crypto Service Provider Validate-CryptoProvider -ProviderName $ProviderName + # Parse Subject for any escaped commas + $parsedSubject = Parse-DNSubject $SubjectText + # Build the SAN entries if provided $sanContent = "" if ($SAN) { @@ -1184,7 +1091,7 @@ $($sanDirectives -join "`n") Signature=`"$`Windows NT$`" [NewRequest] -Subject = "$SubjectText" +Subject = "$parsedSubject" ProviderName = "$ProviderName" MachineKeySet = True HashAlgorithm = SHA256 @@ -1251,7 +1158,6 @@ function Import-SignedCertificate { [byte[]]$RawData, # RawData from the certificate [Parameter(Mandatory = $true)] - [ValidateSet("My", "Root", "CA", "TrustedPublisher", "TrustedPeople")] [string]$StoreName # Store to which the certificate should be imported ) @@ -1328,33 +1234,83 @@ function Test-CryptoServiceProvider { # Function that takes an x509 certificate object and returns the csp function Get-CertificateCSP { + param( + [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert + ) + + try { + # Check if certificate has a private key + if (-not $cert.HasPrivateKey) { + return "No private key" + } + + # Get the private key + $privateKey = $cert.PrivateKey + + if ($privateKey -and $privateKey.CspKeyContainerInfo) { + # For older .NET Framework + $cspKeyContainerInfo = $privateKey.CspKeyContainerInfo + + if ($cspKeyContainerInfo -and $cspKeyContainerInfo.ProviderName) { + return [string]$cspKeyContainerInfo.ProviderName + } + } + + # For newer .NET Core/5+ or CNG keys + try { + $key = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) + if ($key -and $key.GetType().Name -eq "RSACng") { + $cngKey = $key.Key + if ($cngKey -and $cngKey.Provider -and $cngKey.Provider.Provider) { + return [string]$cngKey.Provider.Provider + } + } + } + catch { + Write-Verbose "CNG key detection failed: $($_.Exception.Message)" + } + + # Ensure we always return a string + return "Unknown provider" + + } + catch { + return "Error retrieving CSP: $($_.Exception.Message)" + } +} + +function Get-CertificateCSPV2 { param ( [Parameter(Mandatory = $true)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert ) # Check if the certificate has a private key - if ($Cert -and $Cert.HasPrivateKey) { - try { - $key = $Cert.PrivateKey + if (-not $Cert.HasPrivateKey) { + Write-Warning "Certificate does not have a private key associated with it" + return $null + } - if ($key -is [System.Security.Cryptography.RSACryptoServiceProvider]) { - # CAPI-based key - return $key.CspKeyContainerInfo.ProviderName - } - elseif ($key -is [System.Security.Cryptography.RSACng]) { - # CNG-based key - return $key.Key.Provider - } - else { - return "Unknown provider type: $($key.GetType().FullName)" - } - } catch { - Write-Warning "Could not access provider information: $_" - return $null + $privateKey = $Cert.PrivateKey + if ($privateKey) { + # For older .NET Framework + $cspKeyContainerInfo = $privateKey.CspKeyContainerInfo + + if ($cspKeyContainerInfo) { + return $cspKeyContainerInfo.ProviderName } - } else { - Write-Warning "Certificate has no private key." + } + + try { + $key = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) + if ($key -and $key.GetType().Name -eq "RSACng") { + $cngKey = $key.Key + + return $cngKey.Provider.Provider + } + } + catch { + Write-Warning "CNG key detection failed: $($_.Exception.Message)" return $null } } @@ -1402,4 +1358,188 @@ function Validate-CryptoProvider { } Write-Verbose "Crypto Service Provider '$ProviderName' is valid." +} + +function Parse-DNSubject { + <# + .SYNOPSIS + Parses a Distinguished Name (DN) subject string and properly quotes RDN values containing escaped commas. + + .DESCRIPTION + This function takes a DN subject string and parses the Relative Distinguished Name (RDN) components, + adding proper quotes around values that contain escaped commas and escaping quotes for use in + PowerShell here-strings. Only RDN values with escaped commas get quoted. + + .PARAMETER Subject + The DN subject string to parse (e.g., "CN=Keyfactor,O=Keyfactor\, Inc") + + .EXAMPLE + Parse-DNSubject -Subject "CN=Keyfactor,O=Keyfactor\, Inc" + Returns: CN=Keyfactor,O=""Keyfactor, Inc"" + + .EXAMPLE + Parse-DNSubject -Subject "CN=Test User,O=Company\, LLC,OU=IT Department\, Security" + Returns: CN=Test User,O=""Company, LLC"",OU=""IT Department, Security"" + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Subject + ) + + # Initialize variables + $parsedComponents = @() + $currentComponent = "" + $i = 0 + + # Convert string to character array for easier parsing + $chars = $Subject.ToCharArray() + + while ($i -lt $chars.Length) { + $char = $chars[$i] + + # Check if we hit a comma + if ($char -eq ',') { + # Look back to see if it's escaped + $isEscaped = $false + if ($i -gt 0 -and $chars[$i-1] -eq '\') { + $isEscaped = $true + } + + if ($isEscaped) { + # This is an escaped comma, add it to current component + $currentComponent += $char + } else { + # This is a separator comma, finish current component + if ($currentComponent.Trim() -ne "") { + $parsedComponents += $currentComponent.Trim() + $currentComponent = "" + } + } + } else { + # Regular character, add to current component + $currentComponent += $char + } + + $i++ + } + + # Add the last component + if ($currentComponent.Trim() -ne "") { + $parsedComponents += $currentComponent.Trim() + } + + # Process each component to add quotes where needed + $processedComponents = @() + + foreach ($component in $parsedComponents) { + # Split on first equals sign to get attribute and value + $equalIndex = $component.IndexOf('=') + if ($equalIndex -gt 0) { + $attribute = $component.Substring(0, $equalIndex).Trim() + $value = $component.Substring($equalIndex + 1).Trim() + + # Clean up escaped commas first + $cleanValue = $value -replace '\\,', ',' + + # Check if original value had escaped commas (needs quotes) + if ($value -match '\\,') { + # This RDN value had escaped commas, so wrap in doubled quotes and escape quotes + $escapedValue = $cleanValue -replace '"', '""' + $processedComponents += "$attribute=`"`"$escapedValue`"`"" + } else { + # No escaped commas, keep as simple value but escape any existing quotes + $escapedValue = $cleanValue -replace '"', '""' + $processedComponents += "$attribute=$escapedValue" + } + } else { + # Invalid component format, keep as is + $processedComponents += $component + } + } + + # Join components back together (no outer quotes needed since it goes in PowerShell string) + $subjectString = ($processedComponents -join ',') + return $subjectString +} + +# Note: Removed Test-IISBindingConflict function - we now mimic IIS behavior +# IIS replaces exact matches and allows multiple hostnames (SNI) on same IP:Port +function Get-IISManagementInfo { + [CmdletBinding()] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string]$SiteName + ) + + $hasIISDrive = Ensure-IISDrive + Write-Verbose "IIS Drive available: $hasIISDrive" + + if ($hasIISDrive) { + $null = Import-Module WebAdministration + $sitePath = "IIS:\Sites\$SiteName" + + if (-not (Test-Path $sitePath)) { + $errorMessage = "Site '$SiteName' not found in IIS drive" + Write-Error $errorMessage + return @{ + Success = $false + UseIISDrive = $true + Result = New-ResultObject -Status Error -Code 201 -Step FindWebSite -ErrorMessage $errorMessage -Details @{ SiteName = $SiteName } + } + } + + return @{ + Success = $true + UseIISDrive = $true + Result = $null + } + } + else { + # ServerManager fallback + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" + $iis = New-Object Microsoft.Web.Administration.ServerManager + $site = $iis.Sites[$SiteName] + + if ($null -eq $site) { + $errorMessage = "Site '$SiteName' not found in ServerManager" + Write-Error $errorMessage + return @{ + Success = $false + UseIISDrive = $false + Result = New-ResultObject -Status Error -Code 201 -Step FindWebSite -ErrorMessage $errorMessage -Details @{ SiteName = $SiteName } + } + } + + return @{ + Success = $true + UseIISDrive = $false + Result = $null + } + } +} +function Ensure-IISDrive { + [CmdletBinding()] + param () + + # Try to import the WebAdministration module if not already loaded + if (-not (Get-Module -Name WebAdministration)) { + try { + $null = Import-Module WebAdministration -ErrorAction Stop + } + catch { + Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available." + return $false + } + } + + # Check if IIS drive is available + if (-not (Get-PSDrive -Name 'IIS' -ErrorAction SilentlyContinue)) { + Write-Warning "IIS:\ drive not available. Ensure IIS is installed and the WebAdministration module is imported." + return $false + } + + return $true } \ No newline at end of file diff --git a/README.md b/README.md index 3a5e085..95cf76d 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,9 @@ The Windows Certificate Universal Orchestrator extension implements 3 Certificat This integration is compatible with Keyfactor Universal Orchestrator version 10.1 and later. ## Support -The Windows Certificate Universal Orchestrator extension If you have a support issue, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com. +The Windows Certificate Universal Orchestrator extension is supported by Keyfactor. If you require support for any issues or have feature request, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com. -> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. +> If you want to contribute bug fixes or additional enhancements, use the **[Pull requests](../../pulls)** tab. ## Requirements & Prerequisites @@ -92,12 +92,19 @@ Before installing the Windows Certificate Universal Orchestrator extension, we r
-Using the WinCert Extension on Linux servers: +Using the WinCert Extension on Linux servers and/or with Docker Containers: 1. General SSH Setup Information: PowerShell 6 or higher and SSH must be installed on all computers. Install SSH, including ssh server, that's appropriate for your platform. You also need to install PowerShell from GitHub to get the SSH remoting feature. The SSH server must be configured to create a SSH subsysten to host a PowerShell process on the remote computer. It is suggested to turn off password authentication as this extension uses key-based authentication. 2. SSH Authentication: When creating a Keyfactor certificate store for the WinCert orchestrator extension, the only protocol supported to communicate with Windows servers is ssh. When providing the user id and password, the connection is attempted by creating a temporary private key file using the contents in the Password textbox. Therefore, the password field must contain the full SSH Private key. +3. If you choose to run this extension in a containerized environment, the container image must include PowerShell version 7.5 or later, along with either OpenSSH clients (for SSH-based connections) or OpenSSL (if SSL/TLS operations are required). Additionally, the PWSMan PowerShell module must be installed to support management tasks and remote session functionality. These dependencies are required to ensure full compatibility when connecting from the container to remote Windows servers. Below is an example Docker file snippet: +``` +dnf install https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell-7.5.2-1.rh.x86_64.rpm +pwsh -Command 'Install-Module -Name PSWSMan' +dnf install openssh-clients openssl +``` +
@@ -135,6 +142,26 @@ For customers wishing to use something other than the local administrator accoun - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. +### Using Crypto Service Providers (CSP) +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. 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 the PowerShell command on the target server: + + certutil -csplist + +When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. + +When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. + +Each CSP only supports certain key types and algorithms. + +Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: +|CSP Name|Supports RSA?|Supports ECC?| +|---|---|---| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| + ## Certificate Store Types @@ -257,7 +284,7 @@ the Keyfactor Command Portal | Name | Display Name | Description | Type | Default Value | Entry has a private key | Adding an entry | Removing an entry | Reenrolling an entry | | ---- | ------------ | ---- | ------------- | ----------------------- | ---------------- | ----------------- | ------------------- | ----------- | - | ProviderName | Crypto Provider Name | 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. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | + | ProviderName | Crypto Provider Name | Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | SAN | SAN | String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of = entries separated by ampersands; Example: 'dns=www.example.com&dns=www.example2.com' for multiple SANs. Can be made optional if RFC 2818 is disabled on the CA. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | ✅ Checked | The Entry Parameters tab should look like this: @@ -392,7 +419,7 @@ the Keyfactor Command Portal | SiteName | IIS Site Name | String value specifying the name of the IIS web site to bind the certificate to. Example: 'Default Web Site' or any custom site name such as 'MyWebsite'. | String | Default Web Site | 🔲 Unchecked | ✅ Checked | ✅ Checked | ✅ Checked | | SniFlag | SSL Flags | A 128-Bit Flag that determines what type of SSL settings you wish to use. The default is 0, meaning No SNI. For more information, check IIS documentation for the appropriate bit setting.) | String | 0 | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | Protocol | Protocol | Multiple choice value specifying the protocol to bind to. Example: 'https' for secure communication. | MultipleChoice | https | 🔲 Unchecked | ✅ Checked | ✅ Checked | ✅ Checked | - | ProviderName | Crypto Provider Name | 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. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | + | ProviderName | Crypto Provider Name | Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | SAN | SAN | String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of = entries separated by ampersands; Example: 'dns=www.example.com&dns=www.example2.com' for multiple SANs. Can be made optional if RFC 2818 is disabled on the CA. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | ✅ Checked | The Entry Parameters tab should look like this: @@ -515,7 +542,7 @@ the Keyfactor Command Portal | Name | Display Name | Description | Type | Default Value | Entry has a private key | Adding an entry | Removing an entry | Reenrolling an entry | | ---- | ------------ | ---- | ------------- | ----------------------- | ---------------- | ----------------- | ------------------- | ----------- | | InstanceName | Instance Name | String value specifying the SQL Server instance name to bind the certificate to. Example: 'MSSQLServer' for the default instance or 'Instance1' for a named instance. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | - | ProviderName | Crypto Provider Name | Optional string value specifying the name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing private keys. Example: 'Microsoft Strong Cryptographic Provider'. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | + | ProviderName | Crypto Provider Name | Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | SAN | SAN | String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of = entries separated by ampersands; Example: 'dns=www.example.com&dns=www.example2.com' for multiple SANs. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | ✅ Checked | The Entry Parameters tab should look like this: @@ -593,8 +620,8 @@ The Windows Certificate Universal Orchestrator extension implements 3 Certificat Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "Windows Certificate" or the customized certificate store name from the previous step. | | 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 a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -685,8 +712,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "IIS Bound Certificate" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the IIS 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. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -777,8 +804,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "WinSql" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the SQL Server 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. For more information, see [Client Machine](#note-regarding-client-machine). | diff --git a/docsource/content.md b/docsource/content.md index 39d1793..1078017 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -39,12 +39,19 @@ In version 2.0 of the IIS Orchestrator, the certificate store type has been rena ## Requirements
-Using the WinCert Extension on Linux servers: +Using the WinCert Extension on Linux servers and/or with Docker Containers: 1. General SSH Setup Information: PowerShell 6 or higher and SSH must be installed on all computers. Install SSH, including ssh server, that's appropriate for your platform. You also need to install PowerShell from GitHub to get the SSH remoting feature. The SSH server must be configured to create a SSH subsysten to host a PowerShell process on the remote computer. It is suggested to turn off password authentication as this extension uses key-based authentication. 2. SSH Authentication: When creating a Keyfactor certificate store for the WinCert orchestrator extension, the only protocol supported to communicate with Windows servers is ssh. When providing the user id and password, the connection is attempted by creating a temporary private key file using the contents in the Password textbox. Therefore, the password field must contain the full SSH Private key. +3. If you choose to run this extension in a containerized environment, the container image must include PowerShell version 7.5 or later, along with either OpenSSH clients (for SSH-based connections) or OpenSSL (if SSL/TLS operations are required). Additionally, the PWSMan PowerShell module must be installed to support management tasks and remote session functionality. These dependencies are required to ensure full compatibility when connecting from the container to remote Windows servers. Below is an example Docker file snippet: +``` +dnf install https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell-7.5.2-1.rh.x86_64.rpm +pwsh -Command 'Install-Module -Name PSWSMan' +dnf install openssh-clients openssl +``` +
@@ -82,6 +89,26 @@ For customers wishing to use something other than the local administrator accoun - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. +### Using Crypto Service Providers (CSP) +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. 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 the PowerShell command on the target server: + + certutil -csplist + +When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. + +When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. + +Each CSP only supports certain key types and algorithms. + +Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: +|CSP Name|Supports RSA?|Supports ECC?| +|---|---|---| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| + ## Client Machine Instructions Prior to version 2.6, this extension would only run in the Windows environment. Version 2.6 and greater is capable of running on Linux, however, only the SSH protocol is supported. diff --git a/docsource/images/IISU-advanced-store-type-dialog.png b/docsource/images/IISU-advanced-store-type-dialog.png index 73ada7d..eab1385 100644 Binary files a/docsource/images/IISU-advanced-store-type-dialog.png and b/docsource/images/IISU-advanced-store-type-dialog.png differ diff --git a/docsource/images/IISU-basic-store-type-dialog.png b/docsource/images/IISU-basic-store-type-dialog.png index 30e486e..17be4a5 100644 Binary files a/docsource/images/IISU-basic-store-type-dialog.png and b/docsource/images/IISU-basic-store-type-dialog.png differ diff --git a/docsource/images/IISU-custom-fields-store-type-dialog.png b/docsource/images/IISU-custom-fields-store-type-dialog.png index a3c1017..1e723bb 100644 Binary files a/docsource/images/IISU-custom-fields-store-type-dialog.png and b/docsource/images/IISU-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/IISU-entry-parameters-store-type-dialog.png b/docsource/images/IISU-entry-parameters-store-type-dialog.png index c351f10..e8d8817 100644 Binary files a/docsource/images/IISU-entry-parameters-store-type-dialog.png and b/docsource/images/IISU-entry-parameters-store-type-dialog.png differ diff --git a/docsource/images/WinCert-advanced-store-type-dialog.png b/docsource/images/WinCert-advanced-store-type-dialog.png index 033a466..8b43572 100644 Binary files a/docsource/images/WinCert-advanced-store-type-dialog.png and b/docsource/images/WinCert-advanced-store-type-dialog.png differ diff --git a/docsource/images/WinCert-basic-store-type-dialog.png b/docsource/images/WinCert-basic-store-type-dialog.png index c2be5c0..9f18381 100644 Binary files a/docsource/images/WinCert-basic-store-type-dialog.png and b/docsource/images/WinCert-basic-store-type-dialog.png differ diff --git a/docsource/images/WinCert-custom-fields-store-type-dialog.png b/docsource/images/WinCert-custom-fields-store-type-dialog.png index a3c1017..1e723bb 100644 Binary files a/docsource/images/WinCert-custom-fields-store-type-dialog.png and b/docsource/images/WinCert-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/WinCert-entry-parameters-store-type-dialog.png b/docsource/images/WinCert-entry-parameters-store-type-dialog.png index a27cd95..df44b2b 100644 Binary files a/docsource/images/WinCert-entry-parameters-store-type-dialog.png and b/docsource/images/WinCert-entry-parameters-store-type-dialog.png differ diff --git a/docsource/images/WinSql-advanced-store-type-dialog.png b/docsource/images/WinSql-advanced-store-type-dialog.png index 033a466..8b43572 100644 Binary files a/docsource/images/WinSql-advanced-store-type-dialog.png and b/docsource/images/WinSql-advanced-store-type-dialog.png differ diff --git a/docsource/images/WinSql-basic-store-type-dialog.png b/docsource/images/WinSql-basic-store-type-dialog.png index 00cc691..c276ba9 100644 Binary files a/docsource/images/WinSql-basic-store-type-dialog.png and b/docsource/images/WinSql-basic-store-type-dialog.png differ diff --git a/docsource/images/WinSql-custom-fields-store-type-dialog.png b/docsource/images/WinSql-custom-fields-store-type-dialog.png index 9beed3c..31b5069 100644 Binary files a/docsource/images/WinSql-custom-fields-store-type-dialog.png and b/docsource/images/WinSql-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/WinSql-entry-parameters-store-type-dialog.png b/docsource/images/WinSql-entry-parameters-store-type-dialog.png index 6f1f669..7dd632d 100644 Binary files a/docsource/images/WinSql-entry-parameters-store-type-dialog.png and b/docsource/images/WinSql-entry-parameters-store-type-dialog.png differ diff --git a/integration-manifest.json b/integration-manifest.json index 13088fd..ef70981 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -115,7 +115,7 @@ "DependsOn": "", "DefaultValue": "", "Options": "", - "Description": "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." + "Description": "Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers'" }, { "Name": "SAN", @@ -306,21 +306,21 @@ "Options": "https,http", "Description": "Multiple choice value specifying the protocol to bind to. Example: 'https' for secure communication." }, - { - "Name": "ProviderName", - "DisplayName": "Crypto Provider Name", - "Type": "String", - "RequiredWhen": { - "HasPrivateKey": false, - "OnAdd": false, - "OnRemove": false, - "OnReenrollment": false - }, - "DependsOn": "", - "DefaultValue": "", - "Options": "", - "Description": "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." + { + "Name": "ProviderName", + "DisplayName": "Crypto Provider Name", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false }, + "DependsOn": "", + "DefaultValue": "", + "Options": "", + "Description": "Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers'" + }, { "Name": "SAN", "DisplayName": "SAN", @@ -441,21 +441,21 @@ }, "Description": "String value specifying the SQL Server instance name to bind the certificate to. Example: 'MSSQLServer' for the default instance or 'Instance1' for a named instance." }, - { - "Name": "ProviderName", - "DisplayName": "Crypto Provider Name", - "Type": "String", - "RequiredWhen": { - "HasPrivateKey": false, - "OnAdd": false, - "OnRemove": false, - "OnReenrollment": false - }, - "DependsOn": "", - "DefaultValue": "", - "Options": "", - "Description": "Optional string value specifying the name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing private keys. Example: 'Microsoft Strong Cryptographic Provider'." + { + "Name": "ProviderName", + "DisplayName": "Crypto Provider Name", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false }, + "DependsOn": "", + "DefaultValue": "", + "Options": "", + "Description": "Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers'" + }, { "Name": "SAN", "DisplayName": "SAN",