Public/Configure-PwshRemoting.ps1

<#
    .SYNOPSIS
        This function does the following to a Remote Host:

        - Installs the latest version of PowerShell Core using the Remote Host's Package Management system
        - Configures sshd on the Remote Host to use pwsh by default
        - If the Remote Host is Linux, removes the default setting that causes a password prompt when a sudoer uses runs 'sudo pwsh'

    .DESCRIPTION
        See SYNOPSIS

    .PARAMETER RemoteOSGuess
        This parameter is OPTIONAL.
        
        This parameter takes a string (either "Windows" or "Linux") that represents the type of platform you anticipate the
        Remote Host is running. The default value for this parameter is "Windows".

        IMPORTANT NOTE: If you specify "Linux" and it turns out that the Remote Host is running Windows, this function will fail.
        So, if you're not sure, leave the default value "Windows".

    .PARAMETER RemoteHostNameOrIP
        This parameter is MANDATORY.

        This parameter takes a string that represents the DNS-resolvable HostName/FQDN or IPv4 Address of the target Remote Host

    .PARAMETER LocalUserName
        This parameter is MANDATORY for the Parameter Set 'Local'.

        This parameter takes a string that represents the Local User Account on the Remote Host that you are using to ssh into
        the Remote Host. This string must be in format: <RemoteHostName>\<UserName>

    .Parameter DomainUserName
        This parameter is MANDATORY for the Parameter Set 'Domain'.

        This parameter takes a string that represents the Domain User Account on the Remote Host that you are using to ssh into
        the Remote Host. This string must be in format: <DomainShortName>\<UserName>

    .Parameter LocalPasswordSS
        This parameter is OPTIONAL. (However, either -LocalPasswordSS or -KeyFilePath is mandatory for the 'Domain' Parameter Set)

        This parameter takes a securestring that represents the password for the -LocalUserName you are using to ssh into the
        Remote Host.

    .Parameter DomainPasswordSS
        This parameter is OPTIONAL. (However, either -DomainPasswordSS or -KeyFilePath is mandatory for the 'Domain' Parameter Set)

        This parameter takes a securestring that represents the password for the -DomainUserName you are using to ssh into the
        Remote Host.

    .PARAMETER KeyFilePath
        This parameter is OPTIONAL. (However, either -DomainPasswordSS, -LocalPasswordSS, or -KeyFilePath is required)

        This parameter takes a string that represents the full path to the Key File you are using to ssh into the Remote Host.
        Use this parameter instead of -LocalPasswordSS or -DomainPasswordSS.

    .PARAMETER UsePackageManagement
        This parameter is OPTIONAL, however, it has a default value of $True

        This parameter is a switch. If used (default behavior), the appropriate Package Management system on the Remote Host
        will be used to install PowerShell Core.

        If explicitly set to $False, the appropriate PowerShell Core installation package will be downloaded directly from GitHub
        and installed on the Remote Host.

    .PARAMETER DomainUserForNoSudoPwd
        This parameter is OPTIONAL.

        This parameter takes a string or array of strings that represent Domain Users that you would like to allow to use
        'sudo pwsh' without a password prompt. Each user must be in format: <DomainShortName>\<UserName>

        Only applies to Linux Remote Hosts.

    .PARAMETER LocalUserForNoSudoPwd
        This parameter is OPTIONAL.

        This parameter takes a string or array of strings that represent Local Users on the Remote Host that you would like to
        allow to use 'sudo pwsh' without a password prompt. Each user must be in format: <RemoteHostName>\<UserName>

        Only applies to Linux Remote Hosts.

    .PARAMETER DomainGroupForNoSudoPwd
        This parameter is OPTIONAL.

        This parameter takes a string or array of strings that represent Domain Groups that you would like to allow to use
        'sudo pwsh' without a password prompt.

        Only applies to Linux Remote Hosts.

    .EXAMPLE
        # Minimal parameters...

        $ConfigurePwshRemotingSplatParams = @{
            RemoteHostNameOrIP = "192.168.2.61"
            LocalUserName = "centos7x\vagrant"
            LocalPasswordSS = $(Read-Host -Prompt "Enter password" -AsSecureString)
        }
        $ConfigurePwshRemotingResult = Configure-PwshRemoting @ConfigurePwshRemotingSplatParams
        
#>

