AffectedKeyCredentials.psm1

################################################################################################
# This script scans all key credentials in all apps/serviceprincipals in the specified tenant #
# for credentials with property `hasExtendedValue == true` by calling Microsoft Graph #
# #
# Default output file path is at: $PWD\KeyCredentialsWithExtendedValue.tsv #
# See sample usage at the end of this script #
################################################################################################

Set-StrictMode -Version 2.0
$IncludeExtendedProperties = $false

Function Get-MSGraphEndpoint
{
    param(
        [string]
        $Env
    )

    switch ($Env)
    {
        "AzureCloud" { return "https://graph.microsoft.com" }
        "AzureChinaCloud" { return "https://microsoftgraph.chinacloudapi.cn" }
        "AzureUSGovernment" { return "https://graph.microsoft.us" }
        default { throw "$($Env) is not a valid cloud environment." }
    }
}

# Validate page size to be below the maximum page size
Function Validate-PageSize
{
    param(
        [int]
        $PageSize,

        [int]
        $MaxPageSize
    )

    if ($PageSize -le 0 -or $PageSize -gt $MaxPageSize)
    {
        throw "$($PageSize) must be an integer between 0 and $($MaxPageSize)"
    }
}

# Remove unnecessary new line characters and whitespace in url
Function Trim-Url
{
    param(
        [string]
        $Url
    )

    return $Url -replace '`n','' -replace '\s+', ''
}

Function Generate-Url
{
    param(
        [string]
        $ObjectClass,

        [string]
        $AppId,

        [string]
        $ObjectId,

        [string]
        $Resource,

        [int]
        $PageSize
    )

    $selectValues = "displayName,id,appId,keyCredentials"
    if ($IncludeExtendedProperties)
    {
        $selectValues += ",createdDateTime,identifierUris,signInAudience,web&`$expand=owners(`$select=id,userPrincipalName)"
    }

    $filterValues = ""

    if ($AppId)
    {
        $filterValues = "&filter=appId eq '$($AppId)'"
    }
    elseif ($ObjectId)
    {
        $filterValues = "&filter=id eq '$($ObjectId)'"
    }

    $url = "$($Resource)/beta/myorganization/$($ObjectClass)s?`$select=$($selectValues)&`$top=$($PageSize)$($filterValues)"

    Write-Verbose "Url Generated: $($url)"

    return $url
}

Function Parse-Owners
{
    param(
        [array]
        $Owners
    )

    $result = @()

    foreach ($owner in $Owners)
    {
        $ownerStr = "$($owner.id),$($owner.userPrincipalName)"
        $result += $ownerStr
    }

    return $result -Join ";"
}

Function Get-TotalObjectCount
{
    param(
        [string]
        $AccessToken,

        [string]
        $ObjectClass,

        [string]
        $Resource
    )

    $authHeader = @{
      "Authorization" = "Bearer " + $AccessToken
      "ConsistencyLevel" = "eventual"
    }

    $url = "$($Resource)/beta/myorganization/$($ObjectClass)s/`$count"
    Write-Verbose "GET $($url)"
    try
    {
        $totalObjectCount = Invoke-RestMethod -Uri $url -Headers $authHeader -Method "GET" -Verbose:$false -ErrorAction Stop
    }
    catch
    {
        throw
    }

    Write-Verbose "Total $($ObjectClass) count: $($totalObjectCount)"

    return $totalObjectCount
}

# Make MS Graph request with retry and exponential backoff
Function Make-MSGraphRequest
{
    param(
        [string]
        $Url,

        [string]
        $ObjectClass,

        [string]
        $AccessToken,

        [int]
        $MaxRetryLimit,

        [int]
        $flatMinSeconds = 10
    )

    $authHeader = @{
      "Authorization" = "Bearer " + $AccessToken
    }

    for ($i=1; $i -le $MaxRetryLimit; $i+=1)
    {
        try
        {
            Write-Verbose "GET $($Url)"
            $result = Invoke-RestMethod -Uri $Url -Headers $authHeader -Method "GET" -Verbose:$false
            break
        }
        catch
        {
            if ($_.Exception.Response.StatusCode.value__ -eq 429)
            {
                # Sleep then retry (Exponential backoff)
                $sleepDuration = [Math]::Pow(2,$i) + $flatMinSeconds
                Write-Verbose "Retry after sleeping for $($sleepDuration) seconds"
                Start-Sleep -s $sleepDuration
                continue
            }

            if ($_.Exception.Response.StatusCode.value__ -eq 404)
            {
                if ($AppId)
                {
                    throw "$($ObjectClass) with AppId: $($AppId) not found"
                }

                if ($ObjectId)
                {
                    throw "$($ObjectClass) with ObjectId: $($ObjectId) not found"
                }
            }

            Write-Warning "Unexpected Error. Try again later with -SkipTokenUrl '$($Url)'"
            throw
        }
    }

    if ($i -gt $MaxRetryLimit)
    {
        $Url = Trim-Url -Url $Url
        throw "Max backoff retry limit reached. Try again later with -SkipTokenUrl '$($Url)'"
    }

    return $result
}

