ADLAB PowerShell source file: buildup-adfs.ps1

(C) Ondrej Sevecek, 2019 - www.sevecek.com, ondrej@sevecek.com



$libDir = Split-Path -parent $MyInvocation.MyCommand.Definition
& "$libDir\lib-common.ps1" -defaultConfig -rootDir $libDir -outFile adfsLib
& "$libDir\lib-modifyActions.ps1"
& "$libDir\lib-buildup.ps1"

$vmName = $args[0]

DBG ('ADFS installation library')
Redirect-TempToOutput
Load-VMConfig

Find-MarkedVolumes


$appTag = 'adfs'
$appConfig = $vmConfig.$appTag
$firstAppHost = Get-FirstAppHostInInstance $appTag $appConfig.instance 'waitParams'
$firstAppConfig = (Get-FirstAppHostInInstance $appTag $appConfig.instance 'vmConfig').$appTag
$firstAppHostInInstance = Check-FirstAppHostInInstance $appTag $appConfig.instance
$allAppConfigs = Get-AllAppHostsOfInstance $appTag $appConfig.instance 'svcConfig'



if (Is-Null $appConfig) { DBG ('Invalid appCofnig, exiting'); exit }
#====================
#====================




function global:Build-PrimaryADFS (
  [string] $organizationDNS = 'gopas.cz',
  [string] $identifier = 'adfs.gopas.cz',
  [string] $deviceRegDomain = 'gopas.cz',
  [string] $thumbprint = '0bca5ac6e79a3b4fee22e4b44d593fbabb1c11c7',
  [string] $companyDisplay = 'GOPAS a.s.',
  [string] $gmsa = 'gps\svc-adfs$',
  [int] $extranetLockout = -1,
  [string] $extranetObservation = '00:17:00',
  [bool] $enableKmsi,
  [string] $extendedProtection,
  [pscredential] $svcCred # Note: obtain this with our own Get-PwdCredentials($samLogin = $true)
  )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $adfsRes = $null

  if (Is-ValidString $gmsa) {

    DBG ('Install ADFS farm with GMSA: {0} | {1} | {2} | {3} | {4}' -f $organizationDNS, $identifier, $thumbprint, $companyDisplay, $gmsa)
    DBGSTART
    $adfsRes = Install-AdfsFarm -CertificateThumbprint $thumbprint -FederationServiceName $identifier -FederationServiceDisplayName $companyDisplay -GroupServiceAccountIdentifier $gmsa
    # Note: we could also make use of the following:
    #       -DecryptionCertificateThumbprint
    #       -SigningCertificateThumbprint
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    # Note: the GMSA account must be member of WAAG imperatively
    #       but as it is not created during the DC setup, we must add it
    #       to the group manually now
    # Note: the question is about which domain, but the answer is for
    #       now just the machine domain, because the machine domain is
    #       actually the domain which gets the GMSA created in

    [string] $waagPath = "WinNT://$global:thisComputerDomainNetBIOS/Windows Authorization Access Group"
    DBG ('Get the WAAG group: {0}' -f $waagPath)
    DBGSTART
    $waag = [ADSI] $waagPath
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $waag.Guid }
    DBGIF $MyInvocation.MyCommand.Name { (New-Object System.Security.Principal.SecurityIdentifier $waag.objectSID.Value, 0).Value -ne 'S-1-5-32-560' }
    
    DBG ('Normalize GMSA account to be usable in WinNT:// ADSI path: {0}' -f $gmsa)
    DBGIF $MyInvocation.MyCommand.Name { $gmsa -notmatch '\A[^\\]+\\[^\\]+\Z' }
    [string] $gmsaPathId = $gmsa.Replace('\', '/')
    DBG ('Add the GMSA into WAAG group in the machine domain: {0}' -f $gmsaPathId)
    DBGSTART
    $waag.Add("WinNT://$gmsaPathId,user")
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Restart the ADFS service to update its group membership')
    DBGSTART
    Restart-Service adfssrv
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

  } else {

    DBGIF ('Install ADFS farm with normal SVC account: {0} | {1} | {2} | {3} | {4}' -f $organizationDNS, $identifier, $thumbprint, $companyDisplay, $svcCred.UserName) { $true }
    DBGSTART
    $adfsRes = Install-AdfsFarm -CertificateThumbprint $thumbprint -FederationServiceName $identifier -FederationServiceDisplayName $companyDisplay -ServiceAccountCredential $svcCred
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  DBG ('ADFS install status: {0} | {1} | {2}' -f $adfsRes.Status, $adfsRes.Message, $adfsRes.Context)
  DBGIF $MyInvocation.MyCommand.Name { $adfsRes.Status -ne 'Success' }

  $organizationDNS = $organizationDNS.ToLower()
  $identifier = $identifier.ToLower()

  DBG ('Verify the SSL certificates')
  DBGSTART
  $sslCerts = Get-AdfsSslCertificate
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('ADFS TLS certificates: {0}' -f ($sslCerts | Out-String))
  # Note: the EnterpriseRegistration automatically bound on Windows 2016
  DBGIF ("Weird number of ADFS certificates: {0}" -f $sslCerts.Count) { (($global:thisOSVersionNumber -lt 10) -and ($sslCerts.Count -ne 3)) -or (($global:thisOSVersionNumber -ge 10) -and ($sslCerts.Count -ne 4)) }

  [string] $uniqueCertThumbprint = [string]::Empty
  foreach ($oneSslCert in $sslCerts) {

    DBGIF ('Weird ADFS TLS certificate: {0} | {1} | {2}' -f $oneSslCert.HostName, $oneSslCert.CertificateHash, $oneSslCert.PortNumber) { $oneSslCert.CertificateHash -ne $thumbprint }
    DBGIF ('Weird ADFS TLS certificate: {0} | {1} | {2}' -f $oneSslCert.HostName, $oneSslCert.CertificateHash, $oneSslCert.PortNumber) { ($oneSslCert.Hostname -ne 'localhost') -and ($oneSslCert.Hostname -ne ('EnterpriseRegistration.{0}' -f $deviceRegDomain)) -and ($oneSslCert.Hostname -ne $identifier) -and ($oneSslCert.Hostname -ne ('certauth.{0}' -f $identifier)) }
    DBGIF ('Weird ADFS TLS certificate: {0} | {1} | {2}' -f $oneSslCert.HostName, $oneSslCert.CertificateHash, $oneSslCert.PortNumber) { ($global:thisOSVersionNumber -lt 10) -and ($oneSslCert.Hostname -eq ('EnterpriseRegistration.{0}' -f $deviceRegDomain)) }
    DBGIF ('Weird ADFS TLS certificate: {0} | {1} | {2}' -f $oneSslCert.HostName, $oneSslCert.CertificateHash, $oneSslCert.PortNumber) { ($oneSslCert.PortNumber -ne 443) -and ($oneSslCert.PortNumber -ne 49443) }

    DBGIF ('Non-identical ADFS TLS certificate found: {0} | {1} | {2}' -f $oneSslCert.HostName, $oneSslCert.CertificateHash, $oneSslCert.PortNumber) { (Is-ValidString $uniqueCertThumbprint) -and ($uniqueCertThumbprint -ne $oneSslCert.CertificateHash) }
    $uniqueCertThumbprint = $oneSslCert.CertificateHash
  }



  DBG ('Get ADFS properties')
  DBGSTART
  $adfsProp = Get-AdfsProperties
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $adfsProp }
  
  # Note: verify some defaults
  DBGIF $MyInvocation.MyCommand.Name { $adfsProp.CertificateDuration -ne 365 }
  DBGIF $MyInvocation.MyCommand.Name { $adfsProp.KmsiEnabled }
  DBGIF $MyInvocation.MyCommand.Name { -not $adfsProp.PersistentSsoEnabled }

  $now = [DateTime]::Now
  $urn = 'urn:fdc:{0}:{1:D4}-{2:D2}:{3}' -f $organizationDNS, $now.Year, $now.Month, $identifier
  DBG ('Set ADFS identifier: {0}' -f $urn)
  DBGSTART
  Set-AdfsProperties -Identifier $urn
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if ($enableKmsi) {

    DBG ('Enable KMSI')
    DBGSTART
    Set-AdfsProperties -EnableKmsi $true
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  if ($extranetLockout -gt 0) {

    DBG ('Enable extranet lockout: {0} | {1}' -f $extranetLockout, $extranetObservation)
    DBGSTART
    Set-AdfsProperties -EnableExtranetLockout $true -ExtranetLockoutThreshold $extranetLockout -ExtranetObservationWindow $extranetObservation
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  if (Is-ValidString $extendedProtection) {

    DBG ('Set extended protection for authentication: {0}' -f $extendedProtection)
    DBGSTART
    Set-AdfsProperties -ExtendedProtectionTokenCheck $extendedProtection
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  DBG ('Verify that the anonymous web services are running')
  [void] (Download-WebPage "http://$identifier/adfs/probe")
  
  [System.Xml.XmlDocument] $fedMeta = [XML] (Download-WebPage "https://$identifier/FederationMetadata/2007-06/FederationMetadata.xml")
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $fedMeta }

  [System.Xml.XmlDocument] $mexMeta = [XML] (Download-WebPage "https://$identifier/adfs/services/trust/mex")
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $mexMeta }

  [System.Xml.XmlDocument] $fsMeta = [XML] (Download-WebPage "https://$identifier/adfs/fs/federationserverservice.asmx")
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $fsMeta }

  [void] (Download-WebPage "https://$identifier/adfs/ls")

  DBGIF ('Weird entity ID: {0}' -f $fedMeta.EntityDescriptor.entityId) { $fedMeta.EntityDescriptor.entityId -ne $urn }
  DBGIF ('Weird passive endpoint: {0}' -f (($fedMeta.EntityDescriptor.RoleDescriptor | select -Expand PassiveRequestorEndpoint | select -Expand EndpointReference | select -Expand Address) -join ', ')) { Is-NonNull ($fedMeta.EntityDescriptor.RoleDescriptor | select -Expand PassiveRequestorEndpoint | select -Expand EndpointReference | select -Expand Address | ? { $_ -ne "https://$identifier/adfs/ls/" }) }
}