function Configure-PwshRemoting {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [ValidateSet("Windows","Linux")]
        [string]$RemoteOSGuess = "Windows",

        [Parameter(Mandatory=$True)]
        [string]$RemoteHostNameOrIP,

        [Parameter(
            Mandatory=$True,
            ParameterSetName='Local'
        )]
        [ValidatePattern("\\")] # Must be in format <RemoteHostName>\<User>
        [string]$LocalUserName,

        [Parameter(
            Mandatory=$True,
            ParameterSetName='Domain'    
        )]
        [ValidatePattern("\\")] # Must be in format <DomainShortName>\<User>
        [string]$DomainUserName,

        [Parameter(
            Mandatory=$False,
            ParameterSetName='Local'
        )]
        [securestring]$LocalPasswordSS,

        [Parameter(
            Mandatory=$False,
            ParameterSetName='Domain'
        )]
        [securestring]$DomainPasswordSS,

        [Parameter(Mandatory=$False)]
        [string]$KeyFilePath,

        [Parameter(Mandatory=$False)]
        [ValidatePattern("\\")] # Must be in format <DomainShortName>\<User>
        [string[]]$DomainUserForNoSudoPwd,

        [Parameter(
            Mandatory=$False,
            ParameterSetName='Local'
        )]
        [ValidatePattern("\\")] # Must be in format <DomainShortName>\<User>
        [string[]]$LocalUserForNoSudoPwd,

        [Parameter(
            Mandatory=$False,
            ParameterSetName='Domain'
        )]
        [string[]]$DomainGroupForNoSudoPwd
    )

    #region >> Prep

    if (!$(Get-Command ssh -ErrorAction SilentlyContinue)) {
        Write-Error "Unable to find 'ssh'! Please make sure it is installed and part of your Environment/System Path! Halting!"
        $global:FunctionResult = "1"
        return
    }

    if ($KeyFilePath) {
        if (!$(Test-Path $KeyFilePath)) {
            Write-Error "Unable to find KeyFilePath '$KeyFilePath'! Halting!"
            $global:FunctionResult = "1"
            return
        }

        if (!$LocalUserName -and !$DomainUserName) {
            Write-Error "You must supply either -LocalUserName or -DomainUserName when using the -KeyFilePath parameter! Halting!"
            $global:FunctionResult = "1"
            return
        }
    }

    try {
        $RemoteHostNetworkInfo = ResolveHost -HostNameOrIP $RemoteHostNameOrIP -ErrorAction Stop
    }
    catch {
        Write-Error $_
        Write-Error "Unable to resolve '$RemoteHostNameOrIP'! Halting!"
        $global:FunctionResult = "1"
        return
    }

    if ($LocalPasswordSS -or $DomainPasswordSS -and $KeyFilePath) {
        Write-Error "Please use EITHER -KeyFilePath OR -LocalPasswordSS/-DomainPasswordSS in order to ssh to $RemoteHostNameOrIP! Halting!"
        $global:FunctionResult = "1"
        return
    }

    if ($LocalUserName) {
        if ($($LocalUserName -split "\\")[0] -ne $RemoteHostNetworkInfo.HostName) {
            $ErrMsg = "The HostName indicated by -LocalUserName (i.e. $($($LocalUserName -split "\\")[0]) is not the same as " +
            "the HostName as determined by network resolution (i.e. $($RemoteHostNetworkInfo.HostName))! Halting!"
            Write-Error $ErrMsg
            $global:FunctionResult = "1"
            return
        }
    }
    if ($DomainUserName) {
        if ($($DomainUserName -split "\\")[0] -ne $($RemoteHostNetworkInfo.Domain -split "\.")[0]) {
            $ErrMsg = "The Domain indicated by -DomainUserName (i.e. '$($($DomainUserName -split "\\")[0])') is not the same as " +
            "the Domain as determined by network resolution (i.e. '$($($RemoteHostNetworkInfo.Domain -split "\.")[0])')! Halting!"
            Write-Error $ErrMsg
            $global:FunctionResult = "1"
            return
        }
    }

    # Probe the Remote Host to get OS and Shell Info
    try {
        Write-Host "Probing $RemoteHostNameOrIP to determine OS and available shell..."

        $GetSSHProbeSplatParams = @{
            RemoteHostNameOrIP  = $RemoteHostNameOrIP
        }
        if ($KeyFilePath) {
            $GetSSHProbeSplatParams.Add("KeyFilePath",$KeyFilePath)
        }
        if ($LocalUserName) {
            $GetSSHProbeSplatParams.Add("LocalUserName",$LocalUserName)
        }
        if ($DomainUserName) {
            $GetSSHProbeSplatParams.Add("DomainUserName",$DomainUserName)
        }
        if ($LocalPasswordSS -and !$KeyFilePath) {
            $GetSSHProbeSplatParams.Add("LocalPasswordSS",$LocalPasswordSS)
        }
        if ($DomainPasswordSS -and !$KeyFilePath) {
            $GetSSHProbeSplatParams.Add("DomainPasswordSS",$DomainPasswordSS)
        }
        if ($RemoteOSGuess) {
            $GetSSHProbeSplatParams.Add("RemoteOSGuess",$RemoteOSGuess)
        }
        
        $OSCheck = Get-SSHProbe @GetSSHProbeSplatParams -ErrorAction Stop
    }
    catch {
        Write-Verbose $_.Exception.Message
        $global:FunctionResult = "1"

        try {
            $null = Stop-AwaitSession
        }
        catch {
            Write-Verbose $_.Exception.Message
        }
    }

    if (!$OSCheck.OS -or !$OSCheck.Shell) {
        try {
            Write-Host "Probing $RemoteHostNameOrIP to determine OS and available shell..."

            $GetSSHProbeSplatParams = @{
                RemoteHostNameOrIP  = $RemoteHostNameOrIP
            }
            if ($KeyFilePath) {
                $GetSSHProbeSplatParams.Add("KeyFilePath",$KeyFilePath)
            }
            if ($LocalUserName) {
                $GetSSHProbeSplatParams.Add("LocalUserName",$LocalUserName)
            }
            if ($DomainUserName) {
                $GetSSHProbeSplatParams.Add("DomainUserName",$DomainUserName)
            }
            if ($LocalPasswordSS -and !$KeyFilePath) {
                $GetSSHProbeSplatParams.Add("LocalPasswordSS",$LocalPasswordSS)
            }
            if ($DomainPasswordSS -and !$KeyFilePath) {
                $GetSSHProbeSplatParams.Add("DomainPasswordSS",$DomainPasswordSS)
            }
            if ($RemoteOSGuess) {
                $GetSSHProbeSplatParams.Add("RemoteOSGuess",$RemoteOSGuess)
            }
            
            $OSCheck = Get-SSHProbe @GetSSHProbeSplatParams -ErrorAction Stop
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
    
            try {
                $null = Stop-AwaitSession
            }
            catch {
                Write-Verbose $_.Exception.Message
            }
    
            return
        }
    }

    if (!$OSCheck.OS -or !$OSCheck.Shell) {
        Write-Error "The Get-SSHProbe function was unable to identify $RemoteHostNameOrIP's platform or default shell! Please check your ssh connection/credentials. Halting!"
        $global:FunctionResult = "1"
        return
    }
    
    if ($OSCheck.OS -eq "Linux") {
        # Check to make sure the user has sudo privileges
        try {
            $GetSudoStatusSplatParams = @{
                RemoteHostNameOrIP  = $RemoteHostNameOrIP
            }
            if ($KeyFilePath) {
                $GetSudoStatusSplatParams.Add("KeyFilePath",$KeyFilePath)
            }
            if ($LocalPasswordSS) {
                $GetSudoStatusSplatParams.Add("LocalPasswordSS",$LocalPasswordSS)
            }
            if ($DomainPasswordSS) {
                $GetSudoStatusSplatParams.Add("DomainPasswordSS",$DomainPasswordSS)
            }
            if ($LocalUserName) {
                $GetSudoStatusSplatParams.Add("LocalUserName",$LocalUserName)
            }
            if ($DomainUserName) {
                $GetSudoStatusSplatParams.Add("DomainUserName",$DomainUserName)
            }
            
            $GetSudoStatusResult = Get-SudoStatus @GetSudoStatusSplatParams
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
            return
        }
        
        if (!$GetSudoStatusResult.HasSudoPrivileges) {
            Write-Error "The user does not appear to have sudo privileges on $RemoteHostNameOrIP! Halting!"
            $global:FunctionResult = "1"
            return
        }

        # If the user has sudo privileges but there's a password prompt, but -LocalPasswordSS and -DomainPasswordSS
        # parameters were not used, we need to halt
        if ($GetSudoStatusResult.PasswordPrompt) {
            if (!$LocalPasswordSS -and !$DomainPasswordSS) {
                Write-Error "The user will be prompted for a sudo password, but neither the -LocalPasswordSS nor -DomainPasswordSS parameter was provided! Halting!"
                $global:FunctionResult = "1"
                return
            }
        }
    }

    #endregion >> Prep

    #region >> Main

    try {
        $BootstrapPwshSplatParams = @{
            RemoteHostNameOrIP      = $RemoteHostNameOrIP
            ConfigurePSRemoting     = $True
            ErrorAction             = "Stop"
        }
        if ($LocalUserName) {
            $BootstrapPwshSplatParams.Add('LocalUserName',$LocalUserName)
        }
        if ($DomainUserName) {
            $BootstrapPwshSplatParams.Add('DomainUserName',$DomainUserName)
        }

        if ($KeyFilePath) {
            $BootstrapPwshSplatParams.Add('KeyFilePath',$KeyFilePath)
        }
        if ($LocalPasswordSS) {
            $BootstrapPwshSplatParams.Add('LocalPasswordSS',$LocalPasswordSS)
        }
        if ($DomainPasswordSS) {
            $BootstrapPwshSplatParams.Add('DomainPasswordSS',$DomainPasswordSS)
        }
        $BootstrapPwshResult = Bootstrap-PowerShellCore @BootstrapPwshSplatParams
    }
    catch {
        Write-Error $_
        $global:FunctionResult = "1"
        return
    }

    if ($OSCheck.OS -eq "Linux") {
        $RemoveSudoPwdSplatParams = @{
            RemoteHostNameOrIP      = $RemoteHostNameOrIP
            ErrorAction             = "Stop"
        }
        if ($LocalUserName) {
            $RemoveSudoPwdSplatParams.Add('LocalUserName',$LocalUserName)
        }
        if ($DomainUserName) {
            $RemoveSudoPwdSplatParams.Add('DomainUserName',$DomainUserName)
        }
        if ($KeyFilePath) {
            $RemoveSudoPwdSplatParams.Add('KeyFilePath',$KeyFilePath)
        }
        if ($LocalPasswordSS) {
            $RemoveSudoPwdSplatParams.Add('LocalPasswordSS',$LocalPasswordSS)
        }
        if ($DomainPasswordSS) {
            $RemoveSudoPwdSplatParams.Add('DomainPasswordSS',$DomainPasswordSS)
        }
        if ($DomainUserForNoSudoPwd) {
            $RemoveSudoPwdSplatParams.Add('DomainUserForNoSudoPwd',$DomainUserForNoSudoPwd)
        }
        elseif ($LocalUserForNoSudoPwd) {
            $RemoveSudoPwdSplatParams.Add('LocalUserForNoSudoPwd',$LocalUserForNoSudoPwd)
        }
        elseif ($DomainGroupForNoSudoPwd) {
            $RemoveSudoPwdSplatParams.Add('DomainGroupForNoSudoPwd',$DomainGroupForNoSudoPwd)
        }
        $RemoveSudoPwdResult = Remove-SudoPwd @RemoveSudoPwdSplatParams
    }

    # Test to make sure PwshRemoting is configured properly
    <#
    $NewPSSessionSplatParams = @{
        HostName = $RemoteHostNetworkInfo.IPAddressList[0]
    }
    if ($LocalUserName) {
        $NewPSSessionSplatParams.Add('UserName',$LocalUserName)
    }
    if ($DomainUserName) {
        $NewPSSessionSplatParams.Add('UserName',$DomainUserName)
    }
    if ($KeyFilePath) {
        $NewPSSessionSplatParams.Add('KeyFilePath',$KeyFilePath)
    }

    $ToRemoteHost = New-PSSession @NewPSSessionSplatParams
    $SB = {
        $PSVersionTable | ConvertTo-Json
    }
    $Bytes = [System.Text.Encoding]::Unicode.GetBytes($SB.ToString())
    $EncodedCommandPSVerTable = [Convert]::ToBase64String($Bytes)
    Invoke-Command -Session $ToRemoteHost -ScriptBlock {sudo pwsh -EncodedCommand $using:EncodedCommandPSVerTable} | ConvertFrom-Json
    #>


    [pscustomobject]@{
        GetSudoStatusResult     = $GetSudoStatusResult
        BootstrapPwshResult     = $BootstrapPwshResult
        RemoveSudoPwdResult     = $RemoveSudoPwdResult
    }

    #endregion >> Main
}

