Get-AzureADWHfBKeys.ps1

#
# Get-AzureADWHfBKeys.ps1
#
# Copyright 2019 Microsoft. All rights reserved.
#

<#
    Get-AzureADWHfBKeys
#>

function Get-AzureADWHfBKeys
{
    <#.SYNOPSIS
        Reads Windows Hello for Business (WHfB) keys from Azure Active Directory.
 
    .DESCRIPTION
        Reads Windows Hello for Business (WHfB) keys from Azure Active Directory. Two modes
        are supported: read all keys from the current tenant or all keys from a specific user.
 
    .PARAMETER Tenant
        Specify this parameter to read all keys from the current tenant.
 
    .PARAMETER UserPrincipalName
        Optional. Specify this parameter to read all keys from a specific user. This parameter
        cannot be combined with -All.
 
    .PARAMETER SkipCheckForOrphanedKeys
        Optional. This switch is to suppress check for each key to see if it is linked to a valid
        device object. This option requires additional network queries to check therefore increasing
        run time of the command.
 
    .PARAMETER BatchSize
        Optional. Specify to override the default query batch size.
 
    .PARAMETER Logging
        Optional. This switch will enable logging to a file
 
    .PARAMETER All
        Optional. This switch will cause a scan of all users in the tenant. This parameter
        cannot be combined with -UserPrincipalName.
 
    .PARAMETER Report
        Optional. This switch will cause a summary report to be printed to console and log.
 
    .INPUTS
        None. You cannot pipe objects to Get-AzureADWHfBKeys.
 
    .OUTPUTS
        Custom type representing WHfB key. Get-AzureADWHfBKeys returns one or more based on results.
 
    .EXAMPLE
        PS C:\>Get-AzureADWHfBKeys -Tenant contoso.com -UserPrincipalName user@contoso.com -Report | Out-Null
 
        Scan WHfB keys for a single user in Azure AD tenant and display summary report.
 
    .EXAMPLE
        PS C:\>Get-AzureADWHfBKeys -Tenant contoso.com -Logging -Report -All | Out-Null
 
        Scan all WHfB keys in Azure AD tenant and maintain a log file (WHfBTools-<Timestamp>.log).
 
    .EXAMPLE
        PS C:\>Get-AzureADWHfBKeys -Tenant contoso.com -SkipCheckForOrphanedKeys -Logging -Report -All | Out-Null
 
        Scan all WHfB keys in Azure AD tenant and maintain a log file (WHfBTools-<Timestamp>.log).
        Suppresses extra check for orphaned keys which improves runtime. Orphaned keys are no longer usable because the
        associated device object has been deleted from Azure Active Directory.
 
    .EXAMPLE
        PS C:\>Get-AzureADWHfBKeys -Tenant contoso.com -report -all | Export-Csv "contoso_whfb_keys.csv"
 
        Scan all WHfB keys in Azure AD tenant and export them to a CSV file.
    #>


    [CmdletBinding()]
    param
        (
        [Parameter(Mandatory=$true)]
        $Tenant,
        [Parameter(Mandatory=$false)]
        $UserPrincipalName,
        [Parameter(Mandatory=$false)]
        [Switch]
        $SkipCheckForOrphanedKeys,
        [Parameter(Mandatory=$false)]
        $BatchSize,
        [Parameter(Mandatory=$false)]
        [Switch]
        $logging,
        [Parameter(Mandatory=$false)]
        [Switch]
        $All,
        [Parameter(Mandatory=$false)]
        [Switch]
        $Report
        )
    Begin
    {
        InitializeAzureAD -Tenant $Tenant -UserPrincipalName $UserPrincipalName

        # Setup counters
        $script:totalAzureADWHfBKeys = 0
        $script:totalAzureADOrphanedKeys = 0
        $script:totalAzureADUsersWithWHfBKeys = 0
        $script:totalAzureADUsers = 0
        $script:totalAzureADRocaVulnerableKeys = 0
        $script:totalADUsersWithWHfBKeysOnMobile = 0
    }

    Process
    {
        $script:enableLogging = 0
        $script:authReady = 0
        $script:tenantTracker = $Tenant
        $script:skipCheckOrphaned = $false

        if ($logging -eq $true)
        {
            $script:enableLogging = 1
            DiagLog "Started Get-AzureADtotalAzureADWHfBKeys." -logOnly
        }

        if ($SkipCheckForOrphanedKeys -eq $true) { $script:skipCheckOrphaned = $true }
        if ($report -eq $true) { $script:reporting = 1 }
        if($BatchSize -eq $null) {$BatchSize=100}

        # Parameter checking
        if ($All -ne $true -and $UserPrincipalName -eq $null)
        {
            write-host "Parameter '-UserPrincipalName' must be set if -All not $true" -foregroundcolor red
            return;
        }

        if ($All -eq $true -and $UserPrincipalName -ne $null)
        {
            write-host "Parameter '-UserPrincipalName' must not be set if -All is $true" -foregroundcolor red
            return;
        }

        # Get the authorization token
        Initialize-AuthenticationAzureAD -Tenant $Tenant -UserPrincipalName $UserPrincipalName

        # Initial URI Construction, override this filter for advanced scoping of the user query
        $Searchfilter ="`$filter=accountEnabled eq true"

        # Pick search filter based on single or All users
        if ($All -eq $true)
        {
            $uri = "https://graph.windows.net/$Tenant/users/?`$top=$BatchSize&$($Searchfilter)&api-version=1.6-internal"
        }
        else
        {
            $uri = "https://graph.windows.net/$Tenant/users?`$filter=startswith(userPrincipalName, '" + $UserPrincipalName + "')&api-version=1.6-internal"
        }

        # Initial query for first page of results
        $query = MakeAzureADGraphRequest -Uri $uri -Method Get

        if ($logging)
        {
            $output = "Query result: " + $query
            DiagLog $output -logOnly
        }

        # Setup result tracking
        $moreObjects = $query

        # We're processing all users or a specified user
        if ($All)
        {
            Update-GetAzureADWHfBKeysProgress
            Get-FreshTokenIfNeeded

            $keys = Get-UserFromQuery -query $query -tenant $Tenant |
                Get-KeysForUser -tenant $Tenant |
                    Get-KeyMetadataForSingleKey -tenant $Tenant

            if ($keys)
            {
                Get-OrphanedStatus -Keys $keys
            }
        }
        else
        {
            # Targeting a single user
            $keys = Get-KeysForUser -user $query.value -tenant $Tenant |
                Get-KeyMetadataForSingleKey -tenant $tenant
            
            if ($keys)
            {
                Get-OrphanedStatus -Keys $keys
            }
        }

        # Get all the remaining objects in batches
        if ($All -eq $true -and $query.'odata.nextLink'){
            $moreObjects.'odata.nextLink' = $query.'odata.nextLink'

            do
            {
                $pageFragment = $moreObjects.'odata.nextLink'
                $uriPaged = "https://graph.windows.net/$($script:Tenant)/$pageFragment&`$top=$BatchSize&api-version=1.6-internal"

                Get-FreshTokenIfNeeded
                $moreObjects = MakeAzureADGraphRequest -Uri $uriPaged -Method Get

                $keys = Get-UserFromQuery -query $moreObjects -tenant $Tenant |
                    Get-KeysForUser -tenant $Tenant |
                        Get-KeyMetadataForSingleKey -tenant $Tenant

                Get-FreshTokenIfNeeded

                if ($keys)
                {
                    Get-OrphanedStatus -Keys $keys
                }

            } while ($moreObjects.'odata.nextLink')
        }

        if ($reporting)
        {
            DiagLog "Report of summary results:" -foregroundcolor yellow
            DiagLog "Users scanned: $script:totalAzureADUsers" -foregroundcolor yellow
            DiagLog "Users with WHfB keys: $script:totalAzureADUsersWithWHfBKeys" -foregroundcolor yellow
            DiagLog "Total WHfB Keys: $script:totalAzureADWHfBKeys" -foregroundcolor yellow
            DiagLog "Total ROCA vulnerable keys: $script:totalAzureADRocaVulnerableKeys" -foregroundcolor yellow

            if ($script:skipCheckOrphaned -eq $false)
            {
                DiagLog "Total orphaned keys: $script:totalAzureADOrphanedKeys" -foregroundcolor yellow
            }
            #DiagLog "Total users with WHfB keys on mobile: $script:totalADUsersWithWHfBKeysOnMobile" -foregroundcolor yellow
        }

        clear-variable -Name tenantTracker -Force
    }
}

