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