# SIG # Begin signature block
# MIIMiAYJKoZIhvcNAQcCoIIMeTCCDHUCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUNP08BZ1LhYAnSmaORGQxXS4S
# Ltqgggn9MIIEJjCCAw6gAwIBAgITawAAAB/Nnq77QGja+wAAAAAAHzANBgkqhkiG
# 9w0BAQsFADAwMQwwCgYDVQQGEwNMQUIxDTALBgNVBAoTBFpFUk8xETAPBgNVBAMT
# CFplcm9EQzAxMB4XDTE3MDkyMDIxMDM1OFoXDTE5MDkyMDIxMTM1OFowPTETMBEG
# CgmSJomT8ixkARkWA0xBQjEUMBIGCgmSJomT8ixkARkWBFpFUk8xEDAOBgNVBAMT
# B1plcm9TQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCwqv+ROc1
# bpJmKx+8rPUUfT3kPSUYeDxY8GXU2RrWcL5TSZ6AVJsvNpj+7d94OEmPZate7h4d
# gJnhCSyh2/3v0BHBdgPzLcveLpxPiSWpTnqSWlLUW2NMFRRojZRscdA+e+9QotOB
# aZmnLDrlePQe5W7S1CxbVu+W0H5/ukte5h6gsKa0ktNJ6X9nOPiGBMn1LcZV/Ksl
# lUyuTc7KKYydYjbSSv2rQ4qmZCQHqxyNWVub1IiEP7ClqCYqeCdsTtfw4Y3WKxDI
# JaPmWzlHNs0nkEjvnAJhsRdLFbvY5C2KJIenxR0gA79U8Xd6+cZanrBUNbUC8GCN
# wYkYp4A4Jx+9AgMBAAGjggEqMIIBJjASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsG
# AQQBgjcVAgQWBBQ/0jsn2LS8aZiDw0omqt9+KWpj3DAdBgNVHQ4EFgQUicLX4r2C
# Kn0Zf5NYut8n7bkyhf4wGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDgYDVR0P
# AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUdpW6phL2RQNF
# 7AZBgQV4tgr7OE0wMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovL3BraS9jZXJ0ZGF0
# YS9aZXJvREMwMS5jcmwwPAYIKwYBBQUHAQEEMDAuMCwGCCsGAQUFBzAChiBodHRw
# Oi8vcGtpL2NlcnRkYXRhL1plcm9EQzAxLmNydDANBgkqhkiG9w0BAQsFAAOCAQEA
# tyX7aHk8vUM2WTQKINtrHKJJi29HaxhPaHrNZ0c32H70YZoFFaryM0GMowEaDbj0
# a3ShBuQWfW7bD7Z4DmNc5Q6cp7JeDKSZHwe5JWFGrl7DlSFSab/+a0GQgtG05dXW
# YVQsrwgfTDRXkmpLQxvSxAbxKiGrnuS+kaYmzRVDYWSZHwHFNgxeZ/La9/8FdCir
# MXdJEAGzG+9TwO9JvJSyoGTzu7n93IQp6QteRlaYVemd5/fYqBhtskk1zDiv9edk
# mHHpRWf9Xo94ZPEy7BqmDuixm4LdmmzIcFWqGGMo51hvzz0EaE8K5HuNvNaUB/hq
# MTOIB5145K8bFOoKHO4LkTCCBc8wggS3oAMCAQICE1gAAAH5oOvjAv3166MAAQAA
# AfkwDQYJKoZIhvcNAQELBQAwPTETMBEGCgmSJomT8ixkARkWA0xBQjEUMBIGCgmS
# JomT8ixkARkWBFpFUk8xEDAOBgNVBAMTB1plcm9TQ0EwHhcNMTcwOTIwMjE0MTIy
# WhcNMTkwOTIwMjExMzU4WjBpMQswCQYDVQQGEwJVUzELMAkGA1UECBMCUEExFTAT
# BgNVBAcTDFBoaWxhZGVscGhpYTEVMBMGA1UEChMMRGlNYWdnaW8gSW5jMQswCQYD
# VQQLEwJJVDESMBAGA1UEAxMJWmVyb0NvZGUyMIIBIjANBgkqhkiG9w0BAQEFAAOC
# AQ8AMIIBCgKCAQEAxX0+4yas6xfiaNVVVZJB2aRK+gS3iEMLx8wMF3kLJYLJyR+l
# rcGF/x3gMxcvkKJQouLuChjh2+i7Ra1aO37ch3X3KDMZIoWrSzbbvqdBlwax7Gsm
# BdLH9HZimSMCVgux0IfkClvnOlrc7Wpv1jqgvseRku5YKnNm1JD+91JDp/hBWRxR
# 3Qg2OR667FJd1Q/5FWwAdrzoQbFUuvAyeVl7TNW0n1XUHRgq9+ZYawb+fxl1ruTj
# 3MoktaLVzFKWqeHPKvgUTTnXvEbLh9RzX1eApZfTJmnUjBcl1tCQbSzLYkfJlJO6
# eRUHZwojUK+TkidfklU2SpgvyJm2DhCtssFWiQIDAQABo4ICmjCCApYwDgYDVR0P
# AQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBS5d2bhatXq
# eUDFo9KltQWHthbPKzAfBgNVHSMEGDAWgBSJwtfivYIqfRl/k1i63yftuTKF/jCB
# 6QYDVR0fBIHhMIHeMIHboIHYoIHVhoGubGRhcDovLy9DTj1aZXJvU0NBKDEpLENO
# PVplcm9TQ0EsQ049Q0RQLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNl
# cnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9emVybyxEQz1sYWI/Y2VydGlmaWNh
# dGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlv
# blBvaW50hiJodHRwOi8vcGtpL2NlcnRkYXRhL1plcm9TQ0EoMSkuY3JsMIHmBggr
# BgEFBQcBAQSB2TCB1jCBowYIKwYBBQUHMAKGgZZsZGFwOi8vL0NOPVplcm9TQ0Es
# Q049QUlBLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENO
# PUNvbmZpZ3VyYXRpb24sREM9emVybyxEQz1sYWI/Y0FDZXJ0aWZpY2F0ZT9iYXNl
# P29iamVjdENsYXNzPWNlcnRpZmljYXRpb25BdXRob3JpdHkwLgYIKwYBBQUHMAKG
# Imh0dHA6Ly9wa2kvY2VydGRhdGEvWmVyb1NDQSgxKS5jcnQwPQYJKwYBBAGCNxUH
# BDAwLgYmKwYBBAGCNxUIg7j0P4Sb8nmD8Y84g7C3MobRzXiBJ6HzzB+P2VUCAWQC
# AQUwGwYJKwYBBAGCNxUKBA4wDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOC
# AQEAszRRF+YTPhd9UbkJZy/pZQIqTjpXLpbhxWzs1ECTwtIbJPiI4dhAVAjrzkGj
# DyXYWmpnNsyk19qE82AX75G9FLESfHbtesUXnrhbnsov4/D/qmXk/1KD9CE0lQHF
# Lu2DvOsdf2mp2pjdeBgKMRuy4cZ0VCc/myO7uy7dq0CvVdXRsQC6Fqtr7yob9NbE
# OdUYDBAGrt5ZAkw5YeL8H9E3JLGXtE7ir3ksT6Ki1mont2epJfHkO5JkmOI6XVtg
# anuOGbo62885BOiXLu5+H2Fg+8ueTP40zFhfLh3e3Kj6Lm/NdovqqTBAsk04tFW9
# Hp4gWfVc0gTDwok3rHOrfIY35TGCAfUwggHxAgEBMFQwPTETMBEGCgmSJomT8ixk
# ARkWA0xBQjEUMBIGCgmSJomT8ixkARkWBFpFUk8xEDAOBgNVBAMTB1plcm9TQ0EC
# E1gAAAH5oOvjAv3166MAAQAAAfkwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwx
# CjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGC
# NwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFKuh6K6+pLBI9Xfq
# I6a06R5fgPR1MA0GCSqGSIb3DQEBAQUABIIBAGFoMiXUi6ba+eWP+bySq9c+ZCJ5
# HJNyCEmP9CQbq93qtadnQeIJ4MwT1G3FQAcqWl/FcrS6vOzix1cNoL20g+yazAYF
# YUTRYo0RBx/axmmu+yu37YsqofyaDjgrUObnXs6tM+rdY9Sh+CfNZNkM2kFZBVdm
# nkt4A0CIZfg0XFPd0Dc+fL4fj6JlyEABH98UToJCJ1QsNQGmwL5FJ7gFA0VLvV9q
# X5hwshcUa41Bq+mbnhVVZ3gtXLgbaq2qXSRLqfFv+GQzz2iaiqTDyqnxKmpJIM58
# hEPR4JaN85AELcMPxx0Nse+XUzjgkcd3+GBwGWl31RoxvpuqnsNVZerXTmY=
# SIG # End signature block