Validation/PesterHelper.psm1

# Change Security Protocol to TLS1.2 in each function that requires a secured connection
# Each function will return to the original state so the tests run from the system default.
Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force
$GLOBAL:restoreSecProtocol = [Net.ServicePointManager]::SecurityProtocol
$GLOBAL:targetSecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Connect-Azure {
    <#
    .SYNOPSIS
        Connect to Azure as Service Principal
    .DESCRIPTION
        Connect to Azure as Service Principal neccessary for download or upload.
        This will scan the localmachine store for the certificate specific to the
        service principal and authenicate with that. If the certificate does not
        exist, the user will be prompted for AAD creds and the certificate will be
        retrieved from the devloop keyvault store.
    .EXAMPLE
        PS C:\> Connect-Azure
        Connects to Azure with service principal with download rights
    .EXAMPLE
        PS C:\> Connect-Azure [[-retryCount] <int>] [[-intervalSeconds] <int>] [-upload]
        Connects to Azure with service principal with upload rights and optionally
        set retry and intervals.
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    param ([int]$retryCount = 3, [int]$intervalSeconds = 5, [switch]$upload)

    Set-SecurityProtocol -securityProtocol $targetSecurityProtocol
    
    $testData = Import-LocalizedData -BaseDirectory $here -FileName PesterHelperData.psd1

    if ($upload) {
        $certPath = $testData.pesterdata.spuCertPath
        $certName = $testData.pesterdata.spuCertname
        $applicationId = $testData.pesterdata.spuAppId

    }
    else {
        $certPath = $testData.pesterdata.spCertPath
        $certName = $testData.pesterdata.spCertname
        $ApplicationId = $testData.pesterdata.spAppId
    }
    $tenantId = $testData.pesterdata.spTenantId
    $ptkSubscriptionId = $testData.pesterdata.ptksubscriptionId
    $kvCertName = $testData.pesterdata.spkvCertName
    $kvVaultName = $testData.pesterdata.spkvname
    $spSubscriptionId = $testData.pesterData.spSubscriptionId

    #Get Cert
    do {
        $cert = Get-ChildItem -Path $certPath | Where-Object subject -eq "CN=$certName"
        if ($null -eq $cert) {
            Write-Verbose "Cert was not found in $certPath, user will be prompted for user credentials and attempt to download the certificate from keyvault."
            Login-AzureRmAccount -Subscription $spSubscriptionId | Out-Null
            Write-Verbose "Logged in successfully."
            Write-Verbose "Retrieving Authentication Certificate for Service Principal."
            $outputPath = Join-Path $ENV:TEMP "$kvCertName.pfx"
            $pwd = New-RandomPassword
            $pfx = Save-PFXData -VaultName $kvVaultName -CertName $kvCertName -outputPath $outputPath -pwd $pwd
            Write-Verbose "Importing Authentication Certificate for Service Principal to local store."
            Import-PfxCertificate -Exportable -Password $pfx.pfxpassword -CertStoreLocation $certPath -FilePath $pfx.pfxpath | Out-Null
            if (Test-Path $pfx.pfxpath) {
                Remove-Item $pfx.pfxpath -force
            }
            $retry++
            start-Sleep -Seconds $intervalSeconds
        }
    }
    while (-not $cert -AND $retry -le $retryCount)

    # Login into Azure using cert
    try {
        Login-AzureRmAccount -ServicePrincipal -CertificateThumbprint $cert.Thumbprint -ApplicationId $ApplicationId -TenantId $TenantId -Subscription $ptkSubscriptionId | Out-Null
        Write-Verbose ("Successfully logged in as service principal {0}." -f $ApplicationId)
    }
    catch {
        Write-Error ("Login into Azure Account failed using cert thumbprint {0}" -f $cert.thumbprint)
    }
    #Get Context to return to caller
    $AzureContext = Get-AzureRmContext
    Set-SecurityProtocol -securityProtocol $restoreSecProtocol
    return $AzureContext
}