# Main: Get all affected key credentials for the given object class in tenant
Function Get-AffectedKeyCredentials
{
    [CmdletBinding(HelpURI="https://aka.ms/aad-key-cred-scanner")]
    param(
        # The tenant ID or a verified domain where the test should happen.
        [Parameter(Mandatory = $true,
                   HelpMessage = "Tenant Id (Guid) to search")]
        [guid]
        $TenantId,

        # The cloud environment
        [ValidateSet("AzureCloud", "AzureChinaCloud", "AzureUSGovernment")]
        [Parameter(Mandatory = $false,
                   HelpMessage = "Cloud environment name. 'AzureCloud' by default")]
        [string]
        $Env = "AzureCloud",

        # The directory object class
        [ValidateSet("application", "servicePrincipal")]
        [Parameter(Mandatory = $true,
                   HelpMessage = "The object class. Either Application or ServicePrincipal")]
        [string]
        $ObjectClass,

        # The application id (for singular GET)
        [Parameter(Mandatory = $false,
                   HelpMessage = "The application id or application principal id of the app/sp object to query")]
        [guid]
        $AppId,

        # The object id (for singular GET)
        [Parameter(Mandatory = $false,
                   HelpMessage = "The object id of the app/sp object to query")]
        [guid]
        $ObjectId,

        # The url with skip token to continue from if necessary
        [Parameter(Mandatory = $false,
                   HelpMessage = "The given ms graph url containing skip token to continue from")]
        [string]
        $SkipTokenUrl = $null,

        # The toggle for simple/verbose output
        [Parameter(Mandatory = $false,
                   HelpMessage = "Toggle for additional object properties are to be printed in the output file")]
        [switch]
        $ExtendedOutputSchema,

        # The toggle for scanning single/all objects in the tenant
        [Parameter(Mandatory = $false,
                   HelpMessage = "Toggle to scan all objects in the tenant")]
        [switch]
        $ScanAll,

        # $top passed to the List Applications or List ServicePrincipals call
        [int]
        $PageSize = 200,

        # How long to sleep (in seconds) between paginated calls
        [int]
        $SleepInterval = 2,

        # Max number of retries for List Applications or List ServicePrincipals MS Graph request
        [int]
        $MaxRetryLimit = 5,

        # Max page size
        [int]
        $MaxPageSize = 500
    )

    # Validate page size
    Validate-PageSize -PageSize $PageSize -MaxPageSize $MaxPageSize

    # Get ms graph endpoint resource url
    $resourceUrl = Get-MSGraphEndpoint -Env $Env

    # Output warning message if scanning all objects
    # Otherwise, check if either AppId or ObjectId is specified
    if ($ScanAll)
    {
        Write-Warning "Are you sure you want to run the commandlet for all $($ObjectClass)s in your tenant? The commandlet may take a long time to run, and requests for a large number of $($ObjectClass)s could be throttled." -WarningAction Inquire
    
        if ($AppId -or $ObjectId)
        {
            throw "You cannot specify -AppId or -ObjectId when running in -ScanAll mode"
        }
    }
    else
    {
        if (!$AppId -and !$ObjectId)
        {
            throw "When scanning a single application or service principal, you must provide either an -AppId or an -ObjectId"
        }

        if ($AppId -and $ObjectId)
        {
            throw "Please provide exactly one of the following: -AppId or -ObjectId"
        }
    }

    # Set flag to distinguish between default schema and extended schema for output
    if ($ExtendedOutputSchema)
    {
        $IncludeExtendedProperties = $true
    }

    # Information about installing Az.Accounts module
    Write-Warning "This script requires the powershell module 'Az.Accounts' to installed."
    Write-Warning "If this is not installed, you will be asked to install the module."
    Write-Warning "Please refer: https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-6.5.0"

    # Check if the Az Module is installed and imported
    if(!(Get-Module Az.Accounts)){
        try{Import-Module -Name Az.Accounts -ErrorAction Stop}
        catch{Install-Module -Name Az.Accounts -AllowClobber -Confirm -Scope CurrentUser}
    }

    # Connect to AAD
    Write-Host "`nConnecting to AAD tenant..."
    Connect-AzAccount -Tenant $TenantId -Environment $Env -ErrorAction Stop | out-null
    Write-Host "Connected to $($TenantId)`n" -ForegroundColor Green

    # Get access token
    $accessToken = (Get-AzAccessToken -ResourceUrl $resourceUrl).Token

    # Get total number of application/service prinicpals to scan if scanning all objects in the tenant
    if ($ScanAll)
    {
        $totalObjectCount = Get-TotalObjectCount -AccessToken $accessToken -ObjectClass $ObjectClass -Resource $resourceUrl
    }

    # Generate the url for corresponding Microsoft Graph API endpoint
    $url = Generate-Url -ObjectClass $ObjectClass -AppId $AppId -ObjectId $ObjectId -Resource $resourceUrl -PageSize $PageSize
    if ($SkipTokenUrl)
    {
        $url = Trim-Url -Url $SkipTokenUrl
    }

    # Start key credential scan
    Write-Host "Starting scan..."
    $stopwatch = [System.Diagnostics.Stopwatch]::new()
    $stopwatch.Start()

    $scannedObjectCount = 0
    $keyWithExtenededValueCount = 0

    while ($null -ne $url)
    {
        $result = Make-MSGraphRequest -Url $url -ObjectClass $ObjectClass -AccessToken $accessToken -MaxRetryLimit $MaxRetryLimit
        # If no results are returned, then exit
        if (($null -eq $result) -or ($null -eq $result.Value) -or ($result.Value.Length -eq 0))
        {
            Write-Error "No resource of type $($ObjectClass) found in tenant $($TenantId). Exiting."
            return
        }

        $result.Value | ForEach-Object {
            $reg = $_

            $scannedObjectCount += 1
            $displayName = $reg.displayName

            $appID = $reg.appId
            $objectID = $reg.Id

            Write-Verbose "Scanning key credentials for: $displayName"
            $reg.keyCredentials | ForEach-Object {
                $cred = $_
                if (($cred.hasExtendedValue) -and ($cred.type -eq 'AsymmetricX509Cert') -and ($cred.usage -ne 'Sign')) {
                    $keyWithExtenededValueCount += 1
                    $isExpired = $false
                    $currentDateTime = Get-Date

                    if ($currentDateTime -gt $cred.endDateTime)
                    {
                        $isExpired = $true
                    }

                    $out = [pscustomobject][ordered] @{
                            "ObjectClass" = $ObjectClass
                            "AppId" = $appId
                            "ObjectId" = $objectID
                            "DisplayName" = $displayName
                            "KeyId" = $cred.KeyId
                            "Usage" = $cred.usage
                            "StartDateTime" = $cred.startDateTime
                            "EndDateTime" = $cred.endDateTime
                            "HasExtendedValue" = $cred.hasExtendedValue
                            "IsExpired" = $isExpired
                        }

                    if ($IncludeExtendedProperties)
                    {
                        $identifierUrisStr = ""
                        if ($ObjectClass -eq "application")
                        {
                            $identifierUrisStr = $reg.identifierUris -Join ","
                        }

                        $ownersStr = Parse-Owners -Owners $reg.owners
                        $homePageUrl = ""
                        if ($reg.web)
                        {
                            $homePageUrl = $reg.web.homePageUrl
                        }

                        $out = [pscustomobject][ordered] @{
                                "ObjectClass" = $ObjectClass
                                "AppId" = $appId
                                "ObjectId" = $objectID
                                "DisplayName" = $displayName
                                "KeyId" = $cred.KeyId
                                "Usage" = $cred.usage
                                "StartDateTime" = $cred.startDateTime
                                "EndDateTime" = $cred.endDateTime
                                "HasExtendedValue" = $cred.hasExtendedValue
                                "IsExpired" = $isExpired
                                "IdentifierUris" = $identifierUrisStr
                                "Owners" = $ownersStr
                                "ObjectCreatedDateTime" = $reg.createdDateTime
                                "SignInAudience" = $reg.signInAudience
                                "HomePageUrl" = $homePageUrl
                            }
                    }

                    Write-Verbose $out

                    #output the object into the pipeline
                    $out
                }
            }
        }

        if ($ScanAll)
        {
            $pcomplete = ($scannedObjectCount / $totalObjectCount) * 100
            Write-Progress -Activity "Scanning $($ObjectClass)s" -Status "$($scannedObjectCount) of $($totalObjectCount)" -PercentComplete $pcomplete
        }

        Write-Verbose "[In-Progress] Scanned $($scannedObjectCount) $($ObjectClass)s so far. # of $($ObjectClass)s with KeyCredentials containing extended value: $($keyWithExtenededValueCount)"
        Start-Sleep -s $SleepInterval

        $url = $null
        if ($result | Get-Member -name '@odata.nextLink' -Membertype Properties)
        {
            $url = $result.'@odata.nextLink'
        }
    }

    Write-Verbose "[Finished] Scanned a total of $($scannedObjectCount) $($ObjectClass)s. # of $($ObjectClass)s with KeyCredentials containing extended value: $($keyWithExtenededValueCount)"
    $stopwatch.Stop()
    Write-Verbose "Time elapsed: $($stopwatch.ELAPSED)"
    Write-Host "$($ObjectClass) key credentials successfully scanned." -ForegroundColor Green
}

