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 . `n Output: $output `n Make 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 `n Error:`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+
381331function 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