Public/Set-IISCertificate.ps1

function Set-IISCertificate {
    [CmdletBinding()]
    param(
        [Parameter(Position=0,ValueFromPipelineByPropertyName)]
        [Alias('Thumbprint')]
        [string]$CertThumbprint,
        [Parameter(Position=1,ValueFromPipelineByPropertyName)]
        [string]$PfxFile,
        [Parameter(Position=2,ValueFromPipelineByPropertyName)]
        [securestring]$PfxPass,
        [string]$SiteName='Default Web Site',
        [uint32]$Port=443,
        [string]$IPAddress='*',
        [string[]]$HostHeader=@(''),
        [switch]$RequireSNI,
        [switch]$DisableHTTP2,
        [switch]$DisableOCSPStapling,
        [switch]$DisableQUIC,
        [switch]$DisableTLS13,
        [switch]$DisableLegacyTLS,
        [switch]$RemoveOldCert
    )

    Begin {
        # Make sure we have the New-IISSiteBinding function available from
        # the IISAdministration module. It needs at least version 1.1.0.0 of
        # the module.
        if (-not (Get-Command New-IISSiteBinding -EA Ignore)) {

            $module = Get-Module -ListAvailable IISAdministration -All -Verbose:$false |
                Where-Object { $_.Version -ge [version]'1.1.0.0' } |
                Sort-Object -Descending Version |
                Select-Object -First 1

            if (-not $module) {
                try { throw "The IISAdministration module version 1.1.0.0 or newer is required to use this function. https://blogs.iis.net/iisteam/introducing-iisadministration-in-the-powershell-gallery" }
                catch { $PSCmdlet.ThrowTerminatingError($_) }
            } else {
                if (-not $PSEdition -or $PSEdition -eq 'Desktop') {
                    $module | Import-Module -Verbose:$false
                } else {
                    $module | Import-Module -UseWindowsPowerShell -Verbose:$false
                }
            }
        }

        # The Microsoft.Web.Administration.SslFlags enum is not loaded until we actually
        # make a function call from the module. So do that.
        $null = Get-IISSite

        # build a map of switches to their corresponding SslFlags enum value
        $switchMap = @{
            'RequireSNI' = [Microsoft.Web.Administration.SslFlags]::Sni
            'DisableHTTP2' = [Microsoft.Web.Administration.SslFlags]::DisableHTTP2
            'DisableOCSPStapling' = [Microsoft.Web.Administration.SslFlags]::DisableOCSPStp
            'DisableQUIC' = [Microsoft.Web.Administration.SslFlags]::DisableQUIC
            'DisableTLS13' = [Microsoft.Web.Administration.SslFlags]::DisableTLS13
            'DisableLegacyTLS' = [Microsoft.Web.Administration.SslFlags]::DisableLegacyTLS
        }

        # Different versions of Windows/IIS have different supported flags. So older OSes
        # might have fewer flags in the enum than newer ones. We're going to remove any
        # that got set to $null because they don't exist for the current OS.
        foreach ($key in @($switchMap.Keys)) {
            if (-not $switchMap[$key]) {
                Write-Debug "$key removed from supported SslFlags map because it had no matching value in the enum on this OS"
                $switchMap.Remove($key)
            }
        }

        $psb = $PSBoundParameters
    }

    Process {

        # surface exceptions without terminating the whole pipeline
        trap { $PSCmdlet.WriteError($PSItem); return }

        $CertThumbprint = Confirm-CertInstall @PSBoundParameters

        # verify the site exists
        if (-not (Get-IISSite -Name $SiteName)) {
            throw "Site $SiteName not found."
        }

        # multiple host headers require multiple bindings
        [string[]]$oldThumbPrints = foreach ($hh in $HostHeader) {

            # check for an existing site binding
            $bindMatch = "$($IPAddress):$($Port):$($hh)"
            $binding = (Get-IISSiteBinding -Name $SiteName -Protocol 'https' -WarningAction 'Ignore') | Where-Object {
                $_.bindingInformation -eq $bindMatch
            }

            # The IISAdministration module combines the creation of web binding and SSL binding
            # into New-IISSiteBinding, but there's no Set-IISSiteBinding equivalent that would
            # allow us to update the certificate thumbprint or tweak things like the SslFlags
            # value on the binding. So if we find a binding that is not exactly what we want,
            # we have to delete and re-create it.
            if ($binding) {

                # grab the old/current thumbprint
                $oldThumb = [BitConverter]::ToString($binding.CertificateHash).Replace('-','')

                # sslFlags is a bitwise combination of values from the [Microsoft.Web.Administration.SslFlags]
                # enum. It has added new feature flags over the years between Server 2016/2019/2022 and will
                # likely continue to do so. To avoid overwriting future flags this function may not yet know
                # about, we need to check for each option's flag individually in the current binding value
                # instead of just comparing the calculated sum of the specified switches.

                # adjust the flags based on the specified switches
                $newFlags = $binding.sslFlags
                foreach ($switchName in $switchMap.Keys) {
                    if ($psb.ContainsKey($switchName)) {
                        if ($psb[$switchName]) { $newFlags = $newFlags -bor $switchMap[$switchName] }   # Ensure Set
                        else                   { $newFlags = $newFlags -bxor $switchMap[$switchName] }  # Ensure Unset
                    }
                }

                # remove the binding if flags or thumbprint are different
                if ($binding.sslFlags -ne $newFlags -or $oldThumb -ne $CertThumbprint) {

                    $removeBindingParams = @{
                        Name = $SiteName
                        BindingInformation = $bindMatch
                        Protocol = 'https'
                        RemoveConfigOnly = $true
                        Confirm = $false
                    }
                    Write-Verbose "Deleting IIS site binding for $bindMatch"
                    Remove-IISSiteBinding @removeBindingParams
                    $binding = $null

                    # save the old thumbprint for potential deletion later
                    if ($oldThumb -ne $CertThumbprint) {
                        Write-Output $oldThumb
                    }
                }
            } else {
                # no existing binding means we have to build the sslFlags value from scratch
                $newFlags = [Microsoft.Web.Administration.SslFlags]::None
                foreach ($switchName in $switchMap.Keys) {
                    if ($psb.ContainsKey($switchName)) {
                        if ($psb[$switchName]) { $newFlags = $newFlags -bor $switchMap[$switchName] }   # Ensure Set
                        else                   { $newFlags = $newFlags -bxor $switchMap[$switchName] }  # Ensure Unset
                    }
                }
            }

            # create the new binding if necessary
            if ($binding) {
                Write-Verbose "IIS site binding already exists for $bindMatch"
            } else {

                $newBindingParams = @{
                    Name = $SiteName
                    Protocol = 'https'
                    BindingInformation = $bindMatch
                    SslFlag = $newFlags
                    CertificateThumbprint = $CertThumbprint
                    CertStoreLocation = 'Cert:\LocalMachine\My'
                }
                Write-Verbose "Adding IIS site binding for $bindMatch"
                New-IISSiteBinding @newBindingParams

            }

        }

        # remove the old cert(s) if specified
        if ($RemoveOldCert) {
            $oldThumbprints | Sort-Object -Unique | ForEach-Object {
                Remove-OldCert $_
            }
        }

    }

}