function New-RandomPassword {
    <#
    .SYNOPSIS
        Creates a new random password.
    .DESCRIPTION
        Creates a new random password.
    .EXAMPLE
        PS C:\> New-RandomPassword
        Creates a new random password.
    #>

    Add-Type -AssemblyName System.Web
    $password = [System.Web.Security.Membership]::GeneratePassword(20, 2)
    return $password
}

function Save-PFXData {
    <#
    .SYNOPSIS
        Get PFX secrets from Keyvault and save them to disk
    .DESCRIPTION
        Get PFX secrets from Keyvault and save them to disk with a random password
    .EXAMPLE
        PS C:\> Save-PFXData [[-VaultName] <String>] [[-CertName] <String>] [[-outputPath] <String>]
    #>

    param ([string]$VaultName, [string]$CertName, [string]$outputPath,[string]$pwd)
    Write-Verbose -Message ("Downloading {0} to {1}..." -f $CertName, $outputPath)
    try {
        
        $pfxPassword = ConvertTo-SecureString -String $pwd -AsPlainText -Force
        $kvSecret = Get-AzureKeyVaultSecret -VaultName $VaultName -Name $CertName
        if (-not $kvSecret) {
            throw ("Unable to retrieve $certname from $vaultname")
        }

        $kvSecretBytes = [System.Convert]::FromBase64String($kvSecret.SecretValueText)
        $certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
        $certCollection.Import($kvSecretBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
        $protectedCertificateBytes = $certCollection.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12, $pwd)
        [System.IO.File]::WriteAllBytes($outputPath, $protectedCertificateBytes)
        start-sleep -sec 1
        if (Test-Path -Path $outputPath) {
            Write-Verbose -Message ("Downloading {0} Complete." -f $CertName)
        }
        else {
            throw ("Unable to resovle path $outputPath")
        }
    }
    catch {
        throw $_.exception
    }
    finally {
        Set-SecurityProtocol -securityProtocol $restoreSecProtocol
    }

    return @{'pfxpath' = $outputPath; 'pfxpassword' = $pfxPassword}
}

function Get-x509 {
    <#
    .SYNOPSIS
        Creates x509 object of a given certificate
    .DESCRIPTION
        Creates x509 object of a given certificate
    .EXAMPLE
        PS C:\> Get-x509 [[-certKey] <object>]
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        Expects $certInfo = @{'pfxPath' = <string>;'pfxPassword' = <securestring>}}
    #>

    param($certInfo)
    try {
        Write-Verbose ("Attempting to read certificate {0}" -f $certKey)
        [byte[]]$CertificateBinary = Get-Content $certInfo.pfxPath -Encoding Byte
        $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
        $cert.Import($CertificateBinary, $certInfo.pfxPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::DefaultKeySet)
        Write-Verbose -message ("Successfully read certificate {0}, thumbprint {1}" -f $certInfo.pfxPath, $cert.Thumbprint)
    }
    catch {
        throw $_.exception
    }
    return $cert
}

function Get-PFXHash {
    <#
    .SYNOPSIS
        Calls ParsePFX to retrieve a custom PFX hash object of a given certificate
    .DESCRIPTION
        Calls ParsePFX to retrieve a custom PFX hash object of a given certificate
    .EXAMPLE
        PS C:\> Get-PFXHash [[-certinfo] <object>]
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        Expects $certInfo = @{'pfxPath' = <string>;'pfxPassword' = <securestring>}}
    #>

    param($certInfo)
    try {
        Write-Verbose ("Attempting to read PFX {0}" -f $certKey)
        [byte[]]$CertificateBinary = Get-Content $certInfo.pfxPath -Encoding Byte
        $pfxParseResult = Test-Pfx -certificateBinary $CertificateBinary -certificatePassword $certInfo.pfxPassword
        $pfxData = $pfxParseResult.outputObject
        Write-Verbose -message ("Successfully read PFX {0}" -f $certInfo.pfxPath)
    }
    catch {
        throw $_.exception
    }
    return $pfxData
}

