Public/Install-NSPSslCertificate.ps1
|
function Install-NSPSslCertificate { <# .SYNOPSIS Installs a PFX on a Windows server, start to finish: certificate stores, IIS bindings, and the certificate it replaces. Interactive. .DESCRIPTION What the _Install folder from Complete-NSPSslRequest runs. Step by step: 1. Finds the PFX: -Path, else the .pfx in the current folder (a choice when there are several, a file prompt when there's none). Asks for the password. 2. Imports it with Import-NSPSslPfx into LocalMachine: the certificate in My, the intermediates in CA. When the chain doesn't validate because the root isn't trusted here (a machine without Windows Update), offers to add the root the PFX carries. 3. If IIS is installed, lists its https bindings with their current certificates and proposes the ones the new certificate belongs on (NSP.IIS's matching: the host name is on the certificate, or the current certificate covers the same names). Enter takes the proposal; numbers, A (all) or N (none) choose otherwise. 4. For each certificate a binding was moved off: if no IIS or other http.sys binding uses it any more, offers to delete it and its key. Anything still using it is listed instead. Services that don't go through http.sys (RDP, SQL Server, LDAPS) aren't checked, and the prompt says so. Runs elevated: from a non-elevated session it relaunches itself in a new elevated window (NSP.Bootstrap's Invoke-NSPElevated; UAC prompt). Loads NSP.Console, and NSP.IIS when IIS is installed, on first use. .PARAMETER Path The PFX. Default: the one in the current folder. .PARAMETER Password The PFX password. Prompted for when not given. .PARAMETER TrustRoot Add the PFX's root to Trusted Root without asking, when it isn't trusted here. .PARAMETER SkipIis Import only; leave IIS alone. .PARAMETER NoElevate Don't relaunch elevated; fail instead when the session isn't. .EXAMPLE Install-NSPSslCertificate .EXAMPLE Install-NSPSslCertificate -Path C:\Temp\portal.contoso.com_Install\portal.contoso.com.pfx #> [CmdletBinding()] param( [string]$Path, [securestring]$Password, [switch]$TrustRoot, [switch]$SkipIis, [switch]$NoElevate ) Import-NSPToolkitModule -Name NSP.Console -MinimumVersion 0.1.5 $x509 = 'System.Security.Cryptography.X509Certificates' $simpleName = [System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName if (-not $Path) { $here = (Get-Location).ProviderPath $found = @(Get-ChildItem -LiteralPath $here -File | Where-Object Extension -In '.pfx', '.p12' | Sort-Object Name) if ($found.Count -eq 1) { $Path = $found[0].FullName } elseif ($found.Count -gt 1) { $Path = Read-NSPChoice -Prompt "Which PFX in $here" -Choices @($found | ForEach-Object { [pscustomobject]@{ Label = $_.Name; Value = $_.FullName } }) if (-not $Path) { return } } else { $Path = Read-NSPFilePath -Prompt 'PFX file' -Filter 'PFX files (*.pfx;*.p12)|*.pfx;*.p12|All files (*.*)|*.*' -InitialDirectory $here -AllowBack if (Test-NSPBackSignal $Path) { return } } } # Full path: the elevated window starts in System32. $Path = (Resolve-Path -LiteralPath $Path).ProviderPath Use-NSPSslDependency if (-not (Test-NSPIsAdmin)) { if (-not $NoElevate -and $env:NSP_NO_ELEVATE -ne '1') { $quote = { param([string]$Text) "'" + $Text.Replace("'", "''") + "'" } $relaunch = "Import-Module $(& $quote (Join-Path $script:ModuleRoot 'NSP.SSLCertificates.psd1')); Install-NSPSslCertificate -Path $(& $quote $Path) -NoElevate" if ($TrustRoot) { $relaunch += ' -TrustRoot' } if ($SkipIis) { $relaunch += ' -SkipIis' } if (Invoke-NSPElevated -Command $relaunch -NoExit) { Write-Warning 'Not elevated: the install continues in a new elevated PowerShell window (UAC prompt).' return } } throw 'Installing a certificate needs an elevated PowerShell session. Run as administrator.' } Clear-NSPConsole Write-NSPConsoleHeader -Title 'Certificate Install' -Subtitle $env:COMPUTERNAME Write-NSPConsoleLine " PFX $Path" -Role Muted Write-NSPConsoleLine '' # The password, checked by opening the PFX without keeping its key anywhere. $leaf = $null for ($try = 1; -not $leaf; $try++) { if (-not $Password) { $Password = Read-Host 'PFX password' -AsSecureString } $preview = New-Object "$x509.X509Certificate2Collection" try { $preview.Import($Path, (ConvertFrom-NSPSslSecureString -SecureString $Password), [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet) } catch { if ($try -ge 3) { throw "Could not open $Path after 3 tries. Check the password. ($($_.Exception.Message))" } Write-NSPConsoleLine ' That password did not open the PFX. Try again.' -Role Warning $Password = $null continue } $leaf = @($preview | Where-Object HasPrivateKey)[0] if (-not $leaf) { throw "$Path has no certificate with a private key." } } $names = @(Get-NSPSslDnsName -Certificate $leaf) Write-NSPConsoleLine " Certificate $($leaf.GetNameInfo($simpleName, $false))" -Role Strong Write-NSPConsoleLine " Names $($names -join ', ')" Write-NSPConsoleLine " Expires $($leaf.NotAfter.ToString('yyyy-MM-dd')) Issued by $($leaf.GetNameInfo($simpleName, $true))" Write-NSPConsoleLine " Thumbprint $($leaf.Thumbprint)" -Role Muted # 1. Certificate stores. Write-NSPConsoleRule -Title 'Certificate stores' $import = @{ Path = $Path; Password = $Password; StoreLocation = 'LocalMachine'; NoElevate = $true; WarningAction = 'SilentlyContinue' } $rows = @(Import-NSPSslPfx @import -TrustRoot:$TrustRoot) $certificateRow = $rows | Where-Object Role -EQ 'Certificate' | Select-Object -First 1 $skippedRoot = $rows | Where-Object { $_.Role -eq 'Root' -and $_.Action -eq 'Skipped' } | Select-Object -First 1 if ($skippedRoot -and $certificateRow.ChainStatus -ne 'Valid') { Write-NSPConsoleLine " The chain doesn't validate here ($($certificateRow.ChainStatus))." -Role Warning if (Read-NSPConfirm -Prompt " Add the PFX's root, $($skippedRoot.Subject), to Trusted Root?" -DefaultYes) { $rows = @(Import-NSPSslPfx @import -TrustRoot) $certificateRow = $rows | Where-Object Role -EQ 'Certificate' | Select-Object -First 1 } } foreach ($row in $rows) { $role = switch ($row.Action) { 'Added' { 'Success' } 'AlreadyPresent' { 'Normal' } default { 'Muted' } } $subject = ($row.Subject -split ',')[0] -replace '^CN=', '' Write-NSPConsoleLine (' {0,-13} {1,-15} {2,-22} {3}' -f $row.Role, $row.Action, $row.Store, $subject) -Role $role } if ($certificateRow.ChainStatus -eq 'Valid') { Write-NSPConsoleLine " Chain valid: $($certificateRow.Chain -join ' > ')" -Role Success } else { Write-NSPConsoleLine " Chain problem: $($certificateRow.ChainStatus)" -Role Warning } $store = New-Object "$x509.X509Store" 'My', 'LocalMachine' try { $store.Open('ReadOnly') $installed = @($store.Certificates.Find('FindByThumbprint', $leaf.Thumbprint, $false))[0] } finally { $store.Close() } # 2. IIS bindings. $replaced = [Collections.Generic.List[string]]::new() $iis = -not $SkipIis -and [bool](Get-Service -Name W3SVC -ErrorAction SilentlyContinue) if ($iis) { Import-NSPToolkitModule -Name NSP.IIS -MinimumVersion 0.1.0 Write-NSPConsoleRule -Title 'IIS https bindings' $bindings = @(Get-NSPIisBinding -Certificate $installed) if (-not $bindings) { Write-NSPConsoleLine ' IIS has no https bindings. Add one in IIS Manager, then run this again to bind it.' -Role Muted } else { $proposed = @(); $selectable = @() for ($i = 0; $i -lt $bindings.Count; $i++) { $b = $bindings[$i] $number = $i + 1 if ($b.Match) { $proposed += $number } if ($b.MatchReason -notmatch '^(Already uses|Uses the Central)') { $selectable += $number } $current = if ($b.CertificateFound) { '{0} (expires {1:yyyy-MM-dd})' -f (($b.CertificateSubject -split ',')[0] -replace '^CN=', ''), $b.CertificateNotAfter } elseif ($b.Thumbprint) { "$($b.Thumbprint) (not in the store)" } else { '(none)' } $mark = if ($b.Match) { '*' } else { ' ' } $role = if ($b.Match) { 'Strong' } elseif ($b.MatchReason -like 'Already uses*') { 'Success' } else { 'Normal' } Write-NSPConsoleLine (' {0} {1,2} {2,-24} {3,-34} {4}' -f $mark, $number, $b.SiteName, $b.BindingInformation, $current) -Role $role Write-NSPConsoleLine (' {0}' -f $b.MatchReason) -Role Muted } $hint = if ($proposed) { "Enter = the * ones ($($proposed -join ','))" } else { 'Enter = none' } $chosen = $null while ($null -eq $chosen) { $answer = Read-Host " Rebind which? $hint, numbers (1,3 or 2-4), A = all, N = none" try { $chosen = @(Resolve-NSPSslBindingSelection -Answer $answer -Count $bindings.Count -Proposed $proposed -Selectable $selectable) } catch { Write-NSPConsoleLine " $($_.Exception.Message)" -Role Warning } } foreach ($number in $chosen) { $b = $bindings[$number - 1] try { $result = Set-NSPIisBindingCertificate -SiteName $b.SiteName -BindingInformation $b.BindingInformation -Thumbprint $installed.Thumbprint -Confirm:$false Write-NSPConsoleLine " $($result.Action): $($b.SiteName) $($b.BindingInformation)" -Role Success if ($result.Action -eq 'Updated' -and $result.PreviousThumbprint -and $result.PreviousThumbprint -ne $installed.Thumbprint -and -not $replaced.Contains($result.PreviousThumbprint)) { $replaced.Add($result.PreviousThumbprint) } } catch { Write-NSPConsoleLine " Failed: $($b.SiteName) $($b.BindingInformation): $($_.Exception.Message)" -Role Error } } if (-not $chosen) { Write-NSPConsoleLine ' No bindings changed.' -Role Muted } } } elseif (-not $SkipIis) { Write-NSPConsoleLine '' Write-NSPConsoleLine ' IIS is not installed here; nothing to bind.' -Role Muted } # 3. The certificates the bindings were moved off. if ($replaced.Count) { Write-NSPConsoleRule -Title 'Replaced certificates' $allBindings = @(Get-NSPIisBinding) foreach ($thumbprint in $replaced) { $old = Get-Item -LiteralPath "Cert:\LocalMachine\My\$thumbprint" -ErrorAction SilentlyContinue if (-not $old) { continue } $label = '{0} (expires {1:yyyy-MM-dd}, {2})' -f $old.GetNameInfo($simpleName, $false), $old.NotAfter, $thumbprint $uses = @( $allBindings | Where-Object Thumbprint -EQ $thumbprint | ForEach-Object { "IIS $($_.SiteName) $($_.BindingInformation)" } Get-NSPHttpSysSslBinding -Thumbprint $thumbprint -ExcludeIis | ForEach-Object { "http.sys $($_.Endpoint) (app $($_.Owner))" } ) if ($uses) { Write-NSPConsoleLine " Kept $label - still used by:" -Role Warning foreach ($use in $uses) { Write-NSPConsoleLine " $use" } continue } Write-NSPConsoleLine " $label is no longer bound in IIS or http.sys." -Role Normal Write-NSPConsoleLine ' Not checked: services that load it themselves (RDP, SQL Server, LDAPS, Exchange).' -Role Muted if (Read-NSPConfirm -Prompt ' Delete it and its private key from LocalMachine\My?') { try { Remove-NSPSslStoreCertificate -Thumbprint $thumbprint -Confirm:$false Write-NSPConsoleLine ' Deleted.' -Role Success } catch { Write-NSPConsoleLine " Could not delete it: $($_.Exception.Message)" -Role Error } } } } Write-NSPConsoleLine '' Write-NSPConsoleLine "Done. $($leaf.GetNameInfo($simpleName, $false)) is installed in LocalMachine\My." -Role Success } |