CertificateTools.psm1
class HttpsBinding { [System.String] $Binding [System.String] $IpPort [System.String] $HostnamePort [System.String] $CertificateHash [System.Guid] $ApplicationId [System.String] $CertificateStoreName [System.String] # Maybe better type should be found $VerifyClientCertificateRevocation [System.String] # Maybe better type should be found $VerifyRevocationUsingCachedClientCertificateOnly [System.String] # Maybe better type should be found $UsageCheck [System.String] # Maybe better type should be found $RevocationFreshnessTime [System.String] # Maybe better type should be found $URLRetrievalTimeout [System.String] # Maybe better type should be found $CtlIdentifier [System.String] # Maybe better type should be found $CtlStoreName [System.String] # Maybe better type should be found $DSMapperUsage [System.String] # Maybe better type should be found $NegotiateClientCertificate [System.String] # Maybe better type should be found $RejectConnections [System.String] # Maybe better type should be found $DisableHTTP2 [System.Security.Cryptography.X509Certificates.X509Certificate] $Certificate [System.String] $Application # Used when piping object to Get-WebBinding [System.String] $Protocol = 'https' # Used when piping object to Get-WebBinding [System.UInt16] $Port # Used when piping object to Get-WebBinding [System.String] $IPAddress # Used when piping object to Get-WebBinding [System.String] $HostHeader } # Override Write-Verbose in this module so calling function is added to the message function script:Write-Verbose { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)] [String] $Message ) begin {} process { try { $PSBoundParameters['Message'] = $((Get-PSCallStack)[1].Command) + ': ' + $PSBoundParameters['Message'] } catch {} Microsoft.PowerShell.Utility\Write-Verbose @PSBoundParameters } end {} } function Find-NewestCertificate { <# .SYNOPSIS Find newest "version" of a SSL certificate .DESCRIPTION Find newest "version" of a SSL certificate Find the newest certificate based on common name (CN) - certificates are compared/matched based on the same CN Newest is the certificate with the highest NotAfter date If a non-wildcard certificate is provided, then a newer wildcard certificate will not be returned (because match is done on CN) .PARAMETER Certificate Find newest certificate based on (other older) certificate object .PARAMETER CertificateHash Find newest certificate based on certificate hash .PARAMETER CommonName Find newest certificate based on CN .PARAMETER CertStoreLocation Look for certificates in this location Defaults to Cert:\LocalMachine\My .PARAMETER HasPrivateKey Only return certificate if it has a private key .EXAMPLE Find-NewestCertificate -CertificateHash D5681CB21FC812AF764F5FB491DA6430C9EA73A9 - Find the certificate with thumbprint D568.. from Cert:\LocalMachine\My - Take the common name from that certificate - Find the newest certificate, with the same common name, in Cert:\LocalMachine\My. (This certificate can be D568.., if that is already the newest) .EXAMPLE Find-NewestCertificate -CommonName '*.foobar.tld' -Path Cert:\LocalMachine\My -HasPrivateKey - Find the newest certificate with CN=*.foobar.tld that has a private key .EXAMPLE Get-Item -Path Cert:\LocalMachine\Root\75e0abb6138512271c04f85fddde38e4b7242efe | Find-NewestCertificate - 75e0... should be a root certificate with CN=GlobalSign that expire in 2021 - It should return a certificate with same CN that expire in 2029 with thumbprint D69B... (at least on some computers) #> [OutputType('System.Security.Cryptography.X509Certificates.X509Certificate[]')] [CmdletBinding()] param ( [Parameter(ParameterSetName = 'certificate', Mandatory = $true, ValueFromPipeline = $true)] [System.Security.Cryptography.X509Certificates.X509Certificate] [Alias('Cert')] $Certificate, [Parameter(ParameterSetName = 'certificatehash', Mandatory = $true)] [System.String] [Alias('Hash','Thumbprint')] $CertificateHash, [Parameter(ParameterSetName = 'commonname', Mandatory = $true)] [System.String] [Alias('CN')] $CommonName, [Parameter(ParameterSetName = 'certificatehash')] [Parameter(ParameterSetName = 'commonname' )] [System.String] [Alias('Path','CertificateStore','CertificateStoreLocation','CertStore')] $CertStoreLocation = 'Cert:\CurrentUser\My', [Parameter()] [System.Management.Automation.SwitchParameter] [Alias('PK','PrivateKey','Key')] $HasPrivateKey ) begin { Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)" $origErrorActionPreference = $ErrorActionPreference $verbose = ($PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters['Verbose'].IsPresent) -or ($VerbosePreference -ne 'SilentlyContinue') } process { Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)" try { # Stop execution inside this function, and catch the error $ErrorActionPreference = 'Stop' # Default parameters used when calling other functions $defaultParam = @{ Verbose = $verbose ErrorAction = $ErrorActionPreference } # Find common name based on certificate object or thumbprint if ($Certificate -or $CertificateHash) { if ($Certificate) { $CertStoreLocation = $Certificate.PSParentPath } else { $Certificate = Get-Item @defaultParam -Path (Join-Path -Path $CertStoreLocation -ChildPath $CertificateHash) } if ($Certificate.Subject -match 'CN=([^,]+)') { $CommonName = $Matches[1] } else { Write-Error -Message "Common name not found in certificate subject `"$($Certificate.Subject)`"" } } # Find certificates with matching common name if ($certs = @(Get-ChildItem -Path $CertStoreLocation | Where-Object -FilterScript {($_.Subject -match 'CN=([^,]+)') -and ($Matches[1] -eq $CommonName)} | Sort-Object -Property 'NotAfter' -Descending)) { if (! $HasPrivateKey -or ($certs = @($certs | Where-Object -Property 'HasPrivateKey' -EQ -Value $true))) { # Return newest certificate $certs[0] } else { Write-Error -Message "Certificate with common name `"$($CommonName)`" found in $($CertStoreLocation), but not with a private key" } } else { Write-Error -Message "No certificates found in $($CertStoreLocation) with common name `"$($CommonName)`"" } } catch { # If error was encountered inside this function then stop doing more # But still respect the ErrorAction that comes when calling this function # And also return the line number where the original error occured $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString() Write-Verbose -Message "Encountered an error: $msg" Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg } finally { $ErrorActionPreference = $origErrorActionPreference } Write-Verbose -Message 'Process end' } end { $ErrorActionPreference = $origErrorActionPreference Write-Verbose -Message 'End' } } function Get-HttpsBinding { <# .SYNOPSIS Get certificates on HTTPS bindings .DESCRIPTION Get certificates on HTTPS bindings Microsofts own cmdlets: Add-NetIPHttpsCertBinding and Remove-NetIPHttpsCertBinding are just crap!! Remove-NetIPHttpsCertBinding removes ALL bindings, Add-NetIPHttpsCertBinding only works with IpPort (not HostnamePort) and there's no way to show/get bindings! .PARAMETER Certificate Find bindings using this certificate .PARAMETER Binding Find binding (binding is the same as IpPort or HostnamePort) .PARAMETER IpPort Find binding with IP:Port .PARAMETER HostnamePort Find binding with Hostname:Port .PARAMETER Port Find bindings on this TCP port .PARAMETER IPAddress Find bindding with this IP address .PARAMETER HostHeader Find binding with this hostheader/hostname .PARAMETER CertificateHash Find bindings that uses certificate with this hash/thumbprint Also used when piping object from Get-WebBinding .PARAMETER ApplicationId Find bindings with this application id .PARAMETER CertificateStoreName Find bindings with certificate in this location Also used when piping object from Get-WebBinding .PARAMETER Protocol Find bindings with this protocol. Everyting but https is ignored Also used when piping object from Get-WebBinding .PARAMETER BindingInformation Find bindings with this "bindinginformation" Also used when piping object from Get-WebBinding .PARAMETER SslFlags Used together with BindingInformation Also used when piping object from Get-WebBinding .EXAMPLE Get-HttpsBinding Get all SSL bindings - it's "netsh http show sslcert" wrapped in some PowerShell .EXAMPLE Get-HttpsBinding -Port 443 Get all bindings that use TCP port 443 .EXAMPLE dir Cert:\LocalMachine\My\1234FBC46BB66309EBD861BE4F95062B7C9E5E61 | Get-HttpsBinding Get all bindings that use a specific SSL certificate .EXAMPLE Get-HttpsBinding | Get-WebBinding Get SSL bindings and find IIS bindings (requires that IIS PowerShell tools are installed) .EXAMPLE Get-WebBinding | Get-HttpsBinding Get IIS bindings and find SSL bindings (requires that IIS PowerShell tools are installed) #> [OutputType([HttpsBinding[]])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [System.Security.Cryptography.X509Certificates.X509Certificate] $Certificate, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $Binding, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $IpPort, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $HostnamePort, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.UInt16] $Port, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $IPAddress, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $HostHeader, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $CertificateHash, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.Guid] $ApplicationId, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $CertificateStoreName, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $Protocol, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.String] $BindingInformation, [Parameter(ValueFromPipelineByPropertyName = $true)] [ValidateRange(0,1)] [System.Int16] $SslFlags ) begin { Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)" $origErrorActionPreference = $ErrorActionPreference $verbose = ($PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters['Verbose'].IsPresent) -or ($VerbosePreference -ne 'SilentlyContinue') $certRootPath = 'Cert:\LocalMachine' $netshArray = New-Object -TypeName 'System.Collections.ArrayList' try { # Stop execution inside this function, and catch the error $ErrorActionPreference = 'Stop' # Default parameters used when calling other functions $defaultParam = @{ Verbose = $verbose ErrorAction = $ErrorActionPreference } # Getting bindings as string $netshString = netsh http show sslcert $netshString = $netshString[3..($netshString.length-2)] $obj = $null # Loop through all lines foreach ($line in $netshString) { if ($line -match "^\s*$") { # Empty line if ($obj) { if ($obj.IpPort) { $obj.Binding = $obj.IpPort if ($obj.IpPort -match '^(.+):([0-9]+)$') { if ($Matches[1] -eq '0.0.0.0') { # IIS uses wildcard instead of 0.0.0.0 $obj.IPAddress = '*' } else { $obj.IPAddress = $Matches[1] } $obj.Port = $Matches[2] } } elseif ($obj.HostnamePort) { $obj.Binding = $obj.HostnamePort if ($obj.HostnamePort -match '^(.+):([0-9]+)$') { $obj.HostHeader = $Matches[1] $obj.Port = $Matches[2] } } # Try to find the certificate in certificate store if ($obj.CertificateHash -and $obj.CertificateStoreName) { try { $obj.Certificate = Get-Item -Path (Join-Path -Path (Join-Path -Path $certRootPath -ChildPath $obj.CertificateStoreName) -ChildPath $obj.CertificateHash) } catch { # Nothing } } # Add binding to array of binding-objects $null = $netshArray.Add($obj) } $obj = New-Object -TypeName 'HttpsBinding' } elseif ($line -match "\s+(.*\S)\s+:\s(.+)") { # Line with content $key = $Matches[1] $value = $Matches[2] # Fill the object with info switch ($key) { 'IP:port' { $obj.IpPort = $value } 'Hostname:port' { $obj.HostnamePort = $value } 'Certificate Hash' { $obj.CertificateHash = $value } 'Verify Client Certificate Revocation' { $obj.VerifyClientCertificateRevocation = $value } 'Verify Revocation Using Cached Client Certificate Only' { $obj.VerifyRevocationUsingCachedClientCertificateOnly = $value } 'Usage Check' { $obj.UsageCheck = $value } 'Revocation Freshness Time' { $obj.RevocationFreshnessTime = $value } 'URL Retrieval Timeout' { $obj.URLRetrievalTimeout = $value } 'Ctl Identifier' { $obj.CtlIdentifier = $value } 'Ctl Store Name' { $obj.CtlStoreName = $value } 'DS Mapper Usage' { $obj.DSMapperUsage = $value } 'Negotiate Client Certificate' { $obj.NegotiateClientCertificate = $value } 'Reject Connections' { $obj.RejectConnections = $value } 'Disable HTTP2' { $obj.DisableHTTP2 = $value } 'Certificate Store Name' { if ($value -ne '(null)') {$obj.CertificateStoreName = $value}} 'Application ID' { $obj.ApplicationId = $value if ($script:applicationIdLookupTable.ContainsKey([System.String] $obj.ApplicationId)) { $obj.Application = $script:applicationIdLookupTable[[System.String] $obj.ApplicationId] } } } } } } catch { # If error was encountered inside this function then stop doing more # But still respect the ErrorAction that comes when calling this function # And also return the line number where the original error occured $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString() Write-Verbose -Message "Encountered an error: $msg" Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg } finally { $ErrorActionPreference = $origErrorActionPreference } } process { Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)" try { # Stop execution inside this function, and catch the error $ErrorActionPreference = 'Stop' # Default parameters used when calling other functions $defaultParam = @{ Verbose = $verbose ErrorAction = $ErrorActionPreference } $return = $netshArray # Quick and dirty filtering if ($Certificate) { $return = $return | Where-Object -FilterScript {$_.Certificate.PSPath -eq $Certificate.PSPath } } if ($Binding) { $return = $return | Where-Object -FilterScript {$_.Binding -eq $Binding } } if ($IpPort) { $return = $return | Where-Object -FilterScript {$_.IpPort -eq $IpPort } } if ($HostnamePort) { $return = $return | Where-Object -FilterScript {$_.HostnamePort -eq $HostnamePort } } if ($Port) { $return = $return | Where-Object -FilterScript {$_.Port -eq $Port } } if ($IPAddress) { $return = $return | Where-Object -FilterScript {$_.IPAddress -eq $IPAddress } } if ($HostHeader) { $return = $return | Where-Object -FilterScript {$_.HostHeader -eq $HostHeader } } if ($CertificateHash) { $return = $return | Where-Object -FilterScript {$_.CertificateHash -eq $CertificateHash } } if ($ApplicationId) { $return = $return | Where-Object -FilterScript {$_.ApplicationId -eq $ApplicationId } } if ($CertificateStoreName) { $return = $return | Where-Object -FilterScript {$_.CertificateStoreName -eq $CertificateStoreName} } if ($Protocol) { $return = $return | Where-Object -FilterScript {$_.Protocol -eq $Protocol } } if ($BindingInformation) { # FIXXXME - parameterset so both BindingInformation and SslFlags are set <# IIS BindingInformation SslFlags Net sh binding *:443: 0 0.0.0.0:443 1.2.3.4:443: 0 1.2.3.4:443 *:443:host.name 0 0.0.0.0:443 1.2.3.4:443:host.name 0 1.2.3.4:443 *:443:host.name 1 host.name:443 1.2.3.4:443:host.name 1 host.name:443 #> if ($BindingInformation -match '(.*):(.*):(.*)') { $return = $return | Where-Object -FilterScript {$_.Port -eq $Matches[2]} if ($SslFlags) { # SNI $return = $return | Where-Object -FilterScript {$_.HostHeader -eq $Matches[3]} } else { # Not SNI $return = $return | Where-Object -FilterScript {$_.IPAddress -eq $Matches[1]} } } else { # Not in correct IIS binding format $return = $null } } # Return $return } catch { # If error was encountered inside this function then stop doing more # But still respect the ErrorAction that comes when calling this function # And also return the line number where the original error occured $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString() Write-Verbose -Message "Encountered an error: $msg" Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg } finally { $ErrorActionPreference = $origErrorActionPreference } Write-Verbose -Message 'Process end' } end { $ErrorActionPreference = $origErrorActionPreference Write-Verbose -Message 'End' } } function Set-HttpsBinding { <# .SYNOPSIS Set/replace certificates on HTTPS bindings .DESCRIPTION Set/replace certificates on HTTPS bindings Microsofts own cmdlets: Add-NetIPHttpsCertBinding and Remove-NetIPHttpsCertBinding are just crap!! Remove-NetIPHttpsCertBinding removes ALL bindings, Add-NetIPHttpsCertBinding only works with IpPort (not HostnamePort) and there's no way to show/get bindings! .PARAMETER DryRun Only show what would be changed, but don't change it .PARAMETER ReplaceAllWithNewest Replace certificates on all bindings that have a newer certificate with same common name .PARAMETER Binding Replace certificate on this binding .PARAMETER IpPort Replace certificate on this binding .PARAMETER HostnamePort Replace certificate on this binding .PARAMETER OldCertificateHash Replace certificates on bindings that has certificates with this thumbprint .PARAMETER CertificateHash Replace binding with certificate with this thumbprint .PARAMETER ApplicationId Application ID of binding .PARAMETER CertificateStoreName Certificate store (normally just "My") .PARAMETER PfxPath Path to PFX file to use on binding - will be imported to certificate store .PARAMETER Exportable Should the private key for the imported PFX be exportable .PARAMETER Password Password for PFX file .PARAMETER PasswordClear Password for PFX file in clear text .EXAMPLE Set-HttpsBinding -ReplaceAllWithNewest -DryRun Replace certificates on all bindings where a newer certificate is found (same CN). But don't actuall run netsh - only show what would have run .EXAMPLE Set-HttpsBinding -IpPort 0.0.0.0:443 -CertificateHash '1234fbc46bb66309ebd861be4f95062b7c9e5e61' Update binding on 0.0.0.0:443 with certificate with thumbprint 1234... .EXAMPLE Set-HttpsBinding -OldCertificateHash '4321fbc46bb66309ebd861be4f95062b7c9e5e61' -CertificateHash '12341cb21fc912af764f5fb491da6430c9ea73a8' Change all binding that use 4321... to 1234... .EXAMPLE Get-HttpsBinding -Port 44399 | Set-HttpsBinding -CertificateHash '1234fbc46bb66309ebd861be4f95062b7c9e5e61' Change all binding on TCP port 443399 to certificate with hash 1234... .EXAMPLE Set-HttpsBinding -HostnamePort 'www.foobar.tld:443' -PfxPath 'foobar.pfx' -Exportable -PasswordClear 'Password1!' Import foobar.pfx to certificate store (and make private key exportable) and change binding on www.foobar.tld:443 to use that certificate #> [CmdletBinding()] param ( [Parameter()] [switch] $DryRun, [Parameter(ParameterSetName = 'replaceall', Mandatory = $true)] [System.Management.Automation.SwitchParameter] $ReplaceAllWithNewest, [Parameter(ParameterSetName = 'binding', Mandatory = $true, ValueFromPipeline = $true)] [Parameter(ParameterSetName = 'bindingpfx', Mandatory = $true, ValueFromPipeline = $true)] [Parameter(ParameterSetName = 'bindingpfxclear', Mandatory = $true, ValueFromPipeline = $true)] [HttpsBinding] $Binding, [Parameter(ParameterSetName = 'ipport', Mandatory = $true)] [Parameter(ParameterSetName = 'ipportpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'ipportpfxclear', Mandatory = $true)] [System.String] $IpPort, [Parameter(ParameterSetName = 'hostnameport', Mandatory = $true)] [Parameter(ParameterSetName = 'hostnameportpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'hostnameportpfxclear', Mandatory = $true)] [System.String] $HostnamePort, [Parameter(ParameterSetName = 'oldhash', Mandatory = $true)] [Parameter(ParameterSetName = 'oldhashpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'oldhashpfxclear', Mandatory = $true)] [Alias('Old')] [System.String] $OldCertificateHash, [Parameter(ParameterSetName = 'binding', Mandatory = $true)] [Parameter(ParameterSetName = 'ipport', Mandatory = $true)] [Parameter(ParameterSetName = 'hostnameport', Mandatory = $true)] [Parameter(ParameterSetName = 'oldhash', Mandatory = $true)] [Alias('New')] [System.String] $CertificateHash, [Parameter(ParameterSetName = 'ipport' )] [Parameter(ParameterSetName = 'ipportpfx' )] [Parameter(ParameterSetName = 'ipportpfxclear' )] [Parameter(ParameterSetName = 'hostnameport' )] [Parameter(ParameterSetName = 'hostnameportpfx' )] [Parameter(ParameterSetName = 'hostnameportpfxclear' )] [System.Guid] $ApplicationId, [Parameter(ParameterSetName = 'ipport' )] [Parameter(ParameterSetName = 'ipportpfx' )] [Parameter(ParameterSetName = 'ipportpfxclear' )] [Parameter(ParameterSetName = 'hostnameport' )] [Parameter(ParameterSetName = 'hostnameportpfx' )] [Parameter(ParameterSetName = 'hostnameportpfxclear' )] [System.String] $CertificateStoreName, [Parameter(ParameterSetName = 'bindingpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'bindingpfxclear', Mandatory = $true)] [Parameter(ParameterSetName = 'ipportpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'ipportpfxclear', Mandatory = $true)] [Parameter(ParameterSetName = 'hostnameportpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'hostnameportpfxclear', Mandatory = $true)] [Parameter(ParameterSetName = 'oldhashpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'oldhashpfxclear', Mandatory = $true)] [System.String] $PfxPath, [Parameter(ParameterSetName = 'bindingpfx' )] [Parameter(ParameterSetName = 'bindingpfxclear' )] [Parameter(ParameterSetName = 'ipportpfx' )] [Parameter(ParameterSetName = 'ipportpfxclear' )] [Parameter(ParameterSetName = 'hostnameportpfx' )] [Parameter(ParameterSetName = 'hostnameportpfxclear' )] [Parameter(ParameterSetName = 'oldhashpfx' )] [Parameter(ParameterSetName = 'oldhashpfxclear' )] [System.Management.Automation.SwitchParameter] $Exportable, [Parameter(ParameterSetName = 'bindingpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'ipportpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'hostnameportpfx', Mandatory = $true)] [Parameter(ParameterSetName = 'oldhashpfx', Mandatory = $true)] [System.Security.SecureString] $Password, [Parameter(ParameterSetName = 'bindingpfxclear', Mandatory = $true)] [Parameter(ParameterSetName = 'ipportpfxclear', Mandatory = $true)] [Parameter(ParameterSetName = 'hostnameportpfxclear', Mandatory = $true)] [Parameter(ParameterSetName = 'oldhashpfxclear', Mandatory = $true)] [System.String] $PasswordClear ) begin { Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)" $origErrorActionPreference = $ErrorActionPreference $verbose = ($PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters['Verbose'].IsPresent) -or ($VerbosePreference -ne 'SilentlyContinue') $certRootPath = 'Cert:\LocalMachine' } process { Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)" try { # Stop execution inside this function, and catch the error $ErrorActionPreference = 'Stop' # Default parameters used when calling other functions $defaultParam = @{ Verbose = $verbose ErrorAction = $ErrorActionPreference } if (! $Binding) { # Default to "Personal" if no store name was provided, or no store name was defined in existing binding if (! $CertificateStoreName) { $CertificateStoreName = 'My' } } # Import certificate from PFX if ($PfxPath) { # Convert clear text password if ($PasswordClear) { $Password = ConvertTo-SecureString -String $PasswordClear -Force -AsPlainText } $cert = Import-PfxCertificate @defaultParam -FilePath $PfxPath -Exportable:$Exportable -Password $Password -CertStoreLocation (Join-Path -Path $certRootPath -ChildPath $CertificateStoreName) # CertificateHash to use later in the function is this newly imported one $CertificateHash = $cert.Thumbprint } if ($ReplaceAllWithNewest) { # Loop through the different unique certificates used in bindings foreach ($oldCert in ((Get-HttpsBinding @defaultParam).Certificate | Sort-Object -Property 'PSPath' -Unique)) { try { $newCert = $oldCert | Find-NewestCertificate @defaultParam -HasPrivateKey if ($oldCert.Thumbprint -eq $newCert.Thumbprint) { Write-Verbose -Message "No new certificate for `"$($oldCert.Subject)`"" } else { # Replace all occurrences of one certificate with another - run recursive # This has a flaw if the same certificate is found and used from different certificate stores - should not be a "real" problem! Set-HttpsBinding @defaultParam -OldCertificateHash $oldCert.Thumbprint -CertificateHash $newCert.Thumbprint -DryRun:$DryRun } } catch { Write-Warning -Message $_ } } } elseif ($OldCertificateHash) { # Replace all occurrences of one certificate with another - run recursive with Binding coming from pipeline Get-HttpsBinding @defaultParam -CertificateHash $OldCertificateHash | Set-HttpsBinding @defaultParam -CertificateHash $CertificateHash -DryRun:$DryRun } else { # Something else than ReplaceAllWithNewest or OldCertificateHash # Initialize som variables $cmds = @() $cmdsrun = 0 $id = '' if ($Binding) { # Binding provided as parameter (or from pipeline) if ($Binding.IpPort) { # Binding is of type IpPort $id = "ipport=$($Binding.IpPort)" } elseif ($Binding.HostnamePort) { # Binding is of type HostnamePort $id = "hostnameport=$($Binding.HostnamePort)" } } elseif ($IpPort) { # IpPort provided as parameter $Binding = Get-HttpsBinding @defaultParam -IpPort $IpPort $id = "ipport=$($IpPort)" } elseif ($HostnamePort) { # HostnamePort provided as parameter $Binding = Get-HttpsBinding @defaultParam -HostnamePort $HostnamePort $id = "hostnameport=$($HostnamePort)" } if ($Binding) { # Existing binding found Write-Verbose -Message "Existing binding for $($id) will be removed before new binding is added" # Test/set application id if ($ApplicationId -and ($ApplicationId -ne $Binding.ApplicationId)) { Write-Warning -Message "ApplicationId for $($id) will be changed from $($Binding.ApplicationId) to $($ApplicationId)" } elseif (! $ApplicationId) { $ApplicationId = $Binding.ApplicationId } # Test/set certificate store if ($CertificateStoreName -and ($CertificateStoreName -ne $Binding.CertificateStoreName)) { Write-Warning -Message "CertificateStoreName for $($id) will be changed from $($Binding.CertificateStoreName) to $($CertificateStoreName)" } elseif (! $CertificateStoreName -and $Binding.CertificateStoreName) { $CertificateStoreName = $Binding.CertificateStoreName } else { # Default to "Personal" if no store name was provided, or no store name was defined in existing binding $CertificateStoreName = 'My' } # Add command to remove existing binding to command queue $cmds += "netsh http delete sslcert $($id)" } elseif (! $ApplicationId) { Write-Error -Message "ApplicationId for $($id) not provided and no existing binding found" } # Validate if certificate can be found in certificate store if (! (Get-ChildItem -Path (Join-Path -Path $certRootPath -ChildPath $CertificateStoreName) | Where-Object -FilterScript {$_.Thumbprint -eq $CertificateHash})) { Write-Error -Message "No certificate with hash $($CertificateHash) found in store $($CertificateStoreName)" } # Add command to add new binding to command queue $cmd = "netsh http add sslcert $($id) certhash=$($CertificateHash) appid='{$($ApplicationId)}' certstorename=$($CertificateStoreName)" if (! ($cmd -match "^[a-z0-9 '=\.:_{}-]+$")) { # The check could be better! But linebreaks and semicolon isn't allowed, so command injection should'nt be possible Write-Error -Message "What are you trying to do here!? Why are you trying to execute this stuff: $cmd" } $cmds += $cmd # Running the commands foreach ($cmd in $cmds) { "Running: $cmd" if (! $DryRun) { # Run command Invoke-Expression -Command $cmd if ($LASTEXITCODE) { Write-Error ("Encountered exit code $LASTEXITCODE running: $cmd`r`nAll commands that would have been executed:`r`n" + ($cmds -join "`r`n")) } } } } } catch { # If error was encountered inside this function then stop doing more # But still respect the ErrorAction that comes when calling this function # And also return the line number where the original error occured $msg = $_.ToString() + "`r`n" + $_.InvocationInfo.PositionMessage.ToString() Write-Verbose -Message "Encountered an error: $msg" Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception -Message $msg } finally { $ErrorActionPreference = $origErrorActionPreference } Write-Verbose -Message 'Process end' } end { $ErrorActionPreference = $origErrorActionPreference Write-Verbose -Message 'End' } } # Found on https://www.sevecek.com/Lists/Posts/Post.aspx?ID=9 $script:applicationIdLookupTable = @{ '5d8e2743-ef20-4d38-8751-7e400f200e65' = 'IPHTTPS' 'ba195980-cd49-458b-9e23-c84ee0abcd75' = 'SSTP' '4dc3e181-e14b-4a21-b022-59fc669b0914' = 'IIS' '1d40ebc7-1983-4ac5-82aa-1e17a7ae9a0e' = 'SQL Report Server' 'afebb9ad-9b97-4a91-9ab5-daf4d59122f6' = 'WinRM' 'fed10a98-8cb9-41e2-8608-264b923c2623' = 'Hyper-V Replication' '5d89a20c-beab-4389-9447-324788eb944a' = 'AD FS' 'f955c070-e044-456c-ac00-e9e4275b3f04' = 'Web Application Proxy (WAP)' '214124cd-d05b-4309-9af9-9caa44b2b74a' = 'IIS Express Development Certificate' } Export-ModuleMember -Function Find-NewestCertificate Export-ModuleMember -Function Get-HttpsBinding Export-ModuleMember -Function Set-HttpsBinding |