diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d93681..643c8a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ 2.6.3 * Fixed reenrollment 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. 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 f7bf47b..10d6d70 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -1,4 +1,9 @@ -# Set preferences globally at the script level +# Update notes: +# 8/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 + +# Set preferences globally at the script level $DebugPreference = "Continue" $VerbosePreference = "Continue" $InformationPreference = "Continue" @@ -225,136 +230,80 @@ 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`"" - - Write-Verbose "Running: $traceCommand" - $output = Invoke-Expression $command + # Start building certutil arguments + $arguments = @('-f') - 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 - } - } - - } 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." - - # Get the thumbprint so it can be returned to the calling function - $thumbprint = $cert.Thumbprint - Write-Information "The thumbprint '$thumbprint' was created." - } + $arguments += '-importpfx' + $arguments += $StoreName + $arguments += $tempPfx - Write-Host "Certificate added successfully to $StoreName." - return $thumbprint - } catch { - Write-Error "An error occurred: $_" - return $null - } -} + # Quote any arguments with spaces + $argLine = ($arguments | ForEach-Object { + if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } + }) -join ' ' -function Add-KFCertificateToStoreNEW{ - param ( - [Parameter(Mandatory = $true)] - [string]$Base64Cert, - - [Parameter(Mandatory = $false)] - [string]$PrivateKeyPassword, - - [Parameter(Mandatory = $true)] - [string]$StoreName, - - [Parameter(Mandatory = $false)] - [string]$CryptoServiceProvider - ) + write-Verbose "Running certutil with arguments: $argLine" - try { - Write-Information "Entering PowerShell Script Add-KFCertificate" - Write-Verbose "Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" + # 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 - $thumbprint = $null + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo - 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.Start() | Out-Null - Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'" + $stdOut = $process.StandardOutput.ReadToEnd() + $stdErr = $process.StandardError.ReadToEnd() - # Convert Base64 PFX to bytes and save to temp file - $tempPfxPath = [System.IO.Path]::GetTempFileName() + ".pfx" - [System.IO.File]::WriteAllBytes($tempPfxPath, [Convert]::FromBase64String($Base64Cert)) + $process.WaitForExit() - 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" } + # 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 { - # 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" @@ -378,6 +327,7 @@ function Add-KFCertificateToStoreNEW{ return $null } } + function Remove-KFCertificateFromStore { param ( [string]$Thumbprint, @@ -464,13 +414,17 @@ function New-KFIISSiteBinding { return $result } + Write-Verbose "No binding conflicts found for SiteName: '$SiteName', IPAddress: '$IPAddress', Port: $Port, HostName: '$Hostname'" $searchBindings = "${IPAddress}:${Port}:${Hostname}" $hasIISDrive = Ensure-IISDrive Write-Verbose "IIS Drive is available: $hasIISDrive" if ($hasIISDrive) { - Import-Module WebAdministration + + Write-Verbose "IIS Drive is available, using WebAdministration module." + + $null = Import-Module WebAdministration $sitePath = "IIS:\Sites\$SiteName" if (-not (Test-Path $sitePath)) { $msg = "Site '$SiteName' not found in IIS drive." @@ -480,7 +434,7 @@ function New-KFIISSiteBinding { $site = Get-Item $sitePath $httpsBindings = $site.Bindings.Collection | Where-Object { $_.bindingInformation -eq $searchBindings -and $_.protocol -eq "https" - } + } foreach ($binding in $httpsBindings) { try { @@ -520,6 +474,8 @@ function New-KFIISSiteBinding { } } else { # SERVERMANAGER FALLBACK + Write-Verbose "IIS Drive is not available, using ServerManager fallback." + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" $iis = New-Object Microsoft.Web.Administration.ServerManager $site = $iis.Sites[$SiteName] @@ -578,7 +534,7 @@ function CheckExistingBindings { $conflicts = @() if (Ensure-IISDrive) { - Import-Module WebAdministration + $null = Import-Module WebAdministration Get-Website | Where-Object { $_.Name -ne $TargetSiteName } | ForEach-Object { $siteName = $_.Name @@ -647,7 +603,7 @@ function CheckExistingBindingsORIG { ) if (Ensure-IISDrive) { - Import-Module WebAdministration + $null = Import-Module WebAdministration $conflict = $false @@ -710,7 +666,7 @@ function Ensure-IISDrive { # Try to import the WebAdministration module if not already loaded if (-not (Get-Module -Name WebAdministration)) { try { - Import-Module WebAdministration -ErrorAction Stop + $null = Import-Module WebAdministration -ErrorAction Stop } catch { Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available." @@ -1336,6 +1292,43 @@ function Get-CertificateCSP { [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert ) + # Check if the certificate has a private key + if (-not $Cert.HasPrivateKey) { + Write-Warning "Certificate does not have a private key associated with it" + return $null + } + + $privateKey = $Cert.PrivateKey + if ($privateKey) { + # For older .NET Framework + $cspKeyContainerInfo = $privateKey.CspKeyContainerInfo + + if ($cspKeyContainerInfo) { + return $cspKeyContainerInfo.ProviderName + } + } + + 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 + } +} + +# 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 {