################
#
# Note: Main code
#

if ($global:thisOSVersionNumber -lt 6.3) { 

  DBGIF $MyInvocation.MyCommand.Name { $global:thisOSVersionNumber -lt 6.3 }
  exit
}


DBG ('Verify that the instance name is DNS resolvable: {0}' -f $appConfig.instance)
DBGSTART
[IPAddress[]] $instanceIPs = [Net.Dns]::Resolve($appConfig.instance).AddressList
DBGER $MyInvocation.MyCommand.Name $error
DBGEND
DBG ('Name resolved as: {0}' -f (($instanceIPs | Select -Expand IPAddressToString) -join ','))
DBGIF ('Cannot DNS resolve name: {0}' -f $appConfig.instance) { (Get-CountSafe $instanceIPs) -lt 1 }



# Note: this will always be setting of each individual host node because of the permissions
#       on the template. Both ADFS and WAP servers will have to have such a certificate
#       There may be a problem in fact, because I suspect that the certificate must be exactly the same
#       as ADFS farm synchronizes its thumbprint among the farm members
[System.Xml.XmlElement] $sslCaCert = $appConfig.SelectSingleNode('./cert[one[@appTag="tls"]]')
DBGIF $MyInvocation.MyCommand.Name { Is-Null $sslCaCert }
DBGIF $MyInvocation.MyCommand.Name { Is-Null $sslCaCert.one }
DBGIF $MyInvocation.MyCommand.Name { $sslCaCert.one -isnot 'System.Xml.XmlElement' }

