From 22c36a241ea55e9f5df36c50c4564f22a599bf1a Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 28 Aug 2025 13:45:54 -0700 Subject: [PATCH 1/4] testing --- IISU/PowerShellScripts/WinCertScripts.ps1 | 81 ++++++++++++++--------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index 160db99..decfa1a 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -209,7 +209,9 @@ 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 + $thumbprint = Get-PfxThumbprint -Base64Cert $Base64Cert -Password $PrivateKeyPassword + if (-not $thumbprint) { throw "Failed to get the certificate thumbprint. The PFX may be invalid or the password is incorrect." } if ($CryptoServiceProvider) { @@ -278,29 +280,11 @@ function Add-KFCertificateToStore{ if ($process.ExitCode -ne 0) { throw "certutil failed with code $($process.ExitCode). Output:`n$stdOut`nError:`n$stdErr" } - - # Retrieve thumbprint of the newly imported cert - try { - $cert = Get-ChildItem -Path "Cert:\LocalMachine\$StoreName" | - Sort-Object NotAfter -Descending | - Select-Object -First 1 - if ($cert) { - Write-Information "Imported certificate thumbprint: $($cert.Thumbprint)" - return $cert.Thumbprint - } else { - Write-Warning "Could not retrieve the imported certificate." - return $null - } - } - catch { - Write-Warning "Failed to retrieve thumbprint: $_" - return $null - } } catch { Write-Error "ERROR: $_" } finally { if (Test-Path $tempPfx) { - #Remove-Item $tempPfx -Force + Remove-Item $tempPfx -Force } } @@ -314,13 +298,10 @@ function Add-KFCertificateToStore{ $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 created in store $StoreName." return $thumbprint } catch { Write-Error "An error occurred: $_" @@ -469,12 +450,19 @@ function New-KFIISSiteBinding { $_.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" + try + { + 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" + } + } + catch + { + $result = New-ResultObject -Status Error -Code 202 -Step BindSSL -Message $_ } } } else { @@ -1264,6 +1252,37 @@ function Import-SignedCertificate { ##### # Shared Functions +# Function to return the certificate's thumbprint +function Get-PfxThumbprint { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Base64Cert, + + [Parameter(Mandatory = $false)] + [string]$Password + ) + + try { + # Convert Base64 to byte array + $pfxBytes = [Convert]::FromBase64String($Base64Cert) + + # Convert password to secure string if provided, otherwise use $null + $securePassword = if ($Password) { ConvertTo-SecureString -String $Password -AsPlainText -Force } else { $null } + + # Import certificate + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($pfxBytes, $securePassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) + + # Return thumbprint (formatted) + return $cert.Thumbprint.Replace(" ", "").ToUpper() + } + catch { + Write-Error "Failed to load PFX: $_" + return $null + } +} + # Function to get SAN (Subject Alternative Names) from a certificate function Get-KFSAN($cert) { $san = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq "Subject Alternative Name" } From 0701ddb0f928b2167fefbd307329ed05d612fe5e Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 28 Aug 2025 15:02:51 -0700 Subject: [PATCH 2/4] test --- IISU/PowerShellScripts/WinCertScripts.ps1 | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index decfa1a..b8f8612 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -210,7 +210,14 @@ function Add-KFCertificateToStore{ Write-Verbose "Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" # Get the thumbprint of the passed in certificate - $thumbprint = Get-PfxThumbprint -Base64Cert $Base64Cert -Password $PrivateKeyPassword + # 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 } + + # + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet) + $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) @@ -289,19 +296,15 @@ function Add-KFCertificateToStore{ } } 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 #> $certStore.Add($cert) $certStore.Close(); Write-Information "Store '$StoreName' is closed." - } - Write-Information "The thumbprint '$thumbprint' was created in store $StoreName." + Write-Information "The thumbprint '$thumbprint' was added to store $StoreName." return $thumbprint } catch { Write-Error "An error occurred: $_" From 5f8fffc41a4a45801288814be6319115218b7f75 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Thu, 28 Aug 2025 15:06:06 -0700 Subject: [PATCH 3/4] testing --- IISU/PowerShellScripts/WinCertScripts.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index b8f8612..eca69fa 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -215,7 +215,7 @@ function Add-KFCertificateToStore{ $securePassword = if ($PrivateKeyPassword) { ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force } else { $null } # - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet) + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, 18) $thumbprint = $cert.Thumbprint if (-not $thumbprint) { throw "Failed to get the certificate thumbprint. The PFX may be invalid or the password is incorrect." } From 26aea08899b46fef8de0d347e320bcaae2352d61 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 29 Aug 2025 12:40:25 -0700 Subject: [PATCH 4/4] Fixed a problem returning thumbprints and binding to wrong websites. --- CHANGELOG.md | 6 +- IISU/PowerShellScripts/WinCertScripts.ps1 | 513 +++++++++++++--------- 2 files changed, 318 insertions(+), 201 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 643c8a1..f8cd544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ 2.6.3 -* Fixed reenrollment job when RDN Components contained escaped commas +* Fixed reenrollment 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 +* 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. 2.6.2 * Fixed error when attempting to connect to remote computer using UO service account diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index eca69fa..9e3f378 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -1,7 +1,9 @@ # Update notes: -# 8/12/25 Updated functions to manage IIS bindings and certificates +# 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 # Set preferences globally at the script level $DebugPreference = "Continue" @@ -214,8 +216,11 @@ function Add-KFCertificateToStore{ $bytes = [System.Convert]::FromBase64String($Base64Cert) $securePassword = if ($PrivateKeyPassword) { ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force } else { $null } - # - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($bytes, $securePassword, 18) + # 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." } @@ -298,7 +303,11 @@ function Add-KFCertificateToStore{ } else { $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" Write-Information "Store '$StoreName' is open." - $certStore.Open(5) + + # 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." @@ -365,7 +374,7 @@ function Remove-KFCertificateFromStore { return $isSuccessful } -function New-KFIISSiteBinding { +function New-KFIISSiteBindingOLD { [CmdletBinding()] [OutputType([pscustomobject])] param ( @@ -413,7 +422,7 @@ function New-KFIISSiteBinding { 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 } + $result = New-ResultObject -Status Error -Code 201 -Step FindWebSite -ErrorMessage $msg -Details @{ SiteName = $SiteName; IPAddress = $IPAddress; Port = $Port; HostName = $Hostname } } else { $site = Get-Item $sitePath $httpsBindings = $site.Bindings.Collection | Where-Object { @@ -519,166 +528,255 @@ function New-KFIISSiteBinding { return $result } -function CheckExistingBindings { +# IIS Functions +function New-KFIISSiteBinding { + [CmdletBinding()] + [OutputType([pscustomobject])] param ( - [string]$DesiredIP, - [string]$DesiredPort, - [string]$DesiredHost, - [string]$TargetSiteName + [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 ) - $conflicts = @() - - if (Ensure-IISDrive) { - $null = Import-Module WebAdministration + Write-Information "Entering PowerShell Script: New-KFIISSiteBinding" -InformationAction SilentlyContinue + Write-Verbose "Function: New-KFIISSiteBinding" + Write-Verbose "Parameters: $(($PSBoundParameters.GetEnumerator() | ForEach-Object { "$($_.Key): '$($_.Value)'" }) -join ', ')" - 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 - } - } - } + 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 } - return @($conflicts) - } - else { - # SERVERMANAGER FALLBACK - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager + # 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 + } - 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 - } - } - } - } + # 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 $conflicts + } + catch { + $errorMessage = "Unexpected error in New-KFIISSiteBinding: $($_.Exception.Message)" + Write-Error $errorMessage + return New-ResultObject -Status Error -Code 999 -Step UnexpectedError -ErrorMessage $errorMessage } } - -function CheckExistingBindingsORIG { +function Remove-ExistingIISBinding { + [CmdletBinding()] + [OutputType([pscustomobject])] param ( - [string]$DesiredIP, - [string]$DesiredPort, - [string]$DesiredHost, - [string]$TargetSiteName + [Parameter(Mandatory = $true)] + [string]$SiteName, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$BindingInfo, + + [Parameter(Mandatory = $true)] + [bool]$UseIISDrive ) - if (Ensure-IISDrive) { - $null = 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 "Removing existing bindings for exact match: $BindingInfo on site $SiteName (mimics IIS replace behavior)" + + try { + if ($UseIISDrive) { + $sitePath = "IIS:\Sites\$SiteName" + $site = Get-Item $sitePath + $httpsBindings = $site.Bindings.Collection | Where-Object { + $_.bindingInformation -eq $BindingInfo -and $_.protocol -eq "https" + } + + foreach ($binding in $httpsBindings) { + $bindingInfo = $binding.GetAttributeValue("bindingInformation") + $protocol = $binding.protocol + + Write-Verbose "Removing binding: $bindingInfo ($protocol)" + Remove-WebBinding -Name $SiteName -BindingInformation $bindingInfo -Protocol $protocol -Confirm:$false + Write-Verbose "Successfully removed binding" } } - - return $conflict - } - else { - # SERVERMANAGER FALLBACK - Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" - $iis = New-Object Microsoft.Web.Administration.ServerManager + 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] - $conflict = $false + $httpsBindings = $site.Bindings | Where-Object { + $_.BindingInformation -eq $BindingInfo -and $_.Protocol -eq "https" + } - $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 - } - } + foreach ($binding in $httpsBindings) { + Write-Verbose "Removing binding: $($binding.BindingInformation)" + $site.Bindings.Remove($binding) } + + $iis.CommitChanges() } - return $conflict + return New-ResultObject -Status Success -Code 0 -Step RemoveBinding -Message "Successfully removed existing bindings" + } + catch { + $errorMessage = "Error removing existing binding: $($_.Exception.Message)" + Write-Warning $errorMessage + return New-ResultObject -Status Error -Code 201 -Step RemoveBinding -ErrorMessage $errorMessage } } - -function Ensure-IISDrive { +function Add-IISBindingWithSSL { [CmdletBinding()] - param () + [OutputType([pscustomobject])] + param ( + [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 + ) - # Try to import the WebAdministration module if not already loaded - if (-not (Get-Module -Name WebAdministration)) { - try { - $null = Import-Module WebAdministration -ErrorAction Stop + 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 + } + + # 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 + + # 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 + } + + 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" + } } - catch { - Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available." - return $false + 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) + } + + $newBinding.CertificateStoreName = $StoreName + $newBinding.CertificateHash = [byte[]]$hashBytes + $newBinding.SetAttributeValue("sslFlags", $SslFlags) + } + + $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 ( @@ -730,6 +828,7 @@ function Remove-KFIISSiteBinding { } } +# Called on a renewal to remove any certificats if not bound or used function Remove-KFIISCertificateIfUnused { param ( [Parameter(Mandatory = $true)] @@ -1255,37 +1354,6 @@ function Import-SignedCertificate { ##### # Shared Functions -# Function to return the certificate's thumbprint -function Get-PfxThumbprint { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string]$Base64Cert, - - [Parameter(Mandatory = $false)] - [string]$Password - ) - - try { - # Convert Base64 to byte array - $pfxBytes = [Convert]::FromBase64String($Base64Cert) - - # Convert password to secure string if provided, otherwise use $null - $securePassword = if ($Password) { ConvertTo-SecureString -String $Password -AsPlainText -Force } else { $null } - - # Import certificate - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $cert.Import($pfxBytes, $securePassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) - - # Return thumbprint (formatted) - return $cert.Thumbprint.Replace(" ", "").ToUpper() - } - catch { - Write-Error "Failed to load PFX: $_" - return $null - } -} - # Function to get SAN (Subject Alternative Names) from a certificate function Get-KFSAN($cert) { $san = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq "Subject Alternative Name" } @@ -1348,39 +1416,6 @@ function Get-CertificateCSP { } } -# Function that takes an x509 certificate object and returns the csp -function Get-CertificateCSPOLD { - 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 ($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 - } - } else { - Write-Warning "Certificate has no private key." - return $null - } -} - function Get-CryptoProviders { # Retrieves the list of available Crypto Service Providers using certutil try { @@ -1528,4 +1563,84 @@ function Parse-DNSubject { # 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