function Save-File {
    <#
    .SYNOPSIS
        Retrieves deployment data from keyvault and saves to JSON.
    .DESCRIPTION
        Retrieves deployment data from keyvault and saves to JSON.
    .EXAMPLE
        PS C:\> Save-File [[-secretName] <String>] [[-VaultName] <String>] [[-outputPath] <String>]
    .NOTES
        General notes
    #>

    param ([string]$secretName, [string]$VaultName, [string]$outputPath)
    try {
        Set-SecurityProtocol -securityProtocol $targetSecurityProtocol
        Write-Verbose ("Attempting to retrieve file {0} and save to {1}" -f $secretName, $outputPath)
        $kvSecret = Get-AzureKeyVaultSecret -VaultName $VaultName -Name $secretName
        $kvSecret.SecretValueText | Out-File $outputPath
        start-sleep -sec 1
        if (Test-Path -Path $outputPath) {
            Write-Verbose -Message ("Downloading file {0} to {1} Complete." -f $secretName, $outputPath)
        }
        else {
            throw ("Unable to resovle path $outputPath")
        }

    }
    catch {
        throw $_.exception
    }
    finally {
        Set-SecurityProtocol -securityProtocol $restoreSecProtocol
    }
    return $outputPath
}

function Get-CredentialSecret {
    <#
    .SYNOPSIS
        Retrieves Credential Secrets from Keyvault
    .DESCRIPTION
        Retrieves Credential Secrets from Keyvault
    .EXAMPLE
        PS C:\> Get-CredentialSecret [[-username] <string>] [[-password] <string>] [[-VaultName] <String>]
        Retrieves Credential Secrets from Keyvault
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    param ([string]$username, [string]$password, [string]$VaultName)
    try {
        Set-SecurityProtocol -securityProtocol $targetSecurityProtocol

        $passwordValue = Get-AzureKeyVaultSecret -VaultName $VaultName -Name $password | Select-Object -ExpandProperty SecretValue
        $username = Get-PlainTextSecret -VaultName $VaultName -secretName $username
        $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $passwordValue
    }
    catch {
        throw $_.exception
    }
    finally {
        Set-SecurityProtocol -securityProtocol $restoreSecProtocol
    }
    return $credential
}

function Get-PlainTextSecret {
    <#
    .SYNOPSIS
        Retreives a plain text secret
    .DESCRIPTION
        Retreives a plain text secret
    .EXAMPLE
        PS C:\> Get-PlainTextSecret [[-secretName] <string>] [[-VaultName] <String>]
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    param ([string]$secretName, [string]$VaultName)
    try {
        Set-SecurityProtocol -securityProtocol $targetSecurityProtocol
        $secretValue = Get-AzureKeyVaultSecret -VaultName $VaultName -Name $secretName | Select-Object -ExpandProperty SecretValue
        $fakeuser = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList dummy, $secretValue
        $plainValue = $fakeuser.GetNetworkCredential().Password
    }
    catch {
        throw $_.exception
    }
    finally {
       Set-SecurityProtocol -securityProtocol $restoreSecProtocol
    }
    return $plainValue
}

function Remove-TestFiles {
    <#
    .SYNOPSIS
        Removes files
    .DESCRIPTION
        Checks files exist and then removes files
    .EXAMPLE
        PS C:\> Remove-TestFiles [[-files] <Object>]
        Removes files in array
    .NOTES
        General notes
    #>

    [CmdletBinding()]
    param ([psobject]$in)
    if (-not $keepLogs) {
        Write-Verbose ("Attempting removal of test assets: {0}" -f ($in -join ','))
        foreach ($item in $in) {
            try {
                Get-Item -Path $item.pspath -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -Verbose 
                
            }
            catch {
                Get-Item -Path $item -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -Verbose 
            }
        }
    }
    else {
        Write-Verbose ("Skipping deletion of {0}" -f $in -join ',') -Verbose
    }
}

