TlsCertificateValidation.psm1

# Copyright WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

# Functions should use $moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$moduleRoot = $PSScriptRoot

$assemblyPath = Join-Path -Path $script:moduleRoot -ChildPath 'bin\TlsCertificateValidation.dll' -Resolve
$assembly = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object 'Location' -EQ $assemblyPath
$assemblyTypes = $assembly.GetTypes()
$script:serverCertCallbackShim = $assemblyTypes | Where-Object 'Name' -EQ 'ServerCertificateCallbackShim'
if (-not $script:serverCertCallbackShim)
{
    $msg = "Failed to find [TlsCertificateValidation.ServerCertificateCallbackShim] type from assembly " +
           """${assemblyPath}""."
    Write-Error -Message $msg
}

$functionsPath = Join-Path -Path $moduleRoot -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function Clear-TlsCertificateValidator
{
    <#
    .SYNOPSIS
    Removes the current server certificate validation callback function.
 
    .DESCRIPTION
    The `Clear-TlsCertificateValidator` function removes the current server certificate validaton function. .NET allows
    you to set a custom callback function that it will call when validating TLS certificates. This function sets that
    callback function to `$null` (i.e. sets the `[Net.ServicePointManager]::ServerCertificateValidationCallback`
    property to `$null`).
 
    .EXAMPLE
    Clear-TlsCertificateValidator
 
    Demonstrates how to remove the current custom server certificate validator.
    #>

    [CmdletBinding()]
    param(
    )

    Set-StrictMode -Version 'Latest'

    [Net.ServicePointManager]::ServerCertificateValidationCallback = $null
}




function Disable-TlsCertificateValidation
{
    <#
    .SYNOPSIS
    Turns off TLS server certificate validation in the current PowerShell session.
 
    .DESCRIPTION
    The `Disable-TlsCertificateValidation` function disables TLS server certificate validation for the current
    PowerShell session by setting the `[Net.ServicePointManager]::ServerCertificateValidationCallback` to a function
    that accepts all certificates. All https web requests made after calling `Disable-TlsCertificateValidation` will
    accept all invalid TLS certificates.
 
    To re-enable server certificate validation, use the `Enable-TlsCertificateValidation` function.
 
    .EXAMPLE
    Disable-TlsCertificateValidation
 
    Demonstrates how to turn off TLS server certificate validation.
    #>

    [CmdletBinding()]
    param(
        [switch] $Force
    )

    Set-StrictMode -Version 'Latest'

    Set-TlsCertificateValidator -ScriptBlock { return $true }
}



function Enable-TlsCertificateValidation
{
    <#
    .SYNOPSIS
    Turns on TLS server certificate validation in the current PowerShell session.
 
    .DESCRIPTION
    The `Enable-TlsCertificateValidation` function enables TLS server certificate validation for the current
    PowerShell session by clearing the `[Net.ServicePointManager]::ServerCertificateValidationCallback`. All https web
    requests made after calling `Enable-TlsCertificateValidation` will reject all invalid TLS certificates.
 
    To disable server certificate validation, use the `Disable-TlsCertificateValidation` function.
 
    .EXAMPLE
    Enable-TlsCertificateValidation
 
    Demonstrates how to turn on TLS server certificate validation.
    #>

    [CmdletBinding()]
    param(
        [switch] $Force
    )

    Set-StrictMode -Version 'Latest'

    Clear-TlsCertificateValidator
}



function Set-TlsCertificateValidator
{
    <#
    .SYNOPSIS
    Sets the .NET custom server certificate validation callback function to use a PowerShell script block.
 
    .DESCRIPTION
    The `Set-TlsCertificateValidator` allows you use a PowerShell script block to add custom validation when .NET is
    valiating a server's TLS certificate. Pass the script block to the `ScriptBlock` parameter. Your script block *must*
    return `$true` if the certificate is valid and should be trusted, or `$false` or nothing if the certificate is
    invalid and should not be trusted.
 
    The script block is passed four *optional* parameters, in this order:
 
    * `[Object] $Sender`: contains state information for the validation.
    * `[Security.Cryptography.X509Certificates.X509Certificate2] $Certificate`: the certificate to validate.
    * `[Security.Cryptography.X509Certificates.X509Chain] $Chain`: the certificate chain of the certificate.
    * `[Net.Security.SslPolicyErrors] $PolicyErrors`: a flags enum for the certificate's policy errors (if any).
 
    Your validator will continue to be used in the current PowerShell session until you call the
    `Clear-TlsCertificateValidator` function to remove it.
 
    .EXAMPLE
    Set-TlsCertificateValidator { $true }
 
    Demonstrates how to use this function to trust all TLS certificates.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position=0)]
        [scriptblock] $ScriptBlock
    )

    Set-StrictMode -Version 'Latest'

    $script:serverCertCallbackShim::RegisterScriptBlockValidator($ScriptBlock)
}



function Test-SkipCertificateCheck
{
    <#
    .SYNOPSIS
    Tests if PowerShell supports the `SkipCertificateCheck` parameter on `Invoke-WebRequest` and `Invoke-RestMethod`.
 
    .DESCRIPTION
    The `Test-SkipCertificateCheck` function tests if the `Invoke-WebRequest` and `Invoke-RestMethod` have the
    `SkipCertificateCheck` switch. The purpose of this function is for use in cross-platform or cross-edition scripts
    when they need to ignore invalid TLS certificates. If this function returns `$true`, code should use the
    `SkipCertificateCheck` switch on `Invoke-WebRequest` and `Invoke-RestMethod`. If `$false`, scripts should use the
    `Disable-TlsCertificateValidation` and `Enable-TlsCertificateValidation` functions. This logic is shown in the
    following sample code:
 
        $iwrArgs = @{}
        if( (Test-SkipCertificateCheck) )
        {
            $iwrArgs['SkipCertificateCheck'] = $true
        }
        else
        {
            Disable-TlsCertificateValidation
        }
 
        try
        {
            Invoke-WebRequest -Uri 'https://expired.badssl.com/' @iwrArgs
        }
        finally
        {
            if( -not (Test-SkipCertificateCheck) )
            {
                Enable-TlsCertificateValidation
            }
        }
 
    .EXAMPLE
    Test-SkipCertificateCheck
    #>

    [CmdletBinding()]
    param(
    )

    Set-StrictMode -Version 'Latest'

    return $null -ne (Get-Command -Name 'Invoke-WebRequest' -ParameterName 'SkipCertificateCheck' -ErrorAction Ignore)
}



function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common
    parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't
    get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the
    function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't
    have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that
    causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add
    explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get
    fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]`
        # attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the
        # `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken
    # from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }
}