Skip to content

Commit fb520aa

Browse files
authored
Merge pull request #146 from Keyfactor/74699-Fix_Error_When_PFX_Password_Complex
#ab74699 - Fixed an error with complex PFX passwords
2 parents ee3ee82 + 4d73691 commit fb520aa

File tree

2 files changed

+112
-117
lines changed

2 files changed

+112
-117
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
2.6.3
22
* Fixed reenrollment job when RDN Components contained escaped commas
33
* Updated renewal job for IIS Certs to delete the old cert if not bound or used by other web sites.
4+
* Improved Inventory reporting of CSP when cert uses newer CNG Keys
5+
* Fixed an issue with complex PFX passwords that contained special characters such as '@' or '$', etc.
46

57
2.6.2
68
* Fixed error when attempting to connect to remote computer using UO service account

IISU/PowerShellScripts/WinCertScripts.ps1

Lines changed: 110 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
# Set preferences globally at the script level
1+
# Update notes:
2+
# 8/12/25 Updated functions to manage IIS bindings and certificates
3+
# Updated script to read CSPs correctly using newer CNG Keys
4+
# Fix an error with complex PFX passwords having irregular characters
5+
6+
# Set preferences globally at the script level
27
$DebugPreference = "Continue"
38
$VerbosePreference = "Continue"
49
$InformationPreference = "Continue"
@@ -225,136 +230,80 @@ function Add-KFCertificateToStore{
225230

226231
# Execute certutil based on whether a private key password was supplied
227232
try {
228-
# Build certutil command to import the certificate with exportable private key and CSP
229-
$command = "certutil -f -p `"$PrivateKeyPassword`" -csp `"$CryptoServiceProvider`" -importpfx $StoreName `"$tempPfx`""
230-
$traceCommand = "certutil -f -p `"************`" -csp `"$CryptoServiceProvider`" -importpfx $StoreName `"$tempPfx`""
231-
232-
Write-Verbose "Running: $traceCommand"
233-
$output = Invoke-Expression $command
233+
# Start building certutil arguments
234+
$arguments = @('-f')
234235

235-
if ($LASTEXITCODE -ne 0) {
236-
throw "certutil failed with code $LASTEXITCODE. `nOutput: $output `nMake sure there is no cryptographic mismatch and the CSP supports the imported PFX.`n"
236+
if ($PrivateKeyPassword) {
237+
Write-Verbose "Has a private key"
238+
$arguments += '-p'
239+
$arguments += $PrivateKeyPassword
237240
}
238241

239-
# Get latest cert with private key in the store
240-
$store = "Cert:\LocalMachine\$StoreName"
241-
$cert = Get-ChildItem -Path $store | Where-Object { $_.HasPrivateKey } | Sort-Object NotBefore -Descending | Select-Object -First 1
242-
243-
if ($cert) {
244-
Write-Information "Certificate imported successfully with Thumbprint: $($cert.Thumbprint)"
245-
return $cert.Thumbprint
246-
} else {
247-
throw "Import succeeded, but no certificate with a private key was found in $store"
242+
if ($CryptoServiceProvider) {
243+
Write-Verbose "Has a CryptoServiceProvider: $CryptoServiceProvider"
244+
$arguments += '-csp'
245+
$arguments += $CryptoServiceProvider
248246
}
249247

250-
} catch {
251-
Write-Error "ERROR: $_"
252-
} finally {
253-
if (Test-Path $tempPfx) {
254-
#Remove-Item $tempPfx -Force
255-
}
256-
}
257-
258-
} else {
259-
$bytes = [System.Convert]::FromBase64String($Base64Cert)
260-
$certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine"
261-
Write-Information "Store '$StoreName' is open."
262-
$certStore.Open(5)
263-
264-
$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $PrivateKeyPassword, 18 <# Persist, Machine #>
265-
$certStore.Add($cert)
266-
$certStore.Close();
267-
Write-Information "Store '$StoreName' is closed."
268-
269-
# Get the thumbprint so it can be returned to the calling function
270-
$thumbprint = $cert.Thumbprint
271-
Write-Information "The thumbprint '$thumbprint' was created."
272-
}
248+
$arguments += '-importpfx'
249+
$arguments += $StoreName
250+
$arguments += $tempPfx
273251

274-
Write-Host "Certificate added successfully to $StoreName."
275-
return $thumbprint
276-
} catch {
277-
Write-Error "An error occurred: $_"
278-
return $null
279-
}
280-
}
252+
# Quote any arguments with spaces
253+
$argLine = ($arguments | ForEach-Object {
254+
if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ }
255+
}) -join ' '
281256