<#
    Get-OrphanedStatus
#>

function Get-OrphanedStatus
{
    <#.SYNOPSIS
        Invokes concurrent jobs to query for devices associated with keys
 
    .DESCRIPTION
        For every key query AzureAD for existence of the device.
 
    .PARAMETER Keys
        Specifies the key metadata objects.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [psObject[]]$Keys
        )

    if ($script:skipCheckOrphaned -eq $false)
    {
        Invoke-Async -Set $Keys -SetParam KeyMetadata `
            -Params @{"AuthorizationHeader" = $script:authHeader} `
            -CmdLet DoesDeviceObjectExistAzureAd `
            -ThreadCount $script:logicalCores
    }
}

<#
    Get-UserFromQuery
#>

function Get-UserFromQuery
{
    <#.SYNOPSIS
        Scans all users in the query result.
 
    .DESCRIPTION
        Reads Windows Hello for Business (WHfB) keys from the Azure Active Directory query response.
 
    .PARAMETER query
        Specifies the query results from an Azure Active Directory query.
 
    .PARAMETER tenant
        Specifies the tenant associated with the request.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        $query,
        [Parameter(Mandatory=$true)]
        $tenant
        )
    Process
    {
        $userHasWHfB = 0
        $userHasWHfBOnMobile = 0
        $usersWithKeys += 1
        $batchtotalAzureADWHfBKeys = 0

        foreach ($user in $query.value)
        {
            Write-Output $user
        }

        if ($logging)
        {
            DiagLog "Total scanned: $script:totalAzureADUsers - Batch contained $($query.value.count) users. Total WHfB ROCA vulnerable keys: $script:totalAzureADRocaVulnerableKeys. Total orphaned WHfB keys: $script:totalAzureADOrphanedKeys"
        }
    }
}

<#
    Update-GetAzureADWHfBKeysProgress
#>

function Update-GetAzureADWHfBKeysProgress
{
    <#.SYNOPSIS
        Updates the progress bar with current status.
 
    .DESCRIPTION
        Updates the progress bar with current status.
 
    #>


    $activity = "Get-AzureADWHfBKeys - " + $script:stopwatch.Elapsed.ToString("dd\:hh\:mm\:ss")
    $progressDescription = "Scanning AzureAD and Compiling Statistics"
    if ($script:skipCheckOrphaned -eq $false)
    {
        $progressDescription = $progressDescription + " | # of Orphaned keys"
    }

    $progressResults = "$script:totalAzureADUsers users scanned | $script:totalAzureADWHfBKeys WHfB keys found | $script:totalAzureADRocaVulnerableKeys ROCA vulnerable keys | $script:totalAzureADOrphanedKeys orphaned keys"
    if ($script:skipCheckOrphaned -eq $false)
    {
        $progressResults = $progressResults + " | $script:totalAzureADOrphanedKeys"
    }

    Write-Progress -Id 1 -ParentId -1 -Activity $activity -Status $progressDescription -PercentComplete -1 -CurrentOperation $progressResults
}

<#
    Get-KeysForUser
#>

function Get-KeysForUser
{
    <#.SYNOPSIS
        Scans the Hello for Business (WHfB) keys from a user query result.
 
    .DESCRIPTION
        Reads Windows Hello for Business (WHfB) keys for the user data and emits a
        custom object representing each key and its metadata into the pipeline.
 
    .PARAMETER user
        Specifies the user query result containing the user object data read from
        Azure Active Directory
 
    .PARAMETER tenant
        Specifies the tenant associated with the request.
    #>


    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        $user,
        [Parameter(Mandatory=$true)]
        $tenant
        )

    Process
    {
        $userHasWHfB = 0
        $userHasWHfBOnMobile = 0

        if ($script:enableLogging)
        {
            DiagLog "$script:totalAzureADUsers) Processing user: $($user.userPrincipalName)" -logOnly
        }

        # Ignore any B2B users as their credentials are in home tenant
        if ($user.UserPrincipalName.Contains("#EXT#"))
        {
            return;
        }

        foreach ($key in $user.searchableDeviceKey)
        {
            if ($key.usage -ne "NGC") { continue }
            if ($userHasWHfB -eq 0) { $userHasWHfB += 1 }

            $rawKeyAndUpn = New-Object -TypeName psObject
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name rawKey -Value $key.value__
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name userPrincipalName -Value $user.UserPrincipalName
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name KeyMaterial -Value $key.keyMaterial
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name Tenant -Value $tenant
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name Usage -Value $key.usage
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name KeyIdentifier -Value $key.keyIdentifier
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name CreationTime -Value $key.creationTime
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name CustomKeyInformation -Value $key.customKeyInformation
            $rawKeyAndUpn | Add-Member -MemberType NoteProperty -Name DeviceId -Value $key.deviceId

            Write-Output $rawKeyAndUpn
        }

        $script:totalAzureADUsers += 1
        $script:totalAzureADUsersWithWHfBKeys += $userHasWHfB

        # SupportsNotify
        $script:totalADUsersWithWHfBKeysOnMobile += $userHasWHfBOnMobile
    }
}

<#
    Get-KeyMetadataForSingleKey
#>

function Get-KeyMetadataForSingleKey
{
    <#.SYNOPSIS
        Scans the Hello for Business (WHfB) keys from a user query result.
 
    .DESCRIPTION
        Reads Windows Hello for Business (WHfB) keys for the user data and emits a
        custom object representing each key and its metadata into the pipeline.
 
    .PARAMETER user
        Specifies the user query result containing the user object data read from
        Azure Active Directory
 
    .PARAMETER tenant
        Specifies the tenant associated with the request.
    #>


    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [psObject]$rawKeyAndUpn,
        [Parameter(Mandatory=$true)]
        $tenant
        )

    Process
    {
        $userHasWHfB = 0
        $batchtotalAzureADWHfBKeys += 1
        $script:totalAzureADWHfBKeys += 1

        # Build up custom key metadata object
        $keyObject = New-Object -TypeName psobject
        $keyObject | Add-Member -MemberType NoteProperty -Name userPrincipalName -Value $rawKeyAndUpn.UserPrincipalName
        $keyObject | Add-Member -MemberType NoteProperty -Name Tenant -Value $tenant
        $keyObject | Add-Member -MemberType NoteProperty -Name DeviceId -Value $rawKeyAndUpn.deviceId
        $keyObject | Add-Member -MemberType NoteProperty -Name Usage -Value $rawKeyAndUpn.usage
        $keyObject | Add-Member -MemberType NoteProperty -Name Id -Value $rawKeyAndUpn.keyIdentifier
        $keyObject | Add-Member -MemberType NoteProperty -Name CreationTime -Value $rawKeyAndUpn.creationTime

        # SupportsNotify: indicates mobile device
        # Get the byte array for custom key info
        $bytes = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($rawKeyAndUpn.customKeyInformation))

        # Fourth byte determines if supports notification
        if ($bytes[3] -eq 1)
        {
            if ($userHasWHfBOnMobile -eq 0) { $userHasWHfBOnMobile += 1 }
        }

        # Capture supports notification on the custom object
        if ($userHasWHfBOnMobile -eq 1)
        {
            #$keyObject | Add-Member -MemberType NoteProperty -Name SupportsNotify -Value $true
        }
        else
        {
            #$keyObject | Add-Member -MemberType NoteProperty -Name SupportsNotify -Value $false
        }

        # Test for ROCA vulnerability
        [byte[]]$keyBytes = [System.Convert]::FromBase64String($rawKeyAndUpn.keyMaterial)

        $rocaVulnerable = Probe-KeyForRocaVulnerability -keyBytes ($keyBytes)

        $keyObject | Add-Member -MemberType NoteProperty -Name RocaVulnerable -Value $rocaVulnerable

        if ($rocaVulnerable)
        {
            $script:totalAzureADRocaVulnerableKeys += 1

            if ($logging)
            {
                DiagLog "Key is vulnerable to ROCA: $keyObject.Id" -logOnly
            }
        }

        Update-GetAzureADWHfBKeysProgress

        # Emit keyObject in pipeline
        Write-Output $keyObject
    }
}

# SIG # Begin signature block
# MIIjhgYJKoZIhvcNAQcCoIIjdzCCI3MCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCpOdnh+f3r3wvP
# bKgDO8JjWm2bRRztZNNeebH0ecI5k6CCDYEwggX/MIID56ADAgECAhMzAAABUZ6N
# j0Bxow5BAAAAAAFRMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMTkwNTAyMjEzNzQ2WhcNMjAwNTAyMjEzNzQ2WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCVWsaGaUcdNB7xVcNmdfZiVBhYFGcn8KMqxgNIvOZWNH9JYQLuhHhmJ5RWISy1
# oey3zTuxqLbkHAdmbeU8NFMo49Pv71MgIS9IG/EtqwOH7upan+lIq6NOcw5fO6Os
# +12R0Q28MzGn+3y7F2mKDnopVu0sEufy453gxz16M8bAw4+QXuv7+fR9WzRJ2CpU
# 62wQKYiFQMfew6Vh5fuPoXloN3k6+Qlz7zgcT4YRmxzx7jMVpP/uvK6sZcBxQ3Wg
# B/WkyXHgxaY19IAzLq2QiPiX2YryiR5EsYBq35BP7U15DlZtpSs2wIYTkkDBxhPJ
# IDJgowZu5GyhHdqrst3OjkSRAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUV4Iarkq57esagu6FUBb270Zijc8w
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDU0MTM1MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAWg+A
# rS4Anq7KrogslIQnoMHSXUPr/RqOIhJX+32ObuY3MFvdlRElbSsSJxrRy/OCCZdS
# se+f2AqQ+F/2aYwBDmUQbeMB8n0pYLZnOPifqe78RBH2fVZsvXxyfizbHubWWoUf
# NW/FJlZlLXwJmF3BoL8E2p09K3hagwz/otcKtQ1+Q4+DaOYXWleqJrJUsnHs9UiL
# crVF0leL/Q1V5bshob2OTlZq0qzSdrMDLWdhyrUOxnZ+ojZ7UdTY4VnCuogbZ9Zs
# 9syJbg7ZUS9SVgYkowRsWv5jV4lbqTD+tG4FzhOwcRQwdb6A8zp2Nnd+s7VdCuYF
# sGgI41ucD8oxVfcAMjF9YX5N2s4mltkqnUe3/htVrnxKKDAwSYliaux2L7gKw+bD
# 1kEZ/5ozLRnJ3jjDkomTrPctokY/KaZ1qub0NUnmOKH+3xUK/plWJK8BOQYuU7gK
# YH7Yy9WSKNlP7pKj6i417+3Na/frInjnBkKRCJ/eYTvBH+s5guezpfQWtU4bNo/j
# 8Qw2vpTQ9w7flhH78Rmwd319+YTmhv7TcxDbWlyteaj4RK2wk3pY1oSz2JPE5PNu
# Nmd9Gmf6oePZgy7Ii9JLLq8SnULV7b+IP0UXRY9q+GdRjM2AEX6msZvvPCIoG0aY
# HQu9wZsKEK2jqvWi8/xdeeeSI9FN6K1w4oVQM4Mwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVWzCCFVcCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAVGejY9AcaMOQQAAAAABUTAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgubro2YVw
# YO67leFZ9IllYovp0bMUsRbC8Z4IrV7TjXMwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQApi4FByBZYd1/xDpsavGQ58dcTMg8R0rHvs7qT/xQT
# bSPl5Yvhrul7j6ZEMXkvfyTIEJwOpLh8fIAU/suAeBx+eKDGDRzneS89ET6hWo7x
# JUTLkfwcAr6PExKGXUr7ajBBu8rds8ELY62SehK8pOiX/xptf7yH75Kle4hDDc7s
# EjSnhfUSq3ScIEfUEMUG+995GpN1YkAGbeEpImx3TL0WGymm2zDEwf5sqCTIU24h
# 0Omutru7kGH6ncR5Y+on4xbceasbDXj+fwKc5Wyk3Tpael8Pm28u1d+Gu4UqVKp5
# AVjLld4hLATLHyI+CxYRQOOArAtF/NAdAZWuWl240acnoYIS5TCCEuEGCisGAQQB
# gjcDAwExghLRMIISzQYJKoZIhvcNAQcCoIISvjCCEroCAQMxDzANBglghkgBZQME
# AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEICpNInK2ArKSWYsZAkBxe5t+UUCuFeWwS4SBHX0S
# jlg4AgZdtfNcni8YEzIwMTkxMjA0MTQxMjEzLjA1MlowBIACAfSggdCkgc0wgcox
# CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg
# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
# RVNOOjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
# cCBTZXJ2aWNloIIOPDCCBPEwggPZoAMCAQICEzMAAAEL5Pm+j29MHdAAAAAAAQsw
# DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN
# MTkxMDIzMjMxOTE1WhcNMjEwMTIxMjMxOTE1WjCByjELMAkGA1UEBhMCVVMxCzAJ
# BgNVBAgTAldBMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlv
# bnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0JENC00QjgwLTY5
# QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0G
# CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXAtWdRjFBuM+D2nhUKLVuWv9cZcq1
# /8emykQBplDii8DqwwCNnD0zJhz7n94WtWjFsc5KL/dF8gKWTMRH5MVTa5dxCJu6
# VtZobc+sztM+0JPM5Vmcb/7D+AlFERGAkQGGxO/Z4fxHH1/EcZ/iwUimzafXjBOl
# IQ3RSxUAj980liuAyNCrj8JdunGR3nVSRvxJtWpUZvlIUrYY4LDmJJsFsI8gsch3
# LrchmPeBkoxsvy7RpKhcOQtTYacD48vz7fzT2ciciJqAXxZt7fth8sgqKiUURCVu
# SlcUKXBXm/1dcYCKqOoUz2YGu2i0t4K/X17JWZ5jdN1vxqzSQa9P4PHxAgMBAAGj
# ggEbMIIBFzAdBgNVHQ4EFgQUrR/Z6h2KHpzgmA1QRGX/921e3u8wHwYDVR0jBBgw
# FoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDov
# L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENB
# XzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAx
# MC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDAN
# BgkqhkiG9w0BAQsFAAOCAQEAJuijnanvNrS63e87CK0gwImI8C4JdhxLLPnA6m/p
# USXWel9KCa3t95NRNO36NgemDxhskz7rVHiUigb1pJdm+TB5Shg2DlPi1UhdCTaN
# 5lTWZ+rHAFfDI4i2gdKOwdyug73m5ja2dqfDTl2Di5axwcBgDvGsZLfBm+aGut2v
# UGBBg1QjMKfqQGqMJCYwXPGdHmwRN1UN5MpORBkTmk2DEWWjRm0LKQ1/eV4KYiU5
# cV4GC0/8/q/X71wbrwdyH2Zyvh2mIOE+4T9mZc7H0CzZ8QdqTHd2xbTT1GSNReeY
# YlnTkWlCiELjYkInHUfwumC1pCuZMf4ITNw7KjeOGPyKDTCCBnEwggRZoAMCAQIC
# CmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRp
# ZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoXDTI1MDcwMTIx
# NDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRrdFQQ1aUKAIKF
# ++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmxMEQP8WCIhFRD
# DNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKEHnRhZ5FfgVSx
# z5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBisV39dx898Fd1
# rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpOBpG2iAg16Hgc
# sOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMBAAGjggHmMIIB
# 4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPNDe3xGG8UzaFqF
# bVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
# EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYD
# VR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwv
# cHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEB
# BE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j
# ZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1UdIAEB/wSBlTCB
# kjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsGAQUFBwICMDQe
# MiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABlAG0AZQBuAHQA
# LiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2joSFvs+umzPUx
# vs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJEEvu5U4zM9GAS
# inbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5SpFSAK84Dxf1
# L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJKJ/1Vry/+tuWO
# M7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yjojz6f32WapB4
# pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0v35jWSUPei45
# V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgiCGHasFAeb73x
# 4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iCtHLNHfS4hQEe
# gPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO2ii4sanblrKn
# QqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyXUHHXodLFVeNp
# 3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWzfjUeCLraNtvT
# X4/edIhJEqGCAs4wggI3AgEBMIH4oYHQpIHNMIHKMQswCQYDVQQGEwJVUzELMAkG
# A1UECBMCV0ExEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9u
# cyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozQkQ0LTRCODAtNjlD
# MzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcG
# BSsOAwIaAxUA8f35HTFqU9zwihI9ktmsPgpwMFKggYMwgYCkfjB8MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg
# VGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOGR4XMwIhgPMjAx
# OTEyMDQxNTQwMzVaGA8yMDE5MTIwNTE1NDAzNVowdzA9BgorBgEEAYRZCgQBMS8w
# LTAKAgUA4ZHhcwIBADAKAgEAAgIQoAIB/zAHAgEAAgIR2zAKAgUA4ZMy8wIBADA2
# BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIB
# AAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAJJdEswLZBU3tU9cOxF1QLqN61QQHoLf
# OMghOGfkj/5w0DPJHdoy6cL0GL+AqcWEDTnxTUpLQ+XBjLVj4xLKZQYQY3RxDasW
# 7vU4F5caypgQUezDFYoZWkmYlsMnRAXsS1neiWKGkDSE6ZtQvrNQkn1vGGaGaEEd
# cN8GZk8FgUSsMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAEL5Pm+j29MHdAAAAAAAQswDQYJYIZIAWUDBAIBBQCgggFKMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgjUKblpSV
# QMaD5ODwSViF7Hb3fU5LMUThDH+d6/HtuuEwgfoGCyqGSIb3DQEJEAIvMYHqMIHn
# MIHkMIG9BCA0j9DOIFM+OiSX8XAkXAXivRR0LPHA6cVU/ATAE1xziDCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABC+T5vo9vTB3QAAAA
# AAELMCIEIGaxKp9k22vwtngFQY2A7X7Jm6KMwHIlg3jxwzw8fXJPMA0GCSqGSIb3
# DQEBCwUABIIBAAOR7ifMAuUmBnUzVeL5TebOgvcYS2+B/HUgBfcAud9gzTfKzl03
# awex6I1+jKNrzflHSfLQ9TDwzr/sBAqEylSge7vNsFJpM3UWozRaO7hIRa817TMc
# fh4LXJb9vsmBykMAG8MgO2uNRZM/UbJ7M2SxMm644qHtCeERaeDVOumWyYyBfRhU
# GABkadLKgSxpi+hq+t/3azppyWRVc5CUywloDqH0Z4dSqPBmmAnDXbUDGas/jFwq
# 6Hpw+s+hZXsuMVffMoIzjwJ9vDnaa0OeyJvcZrvfwhR3y+/krQlNT3MPVKJgmJ0B
# +EGFIQgXYm0f9JSSLEuVH3yv1EwpubYCz+k=
# SIG # End signature block