Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
227 changes: 110 additions & 117 deletions IISU/PowerShellScripts/WinCertScripts.ps1
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -378,6 +327,7 @@ function Add-KFCertificateToStoreNEW{
return $null
}
}

function Remove-KFCertificateFromStore {
param (
[string]$Thumbprint,
Expand Down Expand Up @@ -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."
Expand All @@ -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 {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -647,7 +603,7 @@ function CheckExistingBindingsORIG {
)

if (Ensure-IISDrive) {
Import-Module WebAdministration
$null = Import-Module WebAdministration

$conflict = $false

Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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 {
Expand Down
Loading