function Publish-Certificate {
    <#
    .SYNOPSIS
        Upload Certificate to Keyvault
    .DESCRIPTION
        Upload Certificate to Keyvault
    .SYNTAX
 
    .EXAMPLE
        PS C:\> Publish-Certificate [[-InputObject] <Hashtable>] [[-vaultName] <String>] [<CommonParameters>]
        Uploads certificates from hashtable in to a named vaultname.
    .INPUTS
        Hashtable the format of @{'CertFriendlyName' = @ {'pfxPath' = <string>;'pfxPassword' = <securestring>}}
    .OUTPUTS
        Output (if any)
    .NOTES
        Requires an Azure Context with the appropriate permissions to upload to keyvault.
    #>

    [CmdletBinding()]
    # Parameter help description
    param (

        [Parameter(ValueFromPipeline = $True)]
        [hashtable]
        $InputObject,

        [string]
        $vaultName
    )

    Set-SecurityProtocol -securityProtocol $targetSecurityProtocol
    $AzureContext = Get-AzureRmContext
    $data = Import-LocalizedData -BaseDirectory $PSScriptRoot -FileName PesterHelperData.psd1

    if ($AzureContext.Account.Id -ne $data.pesterdata.spuAppId) {
        Connect-Azure -upload
    }

    $i = 1
    foreach ($key in $InputObject.Keys) {
        try {
            [int]$percentageComplete = $i / $InputObject.Keys.count * 100
            Write-Progress -Activity "Uploading Certificates" -Status "$percentageComplete% Complete:" -PercentComplete $percentageComplete -CurrentOperation $key

            #to make sure the keys are exportable we must import leaf seperately with its storage flag set and add to collection.
            $leaf = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
            Write-Verbose -Verbose ("Getting Content for {0}" -f $InputObject[$key].pfxpath)
            [byte[]]$CertificateBinary = Get-Content -Path $InputObject[$key].pfxpath -Encoding Byte
            $CertificatePassword = $InputObject[$key].pfxpassword

            $flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
            $leaf.Import($CertificateBinary, $CertificatePassword, $flag)

            #Add the other certificates as
            $pfxData = Get-PFXData -Password $InputObject[$key].pfxpassword -FilePath $InputObject[$key].pfxpath
            $collection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
            $pfxData.OtherCertificates | ForEach-Object {$collection.add($_) | Out-Null}
            $collection.Add($leaf)

            $pkcs12ContentType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12
            $clearBytes = $collection.Export($pkcs12ContentType)
            $fileContentEncoded = [System.Convert]::ToBase64String($clearBytes)
            $secret = ConvertTo-SecureString -String $fileContentEncoded -AsPlainText -Force
            $secretContentType = 'application/x-pkcs12'
            Set-AzureKeyVaultSecret -VaultName $vaultName -Name $key -SecretValue $Secret -ContentType $secretContentType
            $i++
        }
        catch {
            $_
        }
    }
    Set-SecurityProtocol -securityProtocol $restoreSecProtocol
}

function Set-SecurityProtocol
{
    param ([Net.SecurityProtocolType]$securityProtocol)
    $thisFunction = $MyInvocation.MyCommand.Name

    if ([Net.ServicePointManager]::SecurityProtocol -notmatch $securityProtocol)
    {
        Write-Verbose -Message ("{0} not found in current Service Point Manager. Current protocol(s): {1}. Attempting to add for session." -f $securityProtocol,[Net.ServicePointManager]::SecurityProtocol)
        try
        {
            [Net.ServicePointManager]::SecurityProtocol = $securityProtocol
            Write-Verbose -Message ("Successfully added {0} to Service Point Manager." -f $securityProtocol)
        }
        catch
        {
            Write-Error -Message ("Setting {0} failed with {1}. Script will continue with existing Security Protocol: {2}" -f $securityProtocol,$_.exception,[Net.ServicePointManager]::SecurityProtocol)
        }
    }
    else
    {
        Write-Verbose -Message ("{0} found in current Service Point Manager. No action required." -f $securityProtocol)
    }
}




