GraphValidation/Microsoft.AzureStack.GraphValidation.psm1
function Invoke-AzsGraphValidation { <# .SYNOPSIS Performs Graph Validation before DC Integration with Azure Stack .DESCRIPTION A tool that can be run standalone or as part Graph integration for Azure Stack. Gives operators pre-validation before attempting integration, to ensure all prerequisites are met. Calls into Graph functions to provide validation of readiness for graph integration. Validation: Test Domain - should be resolvable and forest root. Test AD Credentials - test graph credentials have appropriate rights to query AD - as per https://docs.microsoft.com/en-us/azure/azure-stack/azure-stack-integrate-identity#create-user-account-in-the-existing-active-directory-optional Test Global Catalog - should be resolvable and contactable. Test KDC - should be resolvable Test Network Connectivity - as per https://docs.microsoft.com/en-us/azure/azure-stack/azure-stack-integrate-identity#graph-protocols-and-ports .EXAMPLE PS C:\> Invoke-AzureStackGraphValidation -ForestFQDN contoso.com -Credential $cred Validates contoso.com forest can be queried with credential $cred and above validations are performed. .PARAMETER ForestFQDN ForestFQDN - UriHostNameType.Dns - Forest Root with which to integrate Graph. .PARAMETER Credential Credential - PSCredential - Service Account with read rights to integrate Graph. Username must be in domain\username format. .PARAMETER ForceTLS12 Force the use of TLS 1.2, true by default .PARAMETER OutputPath Specifies custom path to save Readiness JSON report and Verbose log file. .PARAMETER CleanReport Specifies whether to purge the existing report and start again with a clean report. Execution history and validation data is lost for all Readiness Checker validations. .PARAMETER PassThru switch - Return output results PSObject .OUTPUTS Output (if any) .NOTES Graph Integration - https://aka.ms/AzsGraphIntegration #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "Enter Forest FQDN", ParameterSetName = "ValidateGraph")] [ValidateScript( {[System.Uri]::CheckHostName($_) -eq 'dns' <#ForestFQDN must be valid DNSHostName#>})] [string] $ForestFQDN, [Parameter(Mandatory = $true, HelpMessage = "Enter Credential with rights to query Forest", ParameterSetName = "ValidateGraph")] [ValidateScript( {$_.UserName -notlike '*@*' <#Username should not be upn#>})] [PSCredential] $Credential, [Parameter(Mandatory=$false,HelpMessage="Force the use of TLS 1.2, true by default")] [bool]$ForceTLS12 = $true, [Parameter(Mandatory = $false, HelpMessage = "Return output results PSObject")] [switch] $PassThru ) $thisFunction = $MyInvocation.MyCommand.Name $GLOBAL:OutputPath = $OutputPath Import-Module $PSScriptRoot\..\Microsoft.AzureStack.ReadinessChecker.Reporting.psm1 -Force # Import localization strings Import-LocalizedData LocalizedData -BaseDirectory $PSScriptRoot -Filename Microsoft.AzureStack.GraphValidation.Strings.psd1 # Load helper Modules Import-Module $PSScriptRoot\..\Microsoft.AzureStack.ReadinessChecker.Utilities.psm1 -Force # Check ActiveDirectory Module exist and prompt to install if not if (-not (Get-Module ActiveDirectory -ListAvailable)) { Write-AzsReadinessLog -message ($LocalizedData.MissingADPSModules) -function $thisFunction -type Error -ToScreen break } Import-Module ActiveDirectory -Force -WarningAction SilentlyContinue Write-Header -invocation $MyInvocation -params $PSBoundParameters # detect if this is on ercs machine or standalone. if ($ENV:COMPUTERNAME -notlike '*-ercs0') { $standalone = $true Write-AzsReadinessLog ("Running in standalone/offstamp mode") -Type Info -Function $thisFunction } if (-not (Resolve-DnsName -Name $ForestFQDN -QuickTimeout -ErrorAction SilentlyContinue)) { throw "Unable to resolve forest $forestFQDN in DNS" } if ($credential.Password) { Write-AzsReadinessLog -Message 'Checking password length and complexity' -Type Info -Function $thisFunction Test-PasswordLength -MinimumCharactersInPassword 8 -Password $Credential.Password -CredentialDescription 'Graph Password' Test-PasswordComplexity -Password $Credential.Password -CredentialDescription 'Graph Password' Write-AzsReadinessLog -Message 'Password length & complexity. Success' -Type Info -Function $thisFunction } else { Write-AzsReadinessLog -Message 'Password length and complexity checking skipped' -Type Info -Function $thisFunction } # Get/Clean Existing Report $readinessReport = Get-AzsReadinessProgress -clean:$CleanReport $readinessReport = Add-AzsReadinessCheckerJob -report $readinessReport # Force Security Profile to TLS1.2 if ($ForceTLS12) { # Change Security Protocol to TLS1.2 for the session and track for clean up later. $restoreSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol $tempSecurityProtocol = [Net.SecurityProtocolType]::Tls12 Set-SecurityProtocol -securityProtocol $tempSecurityProtocol } # Start Checks $results = @() $DomainResult = Test-Domain -ForestFQDN $ForestFQDN Write-Result -in $DomainResult $results += $DomainResult $AdCredResult = Test-ADCredential -credential $credential -ForestFQDN $ForestFQDN Write-Result -in $AdCredResult $results += $AdCredResult if ($DomainResult.result -eq 'OK') { $GCResult = Test-GlobalCatalog -ForestFQDN $ForestFQDN Write-Result -in $GCResult $results += $GCResult $KDCResult = Test-KDC -ForestFQDN $ForestFQDN Write-Result -in $KDCResult $results += $KDCResult $LdapPath = ConvertTo-LdapPath -FQDN $ForestFQDN $ldapSearchResult = Test-LDAPSearch -LdapPath $LdapPath -Credential $Credential Write-Result -in $ldapSearchResult $results += $ldapSearchResult } else { Write-AzsReadinessLog "Domain test failed. Skipping GC and KDC tests." -Type Info -Function $thisFunction } if ($GCResult.outputObject) { $ConnectivityResult = Test-NetworkConnectivity -forestFQDN $ForestFQDN -GlobalCatalog $GCResult.outputObject.Hostname Write-Result -in $ConnectivityResult $results += $ConnectivityResult } # Restore TLS back to default if ($ForceTLS12) { Set-SecurityProtocol -securityProtocol $restoreSecurityProtocol } if ($standalone) { $hash = @{'Test' = 'Standalone'; 'Result' = 'Warning'; 'FailureDetail' = $LocalizedData.StandaloneWarning; 'outputObject' = $null} $object = New-Object PSObject -Property $hash $results += $object } if ($results.result -match "Fail|Warning" -or $standalone) { Write-Result -in $results.failureDetail } # Write results to readiness report $readinessReport.GraphValidation = $results $readinessReport = Close-AzsReadinessCheckerJob -report $readinessReport Write-AzsReadinessProgress -report $readinessReport Write-AzsReadinessReport -report $readinessReport Write-Footer -invocation $MyInvocation if ($PassThru){ Write-Output $results } } function Test-Domain { param ([string]$ForestFQDN) $thisFunction = $MyInvocation.MyCommand.Name $test = 'Test Forest Root' Write-AzsReadinessLog -Message "Test Domain: $ForestFQDN" -Type Info -Function $thisFunction try { $domain = Get-ADDomainController -DomainName $ForestFQDN -ForceDiscover -Discover -ErrorAction SilentlyContinue } catch { if ($_.exception.errorcode -eq '1355') { $failureDetail = $LocalizedData.CannotResolveForestFQDN -f $ForestFQDN Write-AzsReadinessLog -Message $failureDetail -Type Error -Function $thisFunction } else { $failureDetail = $LocalizedData.TestFailed -f $thisFunction, $_.Exception.Message Write-AzsReadinessLog -Message $failureDetail -Type Error -Function $thisFunction } } if (-not $domain) { $failureDetail = $LocalizedData.DomainNotFound -f $ForestFQDN $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Warning -Function $thisFunction } elseif ($domain.domain -ne $domain.forest) { # This can only be a warning, but need to inform that searches will be limited to child domain. $failureDetail = $LocalizedData.NotForestRoot -f $ForestFQDN, $domain.forest $result = 'Warning' Write-AzsReadinessLog -Message $failureDetail -Type Warning -Function $thisFunction } else { $result = 'OK' Write-AzsReadinessLog -Message "Domain Controller found:" -Type Info -Function $thisFunction $domain.psbase.PropertyNames | ForEach-Object {Write-AzsReadinessLog -Message ("{0} : {1}" -f $PSITEM, $domain.$PSITEM) -Type Info -Function $thisFunction} } $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $domain} $object = New-Object PSObject -Property $hash $object } function Test-ADCredential { param ([PSCredential]$credential, [string]$ForestFQDN) $thisFunction = $MyInvocation.MyCommand.Name $test = 'Test Graph Credential' Write-AzsReadinessLog -Message ("Attempting to logon to forest {0} with AD Credential {1}..." -f $forestFQDN, $Credential.Username) -Type Info -Function $thisFunction try { $rootDSE = Get-ADRootDSE -Server $ForestFQDN -Credential $Credential -ErrorAction stop Write-AzsReadinessLog -Message ("AD Credential {0} verified." -f $credential.username) -Type Info -Function $thisFunction Write-AzsReadinessLog -Message ("AD Root {0} found." -f $rootDSE.rootDomainNamingContext) -Type Info -Function $thisFunction $rootDSE.psbase.PropertyNames | ForEach-Object {Write-AzsReadinessLog -Message ("{0} : {1}" -f $PSITEM, $rootDSE.$PSITEM) -Type Info -Function $thisFunction} # Check for supported forest and domain modes $supportedADDomainMode = 'Windows2012Domain', 'Windows2012R2Domain', 'Windows2016Domain' $supportedForestMode = 'Windows2012Forest', 'Windows2012R2Forest', 'Windows2016Forest' if ($rootDSE.domainFunctionality -notin $supportedADDomainMode -or $rootDSE.forestFunctionality -notin $supportedForestMode) { $failureDetail = $LocalizedData.FunctionalLevelNotSupport -f $ForestFQDN, $rootDSE.domainFunctionality, $rootDSE.forestFunctionality $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Warning -Function $thisFunction } else { $result = 'OK' Write-AzsReadinessLog -Message "Forest/Domain functional levels are supported." -Type Info -Function $thisFunction } } catch { if ($_.Exception.InnerException.InnerException.InnerException.Message -match 'The logon attempt failed') { $failureDetail = $LocalizedData.ServerRejectedCreds -f $credential.UserName, $ForestFQDN $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Warning -Function $thisFunction } else { $failureDetail = $LocalizedData.TestFailed -f $thisFunction, $_.Exception.Message $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Warning -Function $thisFunction } } $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $null} $object = New-Object PSObject -Property $hash $object } function Test-GlobalCatalog { param ([PSCredential]$credential, [string]$ForestFQDN) $thisFunction = $MyInvocation.MyCommand.Name $test = 'Test Global Catalog' Write-AzsReadinessLog -Message ("Getting Global Catalog for forest: {0}" -f $ForestFQDN) -Type Info -Function $thisFunction try { $GlobalCatalog = Get-ADDomainController -DomainName $forestFQDN -ForceDiscover -Discover -Service GlobalCatalog -ErrorAction SilentlyContinue } catch { $failureDetail = $LocalizedData.TestFailed -f $thisFunction, $_.Exception.Message $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Error -Function $thisFunction } if (-not $GlobalCatalog) { $failureDetail = $LocalizedData.GCNotFound $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Error -Function $thisFunction } else { $result = 'OK' Write-AzsReadinessLog -Message "Global Catalog found:" -Type Info -Function $thisFunction $GlobalCatalog.psbase.PropertyNames | ForEach-Object {Write-AzsReadinessLog -Message ("{0} : {1}" -f $PSITEM, $GlobalCatalog.$PSITEM) -Type Info -Function $thisFunction} } $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $GlobalCatalog} $object = New-Object PSObject -Property $hash $object } function Test-KDC { param ([PSCredential]$credential, [string]$ForestFQDN) $thisFunction = $MyInvocation.MyCommand.Name $test = 'Test KDC' Write-AzsReadinessLog -Message ("Getting KDC for forest: {0}" -f $ForestFQDN) -Type Info -Function $thisFunction try { $KDC = Get-ADDomainController -DomainName $forestFQDN -ForceDiscover -Discover -Service KDC -ErrorAction SilentlyContinue } catch { $failureDetail = $LocalizedData.TestFailed -f $thisFunction, $_.Exception.Message $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Error -Function $thisFunction } if (-not $KDC) { $failureDetail = $LocalizedData.KDCNotFound $result = 'Fail' Write-AzsReadinessLog -Message $failureDetail -Type Error -Function $thisFunction } else { $result = 'OK' Write-AzsReadinessLog -Message "KDC resolved:" -Type Info -Function $thisFunction $KDC.psbase.PropertyNames | ForEach-Object {Write-AzsReadinessLog -Message ("{0} : {1}" -f $PSITEM, $KDC.$PSITEM) -Type Info -Function $thisFunction} } $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $null} $object = New-Object PSObject -Property $hash $object } function Test-NetworkConnectivity { # on stamp only operation (help:https://aka.ms/AzsADFSIntegrationOffline) param ([string]$forestFQDN, [string]$GlobalCatalog, [string]$ADFSServer) $thisFunction = $MyInvocation.MyCommand.Name $test = 'Test Network Connectivity' $connectivityTests = @{ 'LDAP' = @{ 'port' = 389 'target' = $ForestFQDN 'source' = $null 'result' = $null } 'LDAPSSL' = @{ 'port' = 636 'target' = $ForestFQDN 'source' = $null 'result' = $null } 'LDAPGC' = @{ 'port' = 3268 'target' = $GlobalCatalog 'source' = $null 'result' = $null } 'LDAPGCSSL' = @{ 'port' = 3269 'target' = $GlobalCatalog 'source' = $null 'result' = $null } } if ($ADFSServers) { foreach ($key in $connectivityTests.keys) { $probe = $connectivityTests[$key] $result = Test-NetworkConnectivityInternal -source $ADFSServer -destination $probe.target -port $probe.port $connectivityTests[$key].Result = $result } } else { foreach ($key in $connectivityTests.keys) { $probe = $connectivityTests[$key] $result = Test-NetworkConnectivityInternal -destination $probe.target -port $probe.port $connectivityTests[$key].Result = $result } } # Analyse connectivity Result, success on a single encrypted/unencrypted is a pass $failureDetail = @() if (-not ($connectivityTests.LDAPGCSSL.Result.TcpTestSucceeded -or $connectivityTests.LDAPGC.Result.TcpTestSucceeded)) { $failureDetail += $LocalizedData.LDAPGCConnectivityFailure } if (-not ($connectivityTests.LDAPSSL.Result.TcpTestSucceeded -or $connectivityTests.LDAP.Result.TcpTestSucceeded)) { $failureDetail += $LocalizedData.LDAPConnectivityFailure } if ($failureDetail) { #switch severity depending on the scenario onstamp = fail, offstamp = warning switch ($standalone) { $true {$result = 'Warning'} $false {$result = 'Fail'} } } else { $result = 'OK' } $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $null} $object = New-Object PSObject -Property $hash $object } function Test-NetworkConnectivityInternal { param ($source, $destination, $port) $thisFunction = $MyInvocation.MyCommand.Name try { if ($source) { $result = Invoke-Command -ComputerName $source {Test-NetConnection -ComputerName $destination -Port $port -WarningAction SilentlyContinue} } else { $result = Test-NetConnection -ComputerName $destination -Port $port -WarningAction SilentlyContinue $source = $ENV:COMPUTERNAME } # write result to log file switch ($result.TcpTestSucceeded) { $true {$logLevel = 'Info'} $false {$logLevel = 'Warning'} } Write-AzsReadinessLog -Message ("Connectivity test source = {0}; destination {1}; port = {2}; TcpTestSucceeded = {3}" -f $source, $destination, $port, $result.TcpTestSucceeded) -Type $logLevel -Function $thisFunction } catch { $result = $_.exception Write-AzsReadinessLog -Message ("Connectivity test source = {0}; destination {1}; port = {2} failed with error {3}" -f $source, $destination, $port, $result) -Type Error -Function $thisFunction } $result } function Test-LDAPSearch{ param ($Credential,$LdapPath) $thisFunction = $MyInvocation.MyCommand.Name $test = 'Test LDAP Search' try{ # Create an ADSI Search for 5 users Write-AzsReadinessLog -Message ("Starting user search in {0}, limited to 5 users." -f $LdapPath) -Type Info -Function $thisFunction $Searcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher $Searcher.Filter = "(objectCategory=User)" $Searcher.SizeLimit = '5' $Domain = New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList $LdapPath ,$($Credential.UserName),$($Credential.GetNetworkCredential().password) $Searcher.SearchRoot = $Domain $searchResult = $Searcher.FindAll() } catch{ $failureDetail = $localizedData.TestFailed -f $thisFunction,$_.Exception.Message Write-AzsReadinessLog -Message $failureDetail -Type Error -Function $thisFunction $result = 'Fail' } if ($searchResult) { Write-AzsReadinessLog -Message ("Finished user search in {0}. {1} users found" -f $LdapPath,$searchResult.count) -Type Info -Function $thisFunction $result = 'OK' } $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $null} $object = New-Object PSObject -Property $hash $object } function ConvertTo-LdapPath{ param ($FQDN) $distinguishedName = $ForestFQDN.split('.') | ForEach-Object {"DC=$PSITEM"} $ldapPath = "GC://{0}/{1}" -f ($FQDN,($distinguishedName -join ',')) $ldapPath } function Write-Result { param([psobject]$in) if ($in.test) { Write-Host ("`t{0}: " -f $($in.Test)).PadRight(30) -noNewLine if ($in.Result -eq 'OK') { Write-Host 'OK' -foregroundcolor Green } elseif ($in.Result -eq 'WARNING') { Write-Host 'Warning' -foregroundcolor Yellow } elseif ($in.Result -eq 'Skipped') { Write-Host 'Skipped' -foregroundcolor White } else { Write-Host 'Fail' -foregroundcolor Red } } else { Write-Host "`Details:" $in | ForEach-Object {if ($_) {Write-Host "[-] $_" -foregroundcolor Yellow}} Write-Host ("Additional help URL: {0}" -f "https://aka.ms/AzsGraphIntegration") } } # SIG # Begin signature block # MIIjhAYJKoZIhvcNAQcCoIIjdTCCI3ECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCATR2QpYkz6b4+X # XATyFz+aNOSTGGi9qknzVeq3rHFy+KCCDYEwggX/MIID56ADAgECAhMzAAACUosz # qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I # sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O # L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA # v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o # RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8 # q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3 # uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp # kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7 # l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u # TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1 # o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti # yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z # 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf # 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK # WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW # esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F # 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS # 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/BvW1taslScxMNelDNMYIVWTCCFVUCAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgic+XJtio # NO7waKF2wDRRFKl1SPpl4UzssMiPODQziC4wQgYKKwYBBAGCNwIBDDE0MDKgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN # BgkqhkiG9w0BAQEFAASCAQCEpIA1O/8prqJkTM/yAuxMqub4LL27EwzjC3m4uY/Y # X7ru6EgBMVsK51EHU9/fROOUdMRoH02rSrhrRxzIronYtsB7yWw1ikzoNeEjRtC0 # p9t7XYw0cpZKJg2byF2+uYaIg+bO3EI8LM+VVQ030GDNMZKth74r0hYt6XZxiZaZ # jS/pF4APo8TS9R0d3idLhfuWCHUoaGa6tJuTlLkcZJHNfVTMYuFsomV9m7ehXEjN # 6cZ2YFm4tA5qlA3ulHLatVsK2VCqOPbEUEaHlSiui9fpJZrpp5iAvgtowdZ1m87i # k+b4adwi+XAn3BwF/MiTeu+wJOmZxLjhRJubO8qEpDqHoYIS4zCCEt8GCisGAQQB # gjcDAwExghLPMIISywYJKoZIhvcNAQcCoIISvDCCErgCAQMxDzANBglghkgBZQME # AgEFADCCAVAGCyqGSIb3DQEJEAEEoIIBPwSCATswggE3AgEBBgorBgEEAYRZCgMB # MDEwDQYJYIZIAWUDBAIBBQAEICBMpDK0jskjM7TI73CY6trRSFXyauUZrcYfjdvK # yTZBAgZhktXtBi8YEjIwMjExMTE2MDg0OTQzLjA5WjAEgAIB9KCB0KSBzTCByjEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWlj # cm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBF # U046OEE4Mi1FMzRGLTlEREExJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1w # IFNlcnZpY2Wggg47MIIE8TCCA9mgAwIBAgITMwAAAUtPsqZI1eTCUQAAAAABSzAN # BgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0y # MDExMTIxODI1NTlaFw0yMjAyMTExODI1NTlaMIHKMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP # cGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo4QTgyLUUzNEYtOURE # QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCASIwDQYJ # KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKE2elDHdi4mv+K+hs+gu2lD16BQWXxM # d1ZnpIAogl20/cvbgPf93reiaaaNmMLKtCb6P/W0cMDCNAa47Bi+fv15w8JB8AH3 # UmcSn/A/gEwXZJfIx/yT1HzhG2Eh18Yc9dNarOkIJ81aiVURxRWbwB3+vUuuKRE7 # 7goqjqyUNAkqyAoCl8FT/0ntG52+HDWsRDDQ2TUFEZaOsinv+5ahQh9HityXpTW6 # 06JgiicLzs8+kAlBcZGwN0qdUUXg2la8yLJ66Syfm3863DPzawaWd78c1CmYzOKB # Hxxnx5cQMkk0hnGi/1YAcePbyBQTb0PyK8BPvTqKHG9O/nRljxbnW7ECAwEAAaOC # ARswggEXMB0GA1UdDgQWBBRSqmp+0BKW57orct4+VNOfTUrrxjAfBgNVHSMEGDAW # gBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8v # Y3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNUaW1TdGFQQ0Ff # MjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0YVBDQV8yMDEw # LTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0G # CSqGSIb3DQEBCwUAA4IBAQAW2rnVlz87UB8kri0QHY2vxsYRUPmpDyXyBchAysxl # i110cf5waKqAX/gaa+Y9+XkUBiH6B//xh3erj+IPb4rgu0luz/e/qanIGXWZDi+6 # wrrl0DKlaaJPVbcWJeOyYIiSNIMOwosUFgfnIYWc0U4QyAv47u7iiwfjZ/zSdzZZ # 2dlXr469bTflc9Xpm21QF8VYd0htSR04bU7afjImbXQ59pwi1nTx/OAwyoT5/9JO # BVY0IdtHYRipNZrKsY/r2MzC1UP0EYZNa2LVeOm8TrIp07wf2e5GLcv4LqNie19o # SYFNudMURX6RHHUI1ylJv2izzoIBR6FlTVpHNDoJD+mPMIIGcTCCBFmgAwIBAgIK # YQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm # aWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAxMjE0 # NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZIhvcN # AQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoAgoX7 # 7XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiEVEMM # 1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+BVLHP # k0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3wV3Ws # vYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXoeByw # 6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYwggHi # MBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNoWoVt # VTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0T # AQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNV # HR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9w # cm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEE # TjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2Nl # cnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGVMIGS # MIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIwNB4y # IB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4AdAAu # IB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM9TG+ # zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0YBKK # dsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgPF/Uv # eYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/625Y4z # u2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZqkHim # bdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96LjlX # dqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5vvfHh # AN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiFAR6A # +xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduWsqdC # osnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV42ne # V8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto229Nf # j950iEkSoYICzTCCAjYCAQEwgfihgdCkgc0wgcoxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9w # ZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjhBODItRTM0Ri05RERB # MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYF # Kw4DAhoDFQCROjP3t+x4fE05RJDk79sFVIX57qCBgzCBgKR+MHwxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU # aW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBBQUAAgUA5T1UajAiGA8yMDIx # MTExNjA1NDkzMFoYDzIwMjExMTE3MDU0OTMwWjB2MDwGCisGAQQBhFkKBAExLjAs # MAoCBQDlPVRqAgEAMAkCAQACAQcCAf8wBwIBAAICESIwCgIFAOU+peoCAQAwNgYK # KwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQAC # AwGGoDANBgkqhkiG9w0BAQUFAAOBgQAOzRxG1Qzlsw5X3v2W2+tCD/Q6fTnDFMPx # jXWgpe6J9vcmyjtVOjUgJ/3pTwMyRFOL1DONahacW5tRXniuptbuZBuMhl84Guwk # uaOU0a+1ESFUg5etMYzCfEV+CRM3F7rNPv4zHl/PRZBPeMc6rNtWTr6JZdDOM6Bf # CNkkK0LbZDGCAw0wggMJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy # MDEwAhMzAAABS0+ypkjV5MJRAAAAAAFLMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkq # hkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEII4Ljzc1mZyK # IRo4987eUJ9Uo4ghaSwTzX/VKGZHuxy0MIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB # 5DCBvQQga/buhCJ57GWBLbPxY/6yBb9GGVtcp4Vyjj9oVT1FOSMwgZgwgYCkfjB8 # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N # aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAUtPsqZI1eTCUQAAAAAB # SzAiBCDsN0y3nK23XVF0vqDEg4w2MvZpG/FprdXU72hjvoVTBzANBgkqhkiG9w0B # AQsFAASCAQA4ZCHigBo35H3c2ZfJGYrFiG8O6CBICKiywJSuanJLKLgihSnSidSs # m4TxyX51/45YMR4xLxMERJgz8YugZdQ4MqIy0zu4V6bliZoiDuGYmYcmknfcOH/r # k/csOMto/Q7RfC1YQj2ePKcfC+Nsd8QtmaXjieTM/upMIgxLcSz1eM2YRReIAru5 # uiYk74XiE93m214kGGHjw+Br5cM772aSKjV/xPmTRNB/foWNSQdwFeE2+xtrGcOr # JCQg/Pq5E7o5U0Y71yRyj4CyqrNRCrFZdv9v4v/is7PZUgMMY4c53nSthr+D2jWE # 2y5caQpaklWG/9QoGGkhXJWQQ3B//YDH # SIG # End signature block |