#DBG ('Wait for the TLS certificate CA to finish')
#Wait-Machine (Get-FirstAppHostInInstance 'ca' $sslCaCert.instance 'waitParams')
[string] $tlsCert = Enroll-AppCertificate $sslCaCert.one
DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $tlsCert }



if ($appConfig.type -eq 'adfs') {

  # Note: yet before the ADFS install finishes, because the WAP installation waits for the first ADFS
  #       server only.
  [System.Xml.XmlElement] $firstWAP = $null
  DBG ('We have overall instance members: {0} | wap members = {1}' -f (Get-CountSafe $allAppConfigs), (Get-CountSafe ($allAppConfigs | ? { $_.type -eq 'wap' })))
  $firstWAP = $allAppConfigs | ? { $_.type -eq 'wap' } | Select -First 1

  if (Is-NonNull $firstWAP) {

    DBG ('We have to add the possible WAP install account into local Administrators on all ADFS farm members: {0} | {1}' -f $firstWAP.app.iLogin, $firstWAP.app.iDomain)
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $firstWAP.app.iLogin }
    Add-MemberLocalGroup 'Administrators' $firstWAP.app.iLogin $firstWAP.app.iDomain
  }

  #
  #

  if ($firstAppHostInInstance) {

    DBG ('Check if signature and decryption certificates are requestes')
    [System.Xml.XmlElement] $sigCaCert = $appConfig.SelectSingleNode('./cert[one[@appTag="sig"]]')
    [System.Xml.XmlElement] $decryptCaCert = $appConfig.SelectSingleNode('./cert[one[@appTag="decrypt"]]')
    DBG ('Signature certificate requested: {0} | {1} | {2}' -f (Is-NonNull $sigCaCert), $sigCaCert.instance, $sigCaCert.template)
    DBG ('Decryption certificate requested: {0} | {1} | {2}' -f (Is-NonNull $decryptCaCert), $decryptCaCert.instance, $decryptCaCert.template)

    [string] $sigCert = $null
    if (Is-NonNull $sigCaCert) {

      DBG ('Enroll signature certificate')

      if ($sigCaCert.instance -ne $sslCaCert.instance) {

        DBG ('Wait for the SIGNATURE certificate CA to finish')
        Wait-Machine (Get-FirstAppHostInInstance 'ca' $sigCaCert.instance 'waitParams')
      }

      [string] $sigCert = Enroll-AppCertificate $sigCaCert.one
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sigCert }
    }


    [string] $decryptCert = $null
    if (Is-NonNull $decryptCaCert) {

      DBG ('Enroll decryption certificate')
 
      if ($decryptCaCert.instance -ne $sslCaCert.instance) {

        DBG ('Wait for the DECRYPTION certificate CA to finish')
        Wait-Machine (Get-FirstAppHostInInstance 'ca' $decryptCaCert.instance 'waitParams')
      }

      [string] $decryptCert = Enroll-AppCertificate $decryptCaCert.one
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $decryptCert }
    }

  
    if (Is-ValidString $appConfig.gmsa.login) {

      DBG ('Going to build first ADFS with GMSA: {0} | {1}' -f $appConfig.gmsa.login, $appConfig.gmsa.domain)
      
      # Note: we cannot use the Get-SAMLogin because the account does not exist yet
      [string] $gmsaSAMLogin = $appConfig.gmsa.login
      if ($gmsaSAMLogin -notlike '?*$') {

       $gmsaSAMLogin = '{0}$' -f $gmsaSAMLogin
      }

      [string] $netbiosDomainName = $appConfig.gmsa.domain

      $gmsaSAMLogin = '{0}\{1}' -f $netbiosDomainName, $gmsaSAMLogin
      DBG ('GMSA SAM login determined: {0}' -f $gmsaSAMLogin)

      Build-PrimaryADFS -organizationDNS $appConfig.urnDNS -identifier $appConfig.instance -deviceRegDomain $appConfig.deviceRegDomain -thumbprint $tlsCert -companyDisplay $appConfig.display -gmsa $gmsaSAMLogin -extranetLockout (Parse-IntSafe $appConfig.lockout.threshold) -extranetObservation $appConfig.lockout.window -enableKmsi (Parse-BoolSafe $appConfig.kmsi.enable) -extendedProtection $appConfig.extendedProtection

    } else {

      DBG ('Going to build first ADFS with manual SVC account')

      [System.Xml.XmlElement] $manualSvc = $appConfig.SelectSingleNode('./svc[@appTag="manual"]')
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $manualSvc }

      $svcCred = Get-PwdCredentials -user $manualSvc.login -domain $manualSvc.domain -password $manualSvc.pwd -samLogin $true

      Build-PrimaryADFS -organizationDNS $appConfig.urnDNS -identifier $appConfig.instance -deviceRegDomain $appConfig.deviceRegDomain -thumbprint $tlsCert -companyDisplay $appConfig.display -svcCred $svcCred -extranetLockout (Parse-IntSafe $appConfig.lockout.threshold) -extranetObservation $appConfig.lockout.window -enableKmsi (Parse-BoolSafe $appConfig.kmsi.enable) -extendedProtection $appConfig.extendedProtection
    }

    DBG ('Check if we need to create any claim description')
    DBGSTART
    $claimDescsRequested = $appConfig.SelectNodes('./claimDesc')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Claim descriptions requested: {0}' -f (Get-CountSafe $claimDescsRequested))

    if ((Get-CountSafe $claimDescsRequested) -gt 0) {

      foreach ($oneClaimDesc in $claimDescsRequested) {
    
        DBG ('Define one claim description: {0} | {1} | {2}' -f $oneClaimDesc.display, $oneClaimDesc.urn, $oneClaimDesc.oauth)
        DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $oneClaimDesc.display) -or (Is-EmptyString $oneClaimDesc.urn) -or (Is-EmptyString $oneClaimDesc.oauth) }
        DBGSTART
        Add-AdfsClaimDescription -Name $oneClaimDesc.display -ClaimType $oneClaimDesc.urn -ShortName $oneClaimDesc.oauth
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }
  
  } else {

    DBG ('Wait for the first ADFS server to finish')
    Wait-Machine $firstAppHost
  }

  
  DBG ('Should we install the Office365 redirector: {0}' -f (Parse-BoolSafe $firstAppConfig.o365redir.install))
  if (Parse-BoolSafe $firstAppConfig.o365redir.install) {

    Run-Process (Join-Path $global:rootDir 'IIS\SevecekOffice365Redirector\Debug\SevecekRedirector.bat') ($firstAppConfig.o365redir.replace)
  }
}