282-
function Add-KFCertificateToStoreNEW{
283-
param (
284-
[Parameter(Mandatory = $true)]
285-
[string]$Base64Cert,
286-
287-
[Parameter(Mandatory = $false)]
288-
[string]$PrivateKeyPassword,
289-
290-
[Parameter(Mandatory = $true)]
291-
[string]$StoreName,
292-
293-
[Parameter(Mandatory = $false)]
294-
[string]$CryptoServiceProvider
295-
)
257+
write-Verbose "Running certutil with arguments: $argLine"
296258

297-
try {
298-
Write-Information "Entering PowerShell Script Add-KFCertificate"
299-
Write-Verbose "Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'"
259+
# Setup process execution
260+
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
261+
$processInfo.FileName = "certutil.exe"
262+
$processInfo.Arguments = $argLine.Trim()
263+
$processInfo.RedirectStandardOutput = $true
264+
$processInfo.RedirectStandardError = $true
265+
$processInfo.UseShellExecute = $false
266+
$processInfo.CreateNoWindow = $true
300267

301-
$thumbprint = $null
268+
$process = New-Object System.Diagnostics.Process
269+
$process.StartInfo = $processInfo
302270

303-
if ($CryptoServiceProvider)
304-
{
305-
# Test to see if CSP exists
306-
if(-not (Test-CryptoServiceProvider -CSPName $CryptoServiceProvider))
307-
{
308-
Write-Information "INFO: The CSP $CryptoServiceProvider was not found on the system."
309-
Write-Warning "WARN: CSP $CryptoServiceProvider was not found on the system."
310-
return
311-
}
271+
$process.Start() | Out-Null
312272

313-
Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'"
273+
$stdOut = $process.StandardOutput.ReadToEnd()
274+
$stdErr = $process.StandardError.ReadToEnd()
314275

315-
# Convert Base64 PFX to bytes and save to temp file
316-
$tempPfxPath = [System.IO.Path]::GetTempFileName() + ".pfx"
317-
[System.IO.File]::WriteAllBytes($tempPfxPath, [Convert]::FromBase64String($Base64Cert))
276+
$process.WaitForExit()
318277

319-
try {
320-
# Load the PFX into a PKCS12 object
321-
$pfx = New-Object -ComObject X509Enrollment.CX509Enrollment
322-
$pfx.InitializeImport(1, [System.IO.File]::ReadAllText($tempPfxPath), $PrivateKeyPassword)
323-
324-
# Create new private key with desired CSP
325-
$privateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey
326-
$privateKey.ProviderName = $CryptoServiceProvider
327-
$privateKey.Length = [int]2048
328-
$privateKey.KeySpec = 1 # AT_KEYEXCHANGE
329-
$privateKey.ExportPolicy = 1 # AllowExport
330-
$privateKey.MachineContext = $true
331-
$privateKey.Create()
332-
333-
# Associate private key with enrollment
334-
$pfx.InstallResponse(2, "", 0, $null)
335-
336-
Write-Host "Certificate imported successfully using CSP: $CryptoServiceProvider"
337-
338-
# The most recently added cert (with private key) should be the new one
339-
$latest = $certsBefore | Where-Object { $_.HasPrivateKey } | Sort-Object NotBefore -Descending | Select-Object -First 1
340-
341-
if ($latest) {
342-
Write-Information "Certificate imported successfully with thumbprint: $($latest.Thumbprint)"
343-
return $latest.Thumbprint
344-
} else {
345-
throw "Certificate installed but no cert with private key was found in store '$StoreName'."
278+
if ($process.ExitCode -ne 0) {
279+
throw "certutil failed with code $($process.ExitCode). Output:`n$stdOut`nError:`n$stdErr"
346280
}
347281

282+
# Retrieve thumbprint of the newly imported cert
283+
try {
284+
$cert = Get-ChildItem -Path "Cert:\LocalMachine\$StoreName" |
285+
Sort-Object NotAfter -Descending |
286+
Select-Object -First 1
287+
if ($cert) {
288+
Write-Information "Imported certificate thumbprint: $($cert.Thumbprint)"
289+
return $cert.Thumbprint
290+
} else {
291+
Write-Warning "Could not retrieve the imported certificate."
292+
return $null
293+
}
294+
}
295+
catch {
296+
Write-Warning "Failed to retrieve thumbprint: $_"
297+
return $null
298+
}
348299
} catch {
349-
# Handle any errors and log the exception message
350-
Write-Error "Error during certificate import: $_"
351-
return "Error: $_"
300+
Write-Error "ERROR: $_"
352301
} finally {
353-
# Ensure the temporary file is deleted
354-
if (Test-Path $tempFileName) {
355-
Remove-Item $tempFileName -Force
302+
if (Test-Path $tempPfx) {
303+
#Remove-Item $tempPfx -Force
356304
}
357305
}
306+
358307
} else {
359308
$bytes = [System.Convert]::FromBase64String($Base64Cert)
360309
$certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine"
@@ -378,6 +327,7 @@ function Add-KFCertificateToStoreNEW{
378327
return $null
379328
}
380329
}
330+
381331
function Remove-KFCertificateFromStore {
382332
param (
383333
[string]$Thumbprint,
@@ -464,13 +414,17 @@ function New-KFIISSiteBinding {
464414

465415
return $result
466416
}
417+
Write-Verbose "No binding conflicts found for SiteName: '$SiteName', IPAddress: '$IPAddress', Port: $Port, HostName: '$Hostname'"
467418

468419
$searchBindings = "${IPAddress}:${Port}:${Hostname}"
469420
$hasIISDrive = Ensure-IISDrive
470421
Write-Verbose "IIS Drive is available: $hasIISDrive"
471422

472423
if ($hasIISDrive) {
473-
Import-Module WebAdministration
424+
425+
Write-Verbose "IIS Drive is available, using WebAdministration module."
426+
427+
$null = Import-Module WebAdministration
474428
$sitePath = "IIS:\Sites\$SiteName"
475429
if (-not (Test-Path $sitePath)) {
476430
$msg = "Site '$SiteName' not found in IIS drive."
@@ -480,7 +434,7 @@ function New-KFIISSiteBinding {
480434
$site = Get-Item $sitePath
481435
$httpsBindings = $site.Bindings.Collection | Where-Object {
482436
$_.bindingInformation -eq $searchBindings -and $_.protocol -eq "https"
483-
}
437+
}
484438

485439
foreach ($binding in $httpsBindings) {
486440
try {
@@ -520,6 +474,8 @@ function New-KFIISSiteBinding {
520474
}
521475
} else {
522476
# SERVERMANAGER FALLBACK
477+
Write-Verbose "IIS Drive is not available, using ServerManager fallback."
478+
523479
Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll"
524480
$iis = New-Object Microsoft.Web.Administration.ServerManager
525481
$site = $iis.Sites[$SiteName]
@@ -578,7 +534,7 @@ function CheckExistingBindings {
578534
$conflicts = @()
579535

580536
if (Ensure-IISDrive) {
581-
Import-Module WebAdministration
537+
$null = Import-Module WebAdministration
582538

583539
Get-Website | Where-Object { $_.Name -ne $TargetSiteName } | ForEach-Object {
584540
$siteName = $_.Name
@@ -647,7 +603,7 @@ function CheckExistingBindingsORIG {
647603
)
648604

649605
if (Ensure-IISDrive) {
650-
Import-Module WebAdministration
606+
$null = Import-Module WebAdministration
651607

652608
$conflict = $false
653609

@@ -710,7 +666,7 @@ function Ensure-IISDrive {
710666
# Try to import the WebAdministration module if not already loaded
711667
if (-not (Get-Module -Name WebAdministration)) {
712668
try {
713-
Import-Module WebAdministration -ErrorAction Stop
669+
$null = Import-Module WebAdministration -ErrorAction Stop
714670
}
715671
catch {
716672
Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available."
@@ -1336,6 +1292,43 @@ function Get-CertificateCSP {
13361292
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert
13371293
)
13381294

1295+
# Check if the certificate has a private key
1296+
if (-not $Cert.HasPrivateKey) {
1297+
Write-Warning "Certificate does not have a private key associated with it"
1298+
return $null
1299+
}
1300+
1301+
$privateKey = $Cert.PrivateKey
1302+
if ($privateKey) {
1303+
# For older .NET Framework
1304+
$cspKeyContainerInfo = $privateKey.CspKeyContainerInfo
1305+
1306+
if ($cspKeyContainerInfo) {
1307+
return $cspKeyContainerInfo.ProviderName
1308+
}
1309+
}
1310+
1311+
try {
1312+
$key = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
1313+
if ($key -and $key.GetType().Name -eq "RSACng") {
1314+
$cngKey = $key.Key
1315+
1316+
return $cngKey.Provider.Provider
1317+
}
1318+
}
1319+
catch {
1320+
Write-Warning "CNG key detection failed: $($_.Exception.Message)"
1321+
return $null
1322+
}
1323+
}
1324+
1325+
# Function that takes an x509 certificate object and returns the csp
1326+
function Get-CertificateCSPOLD {
1327+
param (
1328+
[Parameter(Mandatory = $true)]
1329+
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert
1330+
)
1331+
13391332
# Check if the certificate has a private key
13401333
if ($Cert -and $Cert.HasPrivateKey) {
13411334
try {

0 commit comments

Comments
 (0)