# SIG # Begin signature block
# MIIjigYJKoZIhvcNAQcCoIIjezCCI3cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBLx6fxWLxiLGgf
# YgtHo27w/taFfRN5BZaSUJpyv6XGRKCCDYUwggYDMIID66ADAgECAhMzAAABUptA
# n1BWmXWIAAAAAAFSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMTkwNTAyMjEzNzQ2WhcNMjAwNTAyMjEzNzQ2WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCxp4nT9qfu9O10iJyewYXHlN+WEh79Noor9nhM6enUNbCbhX9vS+8c/3eIVazS
# YnVBTqLzW7xWN1bCcItDbsEzKEE2BswSun7J9xCaLwcGHKFr+qWUlz7hh9RcmjYS
# kOGNybOfrgj3sm0DStoK8ljwEyUVeRfMHx9E/7Ca/OEq2cXBT3L0fVnlEkfal310
# EFCLDo2BrE35NGRjG+/nnZiqKqEh5lWNk33JV8/I0fIcUKrLEmUGrv0CgC7w2cjm
# bBhBIJ+0KzSnSWingXol/3iUdBBy4QQNH767kYGunJeY08RjHMIgjJCdAoEM+2mX
# v1phaV7j+M3dNzZ/cdsz3oDfAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU3f8Aw1sW72WcJ2bo/QSYGzVrRYcw
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ1NDEzNjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AJTwROaHvogXgixWjyjvLfiRgqI2QK8GoG23eqAgNjX7V/WdUWBbs0aIC3k49cd0
# zdq+JJImixcX6UOTpz2LZPFSh23l0/Mo35wG7JXUxgO0U+5drbQht5xoMl1n7/TQ
# 4iKcmAYSAPxTq5lFnoV2+fAeljVA7O43szjs7LR09D0wFHwzZco/iE8Hlakl23ZT
# 7FnB5AfU2hwfv87y3q3a5qFiugSykILpK0/vqnlEVB0KAdQVzYULQ/U4eFEjnis3
# Js9UrAvtIhIs26445Rj3UP6U4GgOjgQonlRA+mDlsh78wFSGbASIvK+fkONUhvj8
# B8ZHNn4TFfnct+a0ZueY4f6aRPxr8beNSUKn7QW/FQmn422bE7KfnqWncsH7vbNh
# G929prVHPsaa7J22i9wyHj7m0oATXJ+YjfyoEAtd5/NyIYaE4Uu0j1EhuYUo5VaJ
# JnMaTER0qX8+/YZRWrFN/heps41XNVjiAawpbAa0fUa3R9RNBjPiBnM0gvNPorM4
# dsV2VJ8GluIQOrJlOvuCrOYDGirGnadOmQ21wPBoGFCWpK56PxzliKsy5NNmAXcE
# x7Qb9vUjY1WlYtrdwOXTpxN4slzIht69BaZlLIjLVWwqIfuNrhHKNDM9K+v7vgrI
# bf7l5/665g0gjQCDCN6Q5sxuttTAEKtJeS/pkpI+DbZ/MIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCFVswghVXAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAFSm0CfUFaZdYgAAAAA
# AVIwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEID0i
# 73sOOfgn5RMks20mXKN45PKo6HKP8rQPgrSEGaT0MEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEACe/uTDLX+zIq0zeEyfElPzucMNpY2lOqmWDg
# /JK28h+rY94ylCpnrS0Pob+04+2x4/wp/GkRwBzuL1B5gdY2JIgFddo5idVu1alt
# 3i8apYq2LF4Mk7YniHp4qziKpZo2Iw/TiV94AW2LMr8oL2Ly09ip+tK0xldLrlQN
# B3A+C5+jfcquk5J9LsJtQQdw6DQ2kMfLsoYpIeJHp1G780Je7xkVcc8sYXrdO6aT
# FVfeUCtrezOsfRCG+g1StJxQodO1zEqP/0SmKuKCRyVzt7/diIbHr+RrqR3Ehl11
# uHpfawVt5/ZmK2H08EyraCENaTDG+GhSMuh7AeXGQLfJ/80U86GCEuUwghLhBgor
# BgEEAYI3AwMBMYIS0TCCEs0GCSqGSIb3DQEHAqCCEr4wghK6AgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCBhXHvPW0PV8HDhfCnnGyeywEgE3vpW8f16
# 92d4C1g/lQIGXi8g9ZSUGBMyMDIwMDIxMjIyMTAxMS45MDdaMASAAgH0oIHQpIHN
# MIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL
# ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjozRTdBLUUzNTktQTI1RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDjwwggTxMIID2aADAgECAhMzAAABIBo529lrn63yAAAA
# AAEgMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTE5MTExMzIxNDA0MloXDTIxMDIxMTIxNDA0MlowgcoxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy
# aWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjNFN0EtRTM1
# OS1BMjVEMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIB
# IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtQTrg5qYEuZiyQz1EGhHFch+
# WxwS3OC76uYBhxeSdcj4YW4PDUhK1dHz/jeQkMHuCky1PwUTjqR5TvwVdVBjrPgW
# rxDxkYlWHYQKips1lPk1mwDlcZK56WvxSegOUOXLyDUojmm8jnx3tnTBQkD5YIAc
# 6gt8fZZ5yZFMUqXrX03I+hVQsMWFT5Oec8+DuFKZnohw4lhAaQva+uFR2WTks/PW
# ezC9ZaXpzNq/Plp2m2Sz6FNmO6fUKz80B+4L0Irv84HGGDKplSJXxUz0tYjr2WwK
# pNVOgTUYJzioqw/tPvMrozOAmP9A5WQw5S31O1YPKI7BoO6XlYfjJ2A8JcihsQID
# AQABo4IBGzCCARcwHQYDVR0OBBYEFJnZNgSw30fFH5zd6SxlAnGnIlXPMB8GA1Ud
# IwQYMBaAFNVjOlyKMZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRPME0wS6BJoEeGRWh0
# dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKG
# Pmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljVGltU3RhUENB
# XzIwMTAtMDctMDEuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUH
# AwgwDQYJKoZIhvcNAQELBQADggEBAGLpUHZxSxORQizBFD6QTUsvjgicJmO4oQBe
# CcntEkPHwydAmIbGFP7h1JfKE07l+SUDjV+kJyklP/v9F7ns0hbbmLDfrla7xiHI
# hlwij7j7YBYxSQUlucuMcbq38wLjztdsBiXVQ+CftIoReVq/kTVG1/TD4lPtKrCC
# gz7NqQNyjXVwj/ZuFi4v/hW9CjBsKF+l7QwrwR1cNql26nCLZYeCXzPby34woMEu
# s2yLe4oASji10VmjqqXiH6rAR1YrTzeUbJ3nlz6h6QTk/sArJu5ZKgwDkvfEQ1si
# XvIWQIHnlyxxe4C/5QYEG80nFmMdLYP+PoZT4o8sAAHtEgTA5JgwggZxMIIEWaAD
# AgECAgphCYEqAAAAAAACMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBD
# ZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0xMDA3MDEyMTM2NTVaFw0yNTA3
# MDEyMTQ2NTVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIIBIjANBgkq
# hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0NvHcRijog7PwTl/X6f2mUa3RUENWl
# CgCChfvtfGhLLF/Fw+Vhwna3PmYrW/AVUycEMR9BGxqVHc4JE458YTBZsTBED/Fg
# iIRUQwzXTbg4CLNC3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhgRvJYR4YyhB50YWeR
# X4FUsc+TTJLBxKZd0WETbijGGvmGgLvfYfxGwScdJGcSchohiq9LZIlQYrFd/Xcf
# PfBXday9ikJNQFHRD5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajyeioKMfDaTgaRtogI
# Neh4HLDpmc085y9Euqf03GS9pAHBIAmTeM38vMDJRF1eFpwBBU8iTQIDAQABo4IB
# 5jCCAeIwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVjOlyKMZDzQ3t8RhvF
# M2hahW1VMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAP
# BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjE
# MFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kv
# Y3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEF
# BQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w
# a2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MIGgBgNVHSABAf8E
# gZUwgZIwgY8GCSsGAQQBgjcuAzCBgTA9BggrBgEFBQcCARYxaHR0cDovL3d3dy5t
# aWNyb3NvZnQuY29tL1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0bTBABggrBgEFBQcC
# AjA0HjIgHQBMAGUAZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMAdABhAHQAZQBtAGUA
# bgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCYP4FxAz2do6Ehb7Pr
# psz1Mb7PBeKp/vpXbRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1rVFcIK1GCRBL7uVOM
# zPRgEop2zEBAQZvcXBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3fVo/HPKZeUqRUgCv
# OA8X9S95gWXZqbVr5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2/QThcJ8ySif9Va8v
# /rbljjO7Yl+a21dA6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFjnXshbcOco6I8+n99
# lmqQeKZt0uGc+R38ONiU9MalCpaGpL2eGq4EQoO4tYCbIjggtSXlZOz39L9+Y1kl
# D3ouOVd2onGqBooPiRa6YacRy5rYDkeagMXQzafQ732D8OE7cQnfXXSYIghh2rBQ
# Hm+98eEA3+cxB6STOvdlR3jo+KhIq/fecn5ha293qYHLpwmsObvsxsvYgrRyzR30
# uIUBHoD7G4kqVDmyW9rIDVWZeodzOwjmmC3qjeAzLhIp9cAvVCch98isTtoouLGp
# 25ayp0Kiyc8ZQU3ghvkqmqMRZjDTu3QyS99je/WZii8bxyGvWbWu3EQ8l1Bx16HS
# xVXjad5XwdHeMMD9zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA12u8JJxzVs341Hgi6
# 2jbb01+P3nSISRKhggLOMIICNwIBATCB+KGB0KSBzTCByjELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJp
# Y2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0U3QS1FMzU5
# LUEyNUQxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoB
# ATAHBgUrDgMCGgMVAL9a/LhcpMcNjYNHvPBmpZcTZ15MoIGDMIGApH4wfDELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDh7rczMCIY
# DzIwMjAwMjEzMDE0MTA3WhgPMjAyMDAyMTQwMTQxMDdaMHcwPQYKKwYBBAGEWQoE
# ATEvMC0wCgIFAOHutzMCAQAwCgIBAAICILUCAf8wBwIBAAICEbEwCgIFAOHwCLMC
# AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK
# MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQCw7iMfyZs7qRQ/y29AZ5e1w0sM
# yRotgCiJgEnsTrIadvN9/h5wOzq6hrdpluYkqHN1ne1fcoGGHPhvnM3W6nHsek+S
# vJVQCT21iDPlW0hvRMO/yNmsiP6PUQn5SDtubnlQ6Sk278uPvzFb3QJw0tx/iROD
# pOdtw2LzYMHas+nvjzGCAw0wggMJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAABIBo529lrn63yAAAAAAEgMA0GCWCGSAFlAwQCAQUAoIIB
# SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIBz2
# vqwiJvWrqxFXOa2mXbimDlCRWDCqOGdALua9DSw+MIH6BgsqhkiG9w0BCRACLzGB
# 6jCB5zCB5DCBvQQgHAxQaxScdtf1UeE7BZLSQUmEIBz+xspjrfdonmfAGlUwgZgw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAASAaOdvZa5+t
# 8gAAAAABIDAiBCBFWyvwTtUCgdmaN0QA0rJf7kzCKXZT/D0ig2g4VCcfcDANBgkq
# hkiG9w0BAQsFAASCAQBj6/lE/sRwohHi1GDgkZx4ffpoucJd/QpzZkaNUJQ2SrDz
# uHjenPYmKeRs12fwwc5g1D+et5YQ1X1HcTDEUOluAc0BRATdZl0C4Ek/zXB8KI+s
# h6H70R+Myg/o62FAjwuUdB9jYos7nx6ap+1KzJyB7+LwCS3SnZGqq5XHkzQiKXZz
# LIYuJvnf46IexexsPOQCI+VkMMY0hQ8wClgB6t9Bgodc945w2pWJreE5uQrzox1g
# o6BfA/JgJMGve0zFPp6DBOOITPpCSB5d6R/1CsnDT5saiEOX2oVbKQApQINnkLQW
# twhRx9dXWS62d98cW2yRhvQk2MVBDacg8r0pnMco
# SIG # End signature block