Public/DGateway.ps1
|
. "$PSScriptRoot/../Private/CertificateHelper.ps1" . "$PSScriptRoot/../Private/PlatformHelper.ps1" . "$PSScriptRoot/../Private/TokenHelper.ps1" $script:DGatewayConfigFileName = 'gateway.json' $script:DGatewayCertificateFileName = 'server.crt' $script:DGatewayPrivateKeyFileName = 'server.key' $script:DGatewayProvisionerPublicKeyFileName = 'provisioner.pem' $script:DGatewayProvisionerPrivateKeyFileName = 'provisioner.key' $script:DGatewayDelegationPublicKeyFileName = 'delegation.pem' $script:DGatewayDelegationPrivateKeyFileName = 'delegation.key' $script:DGatewayCustomUsersFileName = 'users.txt' function Get-DGatewayVersion { param( [Parameter(Mandatory = $true, Position = 0)] [ValidateSet('PSModule', 'Installed')] [string] $Type ) if ($Type -eq 'PSModule') { $ManifestPath = "$PSScriptRoot/../DevolutionsGateway.psd1" $Manifest = Import-PowerShellDataFile -Path $ManifestPath $DGatewayVersion = $Manifest.ModuleVersion } elseif ($Type -eq 'Installed') { if ($IsWindows) { $UninstallReg = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' ` | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_ -Match 'Devolutions Gateway' } if ($UninstallReg) { $DGatewayVersion = '20' + $UninstallReg.DisplayVersion } } elseif ($IsMacOS) { throw 'not supported' } elseif ($IsLinux) { $PackageName = 'devolutions-gateway' $DpkgStatus = $(dpkg -s $PackageName 2>$null) $DpkgMatches = $($DpkgStatus | Select-String -AllMatches -Pattern 'version: (\S+)').Matches if ($DpkgMatches) { $VersionQuad = $DpkgMatches.Groups[1].Value $VersionTriple = $VersionQuad -Replace '^(\d+)\.(\d+)\.(\d+)\.(\d+)$', "`$1.`$2.`$3" $DGatewayVersion = $VersionTriple } } } $DGatewayVersion } class DGatewayListener { [string] $InternalUrl [string] $ExternalUrl DGatewayListener() { } DGatewayListener([string] $InternalUrl, [string] $ExternalUrl) { $this.InternalUrl = $InternalUrl $this.ExternalUrl = $ExternalUrl } } function New-DGatewayListener() { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string] $ListenerUrl, [Parameter(Mandatory = $true, Position = 1)] [string] $ExternalUrl ) return [DGatewayListener]::new($ListenerUrl, $ExternalUrl) } class DGatewaySubProvisionerKey { [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Id [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Value [ValidateNotNullOrEmpty()] [ValidateSet('Spki','Rsa')] [string] $Format [ValidateNotNullOrEmpty()] [ValidateSet('Multibase','Base64', 'Base64Pad', 'Base64Url', 'Base64UrlPad')] [string] $Encoding DGatewaySubProvisionerKey( [string] $Id, [string] $Value, [string] $Format = 'Spki', [string] $Encoding = 'Multibase' ) { $this.Id = $Id $this.Value = $Value $this.Format = $Format $this.Encoding = $Encoding } DGatewaySubProvisionerKey([PSCustomObject] $object) { $this.Id = $object.Id $this.Value = $object.Value $this.Format = $object.Format $this.Encoding = $object.Encoding } DGatewaySubProvisionerKey([Hashtable] $table) { $this.Id = $table.Id $this.Value = $table.Value $this.Format = $table.Format $this.Encoding = $table.Encoding } } class DGatewaySubscriber { [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Uri] $Url [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Token DGatewaySubscriber( [System.Uri] $Url, [string] $Token ) { $this.Url = $Url $this.Token = $Token } DGatewaySubscriber([PSCustomObject] $object) { $this.Url = $object.Url $this.Token = $object.Token } DGatewaySubscriber([Hashtable] $table) { $this.Url = $table.Url $this.Token = $table.Token } } class DGatewayNgrokTunnel { [string] $Proto [string] $Metadata [string[]] $AllowCidrs [string[]] $DenyCidrs # HTTP tunnel [string] $Domain [System.Nullable[System.Single]] $CircuitBreaker [System.Nullable[System.Boolean]] $Compression # TCP tunnel [string] $RemoteAddr DGatewayNgrokTunnel() { } } class DGatewayNgrokConfig { [string] $AuthToken [System.Nullable[System.UInt32]] $HeartbeatInterval [System.Nullable[System.UInt32]] $HeartbeatTolerance [string] $Metadata [string] $ServerAddr [PSCustomObject] $Tunnels DGatewayNgrokConfig() { } } function New-DGatewayNgrokTunnel() { [CmdletBinding(DefaultParameterSetName = 'http')] param( [Parameter(Mandatory = $false, ParameterSetName = 'http', HelpMessage = "HTTP tunnel")] [switch] $Http, [Parameter(Mandatory = $false, ParameterSetName = 'tcp', HelpMessage = "TCP tunnel")] [switch] $Tcp, [Parameter(Mandatory = $false, HelpMessage = "User-defined metadata that appears when listing tunnel sessions with ngrok")] [string] $Metadata, [ValidateScript({ $_ -match '^((\d{1,3}\.){3}\d{1,3}\/\d{1,2}|([\dA-Fa-f]{0,4}:){2,7}[\dA-Fa-f]{0,4}\/\d{1,3})$' })] [Parameter(Mandatory = $false, HelpMessage = "Reject connections that do not match the given CIDRs")] [string[]] $AllowCidrs, [ValidateScript({ $_ -match '^((\d{1,3}\.){3}\d{1,3}\/\d{1,2}|([\dA-Fa-f]{0,4}:){2,7}[\dA-Fa-f]{0,4}\/\d{1,3})$' })] [Parameter(Mandatory = $false, HelpMessage = "Reject connections that match the given CIDRs")] [string[]] $DenyCidrs, [ValidateScript({ $_ -match '^(\*\.)?([a-zA-Z0-9](-?[a-zA-Z0-9])*\.)*[a-zA-Z]{2,}$' })] [Parameter(Mandatory = $false, ParameterSetName = 'http', HelpMessage = "Any valid domain or hostname previously registered with ngrok")] [string] $Domain, [ValidateRange(0.0, 1.0)] [Parameter(Mandatory = $false, ParameterSetName = 'http', HelpMessage = "Reject requests when 5XX responses exceed this ratio")] [System.Single] $CircuitBreaker, [Parameter(Mandatory = $false, ParameterSetName = 'http', HelpMessage = "Use gzip compression on HTTP responses")] [System.Boolean] $Compression, [ValidateScript({ $_ -match '^([a-zA-Z0-9](-?[a-zA-Z0-9])*\.)*[a-zA-Z]{2,}:\d{1,5}$' })] [Parameter(Mandatory = $false, ParameterSetName = 'tcp', HelpMessage = "The remote TCP address and port to bind. For example: remote_addr: 2.tcp.ngrok.io:21746")] [string] $RemoteAddr ) $tunnel = [DGatewayNgrokTunnel]::new() if ($Tcp) { $tunnel.Proto = "tcp" } else { $tunnel.Proto = "http" } $properties = [DGatewayNgrokTunnel].GetProperties() | ForEach-Object { $_.Name } foreach ($param in $PSBoundParameters.GetEnumerator()) { if ($properties -Contains $param.Key) { $tunnel.($param.Key) = $param.Value } } $tunnel } function New-DGatewayNgrokConfig() { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $AuthToken ) $ngrok = [DGatewayNgrokConfig]::new() $ngrok.AuthToken = $AuthToken $ngrok } class DGatewayWebAppConfig { [bool] $Enabled [string] $Authentication [System.Nullable[System.UInt32]] $AppTokenMaximumLifetime [System.Nullable[System.UInt32]] $LoginLimitRate [string] $UsersFile DGatewayWebAppConfig() { } } function New-DGatewayWebAppConfig() { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [bool] $Enabled, [Parameter(Mandatory = $true)] [ValidateSet("None", "Custom")] [string] $Authentication ) $webapp = [DGatewayWebAppConfig]::new() $webapp.Enabled = $Enabled $webapp.Authentication = $Authentication $webapp } class DGatewayAgentTunnelConfig { [bool] $Enabled = $false [ValidateRange(1, 65535)] [System.UInt16] $ListenPort = 4433 DGatewayAgentTunnelConfig() { } DGatewayAgentTunnelConfig([bool] $Enabled, [System.UInt16] $ListenPort) { $this.Enabled = $Enabled $this.ListenPort = $ListenPort } } function New-DGatewayAgentTunnelConfig() { [CmdletBinding()] [OutputType('DGatewayAgentTunnelConfig')] param( [switch] $Enabled, [ValidateRange(1, 65535)] [System.UInt16] $ListenPort = 4433 ) [DGatewayAgentTunnelConfig]::new($Enabled.IsPresent, $ListenPort) } enum VerbosityProfile { Default Debug Tls All Quiet } class DGatewayConfig { [System.Nullable[Guid]] $Id [string] $Hostname [string] $RecordingPath [string] $TlsCertificateFile [string] $TlsPrivateKeyFile [string] $TlsPrivateKeyPassword [string] $TlsCertificateSource [string] $TlsCertificateSubjectName [string] $TlsCertificateStoreName [string] $TlsCertificateStoreLocation [string] $ProvisionerPublicKeyFile [string] $ProvisionerPrivateKeyFile [string] $DelegationPublicKeyFile [string] $DelegationPrivateKeyFile [DGatewaySubProvisionerKey] $SubProvisionerPublicKey [DGatewayListener[]] $Listeners [DGatewaySubscriber] $Subscriber [DGatewayNgrokConfig] $Ngrok [DGatewayWebAppConfig] $WebApp [DGatewayAgentTunnelConfig] $AgentTunnel [string] $LogDirective [string] $VerbosityProfile } function Remove-NullObjectProperties { [CmdletBinding()] param( [Parameter(ValueFromPipeline, Mandatory)] [object[]] $InputObject ) process { foreach ($OldObj in $InputObject) { $NonNullProperties = $OldObj.PSObject.Properties | Where-Object { ($_.Value -is [Array] -and $_.Value.Count -gt 0) -or (-Not [string]::IsNullOrEmpty($_.Value)) } | Select-Object -ExpandProperty Name $NewObj = $OldObj | Select-Object $NonNullProperties $NewObj.PSObject.Properties | Where-Object { $_.TypeNameOfValue.EndsWith('PSCustomObject') } | ForEach-Object { $NewObj."$($_.Name)" = $NewObj."$($_.Name)" | Remove-NullObjectProperties } $NewObj } } } function Save-DGatewayConfig { [CmdletBinding()] param( [string] $ConfigPath, [Parameter(Mandatory = $true)] [DGatewayConfig] $Config ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $ConfigFile = Join-Path $ConfigPath $DGatewayConfigFileName $ConfigClean = $Config | ConvertTo-Json -Depth 4 | ConvertFrom-Json # drop class type info $ConfigData = $ConfigClean | Remove-NullObjectProperties | ConvertTo-Json -Depth 4 [System.IO.File]::WriteAllLines($ConfigFile, $ConfigData, $(New-Object System.Text.UTF8Encoding $False)) } function Set-DGatewayConfig { [CmdletBinding()] param( [string] $ConfigPath, [string] $Force, [Guid] $Id, [string] $Hostname, [string] $RecordingPath, [DGatewayListener[]] $Listeners, [DGatewaySubscriber] $Subscriber, [string] $TlsCertificateFile, [string] $TlsPrivateKeyFile, [string] $TlsPrivateKeyPassword, [ValidateSet("External", "System")] [string] $TlsCertificateSource, [string] $TlsCertificateSubjectName, [string] $TlsCertificateStoreName, [ValidateSet("CurrentUser", "LocalMachine", "CurrentService")] [string] $TlsCertificateStoreLocation, [string] $ProvisionerPublicKeyFile, [string] $ProvisionerPrivateKeyFile, [string] $DelegationPublicKeyFile, [string] $DelegationPrivateKeyFile, [DGatewaySubProvisionerKey] $SubProvisionerPublicKey, [DGatewayNgrokConfig] $Ngrok, [DGatewayWebAppConfig] $WebApp, [DGatewayAgentTunnelConfig] $AgentTunnel, [VerbosityProfile] $VerbosityProfile ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath if (-Not (Test-Path -Path $ConfigPath -PathType 'Container')) { New-Item -Path $ConfigPath -ItemType 'Directory' | Out-Null } $ConfigFile = Join-Path $ConfigPath $DGatewayConfigFileName if (-Not (Test-Path -Path $ConfigFile -PathType 'Leaf')) { $config = [DGatewayConfig]::new() } else { $config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties } $properties = [DGatewayConfig].GetProperties() | ForEach-Object { $_.Name } foreach ($param in $PSBoundParameters.GetEnumerator()) { if ($properties -Contains $param.Key) { $config.($param.Key) = $param.Value } } Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function Get-DGatewayConfig { [CmdletBinding()] [OutputType('DGatewayConfig')] param( [string] $ConfigPath, [switch] $NullProperties, [switch] $Expand ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $ConfigFile = Join-Path $ConfigPath $DGatewayConfigFileName $config = [DGatewayConfig]::new() if (-Not (Test-Path -Path $ConfigFile -PathType 'Leaf')) { if ($NullProperties) { return $config } } $ConfigData = Get-Content -Path $ConfigFile -Encoding UTF8 $json = $ConfigData | ConvertFrom-Json [DGatewayConfig].GetProperties() | ForEach-Object { $Name = $_.Name if ($json.PSObject.Properties[$Name]) { $Property = $json.PSObject.Properties[$Name] $Value = $Property.Value $config.$Name = $Value } } if ($Expand) { Expand-DGatewayConfig $config } if (-Not $NullProperties) { $Properties = $Config.PSObject.Properties.Name $NonNullProperties = $Properties.Where( { -Not [string]::IsNullOrEmpty($Config.$_) }) $Config = $Config | Select-Object $NonNullProperties } return $config } function Expand-DGatewayConfig { param( [DGatewayConfig] $Config ) } function Find-DGatewayConfig { [CmdletBinding()] param( [string] $ConfigPath ) if (-Not $ConfigPath) { $CurrentPath = Get-Location $ConfigFile = Join-Path $CurrentPath $DGatewayConfigFileName if (Test-Path -Path $ConfigFile -PathType 'Leaf') { $ConfigPath = $CurrentPath } } if (-Not $ConfigPath) { $ConfigPath = Get-DGatewayPath } if ($Env:DGATEWAY_CONFIG_PATH) { $ConfigPath = $Env:DGATEWAY_CONFIG_PATH } return $ConfigPath } function Enter-DGatewayConfig { [CmdletBinding()] param( [string] $ConfigPath, [switch] $ChangeDirectory ) if ($ConfigPath) { $ConfigPath = Resolve-Path $ConfigPath $Env:DGATEWAY_CONFIG_PATH = $ConfigPath } $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath if ($ChangeDirectory) { Set-Location $ConfigPath } } function Exit-DGatewayConfig { Remove-Item Env:DGATEWAY_CONFIG_PATH } function Get-DGatewayPath() { [CmdletBinding()] param( [Parameter(Position = 0)] [ValidateSet('ConfigPath')] [string] $PathType = 'ConfigPath' ) $DisplayName = 'Gateway' $CompanyName = 'Devolutions' if ($IsWindows) { $ConfigPath = $Env:ProgramData + "\${CompanyName}\${DisplayName}" } elseif ($IsMacOS) { $ConfigPath = "/Library/Application Support/${CompanyName} ${DisplayName}" } elseif ($IsLinux) { $ConfigPath = '/etc/devolutions-gateway' } switch ($PathType) { 'ConfigPath' { $ConfigPath } default { throw("Invalid path type: $PathType") } } } function Get-DGatewayRecordingPath { [CmdletBinding()] param( [string] $ConfigPath ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties $RecordingPath = $Config.RecordingPath if ([string]::IsNullOrEmpty($RecordingPath)) { $RecordingPath = Join-Path $ConfigPath "recordings" } $RecordingPath } function Set-DGatewayRecordingPath { [CmdletBinding()] param( [string] $ConfigPath, [Parameter(Mandatory = $true, Position = 0)] [string] $RecordingPath ) $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties $Config.RecordingPath = $RecordingPath Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function Reset-DGatewayRecordingPath { [CmdletBinding()] param( [string] $ConfigPath ) $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties $Config.RecordingPath = $null Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function Get-DGatewayHostname { [CmdletBinding()] param( [string] $ConfigPath ) $(Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties).Hostname } function Set-DGatewayHostname { [CmdletBinding()] param( [string] $ConfigPath, [Parameter(Mandatory = $true, Position = 0)] [string] $Hostname ) $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties $Config.Hostname = $Hostname Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function Get-DGatewayListeners { [CmdletBinding()] [OutputType('DGatewayListener[]')] param( [string] $ConfigPath ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties $Config.Listeners } function Set-DGatewayListeners { [CmdletBinding()] param( [string] $ConfigPath, [Parameter(Mandatory = $true, Position = 0)] [AllowEmptyCollection()] [DGatewayListener[]] $Listeners ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties $Config.Listeners = $Listeners Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function New-DGatewayCertificate { [CmdletBinding()] param( [string] $ConfigPath, [string] $Hostname, [switch] $Force ) if (-Not $IsWindows) { throw "unsupported platform" } $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties if ([string]::IsNullOrEmpty($Hostname)) { $Hostname = $Config.Hostname } if ([string]::IsNullOrEmpty($Hostname)) { $Hostname = [System.Environment]::MachineName } Set-DGatewayHostname -ConfigPath:$ConfigPath $Hostname $Password = "cert123!" # dummy password (it's just a self-signed certificate) $SecurePassword = ConvertTo-SecureString -String $Password -Force -AsPlainText # Create a self-signed certificate for the specified hostname and export to a .pfx file $NotBefore = Get-Date $ExtendedKeyUsage = "2.5.29.37={text}1.3.6.1.5.5.7.3.1" $Params = @{ DnsName = $Hostname CertStoreLocation = "cert:\CurrentUser\My" KeyExportPolicy = "Exportable" KeyAlgorithm = "RSA" KeyLength = 2048 HashAlgorithm = 'SHA256' TextExtension = @($ExtendedKeyUsage) KeyUsageProperty = "All" KeyUsage = 'CertSign', 'DigitalSignature', 'KeyEncipherment' NotBefore = $NotBefore.AddHours(-1) NotAfter = $NotBefore.AddYears(5) } $Certificate = New-SelfSignedCertificate @Params $PfxCertificateFile = Join-Path ([System.IO.Path]::GetTempPath()) "gateway-$Hostname.pfx" Export-PfxCertificate -Cert $Certificate -FilePath $PfxCertificateFile -Password $securePassword | Out-Null Remove-Item -Path ("cert:\CurrentUser\My\" + $Certificate.Thumbprint) | Out-Null Import-DGatewayCertificate -ConfigPath:$ConfigPath -CertificateFile $PfxCertificateFile -Password $Password Remove-Item $PfxCertificateFile | Out-Null # remove temporary .pfx file } function Import-DGatewayCertificate { [CmdletBinding()] param( [string] $ConfigPath, [string] $CertificateFile, [string] $PrivateKeyFile, [string] $Password ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties $result = Get-PemCertificate -CertificateFile:$CertificateFile ` -PrivateKeyFile:$PrivateKeyFile -Password:$Password $CertificateData = $result.Certificate $PrivateKeyData = $result.PrivateKey New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null $CertificateFile = Join-Path $ConfigPath $DGatewayCertificateFileName $PrivateKeyFile = Join-Path $ConfigPath $DGatewayPrivateKeyFileName Set-Content -Path $CertificateFile -Value $CertificateData -Force Set-Content -Path $PrivateKeyFile -Value $PrivateKeyData -Force $Config.TlsCertificateFile = $DGatewayCertificateFileName $Config.TlsPrivateKeyFile = $DGatewayPrivateKeyFileName Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function New-DGatewayProvisionerKeyPair { [CmdletBinding()] param( [string] $ConfigPath, [int] $KeySize = 2048, [switch] $Force ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties if (-Not (Test-Path -Path $ConfigPath)) { New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null } $PublicKeyFile = Join-Path $ConfigPath $DGatewayProvisionerPublicKeyFileName $PrivateKeyFile = Join-Path $ConfigPath $DGatewayProvisionerPrivateKeyFileName if ((Test-Path -Path $PublicKeyFile) -Or (Test-Path -Path $PrivateKeyFile)) { if (-Not $Force) { throw "$PublicKeyFile or $PrivateKeyFile already exists, use -Force to overwrite" } Remove-Item $PublicKeyFile -Force | Out-Null Remove-Item $PrivateKeyFile -Force | Out-Null } $KeyPair = New-RsaKeyPair -KeySize:$KeySize $PublicKeyData = $KeyPair.PublicKey $Config.ProvisionerPublicKeyFile = $DGatewayProvisionerPublicKeyFileName Set-Content -Path $PublicKeyFile -Value $PublicKeyData -Force $PrivateKeyData = $KeyPair.PrivateKey $Config.ProvisionerPrivateKeyFile = $DGatewayProvisionerPrivateKeyFileName Set-Content -Path $PrivateKeyFile -Value $PrivateKeyData -Force Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function Import-DGatewayProvisionerKey { [CmdletBinding()] param( [string] $ConfigPath, [string] $PublicKeyFile, [string] $PrivateKeyFile ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties if ($PublicKeyFile) { if (-Not (Test-Path -Path $PublicKeyFile)) { throw "$PublicKeyFile doesn't exist" } $PublicKeyData = Get-Content -Path $PublicKeyFile -Encoding UTF8 if (!$PublicKeyData) { throw "$PublicKeyFile appears to be empty" } $OutputFile = Join-Path $ConfigPath $DGatewayProvisionerPublicKeyFileName $Config.ProvisionerPublicKeyFile = $DGatewayProvisionerPublicKeyFileName New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null Set-Content -Path $OutputFile -Value $PublicKeyData -Force } if ($PrivateKeyFile) { if (-Not (Test-Path -Path $PrivateKeyFile)) { throw "$PrivateKeyFile doesn't exist" } $PrivateKeyData = Get-Content -Path $PrivateKeyFile -Encoding UTF8 if (!$PrivateKeyData) { throw "$PrivateKeyFile appears to be empty" } $OutputFile = Join-Path $ConfigPath $DGatewayProvisionerPrivateKeyFileName $Config.ProvisionerPrivateKeyFile = $DGatewayProvisionerPrivateKeyFileName New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null Set-Content -Path $OutputFile -Value $PrivateKeyData -Force } Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function New-DGatewayDelegationKeyPair { [CmdletBinding()] param( [string] $ConfigPath, [int] $KeySize = 2048, [switch] $Force ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties if (-Not (Test-Path -Path $ConfigPath)) { New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null } $PublicKeyFile = Join-Path $ConfigPath $DGatewayDelegationPublicKeyFileName $PrivateKeyFile = Join-Path $ConfigPath $DGatewayDelegationPrivateKeyFileName if ((Test-Path -Path $PublicKeyFile) -Or (Test-Path -Path $PrivateKeyFile)) { if (-Not $Force) { throw "$PublicKeyFile or $PrivateKeyFile already exists, use -Force to overwrite" } Remove-Item $PublicKeyFile -Force | Out-Null Remove-Item $PrivateKeyFile -Force | Out-Null } $KeyPair = New-RsaKeyPair -KeySize:$KeySize $PublicKeyData = $KeyPair.PublicKey $Config.DelegationPublicKeyFile = $DGatewayDelegationPublicKeyFileName Set-Content -Path $PublicKeyFile -Value $PublicKeyData -Force $PrivateKeyData = $KeyPair.PrivateKey $Config.DelegationPrivateKeyFile = $DGatewayDelegationPrivateKeyFileName Set-Content -Path $PrivateKeyFile -Value $PrivateKeyData -Force Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function Import-DGatewayDelegationKey { [CmdletBinding()] param( [string] $ConfigPath, [string] $PublicKeyFile, [string] $PrivateKeyFile ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties if ($PublicKeyFile) { $PublicKeyData = Get-Content -Path $PublicKeyFile -Encoding UTF8 $OutputFile = Join-Path $ConfigPath $DGatewayDelegationPublicKeyFileName $Config.DelegationPublicKeyFile = $DGatewayDelegationPublicKeyFileName New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null Set-Content -Path $OutputFile -Value $PublicKeyData -Force } if ($PrivateKeyFile) { $PrivateKeyData = Get-Content -Path $PrivateKeyFile -Encoding UTF8 $OutputFile = Join-Path $ConfigPath $DGatewayDelegationPrivateKeyFileName $Config.DelegationPrivateKeyFile = $DGatewayDelegationPrivateKeyFileName New-Item -Path $ConfigPath -ItemType 'Directory' -Force | Out-Null Set-Content -Path $OutputFile -Value $PrivateKeyData -Force } Save-DGatewayConfig -ConfigPath:$ConfigPath -Config:$Config } function New-DGatewayToken { [CmdletBinding()] param( [string] $ConfigPath, [ValidateSet('ASSOCIATION', 'SCOPE', 'BRIDGE', 'JMUX', 'JREC')] [Parameter(Mandatory = $true)] [string] $Type, # token type # public common claims [DateTime] $ExpirationTime, # exp [DateTime] $NotBefore, # nbf [DateTime] $IssuedAt, # iat # private association claims [string] $AssociationId, # jet_aid [ValidateSet('unknown', 'wayk', 'rdp', 'ard', 'vnc', 'ssh', 'ssh-pwsh', 'sftp', 'scp', 'winrm-http-pwsh', 'winrm-https-pwsh', 'http', 'https', 'ldap', 'ldaps')] [string] $ApplicationProtocol, # jet_ap [ValidateSet('fwd', 'rdv')] [string] $ConnectionMode, # jet_cm [string] $DestinationHost, # dst_hst # private jrec claims [ValidateSet('push', 'pull')] [string] $RecordingOperation = 'push', # jet_rop # private scope claims [string] $Scope, # scope # private bridge claims [string] $Target, # target # signature parameters [string] $PrivateKeyFile ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties if (-Not $PrivateKeyFile) { if (-Not $Config.ProvisionerPrivateKeyFile) { throw "Config file is missing ``ProvisionerPrivateKeyFile``. Alternatively, use -PrivateKeyFile argument." } if ([System.IO.Path]::IsPathRooted($Config.ProvisionerPrivateKeyFile)) { $PrivateKeyFile = $Config.ProvisionerPrivateKeyFile } else { $PrivateKeyFile = Join-Path $ConfigPath $Config.ProvisionerPrivateKeyFile } } if (-Not (Test-Path -Path $PrivateKeyFile -PathType 'Leaf')) { throw "$PrivateKeyFile cannot be found." } $PrivateKey = ConvertTo-RsaPrivateKey $(Get-Content $PrivateKeyFile -Raw) $CurrentTime = Get-Date if (-Not $NotBefore) { $NotBefore = $CurrentTime } if (-Not $IssuedAt) { $IssuedAt = $CurrentTime } if (-Not $ExpirationTime) { $ExpirationTime = $CurrentTime.AddMinutes(2) } $iat = [System.DateTimeOffset]::new($IssuedAt).ToUnixTimeSeconds() $nbf = [System.DateTimeOffset]::new($NotBefore).ToUnixTimeSeconds() $exp = [System.DateTimeOffset]::new($ExpirationTime).ToUnixTimeSeconds() $jti = (New-Guid).ToString() $Header = [PSCustomObject]@{ alg = 'RS256' typ = 'JWT' cty = $Type } $Payload = [PSCustomObject]@{ iat = $iat nbf = $nbf exp = $exp jti = $jti } if ($Type -eq 'ASSOCIATION') { if (-Not $ApplicationProtocol) { if ($ConnectionMode -eq 'fwd') { $ApplicationProtocol = 'rdp' } else { $ApplicationProtocol = 'wayk' } } $Payload | Add-Member -MemberType NoteProperty -Name 'jet_ap' -Value $ApplicationProtocol if (-Not $ConnectionMode) { if ($DestinationHost) { $ConnectionMode = 'fwd' } else { $ConnectionMode = 'rdv' } } $Payload | Add-Member -MemberType NoteProperty -Name 'jet_cm' -Value $ConnectionMode if (-Not $AssociationId) { $AssociationId = New-Guid } $Payload | Add-Member -MemberType NoteProperty -Name 'jet_aid' -Value $AssociationId if ($DestinationHost) { $Payload | Add-Member -MemberType NoteProperty -Name 'dst_hst' -Value $DestinationHost } } if ($Type -eq 'JMUX') { if (-Not $DestinationHost) { throw "DestinationHost is required" } if ($ApplicationProtocol) { $Payload | Add-Member -MemberType NoteProperty -Name 'jet_ap' -Value $ApplicationProtocol } if (-Not $AssociationId) { $AssociationId = New-Guid } $Payload | Add-Member -MemberType NoteProperty -Name 'jet_aid' -Value $AssociationId $Payload | Add-Member -MemberType NoteProperty -Name 'dst_hst' -Value $DestinationHost } if ($Type -eq 'JREC') { if (-Not $RecordingOperation) { throw "RecordingOperation is required" } if ($ApplicationProtocol) { $Payload | Add-Member -MemberType NoteProperty -Name 'jet_ap' -Value $ApplicationProtocol } $Payload | Add-Member -MemberType NoteProperty -Name 'jet_rop' -Value $RecordingOperation.ToLower() if (-Not $AssociationId) { $AssociationId = New-Guid } $Payload | Add-Member -MemberType NoteProperty -Name 'jet_aid' -Value $AssociationId if ($DestinationHost) { $Payload | Add-Member -MemberType NoteProperty -Name 'dst_hst' -Value $DestinationHost } } if (($Type -eq 'SCOPE') -and ($Scope)) { $Payload | Add-Member -MemberType NoteProperty -Name 'scope' -Value $Scope } if (($Type -eq 'BRIDGE') -and ($Target)) { $Payload | Add-Member -MemberType NoteProperty -Name 'target' -Value $Target } New-JwtRs256 -Header $Header -Payload $Payload -PrivateKey $PrivateKey } function ConvertTo-DGatewayHash { [CmdletBinding()] param( [Parameter(Mandatory=$true, Position=0)] [string] $Password ) $parameters = [Devolutions.Picky.Argon2Params]::New.Invoke(@()) $algorithm = [Devolutions.Picky.Argon2Algorithm]::Argon2id $argon2 = [Devolutions.Picky.Argon2]::New.Invoke(@($algorithm, $parameters)) $argon2.HashPassword($Password) } function Get-DGatewayUsersFilePath { [CmdletBinding()] param( [string] $ConfigPath ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $Config = Get-DGatewayConfig -ConfigPath:$ConfigPath -NullProperties if ($Config.WebApp.UsersFile) { $fileName = $Config.WebApp.UsersFile } else { $fileName = $script:DGatewayCustomUsersFileName } $filePath = Join-Path -Path $ConfigPath -ChildPath $fileName return $filePath } function Set-DGatewayUser { [CmdletBinding()] param( [string] $ConfigPath, [Parameter(Mandatory=$true, Position=0)] [string] $Username, [Parameter(Mandatory=$true, Position=1)] [string] $Password ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $filePath = Get-DGatewayUsersFilePath -ConfigPath $ConfigPath $hash = ConvertTo-DGatewayHash -Password $Password $fileContent = @() if (Test-Path $filePath) { try { $fileContent = [System.IO.File]::ReadLines($filePath) } catch { Write-Host "Error reading file: $_" return } } $entry = "$Username`:$hash" $updated = $false $fileContentList = New-Object System.Collections.Generic.List[System.String] foreach ($line in $fileContent) { $fileContentList.Add($line) } for ($i = 0; $i -lt $fileContentList.Count; $i++) { if ((-Not [string]::IsNullOrEmpty($fileContentList[$i])) -And $fileContentList[$i].StartsWith("${Username}:")) { $fileContentList[$i] = $entry $updated = $true break } } if (-Not $updated) { $fileContentList.Add($entry) } [System.IO.File]::WriteAllLines($filePath, $fileContentList) } function Remove-DGatewayUser { [CmdletBinding()] param( [string] $ConfigPath, [Parameter(Mandatory=$true, Position=0)] [string] $Username ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $filePath = Get-DGatewayUsersFilePath -ConfigPath $ConfigPath $fileContent = Get-Content $filePath $newContent = $fileContent | Where-Object { $_ -notmatch "^${Username}:" } Set-Content -Path $filePath -Value $newContent } function Get-DGatewayUser { [CmdletBinding()] param( [string] $ConfigPath, [string] $Username ) $ConfigPath = Find-DGatewayConfig -ConfigPath:$ConfigPath $filePath = Get-DGatewayUsersFilePath -ConfigPath $ConfigPath $fileContent = Get-Content $filePath $users = @() foreach ($line in $fileContent) { # Splitting at the first ':' character $splitIndex = $line.IndexOf(':') if ($splitIndex -lt 0) { continue } $user = $line.Substring(0, $splitIndex) $hash = $line.Substring($splitIndex + 1) $users += New-Object PSObject -Property @{ User = $user Hash = $hash } } if ($Username) { $user = $users | Where-Object { $_.User -eq $Username } return $user } else { return $users } } function Get-DGatewayPackage { [CmdletBinding()] param( [string] $RequiredVersion, [ValidateSet('Windows', 'Linux')] [string] $Platform ) $Version = Get-DGatewayVersion 'PSModule' if ($RequiredVersion) { $Version = $RequiredVersion } if (-Not $Platform) { if ($IsWindows) { $Platform = 'Windows' } else { $Platform = 'Linux' } } $GitHubDownloadUrl = 'https://github.com/Devolutions/devolutions-gateway/releases/download/' if ($Platform -eq 'Windows') { $PackageFileName = "DevolutionsGateway-${Version}-x64.msi" } elseif ($Platform -eq 'Linux') { $PackageFileName = "devolutions-gateway-${Version}-x64.deb" } $DownloadUrl = "${GitHubDownloadUrl}v${Version}/$PackageFileName" [PSCustomObject]@{ Url = $DownloadUrl; Version = $Version; } } function Install-DGatewayPackage { <# .SYNOPSIS Downloads and installs (or upgrades) the Devolutions Gateway package. .PARAMETER ServiceAccount Windows only. The account the Gateway service logs on as, in 'DOMAIN\Name' or '.\Name' form, such as a group managed service account ('CONTOSO\gateway$') or the virtual account 'NT SERVICE\DevolutionsGateway'. Defaults to the account of the existing service on upgrade, and to 'NT AUTHORITY\NetworkService' on a first install. .PARAMETER ServiceCredential Windows only. Credential of a service account that logs on with a password. The user name is used as the service account when -ServiceAccount is not specified. Passwordless accounts (NETWORK SERVICE, virtual accounts, managed service accounts) do not need it. Required again for a major upgrade under a password-based account. If ServiceAccount is also supplied, it must match the credential user name. The password is visible in the installer process command line; avoid logging that command line. #> [CmdletBinding()] param( [string] $RequiredVersion, [switch] $Quiet, [switch] $Force, [string] $ServiceAccount, [PSCredential] $ServiceCredential ) if (($ServiceAccount -or $ServiceCredential) -and -not $IsWindows) { throw 'ServiceAccount and ServiceCredential are supported only on Windows' } if ($ServiceAccount -and $ServiceCredential -and $ServiceAccount -ine $ServiceCredential.UserName) { throw 'ServiceAccount must match ServiceCredential.UserName; omit ServiceAccount to use the credential user name' } $Version = Get-DGatewayVersion 'PSModule' if ($RequiredVersion) { $Version = $RequiredVersion } $InstalledVersion = Get-DGatewayVersion 'Installed' if (($InstalledVersion -eq $Version) -and (-Not $Force)) { Write-Host "Devolutions Gateway is already installed ($Version)" return } $TempPath = Join-Path $([System.IO.Path]::GetTempPath()) "dgateway-${Version}" New-Item -ItemType Directory -Path $TempPath -ErrorAction SilentlyContinue | Out-Null $Package = Get-DGatewayPackage -RequiredVersion $Version $DownloadUrl = $Package.Url $DownloadFile = Split-Path -Path $DownloadUrl -Leaf $DownloadFilePath = Join-Path $TempPath $DownloadFile Write-Host "Downloading $DownloadUrl" $WebClient = [System.Net.WebClient]::new() $WebClient.DownloadFile($DownloadUrl, $DownloadFilePath) $WebClient.Dispose() $DownloadFilePath = Resolve-Path $DownloadFilePath if ($IsWindows) { $Display = '/passive' if ($Quiet) { $Display = '/quiet' } $InstallLogFile = Join-Path $TempPath 'DGateway_Install.log' $MsiArgs = @( '/i', "`"$DownloadFilePath`"", $Display, '/norestart', '/log', "`"$InstallLogFile`"" ) if ($ServiceCredential -and -Not $ServiceAccount) { $ServiceAccount = $ServiceCredential.UserName } if ($ServiceAccount) { $MsiArgs += ConvertTo-DGatewayMsiProperty -Name 'P.SERVICEACCOUNT' -Value $ServiceAccount } if ($ServiceCredential) { # Hidden MSI properties do not protect process command lines or the Debug=7 logging policy. $MsiArgs += ConvertTo-DGatewayMsiProperty -Name 'P.SERVICEPASSWORD' -Value $ServiceCredential.GetNetworkCredential().Password } $Process = Start-Process 'msiexec.exe' -ArgumentList $MsiArgs -Wait -NoNewWindow -PassThru if ($Process.ExitCode -eq 3010) { Write-Warning 'Devolutions Gateway installation requires a restart' } elseif ($Process.ExitCode -ne 0) { throw "Devolutions Gateway installation failed (MSI exit code $($Process.ExitCode)). Log: $InstallLogFile" } Remove-Item -Path $InstallLogFile -Force -ErrorAction SilentlyContinue } elseif ($IsMacOS) { throw 'unsupported platform' } elseif ($IsLinux) { $DpkgArgs = @( '-i', $DownloadFilePath ) if ((id -u) -eq 0) { Start-Process 'dpkg' -ArgumentList $DpkgArgs -Wait } else { $DpkgArgs = @('dpkg') + $DpkgArgs Start-Process 'sudo' -ArgumentList $DpkgArgs -Wait } } Remove-Item -Path $TempPath -Force -Recurse } function Uninstall-DGatewayPackage { [CmdletBinding()] param( [switch] $Quiet ) if ($IsWindows) { $UninstallReg = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' ` | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_ -Match 'Devolutions Gateway' } if ($UninstallReg) { $UninstallString = $($UninstallReg.UninstallString ` -Replace 'msiexec.exe', '' -Replace '/I', '' -Replace '/X', '').Trim() $Display = '/passive' if ($Quiet) { $Display = '/quiet' } $MsiArgs = @( '/X', $UninstallString, $Display ) Start-Process 'msiexec.exe' -ArgumentList $MsiArgs -Wait } } elseif ($IsMacOS) { throw 'unsupported platform' } elseif ($IsLinux) { if (Get-DGatewayVersion 'Installed') { $AptArgs = @( '-y', 'remove', 'devolutions-gateway', '--purge' ) if ((id -u) -eq 0) { Start-Process 'apt-get' -ArgumentList $AptArgs -Wait } else { $AptArgs = @('apt-get') + $AptArgs Start-Process 'sudo' -ArgumentList $AptArgs -Wait } } } } function Start-DGateway { [CmdletBinding()] param( [string]$ServiceName ) if (-Not [String]::IsNullOrWhiteSpace($ServiceName)) { $DGatewayService = $ServiceName } elseif ($IsWindows) { $DGatewayService = 'devolutionsgateway' } elseif ($IsLinux) { $DGatewayService = 'devolutions-gateway.service' } else { throw 'Service name is empty' } if ($IsWindows) { Start-Service -Name $DGatewayService } elseif ($IsLinux) { & systemctl start $DGatewayService } else { throw 'Not implemented' } } function Stop-DGateway { [CmdletBinding()] param( [string]$ServiceName ) if (-Not [String]::IsNullOrWhiteSpace($serviceName)) { $DGatewayService = $ServiceName } elseif ($IsWindows) { $DGatewayService = 'devolutionsgateway' } elseif ($IsLinux) { $DGatewayService = 'devolutions-gateway.service' } else { throw 'Service name is empty' } if ($IsWindows) { Stop-Service -Name $DGatewayService } elseif ($IsLinux) { & systemctl stop $DGatewayService } else { throw 'Not implemented' } } function Restart-DGateway { [CmdletBinding()] param( [string]$ServiceName ) if (-Not [String]::IsNullOrWhiteSpace($ServiceName)) { $DGatewayService = $ServiceName } elseif ($IsWindows) { $DGatewayService = 'devolutionsgateway' } elseif ($IsLinux) { $DGatewayService = 'devolutions-gateway.service' } else { throw 'Service name is empty' } if ($IsWindows) { Restart-Service -Name $DGatewayService } elseif ($IsLinux) { & systemctl restart $DGatewayService } else { throw 'Not implemented' } } function Get-DGatewayService { [CmdletBinding()] param( [string]$ServiceName ) if (-Not [String]::IsNullOrWhiteSpace($ServiceName)) { $DGatewayService = $ServiceName } elseif ($IsWindows) { $DGatewayService = 'devolutionsgateway' } elseif ($IsLinux) { $DGatewayService = 'devolutions-gateway.service' } else { throw 'Service name is empty' } if ($IsWindows) { $Result = Get-Service -Name $DGatewayService If ($Result) { [PSCustomObject]@{ "Name" = $Result.Name "Status" = $Result.Status } } else { throw 'Service not found' } } elseif ($IsLinux) { $Result = & systemctl list-units --type=service --all --no-pager $DGatewayService --no-legend if ($Result) { $ID, $Load, $Active, $Status, $Description = ($Result.Trim()) -Split '\s+', 5 If ($ID -And $Active) { [PSCustomObject]@{ "Name" = $ID "Status" = if ($Active -EQ 'active') { 'Running' } else { 'Stopped' } } } else { throw 'Unable to return service status' } } else { throw 'Service not found' } } else { throw 'Not implemented' } } # SIG # Begin signature block # MIIvWwYJKoZIhvcNAQcCoIIvTDCCL0gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDnx/4Wek2wLsZw # Tddq6apPcHopxIn5qCERf9NQdnc1XaCCFBkwggVyMIIDWqADAgECAhB2U/6sdUZI # k/Xl10pIOk74MA0GCSqGSIb3DQEBDAUAMFMxCzAJBgNVBAYTAkJFMRkwFwYDVQQK # ExBHbG9iYWxTaWduIG52LXNhMSkwJwYDVQQDEyBHbG9iYWxTaWduIENvZGUgU2ln # bmluZyBSb290IFI0NTAeFw0yMDAzMTgwMDAwMDBaFw00NTAzMTgwMDAwMDBaMFMx # CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSkwJwYDVQQD # EyBHbG9iYWxTaWduIENvZGUgU2lnbmluZyBSb290IFI0NTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBALYtxTDdeuirkD0DcrA6S5kWYbLl/6VnHTcc5X7s # k4OqhPWjQ5uYRYq4Y1ddmwCIBCXp+GiSS4LYS8lKA/Oof2qPimEnvaFE0P31PyLC # o0+RjbMFsiiCkV37WYgFC5cGwpj4LKczJO5QOkHM8KCwex1N0qhYOJbp3/kbkbuL # ECzSx0Mdogl0oYCve+YzCgxZa4689Ktal3t/rlX7hPCA/oRM1+K6vcR1oW+9YRB0 # RLKYB+J0q/9o3GwmPukf5eAEh60w0wyNA3xVuBZwXCR4ICXrZ2eIq7pONJhrcBHe # OMrUvqHAnOHfHgIB2DvhZ0OEts/8dLcvhKO/ugk3PWdssUVcGWGrQYP1rB3rdw1G # R3POv72Vle2dK4gQ/vpY6KdX4bPPqFrpByWbEsSegHI9k9yMlN87ROYmgPzSwwPw # jAzSRdYu54+YnuYE7kJuZ35CFnFi5wT5YMZkobacgSFOK8ZtaJSGxpl0c2cxepHy # 1Ix5bnymu35Gb03FhRIrz5oiRAiohTfOB2FXBhcSJMDEMXOhmDVXR34QOkXZLaRR # kJipoAc3xGUaqhxrFnf3p5fsPxkwmW8x++pAsufSxPrJ0PBQdnRZ+o1tFzK++Ol+ # A/Tnh3Wa1EqRLIUDEwIrQoDyiWo2z8hMoM6e+MuNrRan097VmxinxpI68YJj8S4O # JGTfAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G # A1UdDgQWBBQfAL9GgAr8eDm3pbRD2VZQu86WOzANBgkqhkiG9w0BAQwFAAOCAgEA # Xiu6dJc0RF92SChAhJPuAW7pobPWgCXme+S8CZE9D/x2rdfUMCC7j2DQkdYc8pzv # eBorlDICwSSWUlIC0PPR/PKbOW6Z4R+OQ0F9mh5byV2ahPwm5ofzdHImraQb2T07 # alKgPAkeLx57szO0Rcf3rLGvk2Ctdq64shV464Nq6//bRqsk5e4C+pAfWcAvXda3 # XaRcELdyU/hBTsz6eBolSsr+hWJDYcO0N6qB0vTWOg+9jVl+MEfeK2vnIVAzX9Rn # m9S4Z588J5kD/4VDjnMSyiDN6GHVsWbcF9Y5bQ/bzyM3oYKJThxrP9agzaoHnT5C # JqrXDO76R78aUn7RdYHTyYpiF21PiKAhoCY+r23ZYjAf6Zgorm6N1Y5McmaTgI0q # 41XHYGeQQlZcIlEPs9xOOe5N3dkdeBBUO27Ql28DtR6yI3PGErKaZND8lYUkqP/f # obDckUCu3wkzq7ndkrfxzJF0O2nrZ5cbkL/nx6BvcbtXv7ePWu16QGoWzYCELS/h # AtQklEOzFfwMKxv9cW/8y7x1Fzpeg9LJsy8b1ZyNf1T+fn7kVqOHp53hWVKUQY9t # W76GlZr/GnbdQNJRSnC0HzNjI3c/7CceWeQIh+00gkoPP/6gHcH1Z3NFhnj0qinp # J4fGGdvGExTDOUmHTaCX4GUT9Z13Vunas1jHOvLAzYIwggboMIIE0KADAgECAhB3 # vQ4Ft1kLth1HYVMeP3XtMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNVBAYTAkJFMRkw # FwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSkwJwYDVQQDEyBHbG9iYWxTaWduIENv # ZGUgU2lnbmluZyBSb290IFI0NTAeFw0yMDA3MjgwMDAwMDBaFw0zMDA3MjgwMDAw # MDBaMFwxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTIw # MAYDVQQDEylHbG9iYWxTaWduIEdDQyBSNDUgRVYgQ29kZVNpZ25pbmcgQ0EgMjAy # MDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMsg75ceuQEyQ6BbqYoj # /SBerjgSi8os1P9B2BpV1BlTt/2jF+d6OVzA984Ro/ml7QH6tbqT76+T3PjisxlM # g7BKRFAEeIQQaqTWlpCOgfh8qy+1o1cz0lh7lA5tD6WRJiqzg09ysYp7ZJLQ8LRV # X5YLEeWatSyyEc8lG31RK5gfSaNf+BOeNbgDAtqkEy+FSu/EL3AOwdTMMxLsvUCV # 0xHK5s2zBZzIU+tS13hMUQGSgt4T8weOdLqEgJ/SpBUO6K/r94n233Hw0b6nskEz # IHXMsdXtHQcZxOsmd/KrbReTSam35sOQnMa47MzJe5pexcUkk2NvfhCLYc+YVaMk # oog28vmfvpMusgafJsAMAVYS4bKKnw4e3JiLLs/a4ok0ph8moKiueG3soYgVPMLq # 7rfYrWGlr3A2onmO3A1zwPHkLKuU7FgGOTZI1jta6CLOdA6vLPEV2tG0leis1Ult # 5a/dm2tjIF2OfjuyQ9hiOpTlzbSYszcZJBJyc6sEsAnchebUIgTvQCodLm3HadNu # twFsDeCXpxbmJouI9wNEhl9iZ0y1pzeoVdwDNoxuz202JvEOj7A9ccDhMqeC5LYy # AjIwfLWTyCH9PIjmaWP47nXJi8Kr77o6/elev7YR8b7wPcoyPm593g9+m5XEEofn # GrhO7izB36Fl6CSDySrC/blTAgMBAAGjggGtMIIBqTAOBgNVHQ8BAf8EBAMCAYYw # EwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUJZ3Q/FkJhmPF7POxEztXHAOSNhEwHwYDVR0jBBgwFoAUHwC/RoAK/Hg5t6W0 # Q9lWULvOljswgZMGCCsGAQUFBwEBBIGGMIGDMDkGCCsGAQUFBzABhi1odHRwOi8v # b2NzcC5nbG9iYWxzaWduLmNvbS9jb2Rlc2lnbmluZ3Jvb3RyNDUwRgYIKwYBBQUH # MAKGOmh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2NvZGVzaWdu # aW5ncm9vdHI0NS5jcnQwQQYDVR0fBDowODA2oDSgMoYwaHR0cDovL2NybC5nbG9i # YWxzaWduLmNvbS9jb2Rlc2lnbmluZ3Jvb3RyNDUuY3JsMFUGA1UdIAROMEwwQQYJ # KwYBBAGgMgECMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24u # Y29tL3JlcG9zaXRvcnkvMAcGBWeBDAEDMA0GCSqGSIb3DQEBCwUAA4ICAQAldaAJ # yTm6t6E5iS8Yn6vW6x1L6JR8DQdomxyd73G2F2prAk+zP4ZFh8xlm0zjWAYCImbV # YQLFY4/UovG2XiULd5bpzXFAM4gp7O7zom28TbU+BkvJczPKCBQtPUzosLp1pnQt # pFg6bBNJ+KUVChSWhbFqaDQlQq+WVvQQ+iR98StywRbha+vmqZjHPlr00Bid/XSX # hndGKj0jfShziq7vKxuav2xTpxSePIdxwF6OyPvTKpIz6ldNXgdeysEYrIEtGiH6 # bs+XYXvfcXo6ymP31TBENzL+u0OF3Lr8psozGSt3bdvLBfB+X3Uuora/Nao2Y8nO # ZNm9/Lws80lWAMgSK8YnuzevV+/Ezx4pxPTiLc4qYc9X7fUKQOL1GNYe6ZAvytOH # X5OKSBoRHeU3hZ8uZmKaXoFOlaxVV0PcU4slfjxhD4oLuvU/pteO9wRWXiG7n9dq # cYC/lt5yA9jYIivzJxZPOOhRQAyuku++PX33gMZMNleElaeEFUgwDlInCI2Oor0i # xxnJpsoOqHo222q6YV8RJJWk4o5o7hmpSZle0LQ0vdb5QMcQlzFSOTUpEYck08T7 # qWPLd0jV+mL8JOAEek7Q5G7ezp44UCb0IXFl1wkl1MkHAHq4x/N36MXU4lXQ0x72 # f1LiSY25EXIMiEQmM2YBRN/kMw4h3mKJSAfa9TCCB7MwggWboAMCAQICDEwWQjHr # NX4fj8JFKDANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQ # R2xvYmFsU2lnbiBudi1zYTEyMDAGA1UEAxMpR2xvYmFsU2lnbiBHQ0MgUjQ1IEVW # IENvZGVTaWduaW5nIENBIDIwMjAwHhcNMjUxMjA0MjEwMjI0WhcNMjgxMjA0MjEw # MjI0WjCB8zEdMBsGA1UEDwwUUHJpdmF0ZSBPcmdhbml6YXRpb24xEzARBgNVBAUT # CjExNjI1NDQ2ODkxEzARBgsrBgEEAYI3PAIBAxMCQ0ExFzAVBgsrBgEEAYI3PAIB # AhMGUXVlYmVjMQswCQYDVQQGEwJDQTEPMA0GA1UECBMGUXVlYmVjMRIwEAYDVQQH # EwlMYXZhbHRyaWUxGTAXBgNVBAoTEERFVk9MVVRJT05TIElOQy4xGTAXBgNVBAMT # EERFVk9MVVRJT05TIElOQy4xJzAlBgkqhkiG9w0BCQEWGHNlY3VyaXR5QGRldm9s # dXRpb25zLm5ldDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKvGSmps # CDxiV4fapsFPCVhNMpfdlgrzFhCiXnX0Gy7Zu2e7cDMl2RNo0H8hpKNHhSODkguW # YNLZfmM+bpvE2FbLN0oYxUmlEHiBZWN8+0dq10ybkfQZp+O+95S3mX3ecdKTBhPY # B7I5HYNWBPR1Au86Y68x+LBaTtM3AY/zvwB2gMbefZsKAa9L9np1l7UjN6qfZskQ # G+zRxCj+88Cg6srkCYyNu/LMnFtWInbbxuqkh1ddTTAmVVSUOXXn+vKtUE2hTw/2 # 5x3psbHLLgcLbq0E4ZwzmRPB2ps972aouEaMkPhYXO4KTtTZFj6GoYy2sAUaGxgP # gADEeZY/guhqSBANnn2Wa6jaocgMvvNhSJd7aQfldvps0ZQyerVra7zgoqT0JkyL # NHm9AKiNV+mJQHqirXxL+Y70vO4uFJVukyvceNO+C+y0Re77pb8BntRfuSOdcySj # 1v/NQ7eHAV6/zY21xlYswIz7uysoHQeZEMWlEX8bjJNjDvkT1BUF9j655hVeyxpZ # zvaSgLdpdpZVSOQzSkdiH0KOhBiQ9R5Ppa2/W9qhdLNi+sKjr1/xEKSOd1fFG56R # iN4ems9YdEQR9KrnGrVpQQ0QKSutqFiWUlUqCgWuDwTPe2V+UvHa2xAlZlbYAzGf # X8c6OIniFLjWcxXKV21R7VP8UfQxrf38PapJAgMBAAGjggHbMIIB1zAOBgNVHQ8B # Af8EBAMCB4AwgZ8GCCsGAQUFBwEBBIGSMIGPMEwGCCsGAQUFBzAChkBodHRwOi8v # c2VjdXJlLmdsb2JhbHNpZ24uY29tL2NhY2VydC9nc2djY3I0NWV2Y29kZXNpZ25j # YTIwMjAuY3J0MD8GCCsGAQUFBzABhjNodHRwOi8vb2NzcC5nbG9iYWxzaWduLmNv # bS9nc2djY3I0NWV2Y29kZXNpZ25jYTIwMjAwVQYDVR0gBE4wTDBBBgkrBgEEAaAy # AQIwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xvYmFsc2lnbi5jb20vcmVw # b3NpdG9yeS8wBwYFZ4EMAQMwCQYDVR0TBAIwADBHBgNVHR8EQDA+MDygOqA4hjZo # dHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL2dzZ2NjcjQ1ZXZjb2Rlc2lnbmNhMjAy # MC5jcmwwIwYDVR0RBBwwGoEYc2VjdXJpdHlAZGV2b2x1dGlvbnMubmV0MBMGA1Ud # JQQMMAoGCCsGAQUFBwMDMB8GA1UdIwQYMBaAFCWd0PxZCYZjxezzsRM7VxwDkjYR # MB0GA1UdDgQWBBQc9AV01XqvHzkkeRDj0FeokGPnwDANBgkqhkiG9w0BAQsFAAOC # AgEAuuy64I5XgYWoKznXOKzBMUz4NTGnv1cN8ovX9pJlVUfeIhipffE6WZH9weHZ # +3OheToB4v8JWEobOBfZbz/8FI20WiXgtPvqU4T/ju8ZnVUv+XidlTdTRn6AzXjy # kpyJaLXMJA4BMvl5ISxUVYgnScOpP6hAqWsPDyp5CVOLmYWzZNKWvfBGMiEIwC+h # M+mkqKBnDu1V0tU9QDbqXgBqwBp1BKxWg5bxMYCNkC15WwqP8VVXRk/354x1Ua87 # LHGOSHBIobnySRbK+onnGESqKeVCzqL5DvTVBGZyjRARcYU0mY51raBnJ5/Yad3y # 7R+eSIuUjh35JNXwYAHYlWXYsVzfaH4OgFSaSkJcL0ieH4MT2xT8kITOb+9KElvc # YyFlxWhgIPW/vvY6vqU5aMlMCTcIWTZBbzp9dwCNsAYhhq4Okn5J1e5j+baIT58N # gOhsZjsME6VO1sYj1zUqKJnDza8vViZO7kKaHXtg6w2CgvfPNKO+tZncWEX3YIEa # dpt/t0UFlJ5mAWtlIzxf+79A/a8L7JWxcwNcZ1zUvy/mj+b/0G1S3rPCtsh7BaUs # sMVcHRzuanz+zMMBcQcdwxwyM1U5fnyFjBMbvsAhRJB6c68m1IbB8rM9QNrjZX4Y # 6YTxz1UOCMk2DusI+QHYIlidzJl16tYD3DlF5xefxyQcdEsxghqYMIIalAIBATBs # MFwxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTIwMAYD # VQQDEylHbG9iYWxTaWduIEdDQyBSNDUgRVYgQ29kZVNpZ25pbmcgQ0EgMjAyMAIM # TBZCMes1fh+PwkUoMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAI # oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB # CzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKHjOxbaeVyB8JhscFj/ # l/PY9bAX8Y7da5kR8riKsxSuMA0GCSqGSIb3DQEBAQUABIICAIy6ror95ILt+AVV # S2VWsp6/1CmR3KFmj/vGRZRHNLeC7tuvxRdi8thS2P3G0PdPsodNTe9lkdb2Aw2r # +OXAWqNYOcJ+5UJ/U/k4yPi42+X4OxPN/7uM9cXVWXL75v1WLCPzKprz4mlaqkX8 # 0OjoxonMDeOFoWYLJ0FL/HG6Sbfrl3+0RifCh2R9ZDiN419jlR+yZmSVfl/dWA3z # gz2ur2t29UHqP738kSq4WH6FF8AxJm/QknwAQzrDSkI8TEvFUHcdReu8q8Cx0sl5 # SZOYQwLOQZF6GdDJJ7OGausqru6e1uCdXvkNPd/Vg7QZHe8KyKwDQAOvD6C7RFll # mh8DNhSXYw8L9mU1+yBUZQOc4stzqA662T+yfxQ1I1Q97NeB1AAxFf3YN4kOQZOl # tmIcbFXKs9HtCX2MdF/R/5sISib9NUQpA8Vx44qA+h2MI3y6aWviA+Ywmn5+mNKh # O36s81CpCWyZ0sMDem6Q4/GVsJR2HqEeRZ0YqSGmyhbba1/+l/DtM3Q9uboxA8tj # fE2JCsb8sMisq0Pdb3yRKK0qq69lnGR0H2duV27XIVlHf3wEYSOhg56+Zox83C3A # WhIILqrafXcmcXywBksFuCgQ6I8q39cgUoU6UMYEGyh6qBhEFH+E53PZw+j9wDyC # y1nQcNniYnDnkpXmAXaF/qV9oR4OoYIXdjCCF3IGCisGAQQBgjcDAwExghdiMIIX # XgYJKoZIhvcNAQcCoIIXTzCCF0sCAQMxDzANBglghkgBZQMEAgEFADB3BgsqhkiG # 9w0BCRABBKBoBGYwZAIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIHDV # 7U5XpMtTln0IpbW+ICipM/+zps7ZbAJJ63H1GQz8AhBPZwuJe4xrExo5VEyfxz5X # GA8yMDI2MTAxMDAxMjI1N1qgghM6MIIG7TCCBNWgAwIBAgIQCE/cM09+RU7bww+P # +ZIYNTANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0 # YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExMB4XDTI2MDgwNTAwMDAwMFoX # DTM3MTEwNDIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3Rh # bXAgUmVzcG9uZGVyIDIwMjYgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBALZ7pvLJ/s1K+NSbTGWz/TjGMPh8CQ6RucZCLv5anHzWJjF/NWJrFIhy24fc # pKXlgRiky4WAawDfU3YP0BMxt9l3Dm5oCG5Z69AqEN1kgHg2epx+l+lZBcmJCcN0 # ASURML5uFIS80sZsDwO3BSkUxDjLJhBI+qiZP3aixAC/qEGLjsBNlLol9VZ7pfGE # XiMlneJIC5/YKuizVzNFKZZEeoy/0B8Zm+nzKBgSWG52lCO1w+nCg6XpCtklTJXe # Ig283hw7TmmsZXR+SMbjbrEOvZ3fP2VxIgeR28Y90ZStd3F9VuA5RVynb/whITPA # o9b75Zr4Ta6Mj3URm26QZYMn/FnbuTegcoRcFEZ9FOqM5T6MTdtr/n74lIT/ug0e # eOzmZ6QTFg33otX+bFRsIolvykE1jive4PuESaT8zzVeFWDAMDtozNgLctkGD1Zj # kEyZtJrLl5ya0m5doH/ScpaZCZVl6pNUOCybMc/kxC6EAmSJY24L0yYKD1Nkddsn # b/ItVKi/2nXpQNMu1PT5prW83vV8d67WowuUs0HdY4H8AMLGvdL/WHEj3ZnqMqAQ # QP9u3Ai9t+5eQ02GDwy0ODjdzi0xlp70W+ow63/0++YDEX1M0iwgUHwbrJvfpklk # ZQvw3+kv3vUPItdwroczk9icflf55W1zOEKAcJVAIXpcMCU9AgMBAAGjggGVMIIB # kTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQUyWOKMC7USvtulPPm40B+9ezN4jAf # BgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4Aw # FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsG # AQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0 # dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVT # dGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQ # hk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1l # U3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZn # gQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQCNxTphHp1SCt+Z # rAmAfn0oQLFr0mLywSLaDXQIENoyKqxrFbJblzCVP/pkXmwXOdrOpWygLzlT12os # 5ipDCy35RBCg2UMeApEtrfGhz45F4Wt4WGdNdIbRWt3YTYJmpR+b7lr4d7Uwn+H6 # 00u4D7RnOGf8Wj4UNgAdZkfHhHv1mx9EVh71SJelcEN/oORSjXzdjfw1iZH9d8Nh # /thn6hH23d+VsPAr6GAYyzSA02nXD1nYLI7Ijmiv+xLCiYC41DSFYL3GhTiy0Pxp # awPtGRyaBVGzq+UiTfM8pD7KVyF5aQyWP4KhVGUUTnmm/RlYJoW3TiXA/+t0YcT2 # oRVBm3JETjajHug2AL+v5jhtKVnd3D0rbHXEu27o+Q8p4sEWPMqKDB+qbceb6T/6 # WcwTwXmQ9lOCLLYcsQeSWmvKqzpAec9etE14jOQAzLKWdE3w/TCaKtLRaRT7LCkR # YVnhA2D73FLje1O5b3HR5eHs0NzU/+xX7NbEdcofy0W3Wdwd1XOqtlpg/JgwtKfZ # M5dqO94lbUveOiJBI+xZEbGRsMNbXmMREUTgu+Oca7Y73MPWcslIx2VhkSKSXjDb # D6rgg39H5Mh7QfieAIjWagkJNt68Yfim6cjEzVSiLSeZfdkr5dtFPTW6jATlWJdY # eeDRGCyatf8R1hSjzSvdN8yWQPT9gzCCBrQwggScoAMCAQICEA3HrFcF/yGZLkBD # Igw6SYYwDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp # Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMY # RGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTI1MDUwNzAwMDAwMFoXDTM4MDEx # NDIzNTk1OVowaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu # MUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0 # MDk2IFNIQTI1NiAyMDI1IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBALR4MdMKmEFyvjxGwBysddujRmh0tFEXnU2tjQ2UtZmWgyxU7UNqEY81FzJs # Qqr5G7A6c+Gh/qm8Xi4aPCOo2N8S9SLrC6Kbltqn7SWCWgzbNfiR+2fkHUiljNOq # nIVD/gG3SYDEAd4dg2dDGpeZGKe+42DFUF0mR/vtLa4+gKPsYfwEu7EEbkC9+0F2 # w4QJLVSTEG8yAR2CQWIM1iI5PHg62IVwxKSpO0XaF9DPfNBKS7Zazch8NF5vp7ea # Z2CVNxpqumzTCNSOxm+SAWSuIr21Qomb+zzQWKhxKTVVgtmUPAW35xUUFREmDrMx # SNlr/NsJyUXzdtFUUt4aS4CEeIY8y9IaaGBpPNXKFifinT7zL2gdFpBP9qh8SdLn # Eut/GcalNeJQ55IuwnKCgs+nrpuQNfVmUB5KlCX3ZA4x5HHKS+rqBvKWxdCyQEEG # cbLe1b8Aw4wJkhU1JrPsFfxW1gaou30yZ46t4Y9F20HHfIY4/6vHespYMQmUiote # 8ladjS/nJ0+k6MvqzfpzPDOy5y6gqztiT96Fv/9bH7mQyogxG9QEPHrPV6/7umw0 # 52AkyiLA6tQbZl1KhBtTasySkuJDpsZGKdlsjg4u70EwgWbVRSX1Wd4+zoFpp4Ra # +MlKM2baoD6x0VR4RjSpWM8o5a6D8bpfm4CLKczsG7ZrIGNTAgMBAAGjggFdMIIB # WTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTvb1NK6eQGfHrK4pBW9i/U # SezLTjAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8E # BAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsG # AQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0 # dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQu # Y3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsG # CWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAF877FoAc/gc9EXZxML2+C8i1 # NKZ/zdCHxYgaMH9Pw5tcBnPw6O6FTGNpoV2V4wzSUGvI9NAzaoQk97frPBtIj+ZL # zdp+yXdhOP4hCFATuNT+ReOPK0mCefSG+tXqGpYZ3essBS3q8nL2UwM+NMvEuBd/ # 2vmdYxDCvwzJv2sRUoKEfJ+nN57mQfQXwcAEGCvRR2qKtntujB71WPYAgwPyWLKu # 6RnaID/B0ba2H3LUiwDRAXx1Neq9ydOal95CHfmTnM4I+ZI2rVQfjXQA1WSjjf4J # 2a7jLzWGNqNX+DF0SQzHU0pTi4dBwp9nEC8EAqoxW6q17r0z0noDjs6+BFo+z7bK # SBwZXTRNivYuve3L2oiKNqetRHdqfMTCW/NmKLJ9M+MtucVGyOxiDf06VXxyKkOi # rv6o02OoXN4bFzK0vlNMsvhlqgF2puE6FndlENSmE+9JGYxOGLS/D284NHNboDGc # mWXfwXRy4kbu4QFhOm0xJuF2EZAOk5eCkhSxZON3rGlHqhpB/8MluDezooIs8CVn # rpHMiD2wL40mm53+/j7tFaxYKIqL0Q4ssd8xHZnIn/7GELH3IdvG2XlM9q7WP/Uw # gOkw/HQtyRN62JK4S1C8uw3PdBunvAZapsiI5YKdvlarEvf8EA+8hcpSM9LHJmyr # xaFtoza2zNaQ9k+5t1wwggWNMIIEdaADAgECAhAOmxiO+dAt5+/bUOIIQBhaMA0G # CSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0 # IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0zMTExMDkyMzU5 # NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNV # BAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQg # Um9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvk # XUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdt # HauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu # 34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0 # QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2 # kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM # 1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmI # dph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZ # K37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72 # gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqs # X40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyh # HsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAPBgNVHRMBAf8E # BTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSMEGDAW # gBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYweQYIKwYBBQUH # AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI # KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz # c3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybDMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDARBgNVHSAE # CjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4X6CjdBs9thbX # 979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc47/T/gLn4offy # ct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0mcudT6cGAxN3 # J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk1T3osdz9HNj0 # d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLBsln1VWvPJ6ts # ds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGpn1eqXijiuZQx # ggN8MIIDeAIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg # UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAhP3DNPfkVO28MPj/mSGDUwDQYJYIZI # AWUDBAIBBQCggdEwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3 # DQEJBTEPFw0yNjEwMTAwMTIyNTdaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFFHZ # q9oDSXPYT0JmrKSCSOazacQ5MC8GCSqGSIb3DQEJBDEiBCAczJCliBVhHRKgMWQp # Xe7wfJ2sTkVmxPGaQhEwva0jiTA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCAtoJ2n # 9BMfn+cttsXm6cllZ1WvBD8ep0LMDSEg4UHr/DANBgkqhkiG9w0BAQEFAASCAgAo # q0Wj6AfuSN+sRfeva33nrE9DIfHY72cx9EPs6Ff10DZ9buvmAAPYj7YzrRAp4V4i # gRKMiHM504fBgHK0lW/k/e/8AZZm5UxgI5a71EC3yKRnuzI9pHTXNdaD4Nw9eOTf # +Z9GPk0BZpBDo0G0aRB437w4qmu11BRC2j5Wh8B2jOeSb7rQmFm2Z5vmlLfL1Jq0 # 91hbAAKpd+HEDIPFUqOWH/VCvgGf6BrmTIgNBTxecb98PgO2ElrlyYoIorxrVtXq # a6KIhfuasMWfNA4IJWGew32wEuOjxTVH7b0BU+6pLUKNm9eFY+P5OOQ5D9yg1awg # V0lrDT1/WfrJIcccmL0weK4JHdQFS/hWxBtdYZ1u/Bu0royRf5FVCMolzk/qac0y # EpQ1TRrvGzoD2d03mI2TRPzoYzNXdTnps0uyBc7ngaMownQvrrcccF5e0cpFMQj7 # rkqFa4e49DiMQ4nXF3CHLXKgflNTgdnTkIBn/qdi4DQ8hFYqTzk04gsb0/aPzi87 # Qd6mOh9wmiW5fhtNN75Ii1HTWy5/ROket0gL37oM8wS0n3lnPGw9qXELwjDkAqxi # QqnXPuYLhyRzuBKBsZNDVKwG8gt3CCpGUpYIkbm6qrd3Q8yAcCeWoD0KtbLqbqYl # UPzpCuA1GsylY5Yw3JiHzpqpt11AECm7i6osICAqKg== # SIG # End signature block |