if ($appConfig.type -eq 'wap') {

  DBG ('Doing WAP installation: {0} | {1}' -f $tlsCert, $appConfig.instance)

  DBG ('Wait for the first APP host to finish')
  DBGIF $MyInvocation.MyCommand.Name { $firstAppHostInInstance }
  Wait-Machine $firstAppHost

  DBG ('ADFS machine finished but the service is in delayed load, so give it some more time to come online: {0}' -f $appConfig.instance)
  Wait-Periodically -maxTrialCount 15 -sleepSec 7 -sleepMsg 'Waiting until the ADFS server comes online' -scriptBlockWhichReturnsTrueToStop {

    [bool] $online = $false
    [System.Net.WebClient] $webClient = New-Object System.Net.WebClient
    [string] $fedMetaUri = 'https://{0}/FederationMetadata/2007-06/FederationMetadata.xml' -f $appConfig.instance
    DBG ('Downloading federation metadata: {0}' -f $fedMetaUri)
    DBGSTART
    [string] $xmlStr = $webClient.DownloadString($fedMetaUri)
    $online = (([int] $error.Length) -eq 0) -and ($xmlStr.Length -gt 200)
    DBGEND
    if ($online) { 

      DBG ('Downloaded ADFS federation metadata: {0} | {1}' -f $xmlStr.Length, $xmlStr.SubString(0, 200))
    }

    return $online
  }


  DBG ('Create credential for the current install account: {0} | {1}' -f $appConfig.app.iLogin, $appConfig.app.iDomain)
  $adfsAdminCred = Get-PwdCredentials $appConfig.app.iLogin $appConfig.app.iDomain $appConfig.app.iPwd

  DBG ('Do the installation as such: {0} | {1} | {2}' -f $adfsAdminCred.UserName, $tlsCert, $appConfig.instance)
  DBGSTART
  Install-WebApplicationProxy -FederationServiceTrustCredential $adfsAdminCred -CertificateThumbprint $tlsCert -FederationServiceName $appConfig.instance
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
}