Export-ModuleMember -Function Get-AffectedKeyCredentials

# Sample usage:
# Import-Module AffectedKeyCredentials.psm1
# Get-AffectedKeyCredentials -TenantId <guid> -ObjectClass <String> [-AppId <guid>] [-ObjectId <guid>] [-Env <String>] [-SkipTokenUrl <String>] [-ExtendedOutputSchema] [-ScanAll] [-Verbose]
# SIG # Begin signature block
# MIInQAYJKoZIhvcNAQcCoIInMTCCJy0CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDuCfhf+9pwcP68
# sfN7OZJkhuT+EmknnXEyQuXlbbMbHqCCEXkwggiJMIIHcaADAgECAhM2AAABfv9v
# /QSkJVgSAAIAAAF+MA0GCSqGSIb3DQEBCwUAMEExEzARBgoJkiaJk/IsZAEZFgNH
# QkwxEzARBgoJkiaJk/IsZAEZFgNBTUUxFTATBgNVBAMTDEFNRSBDUyBDQSAwMTAe
# Fw0yMTA5MDkwMTI2MjZaFw0yMjA5MDkwMTI2MjZaMCQxIjAgBgNVBAMTGU1pY3Jv
# c29mdCBBenVyZSBDb2RlIFNpZ24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
# AoIBAQCQh1zMc6GVq9fygCskp/O9g6jS0ilJ3idmz+2JkE+9AarM0AiJ1/CDQETS
# X56JOh9Vm8kdffjdqJfD2NoSV2lO1eKAFKETKyiJKvbcW38H7JhH1h+yCBjajiWy
# wcAZ/ipRX3sMYM5nXl5+GxEZpGQbLIsrLj24Zi9dj2kdHc0DxqbemzlCySiB+n9r
# HFdi9zEn6XzuTf/3i6XM36lUPZ+xt6Zckupu0CAnu4dZr1XiwHvbJvqq3RcXOU5j
# p1m/AKk4Ov+9jaEKOnYiHJbnpC+vKx/Zv8aZajhPyVY3fXb/tygGOyb607EYn7F2
# v4AcJL5ocPTT3BGWtve1KuOwRRs3AgMBAAGjggWVMIIFkTApBgkrBgEEAYI3FQoE
# HDAaMAwGCisGAQQBgjdbAQEwCgYIKwYBBQUHAwMwPQYJKwYBBAGCNxUHBDAwLgYm
# KwYBBAGCNxUIhpDjDYTVtHiE8Ys+hZvdFs6dEoFgg93NZoaUjDICAWQCAQwwggJ2
# BggrBgEFBQcBAQSCAmgwggJkMGIGCCsGAQUFBzAChlZodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpaW5mcmEvQ2VydHMvQlkyUEtJQ1NDQTAxLkFNRS5HQkxfQU1F
# JTIwQ1MlMjBDQSUyMDAxKDIpLmNydDBSBggrBgEFBQcwAoZGaHR0cDovL2NybDEu
# YW1lLmdibC9haWEvQlkyUEtJQ1NDQTAxLkFNRS5HQkxfQU1FJTIwQ1MlMjBDQSUy
# MDAxKDIpLmNydDBSBggrBgEFBQcwAoZGaHR0cDovL2NybDIuYW1lLmdibC9haWEv
# QlkyUEtJQ1NDQTAxLkFNRS5HQkxfQU1FJTIwQ1MlMjBDQSUyMDAxKDIpLmNydDBS
# BggrBgEFBQcwAoZGaHR0cDovL2NybDMuYW1lLmdibC9haWEvQlkyUEtJQ1NDQTAx
# LkFNRS5HQkxfQU1FJTIwQ1MlMjBDQSUyMDAxKDIpLmNydDBSBggrBgEFBQcwAoZG
# aHR0cDovL2NybDQuYW1lLmdibC9haWEvQlkyUEtJQ1NDQTAxLkFNRS5HQkxfQU1F
# JTIwQ1MlMjBDQSUyMDAxKDIpLmNydDCBrQYIKwYBBQUHMAKGgaBsZGFwOi8vL0NO
# PUFNRSUyMENTJTIwQ0ElMjAwMSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2Vy
# dmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1BTUUsREM9R0JM
# P2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0
# aG9yaXR5MB0GA1UdDgQWBBRufMhNVeWweAyGzdFbxkxa8y1WjDAOBgNVHQ8BAf8E
# BAMCB4AwUAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRp
# b25zIFB1ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzYxNjcrNDY3OTc0MIIB5gYDVR0f
# BIIB3TCCAdkwggHVoIIB0aCCAc2GP2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9w
# a2lpbmZyYS9DUkwvQU1FJTIwQ1MlMjBDQSUyMDAxKDIpLmNybIYxaHR0cDovL2Ny
# bDEuYW1lLmdibC9jcmwvQU1FJTIwQ1MlMjBDQSUyMDAxKDIpLmNybIYxaHR0cDov
# L2NybDIuYW1lLmdibC9jcmwvQU1FJTIwQ1MlMjBDQSUyMDAxKDIpLmNybIYxaHR0
# cDovL2NybDMuYW1lLmdibC9jcmwvQU1FJTIwQ1MlMjBDQSUyMDAxKDIpLmNybIYx
# aHR0cDovL2NybDQuYW1lLmdibC9jcmwvQU1FJTIwQ1MlMjBDQSUyMDAxKDIpLmNy
# bIaBvWxkYXA6Ly8vQ049QU1FJTIwQ1MlMjBDQSUyMDAxKDIpLENOPUJZMlBLSUNT
# Q0EwMSxDTj1DRFAsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2Vydmlj
# ZXMsQ049Q29uZmlndXJhdGlvbixEQz1BTUUsREM9R0JMP2NlcnRpZmljYXRlUmV2
# b2NhdGlvbkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxEaXN0cmlidXRpb25Qb2lu
# dDAfBgNVHSMEGDAWgBSWUYTga297/tgGq8PyheYprmr51DAfBgNVHSUEGDAWBgor
# BgEEAYI3WwEBBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAU1RmrZsQtaYx
# 8dBu9zC6w4TXEtumd3O0ArP7W0Co7nNFCDTv8pxqOM2bz/pH49DXdnzcXCTjUjci
# o03V+QPO3Ql8xOMqm8bE9Kcof+fPk4DyDY5y+YzxQyk49URn4ea3WhihAJkg/xnF
# LiKnbWW8iyqxie+B44u9dPfbsWrxcgedzSnH0aXwfIt29IKCpGHL74rBDbKHXdL0
# pEjf9c2YA6OiS1IH7X/suBjEFa4LEYPTSFK2AJXpgM7q9dmSvta4CyudRoYf1BXP
# KR+CzNT9XL5ZJX8LUuC5LrZgbt7LzjlW+1Umo2OsmUO3YA7/s5vH6Tqc6uZ9isIw
# sit0XfouHTCCCOgwggbQoAMCAQICEx8AAABR6o/2nHMMqDsAAAAAAFEwDQYJKoZI
# hvcNAQELBQAwPDETMBEGCgmSJomT8ixkARkWA0dCTDETMBEGCgmSJomT8ixkARkW
# A0FNRTEQMA4GA1UEAxMHYW1lcm9vdDAeFw0yMTA1MjExODQ0MTRaFw0yNjA1MjEx
# ODU0MTRaMEExEzARBgoJkiaJk/IsZAEZFgNHQkwxEzARBgoJkiaJk/IsZAEZFgNB
# TUUxFTATBgNVBAMTDEFNRSBDUyBDQSAwMTCCASIwDQYJKoZIhvcNAQEBBQADggEP
# ADCCAQoCggEBAMmaUgl9AZ6NVtcqlzIU+gVJSWVqWuKd8RXokxzuL5tkOgv2s0ec
# cMZ8mB65Ehg7Utj/V/igxOuFdtJphEJLm8ZzzXjlZxNkb3TxsYMJavgYUtzjXVbE
# D4+/au14BzPR4cwffqpNDwvSjdc5vaf7HsokUuiRdXWzqkX9aVJexQFcZoIghYFf
# IRyG/6wz14oOxQ4t0tMhMdglA1aSKvIxIRvGp1BRNVmMTPp4tEuSh8MCjyleKshg
# 6AzvvQJg6JmtwocruVg5VuXHbal01rBjxN7prZ1+gJpZXVBS5rODlUeILin/p+Sy
# AQgum04qHH1z6JqmI2EysewBjH2lS2ml5oUCAwEAAaOCBNwwggTYMBIGCSsGAQQB
# gjcVAQQFAgMCAAIwIwYJKwYBBAGCNxUCBBYEFBJoJEIhR8vUa74xzyCkwAsjfz9H
# MB0GA1UdDgQWBBSWUYTga297/tgGq8PyheYprmr51DCCAQQGA1UdJQSB/DCB+QYH
# KwYBBQIDBQYIKwYBBQUHAwEGCCsGAQUFBwMCBgorBgEEAYI3FAIBBgkrBgEEAYI3
# FQYGCisGAQQBgjcKAwwGCSsGAQQBgjcVBgYIKwYBBQUHAwkGCCsGAQUFCAICBgor
# BgEEAYI3QAEBBgsrBgEEAYI3CgMEAQYKKwYBBAGCNwoDBAYJKwYBBAGCNxUFBgor
# BgEEAYI3FAICBgorBgEEAYI3FAIDBggrBgEFBQcDAwYKKwYBBAGCN1sBAQYKKwYB
# BAGCN1sCAQYKKwYBBAGCN1sDAQYKKwYBBAGCN1sFAQYKKwYBBAGCN1sEAQYKKwYB
# BAGCN1sEAjAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYw
# EgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBQpXlFeZK40ueusnA2njHUB
# 0QkLKDCCAWgGA1UdHwSCAV8wggFbMIIBV6CCAVOgggFPhjFodHRwOi8vY3JsLm1p
# Y3Jvc29mdC5jb20vcGtpaW5mcmEvY3JsL2FtZXJvb3QuY3JshiNodHRwOi8vY3Js
# Mi5hbWUuZ2JsL2NybC9hbWVyb290LmNybIYjaHR0cDovL2NybDMuYW1lLmdibC9j
# cmwvYW1lcm9vdC5jcmyGI2h0dHA6Ly9jcmwxLmFtZS5nYmwvY3JsL2FtZXJvb3Qu
# Y3JshoGqbGRhcDovLy9DTj1hbWVyb290LENOPUFNRVJvb3QsQ049Q0RQLENOPVB1
# YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRp
# b24sREM9QU1FLERDPUdCTD9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0P2Jhc2U/
# b2JqZWN0Q2xhc3M9Y1JMRGlzdHJpYnV0aW9uUG9pbnQwggGrBggrBgEFBQcBAQSC
# AZ0wggGZMEcGCCsGAQUFBzAChjtodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtp
# aW5mcmEvY2VydHMvQU1FUm9vdF9hbWVyb290LmNydDA3BggrBgEFBQcwAoYraHR0
# cDovL2NybDIuYW1lLmdibC9haWEvQU1FUm9vdF9hbWVyb290LmNydDA3BggrBgEF
# BQcwAoYraHR0cDovL2NybDMuYW1lLmdibC9haWEvQU1FUm9vdF9hbWVyb290LmNy
# dDA3BggrBgEFBQcwAoYraHR0cDovL2NybDEuYW1lLmdibC9haWEvQU1FUm9vdF9h
# bWVyb290LmNydDCBogYIKwYBBQUHMAKGgZVsZGFwOi8vL0NOPWFtZXJvb3QsQ049
# QUlBLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNv
# bmZpZ3VyYXRpb24sREM9QU1FLERDPUdCTD9jQUNlcnRpZmljYXRlP2Jhc2U/b2Jq
# ZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTANBgkqhkiG9w0BAQsFAAOC
# AgEAUBAjt08P6N9e0a3e8mnanLMD8dS7yGMppGkzeinJrkbehymtF3u91MdvwEN9
# E34APRgSZ4MHkcpCgbrEc8jlNe4iLmyb8t4ANtXcLarQdA7KBL9VP6bVbtr/vnaE
# wif4vhm7LFV5IGl/B/uhDhhJk+Hr6eBm8EeB8FpXPg73/Bx/D3VANmdOAr3MCH3J
# EoqWzZvOI8SfF45kxU1rHJXS/XnY9jbGOohp8iRSMrq9j0u1UWMld6dVQCafdYI9
# Y0ULVhMggfD+YPZxN8/LtADWlP4Y8BEAq3Rsq2r1oJ39ibRvm09umAKJG3PJvt9s
# 1LV0TvjSt7QI4TrthXbBt6jaxeLHO8t+0fwvuz3G/3BX4bbarIq3qWYouMUrXIzD
# g2Ll8xptyCbNG9KMBxuqCne2Thrx6ZpofSvPwy64g/7KvG1EQ9dKov8LlvMzOyKS
# 4Nb3EfXSCtpnNKY+OKXOlF9F27bT/1RCYLt5U9niPVY1rWio8d/MRPcKEjMnpD0b
# c08IH7srBfQ5CYrK/sgOKaPxT8aWwcPXP4QX99gx/xhcbXktqZo4CiGzD/LA7pJh
# Kt5Vb7ljSbMm62cEL0Kb2jOPX7/iSqSyuWFmBH8JLGEUfcFPB4fyA/YUQhJG1KEN
# lu5jKbKdjW6f5HJ+Ir36JVMt0PWH9LHLEOlky2KZvgKAlCUxghUdMIIVGQIBATBY
# MEExEzARBgoJkiaJk/IsZAEZFgNHQkwxEzARBgoJkiaJk/IsZAEZFgNBTUUxFTAT
# BgNVBAMTDEFNRSBDUyBDQSAwMQITNgAAAX7/b/0EpCVYEgACAAABfjANBglghkgB
# ZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3
# AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgwgBPlo8pHLELJrVK
# 08K2XXu6ocKwDu4fOOMBq4a80uswQgYKKwYBBAGCNwIBDDE0MDKgFIASAE0AaQBj
# AHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTANBgkqhkiG
# 9w0BAQEFAASCAQA2X289tKS7zLRV8isUD8gE6oC+irIKv1L/DnIXJg9WcMRE1TeS
# KcRYmIk6dVFde+jTYZ13TyDe5b+t1hLhyKvZ7W8Cai4iVDa0J6xk65tzc1xJvVbY
# 3JfTnNh7dnSLSHCXESj4ErzqWw+xrOxYt8X6/3VHo4nhJkb2YHfQZTJzAWdHLNpn
# oo3EoJOHCfp7SXQxmw/p+2YCSJCgRW8Br2PBOp/9nBTsL8Fq1qnyeajMH/85X74O
# 9KdWP0g3vp5TYprJQubhZHfZaC0qticL7LZ0tkMRh8geJxSpV+tJ8vdZluv+bDW8
# EEkXJtyA2u6LOeXJAVQa75u0gAxyT7bFz8onoYIS5TCCEuEGCisGAQQBgjcDAwEx
# ghLRMIISzQYJKoZIhvcNAQcCoIISvjCCEroCAQMxDzANBglghkgBZQMEAgEFADCC
# AVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMBMDEwDQYJ
# YIZIAWUDBAIBBQAEIDHraMJsb6VX55m0qs861SCxXtsC4dfq7gaxelvOfpRBAgZh
# kGjTrCkYEzIwMjExMTE2MjI1MzQwLjE4OVowBIACAfSggdCkgc0wgcoxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29m
# dCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkFF
# MkMtRTMyQi0xQUZDMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2
# aWNloIIOPDCCBPEwggPZoAMCAQICEzMAAAFIoohFVrwvgL8AAAAAAUgwDQYJKoZI
# hvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO
# BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEm
# MCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjAxMTEy
# MTgyNTU2WhcNMjIwMjExMTgyNTU2WjCByjELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0
# aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046QUUyQy1FMzJCLTFBRkMxJTAj
# BgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQD3/3ivFYSK0dGtcXaZ8pNLEARbraJewryi/Jgb
# aKlq7hhFIU1EkY0HMiFRm2/Wsukt62k25zvDxW16fphg5876+l1wYnClge/rFlrR
# 2Uu1WwtFmc1xGpy4+uxobCEMeIFDGhL5DNTbbOisLrBUYbyXr7fPzxbVkEwJDP5F
# G2n0ro1qOjegIkLIjXU6qahduQxTfsPOEp8jgqMKn++fpH6fvXKlewWzdsfvhiZ4
# H4Iq1CTOn+fkxqcDwTHYkYZYgqm+1X1x7458rp69qjFeVP3GbAvJbY3bFlq5uyxr
# iPcZxDZrB6f1wALXrO2/IdfVEdwTWqJIDZBJjTycQhhxS3i1AgMBAAGjggEbMIIB
# FzAdBgNVHQ4EFgQUhzLwaZ8OBLRJH0s9E63pIcWJokcwHwYDVR0jBBgwFoAU1WM6
# XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5t
# aWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENBXzIwMTAt
# MDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0w
# MS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG
# 9w0BAQsFAAOCAQEAZhKWwbMnC9Qywcrlgs0qX9bhxiZGve+8JED27hOiyGa8R9nq
# zHg4+q6NKfYXfS62uMUJp2u+J7tINUTf/1ugL+K4RwsPVehDasSJJj+7boIxZP8A
# U/xQdVY7qgmQGmd4F+c5hkJJtl6NReYE908Q698qj1mDpr0Mx+4LhP/tTqL6HpZE
# URlhFOddnyLStVCFdfNI1yGHP9n0yN1KfhGEV3s7MBzpFJXwOflwgyE9cwQ8jjOT
# VpNRdCqL/P5ViCAo2dciHjd1u1i1Q4QZ6xb0+B1HdZFRELOiFwf0sh3Z1xOeSFcH
# g0rLE+rseHz4QhvoEj7h9bD8VN7/HnCDwWpBJTCCBnEwggRZoAMCAQICCmEJgSoA
# AAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoXDTI1MDcwMTIxNDY1NVow
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEiMA0GCSqGSIb3DQEBAQUA
# A4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRrdFQQ1aUKAIKF++18aEss
# X8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmxMEQP8WCIhFRDDNdNuDgI
# s0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKEHnRhZ5FfgVSxz5NMksHE
# pl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBisV39dx898Fd1rL2KQk1A
# UdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpOBpG2iAg16HgcsOmZzTzn
# L0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMBAAGjggHmMIIB4jAQBgkr
# BgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPNDe3xGG8UzaFqFbVUwGQYJ
# KwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF
# MAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8w
# TTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVj
# dHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBK
# BggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9N
# aWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1UdIAEB/wSBlTCBkjCBjwYJ
# KwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwA
# ZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABlAG0AZQBuAHQALiAdMA0G
# CSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2joSFvs+umzPUxvs8F4qn+
# +ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJEEvu5U4zM9GASinbMQEBB
# m9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5SpFSAK84Dxf1L3mBZdmp
# tWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJKJ/1Vry/+tuWOM7tiX5rb
# V0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yjojz6f32WapB4pm3S4Zz5
# Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0v35jWSUPei45V3aicaoG
# ig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgiCGHasFAeb73x4QDf5zEH
# pJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iCtHLNHfS4hQEegPsbiSpU
# ObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO2ii4sanblrKnQqLJzxlB
# TeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyXUHHXodLFVeNp3lfB0d4w
# wP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWzfjUeCLraNtvTX4/edIhJ
# EqGCAs4wggI3AgEBMIH4oYHQpIHNMIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRp
# b25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpBRTJDLUUzMkItMUFGQzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIa
# AxUAhyuClrocWf4SIcRafAEX1Rhs6zmggYMwgYCkfjB8MQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1T
# dGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOU+MwIwIhgPMjAyMTExMTYy
# MTM5MTRaGA8yMDIxMTExNzIxMzkxNFowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA
# 5T4zAgIBADAKAgEAAgIQkgIB/zAHAgEAAgIRTTAKAgUA5T+EggIBADA2BgorBgEE
# AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag
# MA0GCSqGSIb3DQEBBQUAA4GBAD4IhsH7VGEZq3Et8NzhOCvICoWIyY64XnKWVMz5
# dvsKi//vfT2mX6mbOE3knjHzrGrvDAm3Kj+jbkoXLu4yKODjhIV6tXfcWeBtg0hb
# 5dUWmRidX8nkQpPzDCVUbCqjm3aP3cp4NHbfEBdT4M0YjxTVSDkUT7GSo7xRXJJc
# 8rg8MYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAC
# EzMAAAFIoohFVrwvgL8AAAAAAUgwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3
# DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgKoW4Cm5HKxG1c7SQ
# dE3HFX6LSG3bNVukGLbTMKKohEcwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9
# BCCpkBrqjHmhvyYf5tTcTvD5Y4a+V79TwVV6T1aAwdto2DCBmDCBgKR+MHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABSKKIRVa8L4C/AAAAAAFIMCIE
# IG/qeIp6mbVd8Dr+EThpnN/6w78fmvUoISM5Bb/WyvbkMA0GCSqGSIb3DQEBCwUA
# BIIBAOZ5Si1j3uIZCB4kK/fNwMIXekGCwIFdUG1hi/tEHBllgjp/cSp7oTOdc8bv
# X9k5horUQJMZN6Bf4x2uUhMW1VMld+oM49g2C52atPYRlggUyMyisZEM6cIt3Yby
# MZqTEa4Am+BRUJonkDr+ig4PERUr6R8OxXbKSVacG5ugAcXUA7dtWIdkvDBLO75S
# ZZiepsy/KcdrK4vGKH/ySrLaXbJ+I+E4hCQa9+CNBuKxbqTqbQEDykphbEOet/5E
# Dsgm3Lo/nUaGW9B9lPDXANZA873j0jC6YHEsm1czzJBU5x+OgFi2zJmdhfmpRniL
# NMbHF8axH9LfihtpE2XsKhpsTw0=
# SIG # End signature block