DBG ('Throw up any remaining errors')
DBGSTART; DBGEND




# SIG # Begin signature block
# MIIc/QYJKoZIhvcNAQcCoIIc7jCCHOoCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCl6BJh3Izaj9SF
# +4ojAHZ6sV1RWEJf1D/wi4qigIKzPqCCGAQwggTlMIIDzaADAgECAhA5vUKe0oFu
# utW8yQO0umXnMA0GCSqGSIb3DQEBCwUAMHUxCzAJBgNVBAYTAklMMRYwFAYDVQQK
# Ew1TdGFydENvbSBMdGQuMSkwJwYDVQQLEyBTdGFydENvbSBDZXJ0aWZpY2F0aW9u
# IEF1dGhvcml0eTEjMCEGA1UEAxMaU3RhcnRDb20gQ2xhc3MgMiBPYmplY3QgQ0Ew
# HhcNMTYxMjAxMTU1MTEzWhcNMTgxMjAxMTU1MTEzWjBRMQswCQYDVQQGEwJDWjEa
# MBgGA1UECAwRSmlob21vcmF2c2t5IEtyYWoxDTALBgNVBAcMBEJybm8xFzAVBgNV
# BAMMDk9uZHJlaiBTZXZlY2VrMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
# AQEAr9E9hNj06bash9JX97kpsqK9Z/ciOBC6trI4nvlW9CPwhKBTb5wArhxLYZBG
# 9jWPWrdy1nL/cm5qMqBb/mogYwMwvEYWMvsIOOVn6HD9lVhNAovD6PHz0ziBBKIs
# zXTjyUPQaoIlIELovz967m78HJdUZJGxqhluAsS9o9/fEzA7XXUhUuqRKsetuZV/
# Asfh5sOveeoRsbeW4daTWvtz3TJuULL0w43LNVYJkd6LL8cegvLPVZUe1N7skvid
# EvntdlowQsJlqFdrH3SGKIPKA6ObcY8SZWkEQSbVBF8Kum1UT+jN0gm+84FwOg5W
# qKx+VvTK2ljVWnPrCD0Zzu2oIQIDAQABo4IBkzCCAY8wDgYDVR0PAQH/BAQDAgeA
# MBMGA1UdJQQMMAoGCCsGAQUFBwMDMAkGA1UdEwQCMAAwHQYDVR0OBBYEFG2vSo3N
# hQWILeUs0oN9XzHTejcfMB8GA1UdIwQYMBaAFD5ik5rXxxnuPo9JEIVVFSDjlIQc
# MG0GCCsGAQUFBwEBBGEwXzAkBggrBgEFBQcwAYYYaHR0cDovL29jc3Auc3RhcnRz
# c2wuY29tMDcGCCsGAQUFBzAChitodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0
# cy9zY2EuY29kZTIuY3J0MDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwuc3Rh
# cnRzc2wuY29tL3NjYS1jb2RlMi5jcmwwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5z
# dGFydHNzbC5jb20vMFEGA1UdIARKMEgwCAYGZ4EMAQQBMDwGCysGAQQBgbU3AQIF
# MC0wKwYIKwYBBQUHAgEWH2h0dHBzOi8vd3d3LnN0YXJ0c3NsLmNvbS9wb2xpY3kw
# DQYJKoZIhvcNAQELBQADggEBAJuRiEvHtIYSpsmMkPhTz4QOOShN3p5KWdf8vm71
# A33CR9fds10d8D2B2aE+vjmHJ69GY0bbfg5oZY2Lsq2euL7Da5/hS8+6T3MEtD4h
# njfHV7mxmoSfFuy/KDipoV6uwhI+ksqchXYdUH+5uCQO0MOO8ITjAgzUQsnZ4UIB
# HBGeP+e+3ljxSYSXWdPIrgxdR971P/HhWSVfKNlmBgEKMQM5Jy0aAd4jxSl/AzdY
# t0+6pliFJ1peGhdFni2Fm8fu5oN68aTIrNtc5WY7Lzgf+sRTVeWORWS37+1zAD0m
# jzd8gyfBLxRuaRSfjYxny0rLXelAwfiA3ze2DU2Bfg9/rfcwggXYMIIDwKADAgEC
# AhBsO9J+3TyUnpWOKKmzx1egMA0GCSqGSIb3DQEBCwUAMH0xCzAJBgNVBAYTAklM
# MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMSswKQYDVQQLEyJTZWN1cmUgRGlnaXRh
# bCBDZXJ0aWZpY2F0ZSBTaWduaW5nMSkwJwYDVQQDEyBTdGFydENvbSBDZXJ0aWZp
# Y2F0aW9uIEF1dGhvcml0eTAeFw0xNTEyMTYwMTAwMDVaFw0zMDEyMTYwMTAwMDVa
# MHUxCzAJBgNVBAYTAklMMRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMSkwJwYDVQQL
# EyBTdGFydENvbSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEjMCEGA1UEAxMaU3Rh
# cnRDb20gQ2xhc3MgMiBPYmplY3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
# ggEKAoIBAQC5FARY97LFhiwIMmCtCCbAgXe5aBnZFSsdGGnk2hqWBZcuZHkaqT1R
# M1rQd2r0ApNBw466cBur2Ht0b5jo17mpPmh2pImgIqwX1in4u7hhn9IH0GYOMEcg
# K3ACHv5zCRxxNLXifqmsqKfxjjpABnaSyvd4bO9YBXN9f4NQ6aJVAuMArpanxsJk
# e+P4WECVLk17v92CAN5JVaczI+baT/lgo5NVcTEkloCViSbIfU6ILeyhOSQZvpom
# MYk8eJqI0nimOTJJfmXangNDsrX8np+3lXD0+6rCZisXRWIaeffyTMHZ31Qj1D50
# WYdRtX5yev4WgaXoKJQN3lkgXUcytvyHAgMBAAGjggFaMIIBVjAOBgNVHQ8BAf8E
# BAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAy
# BgNVHR8EKzApMCegJaAjhiFodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9zZnNjYS5j
# cmwwZgYIKwYBBQUHAQEEWjBYMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5zdGFy
# dHNzbC5jb20wMAYIKwYBBQUHMAKGJGh0dHA6Ly9haWEuc3RhcnRzc2wuY29tL2Nl
# cnRzL2NhLmNydDAdBgNVHQ4EFgQUPmKTmtfHGe4+j0kQhVUVIOOUhBwwHwYDVR0j
# BBgwFoAUTgvvGqRAW6UXaYcwyjRoQ9BBrvIwPwYDVR0gBDgwNjA0BgRVHSAAMCww
# KgYIKwYBBQUHAgEWHmh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeTANBgkq
# hkiG9w0BAQsFAAOCAgEAY6U81bNtJyjY67pTrzAL6kpdEtX5mspw+kxjjNdNVH5G
# 6lLnhaEkIxqdpvY/Wdw+UdNtExs+N8efKPSwh2m/BxXj2fSeLMwXcwHFookScEER
# 8ez0quCNzioqNHac7LCXPEnQzbtG2FHlePKNDWh8eU6KxiAzNzIrIxPthinHGgLT
# BOACHQM2YTlD8YoU5oN3dLmBOqtH0BDMZoLcjEIoEW1zC+TnVb3yU1G0xub6gnN7
# lP50vbAiHJYrnywQiXaloBV8B9YYfe6ZgvjqxwufwFcMVyE3UmCuDTsOpjqDEKpJ
# 25s+FUdkie5VqCS1aaudLo31X+9UvP45pfgyRqzyfUnVEhH4ZXxlBWZMzj2Xov5+
# m/+H3kxYuFA5xdqdshj/Zx00S7PkCSF+8M1NCcvFgQwjIw61bZAjDBl3P3a8xNTX
# sb2CjFdiNKbT3LD6IGeIf0b/EbPf0FXdvBrxm0ofMOhnngdPolPYCtoOGtZPAVe/
# xeu+/ZyKv6TSHlshaUO0iYfsmbXnZ51vvt/kkjwms9/qPFxSuE0fjEfF7aQazwRE
# Df2hiVPR0pAhvShtM3oU4XreEFEUWEYHs25fYV4WMmxkUKSgmSmwRq45tvtGH4LT
# b5+cd+iLqK8rBQL0E6xaUjjGfsYx7bueIvqTvCkrQvoxMbn/qDHCiypowDVq6TAw
# ggZqMIIFUqADAgECAhADAZoCOv9YsWvW1ermF/BmMA0GCSqGSIb3DQEBBQUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0Et
# MTAeFw0xNDEwMjIwMDAwMDBaFw0yNDEwMjIwMDAwMDBaMEcxCzAJBgNVBAYTAlVT
# MREwDwYDVQQKEwhEaWdpQ2VydDElMCMGA1UEAxMcRGlnaUNlcnQgVGltZXN0YW1w
# IFJlc3BvbmRlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNkXfx8
# s+CCNeDg9sYq5kl1O8xu4FOpnx9kWeZ8a39rjJ1V+JLjntVaY1sCSVDZg85vZu7d
# y4XpX6X51Id0iEQ7Gcnl9ZGfxhQ5rCTqqEsskYnMXij0ZLZQt/USs3OWCmejvmGf
# rvP9Enh1DqZbFP1FI46GRFV9GIYFjFWHeUhG98oOjafeTl/iqLYtWQJhiGFyGGi5
# uHzu5uc0LzF3gTAfuzYBje8n4/ea8EwxZI3j6/oZh6h+z+yMDDZbesF6uHjHyQYu
# RhDIjegEYNu8c3T6Ttj+qkDxss5wRoPp2kChWTrZFQlXmVYwk/PJYczQCMxr7GJC
# kawCwO+k8IkRj3cCAwEAAaOCAzUwggMxMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMB
# Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYDVR0gBIIBtjCCAbIw
# ggGhBglghkgBhv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdp
# Y2VydC5jb20vQ1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBBAG4AeQAgAHUAcwBl
# ACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBu
# AHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0
# AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABTACAAYQBuAGQAIAB0
# AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABBAGcAcgBlAGUAbQBl
# AG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5
# ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBhAHQAZQBkACAAaABl
# AHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAuMAsGCWCGSAGG/WwD
# FTAfBgNVHSMEGDAWgBQVABIrE5iymQftHt+ivlcNK2cCzTAdBgNVHQ4EFgQUYVpN
# JLZJMp1KKnkag0v0HonByn0wfQYDVR0fBHYwdDA4oDagNIYyaHR0cDovL2NybDMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwOKA2oDSGMmh0
# dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3Js
# MHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNl
# cnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20v
# RGlnaUNlcnRBc3N1cmVkSURDQS0xLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAnSV+
# GzNNsiaBXJuGziMgD4CH5Yj//7HUaiwx7ToXGXEXzakbvFoWOQCd42yE5FpA+94G
# AYw3+puxnSR+/iCkV61bt5qwYCbqaVchXTQvH3Gwg5QZBWs1kBCge5fH9j/n4hFB
# pr1i2fAnPTgdKG86Ugnw7HBi02JLsOBzppLA044x2C/jbRcTBu7kA7YUq/OPQ6dx
# nSHdFMoVXZJB2vkPgdGZdA0mxA5/G7X1oPHGdwYoFenYk+VVFvC7Cqsc21xIJ2bI
# o4sKHOWV2q7ELlmgYd3a822iYemKC23sEhi991VUQAOSK2vCUcIKSK+w1G7g9BQK
# Ohvjjz3Kr2qNe9zYRDCCBs0wggW1oAMCAQICEAb9+QOWA63qAArrPye7uhswDQYJ
# KoZIhvcNAQEFBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu
# YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQg
# QXNzdXJlZCBJRCBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAw
# MFowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE
# CxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJ
# RCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6IItmfnKwkKV
# pYBzQHDSnlZUXKnE0kEGj8kz/E1FkVyBn+0snPgWWd+etSQVwpi5tHdJ3InECtqv
# y15r7a2wcTHrzzpADEZNk+yLejYIA6sMNP4YSYL+x8cxSIB8HqIPkg5QycaH6zY/
# 2DDD/6b3+6LNb3Mj/qxWBZDwMiEWicZwiPkFl32jx0PdAug7Pe2xQaPtP77blUjE
# 7h6z8rwMK5nQxl0SQoHhg26Ccz8mSxSQrllmCsSNvtLOBq6thG9IhJtPQLnxTPKv
# mPv2zkBdXPao8S+v7Iki8msYZbHBc63X8djPHgp0XEK4aH631XcKJ1Z8D2KkPzIU
# YJX9BwSiCQIDAQABo4IDejCCA3YwDgYDVR0PAQH/BAQDAgGGMDsGA1UdJQQ0MDIG
# CCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcD
# CDCCAdIGA1UdIASCAckwggHFMIIBtAYKYIZIAYb9bAABBDCCAaQwOgYIKwYBBQUH
# AgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5o
# dG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0
# AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1
# AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABp
# AGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBl
# AGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBo
# AGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg
# AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAg
# AGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMBIGA1UdEwEB
# /wQIMAYBAf8CAQAweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6
# MHgwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3Vy
# ZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwHQYDVR0OBBYEFBUAEisTmLKZB+0e
# 36K+Vw0rZwLNMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqG
# SIb3DQEBBQUAA4IBAQBGUD7Jtygkpzgdtlspr1LPUukxR6tWXHvVDQtBs+/sdR90
# OPKyXGGinJXDUOSCuSPRujqGcq04eKx1XRcXNHJHhZRW0eu7NoR3zCSl8wQZVann
# 4+erYs37iy2QwsDStZS9Xk+xBdIOPRqpFFumhjFiqKgz5Js5p8T1zh14dpQlc+Qq
# q8+cdkvtX8JLFuRLcEwAiR78xXm8TBJX/l/hHrwCXaj++wc4Tw3GXZG5D2dFzdaD
# 7eeSDY2xaYxP+1ngIw/Sqq4AfO6cQg7PkdcntxbuD8O9fAqg7iwIVYUiuOsYGk38
# KiGtSTGDR5V3cdyxG0tLHBCcdxTBnU8vWpUIKRAmMYIETzCCBEsCAQEwgYkwdTEL
# MAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKTAnBgNVBAsTIFN0
# YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSMwIQYDVQQDExpTdGFydENv
# bSBDbGFzcyAyIE9iamVjdCBDQQIQOb1CntKBbrrVvMkDtLpl5zANBglghkgBZQME
# AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM
# BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG
# SIb3DQEJBDEiBCC+NzKQMQqIM2t6RA3KN7xZsU7O0BKX13IWcjVqC7hHkzANBgkq
# hkiG9w0BAQEFAASCAQBhrbdN1FAOWUrAgivAoofC6eOadsS6vnUhd+2NbYGi6z2d
# y2BZ9bWZH3X5l1cubBzGhzhYj5tWaCyNGeqxYOvAmu6Xw5iNNpso4fmKhD7prd8r
# 7HnHFPdBTeFYzI1m6FR+ZsnvQyGfTXe1O7SC2Xnp1mgARorYYisvMlYcUfuUYW+R
# OjB8OToNNml0uostm8gkitdnDdHvjPPx3ARzsSL8wmLMbWyJU7x7lCK52HrQcM9h
# pN57b0nq3fryV23JdUK25RWmkc5YGRQ01/VIsn/JvTVdnTdXRw40cYWu4m+g6kIs
# B3K2vYc6ufyuUWpbsY75akHC3FoqdSk9bTPEvGc/oYICDzCCAgsGCSqGSIb3DQEJ
# BjGCAfwwggH4AgEBMHYwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0
# IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNl
# cnQgQXNzdXJlZCBJRCBDQS0xAhADAZoCOv9YsWvW1ermF/BmMAkGBSsOAwIaBQCg
# XTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xODAy
# MDYwODM2MDhaMCMGCSqGSIb3DQEJBDEWBBTmQQNw0wI0SQa3BxFV5gGOnC12kDAN
# BgkqhkiG9w0BAQEFAASCAQAabnxtb9sVx8V0pOcH+TEFe50kp9jQwQpjOTAR36R8
# OTUQUEW9oIlVXBvoU9/t5iZlVZ+daaZ6f5RRd/8zjxi8kTf7mPYpHvp3xyTlAP4d
# ZCZuPX8z4jipIFD8F9w7rGc7aFwWHFduXX+pfCb8ue3jj6gZYqTyfHR4yIjzCJPy
# ixWEpvmab3qv4XTC7lNviT92TFU3LWXK0ch32C4fwjwWWjD2+CQ+kJQgEFj5n1m6
# CwOEsDnh8QVev9WIvD2Y+F9TBwVHNw05jtDFlHF4BilTEvrXSBZj1/3JgzimUaun
# vyFuyt8inoLbj6UZIqLAl0MAJE2gJw8I9HotqWCRyIrZ
# SIG # End signature block