Private/app-service.ps1
|
function GetCertMasterAppServiceName ($CertMasterResourceGroup, $SCEPmanAppServiceName) { # Criteria: # - Configuration value AppConfig:SCEPman:URL must be present, then it must be a CertMaster # - In a default installation, the URL must contain SCEPman's app service name. We require this. $strangeCertMasterFound = $false $rgwebapps = Invoke-Az -azCommand @("graph", "query", "-q", "Resources | where type == 'microsoft.web/sites' and resourceGroup == '$CertMasterResourceGroup' and name !~ '$SCEPmanAppServiceName' | project name") | Convert-LinesToObject Write-Information "$($rgwebapps.count) web apps found in the resource group $CertMasterResourceGroup (excluding SCEPman). We are finding if the CertMaster app is already created" if($rgwebapps.count -gt 0) { ForEach($potentialcmwebapp in $rgwebapps.data) { $scepmanUrl = ReadAppSetting -AppServiceName $potentialcmwebapp.name -ResourceGroup $CertMasterResourceGroup -SettingName 'AppConfig:SCEPman:URL' if($null -eq $scepmanUrl) { $isCmCandidateLinux = IsAppServiceLinux -AppServiceName $potentialcmwebapp.name -ResourceGroup $CertMasterResourceGroup if ($isCmCandidateLinux) { $candidateOs = "Linux" } else { $candidateOs = "Windows" } Write-Verbose "Web app $($potentialcmwebapp.name) running on $candidateOs is not a Certificate Master, continuing search ..." } else { $hascorrectscepmanurl = $scepmanUrl.ToUpperInvariant().Contains($SCEPmanAppServiceName.ToUpperInvariant()) # this works for deployment slots, too if($hascorrectscepmanurl -eq $true) { Write-Information "Certificate Master web app $($potentialcmwebapp.name) found." return $potentialcmwebapp.name } else { Write-Information "Certificate Master web app $($potentialcmwebapp.name) found, but its setting AppConfig:SCEPman:URL is $scepmanURL, which we could not identify with the SCEPman app service. It may or may not be the correct Certificate Master and we ignore it." $strangeCertMasterFound = $true } } } } if ($strangeCertMasterFound) { Write-Warning "There is at least one Certificate Master App Service in resource group $CertMasterResourceGroup, but we are not sure whether it belongs to SCEPman $SCEPmanAppServiceName." } Write-Warning "Unable to determine the Certificate Master app service name" return $null } function SelectBestDotNetRuntime ($ForLinux = $false) { if ($ForLinux) { $runtimePrefix = "DOTNETCORE" $os = "linux" } else { $runtimePrefix = "dotnet" $os = "windows" } $defaultRuntime = if ($ForLinux) { "DOTNETCORE:10.0" } else { "dotnet:10" } try { $runtimes = Invoke-Az @("webapp", "list-runtimes", "--os", $os, "--output", "tsv") # TODO: # The output will be changed in next breaking change release(2.87.0) scheduled for June 2026. # The output will change from a flat list of strings to a structured list of objects with keys: os, runtime, version, config, support, end_of_life. # Update scripts that parse the current string-list output. The new output is a list of dicts with keys: os, runtime, version, config, support, end_of_life. # New --runtime and --support filter parameters will be added. Use -o table for a human-readable view, or -o json and parse the new structured format. [String []]$dotnetRuntimes = $runtimes | Where-Object { $_.ToLower().StartsWith($runtimePrefix.ToLower()) } if ($dotnetRuntimes.Count -gt 0) { Write-Verbose "Available .NET runtimes for $os : $($dotnetRuntimes -join ", ")" return $dotnetRuntimes[0] } else { Write-Warning "No .NET runtimes found for $os. Defaulting to $defaultRuntime" return $defaultRuntime } } catch { Write-Warning "Could not retrieve available runtimes for $os. Defaulting to $defaultRuntime" return $defaultRuntime } } function New-CertMasterAppService { [CmdletBinding(SupportsShouldProcess=$true)] [OutputType([String])] param ( [Parameter(Mandatory=$true)] [string]$TenantId, [Parameter(Mandatory=$true)] [string]$SCEPmanResourceGroup, [Parameter(Mandatory=$true)] [string]$SCEPmanAppServiceName, [Parameter(Mandatory=$true)] [string]$CertMasterResourceGroup, [Parameter(Mandatory=$false)][AllowEmptyString()] [string]$CertMasterAppServiceName, [Parameter(Mandatory=$false)] [string]$DeploymentSlotName, [Parameter(Mandatory=$false)] [string]$UpdateChannel = "prod" ) if ([String]::IsNullOrWhiteSpace($CertMasterAppServiceName)) { $CertMasterAppServiceName = GetCertMasterAppServiceName -CertMasterResourceGroup $CertMasterResourceGroup -SCEPmanAppServiceName $SCEPmanAppServiceName $ShallCreateCertMasterAppService = [String]::IsNullOrWhiteSpace($CertMasterAppServiceName) } else { # Check whether a cert master app service with the passed in name exists $CertMasterWebApps = Invoke-Az -azCommand @("graph", "query", "-q", "Resources | where type == 'microsoft.web/sites' and resourceGroup == '$CertMasterResourceGroup' and name =~ '$CertMasterAppServiceName' | project name") | Convert-LinesToObject $ShallCreateCertMasterAppService = 0 -eq $CertMasterWebApps.count } $scwebapp = Invoke-Az -azCommand @("graph", "query", "-q", "Resources | where type == 'microsoft.web/sites' and resourceGroup == '$SCEPmanResourceGroup' and name =~ '$SCEPmanAppServiceName'") | Convert-LinesToObject if([String]::IsNullOrWhiteSpace($CertMasterAppServiceName)) { $CertMasterAppServiceName = $scwebapp.data.name if ($CertMasterAppServiceName.Length -gt 57) { $CertMasterAppServiceName = $CertMasterAppServiceName.Substring(0,57) } $CertMasterAppServiceName += "-cm" $potentialCertMasterAppServiceName = Read-Host "CertMaster web app not found. Please hit enter now if you want to create the app with name $CertMasterAppServiceName or enter the name of your choice, and then hit enter" if($potentialCertMasterAppServiceName) { $CertMasterAppServiceName = $potentialCertMasterAppServiceName } } if ($true -eq $ShallCreateCertMasterAppService) { Write-Information "User selected to create the app with the name $CertMasterAppServiceName" $isLinuxAppService = IsAppServiceLinux -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup $platform = if ($isLinuxAppService){ "linux" } else { "windows" } $runtime = SelectBestDotNetRuntime -ForLinux $isLinuxAppService if ($PSCmdlet.ShouldProcess($CertMasterAppServiceName, ("Creating Certificate Master App Service with .NET Runtime {0}" -f $runtime))) { $null = Invoke-Az @("webapp", "create", "--resource-group", $CertMasterResourceGroup, "--plan", $scwebapp.data.properties.serverFarmId, "--name", $CertMasterAppServiceName, "--assign-identity", "[system]", "--https-only", 'true', "--runtime", $runtime) Write-Information "CertMaster web app $CertMasterAppServiceName created" # Do all the configuration that the ARM template does normally $SCEPmanHostname = $scwebapp.data.properties.defaultHostName if (-not [String]::IsNullOrWhiteSpace($DeploymentSlotName)) { $selectedSlot = Invoke-Az -azCommand @("graph", "query", "-q", "Resources | where type == 'microsoft.web/sites/slots' and resourceGroup == '$SCEPmanResourceGroup' and name =~ '$SCEPmanAppServiceName/$DeploymentSlotName'") | Convert-LinesToObject $SCEPmanHostname = $selectedSlot.data.properties.defaultHostName } $CertmasterAppSettingsTable = @{ WEBSITE_RUN_FROM_PACKAGE = $Artifacts_Certmaster[$platform][$UpdateChannel]; "AppConfig:AuthConfig:TenantId" = $TenantId; "AppConfig:SCEPman:URL" = "https://$SCEPmanHostname/"; } $isCertMasterLinux = IsAppServiceLinux -AppServiceName $CertMasterAppServiceName -ResourceGroup $CertMasterResourceGroup $CertMasterAppSettingsJson = AppSettingsHashTable2AzJson -psHashTable $CertmasterAppSettingsTable -convertForLinux $isCertMasterLinux Write-Verbose 'Configuring CertMaster web app settings' $null = Invoke-Az -azCommand @( "webapp", "config", "appsettings", "set", "--name", $CertMasterAppServiceName, "--resource-group", $CertMasterResourceGroup, "--settings", $CertMasterAppSettingsJson) $null = Invoke-Az -azCommand @( "webapp", "config", "set", "--name", $CertMasterAppServiceName, "--resource-group", $CertMasterResourceGroup, "--use-32bit-worker-process", "false", "--ftps-state", "Disabled", "--always-on", "true") } else { return "Skipped" } } return $CertMasterAppServiceName } function CreateSCEPmanAppService ( $SCEPmanResourceGroup, $SCEPmanAppServiceName, $AppServicePlanId) { # Find out which OS the App Service Plan uses $isLinuxAsp = IsAppServicePlanLinux -AppServicePlanId $AppServicePlanId $runtime = SelectBestDotNetRuntime -ForLinux $isLinuxAsp $null = Invoke-Az @("webapp", "create", "--resource-group", $SCEPmanResourceGroup, "--plan", $AppServicePlanId, "--name", $SCEPmanAppServiceName, "--assign-identity", "[system]", "--runtime", $runtime) Write-Information "SCEPman web app $SCEPmanAppServiceName created" Write-Verbose 'Configuring SCEPman General web app settings' $null = Invoke-Az @("webapp", "config", "set", "--name", $SCEPmanAppServiceName, "--resource-group", $SCEPmanResourceGroup, "--use-32bit-worker-process", "false", "--ftps-state", "Disabled", "--always-on", "true") $null = Invoke-Az @("webapp", "update", "--name", $SCEPmanAppServiceName, "--resource-group", $SCEPmanResourceGroup, "--client-affinity-enabled", "false") } function Confirm-AppServiceStack ($AppServiceName, $ResourceGroup) { $isAppServiceLinux = IsAppServiceLinux -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup $intendedStack = SelectBestDotNetRuntime -ForLinux $isAppServiceLinux if ($isAppServiceLinux) { # This will return a value in form 'DOTNETCORE|10.0' $query = 'linuxFxVersion' } else { # This will return a value in form 'v10.0' $query = 'netFrameworkVersion' } $actualStack = Invoke-Az @("webapp", "config", "show", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--query", $query, "--output", "tsv") if ([string]::IsNullOrWhiteSpace($actualStack)) { Write-Information "App Service $AppServiceName in resource group $ResourceGroup does not have a stack configured" Set-AppServiceStack -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup -Stack $intendedStack return } # We extract the version for the following expected formats # - Windows: 'v10.0' # - Linux: 'DOTNETCORE|10.0' # - Linux: 'DOTNETCORE:10.0' - this should not occur but is the form we use when setting the stack, so we want to be sure to handle it correctly in case it is returned by Azure in some cases # For an unexpected format, the casting might fail try { $actualVersion = [double]($actualStack -replace '(.*\||.*:|^v)') } catch { Write-Warning "Failed to parse actual stack version from stack string '$actualStack' for App Service $AppServiceName in resource group $ResourceGroup. Skipping stack check to avoid potential misconfiguration." return } if ($null -eq $actualVersion) { Write-Warning "Could not parse actual stack version from stack string '$actualStack' for App Service $AppServiceName in resource group $ResourceGroup. Skipping stack check to avoid potential misconfiguration." return } $intendedVersion = [double]($intendedStack -replace '.*:') if ($actualVersion -gt $intendedVersion) { Write-Verbose "The actual stack version $actualVersion is higher than the intended stack version $intendedVersion, skipping stack update to avoid downgrade" return } if ($actualVersion -eq $intendedVersion) { Write-Verbose "App Service $AppServiceName in resource group $ResourceGroup has the expected stack $intendedStack" } else { Write-Information "App Service $AppServiceName in resource group $ResourceGroup is expected to have stack version $intendedVersion but has stack version $actualVersion" Set-AppServiceStack -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup -Stack $intendedStack } } function Set-AppServiceStack { [CmdletBinding(SupportsShouldProcess=$true)] param( [Parameter(Mandatory=$true)][string]$AppServiceName, [Parameter(Mandatory=$true)][string]$ResourceGroup, [Parameter(Mandatory=$true)][string]$Stack ) Write-Information "Setting App Service $AppServiceName in resource group $ResourceGroup to stack $Stack" if ($PSCmdlet.ShouldProcess($AppServiceName, ("Setting stack to {0}" -f $Stack))) { $null = Invoke-Az @("webapp", "config", "set", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--runtime", $Stack) } } function GetAppServicePlan ( $AppServicePlanName, $ResourceGroup, $SubscriptionId) { $asp = ExecuteAzCommandRobustly -azCommand "az appservice plan list -g $ResourceGroup --query `"[?name=='$AppServicePlanName']`" --subscription $SubscriptionId" | Convert-LinesToObject return $asp } New-Variable -Name "CacheAppServiceKinds" -Value @{} -Scope "Script" -Option ReadOnly function IsAppServiceLinux ($AppServiceName, $ResourceGroup) { if ($CacheAppServiceKinds.ContainsKey("$AppServiceName $ResourceGroup")) { $kind = $CacheAppServiceKinds["$AppServiceName $ResourceGroup"] } else { $kind = Invoke-Az @("webapp", "show", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--query", 'kind', "--output", "tsv") $CacheAppServiceKinds["$AppServiceName $ResourceGroup"] = $kind } return $kind -eq "app,linux" } function IsAppServicePlanLinux ($AppServicePlanId) { $kind = Invoke-Az @("appservice", "plan", "show", "--id", $AppServicePlanId, "--query", 'kind', "--output", "tsv") return $kind -eq "linux" } function GetAppServiceHostNames ($SCEPmanResourceGroup, $AppServiceName, $DeploymentSlotName = $null) { if ($null -eq $DeploymentSlotName) { return ExecuteAzCommandRobustly -azCommand "az webapp config hostname list --webapp-name $AppServiceName --resource-group $SCEPmanResourceGroup --query `"[].name`" --output tsv" } else { return ExecuteAzCommandRobustly -azCommand "az webapp config hostname list --webapp-name $AppServiceName --resource-group $SCEPmanResourceGroup --slot $DeploymentSlotName --query `"[].name`" --output tsv" } } function GetPrimaryAppServiceHostName ($SCEPmanResourceGroup, $AppServiceName, $DeploymentSlotName = $null) { $SCEPmanHostNames = GetAppServiceHostNames -SCEPmanResourceGroup $SCEPmanResourceGroup -AppServiceName $SCEPmanAppServiceName -DeploymentSlotName $DeploymentSlotName if ($SCEPmanHostNames -is [array]) { return $SCEPmanHostNames[0] } else { return $SCEPmanHostNames } } function GetAppServiceVnetId ($AppServiceName, $ResourceGroup) { $vnetId = ExecuteAzCommandRobustly -callAzNatively -azCommand @("webapp", "show", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--query", 'virtualNetworkSubnetId', "--output", "tsv") return $vnetId } function SetAppServiceVnetId ($AppServiceName, $ResourceGroup, $vnetId, $DeploymentSlotName) { $command = @("webapp", "update", "--name", $AppServiceName, "-g", $ResourceGroup, "--set", "virtualNetworkSubnetId=$vnetId") if ($null -ne $DeploymentSlotName) { $command += @("--slot", $DeploymentSlotName) } $null = ExecuteAzCommandRobustly -callAzNatively -azCommand $command } function CreateSCEPmanDeploymentSlot ($SCEPmanResourceGroup, $SCEPmanAppServiceName, $DeploymentSlotName) { $existingHostnameConfiguration = ReadAppSetting -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -SettingName "AppConfig:AuthConfig:ManagedIdentityEnabledForWebsiteHostname" if([string]::IsNullOrEmpty($existingHostnameConfiguration)) { $SCEPmanSlotHostName = GetPrimaryAppServiceHostName -SCEPmanResourceGroup $SCEPmanResourceGroup -AppServiceName $SCEPmanAppServiceName SetAppSettings -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -Settings @(@{name="AppConfig:AuthConfig:ManagedIdentityEnabledForWebsiteHostname"; value=$SCEPmanSlotHostName}) -AsSlotSettings $true -Slot $DeploymentSlotName Write-Information "Specified Production Slot Activation as such via AppConfig:AuthConfig:ManagedIdentityEnabledForWebsiteHostname" } $azOutput = az webapp deployment slot create --name $SCEPmanAppServiceName --resource-group $SCEPmanResourceGroup --slot $DeploymentSlotName --configuration-source $SCEPmanAppServiceName $null = CheckAzOutput -azOutput $azOutput -fThrowOnError $true Write-Information "Created SCEPman Deployment Slot $DeploymentSlotName" return Convert-LinesToObject -lines $(az webapp identity assign --name $SCEPmanAppServiceName --resource-group $SCEPmanResourceGroup --slot $DeploymentSlotName --identities [system]) } function GetDeploymentSlots($appServiceName, $resourceGroup) { $deploymentSlots = ExecuteAzCommandRobustly -azCommand "az webapp deployment slot list --name $appServiceName --resource-group $resourceGroup --query '[].name'" | Convert-LinesToObject if ($null -eq $deploymentSlots) { return @() } else { return [array]$deploymentSlots } } function MarkDeploymentSlotAsConfigured($SCEPmanResourceGroup, $SCEPmanAppServiceName, $PermissionLevel, $DeploymentSlotName = $null) { # Add a setting to tell the Deployment slot that it has been configured $SCEPmanSlotHostName = GetPrimaryAppServiceHostName -SCEPmanResourceGroup $SCEPmanResourceGroup -AppServiceName $SCEPmanAppServiceName -DeploymentSlotName $DeploymentSlotName $managedIdentityEnabledOn = ([DateTimeOffset]::UtcNow).ToUnixTimeSeconds() Write-Verbose "[$SCEPmanAppServiceName-$DeploymentSlotName] Marking SCEPman App Service as configured (timestamp $managedIdentityEnabledOn)" $MarkAsConfiguredSettings = @( @{name="AppConfig:AuthConfig:ManagedIdentityEnabledOnUnixTime"; value=$managedIdentityEnabledOn}, @{name="AppConfig:AuthConfig:ManagedIdentityEnabledForWebsiteHostname"; value=$SCEPmanSlotHostName}, @{name="AppConfig:AuthConfig:ManagedIdentityPermissionLevel"; value=$PermissionLevel} ) SetAppSettings -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -Settings $MarkAsConfiguredSettings -Slot $DeploymentSlotName -AsSlotSettings $true } $RegExGuid = "[({]?[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12}[})]?" function ConfigureSCEPmanInstance ($SCEPmanResourceGroup, $SCEPmanAppServiceName, $ScepManAppSettings, $PermissionLevel, $SCEPmanAppId, $DeploymentSlotName = $null) { $existingApplicationId = ReadAppSetting -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -SettingName "AppConfig:AuthConfig:ApplicationId" -Slot $DeploymentSlotName Write-Debug "Existing Application Id is set to $existingApplicationId in slot $DeploymentSlotName. The new ID shall be $SCEPmanAppId." if ($null -ne $existingApplicationId) { $existingApplicationId = $existingApplicationId.Trim('"') } Write-Debug "The new ID is different to the existing ID? $($existingApplicationId -ne $SCEPmanAppId)" if(![string]::IsNullOrEmpty($existingApplicationId) -and $existingApplicationId -ne $SCEPmanAppId) { if ($existingApplicationId -notmatch $RegExGuid) { Write-Debug "Existing SCEPman Application ID is not a Guid. IsNullOrEmpty? $([string]::IsNullOrEmpty($existingApplicationId)); Is it different than the new one? $($existingApplicationId -ne $SCEPmanAppId); New ID: $SCEPmanAppId; Existing ID: $existingApplicationId" throw "SCEPman Application ID $existingApplicationId (Setting AppConfig:AuthConfig:ApplicationId) is not a GUID (Deployment Slot: $DeploymentSlotName). Aborting on unexpected setting." } Write-Debug "Creating backup of existing ApplicationId $existingApplicationId in slot $DeploymentSlotName" SetAppSettings -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -Settings @(@{name="BackUp:AppConfig:AuthConfig:ApplicationId"; value=$existingApplicationId}) -Slot $DeploymentSlotName Write-Verbose "[$SCEPmanAppServiceName-$DeploymentSlotName] Backed up ApplicationId" } SetAppSettings -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -Settings $ScepManAppSettings -Slot $DeploymentSlotName # The following setting AppConfig:AuthConfig:ApplicationKey is a secret, but we make sure it doesn't appear in the logs. Not even through az's echoes. Therefore we can ignore the leakage warning $existingApplicationKeySc = ReadAppSetting -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -SettingName "AppConfig:AuthConfig:ApplicationKey" -Slot $DeploymentSlotName Write-Verbose "[$SCEPmanAppServiceName-$DeploymentSlotName] Wrote SCEPman application settings" if(![string]::IsNullOrEmpty($existingApplicationKeySc)) { if ($existingApplicationKeySc.Contains("'")) { throw "SCEPman Application Key contains at least one single-quote character ('), which is unexpected. Aborting on unexpected setting" } SetAppSettings -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup -Settings @(@{name="BackUp:AppConfig:AuthConfig:ApplicationKey"; value=$existingApplicationKeySc}) -Slot $DeploymentSlotName $isScepmanLinux = IsAppServiceLinux -AppServiceName $SCEPmanAppServiceName -ResourceGroup $SCEPmanResourceGroup $applicationKeyKey = "AppConfig:AuthConfig:ApplicationKey" if ($isScepmanLinux) { $applicationKeyKey = $applicationKeyKey.Replace(':', '__') } $azCommand = @("webapp", "config", "appsettings", "delete", "--name", $SCEPmanAppServiceName, "--resource-group", $SCEPmanResourceGroup, "--setting-names", $applicationKeyKey) if ($null -ne $DeploymentSlotName) { $azCommand += @("--slot", $DeploymentSlotName) } $null = ExecuteAzCommandRobustly -callAzNatively -azCommand $azCommand Write-Verbose "[$SCEPmanAppServiceName-$DeploymentSlotName] Backed up ApplicationKey" } MarkDeploymentSlotAsConfigured -SCEPmanResourceGroup $SCEPmanResourceGroup -SCEPmanAppServiceName $SCEPmanAppServiceName -DeploymentSlotName $DeploymentSlotName -PermissionLevel $PermissionLevel } function ConfigureScepManAppService($SCEPmanResourceGroup, $SCEPmanAppServiceName, $DeploymentSlotName, $CertMasterBaseURL, $SCEPmanAppId, $PermissionLevel) { Write-Verbose "Configuring SCEPman web app settings for Deployment Slot [$DeploymentSlotName]" # Add ApplicationId and some additional defaults in SCEPman web app settings $ScepManAppSettings = @( @{ name='AppConfig:AuthConfig:ApplicationId'; value=$SCEPmanAppID } ) if (-not [string]::IsNullOrEmpty($CertMasterBaseURL)) { $ScepManAppSettings += @{ name='AppConfig:CertMaster:URL'; value=$CertMasterBaseURL } $ScepManAppSettings += @{ name='AppConfig:DirectCSRValidation:Enabled'; value='true' } } ConfigureSCEPmanInstance -SCEPmanResourceGroup $SCEPmanResourceGroup -SCEPmanAppServiceName $SCEPmanAppServiceName -ScepManAppSettings $ScepManAppSettings -PermissionLevel $PermissionLevel -SCEPmanAppId $SCEPmanAppID -DeploymentSlotName $DeploymentSlotName } function ConfigureCertMasterAppService($CertMasterResourceGroup, $CertMasterAppServiceName, $SCEPmanAppId, $CertMasterAppId, $PermissionLevel) { Write-Information "Setting Certificate Master configuration" $managedIdentityEnabledOn = ([DateTimeOffset]::UtcNow).ToUnixTimeSeconds() # Add ApplicationId and SCEPman API scope in certmaster web app settings $CertmasterAppSettings = @{ 'AppConfig:AuthConfig:ApplicationId' = $CertMasterAppId 'AppConfig:AuthConfig:SCEPmanAPIScope' = "api://$SCEPmanAppId" } if ($PermissionLevel -ge 0) { $CertmasterAppSettings['AppConfig:AuthConfig:ManagedIdentityEnabledOnUnixTime'] = "$managedIdentityEnabledOn" $CertmasterAppSettings['AppConfig:AuthConfig:ManagedIdentityPermissionLevel'] = $PermissionLevel } $isCertMasterLinux = IsAppServiceLinux -AppServiceName $CertMasterAppServiceName -ResourceGroup $CertMasterResourceGroup $CertmasterAppSettingsJson = AppSettingsHashTable2AzJson -psHashTable $CertmasterAppSettings -convertForLinux $isCertMasterLinux $null = ExecuteAzCommandRobustly -azCommand "az webapp config appsettings set --name $CertMasterAppServiceName --resource-group $CertMasterResourceGroup --settings '$CertmasterAppSettingsJson'" } function Update-ToConfiguredChannel { [CmdletBinding(SupportsShouldProcess=$true)] param ( [Parameter(Mandatory=$true)] [string]$AppServiceName, [Parameter(Mandatory=$true)] [string]$ResourceGroup, [Parameter(Mandatory=$true)] [hashtable]$ChannelArtifacts ) $intendedChannel = ExecuteAzCommandRobustly -azCommand @("webapp", "config", "appsettings", "list", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--query", "[?name=='Update_Channel'].value | [0]", "--output", "tsv") -callAzNatively -noSecretLeakageWarning $platform = if (IsAppServiceLinux -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup){ "linux" } else { "windows" } if (-not [string]::IsNullOrWhiteSpace($intendedChannel) -and "none" -ne $intendedChannel) { Write-Information "Switching app $AppServiceName to update channel $intendedChannel" $ArtifactsUrl = $ChannelArtifacts[$platform][$intendedChannel] if ([string]::IsNullOrWhiteSpace($ArtifactsUrl)) { Write-Warning "Could not find Artifacts URL for Channel $intendedChannel of App Service $AppServiceName on platform $platform. Available channels: $(Join-String -Separator ',' -InputObject $ChannelArtifacts[$platform].Keys)" } else { Write-Verbose "Artifacts URL is $ArtifactsUrl" if ($PSCmdlet.ShouldProcess($AppServiceName, ("Switching App Service to channel {0}" -f $intendedChannel))) { $null = ExecuteAzCommandRobustly -azCommand @("webapp", "config", "appsettings", "set", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--settings", "WEBSITE_RUN_FROM_PACKAGE=$ArtifactsUrl") -callAzNatively $null = ExecuteAzCommandRobustly -azCommand "az webapp config appsettings delete --name $AppServiceName --resource-group $ResourceGroup --setting-names ""Update_Channel""" } } } } function Confirm-ArtifactPlatform { [CmdletBinding(SupportsShouldProcess=$true)] [OutputType([bool])] param ( [Parameter(Mandatory=$true)] [string]$AppServiceName, [Parameter(Mandatory=$true)] [string]$ResourceGroup, [Parameter(Mandatory=$true)] [hashtable]$ChannelArtifacts ) $currentArtifactUrl = ReadAppSetting -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup -SettingName "WEBSITE_RUN_FROM_PACKAGE" $appPlatform = if (IsAppServiceLinux -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup){ "linux" } else { "windows" } $knownChannel = $false $artifactPlatform = $null $artifactChannel = $null foreach ($platform in $ChannelArtifacts.GetEnumerator()) { foreach ($channel in $platform.Value.GetEnumerator()) { if ($channel.Value -eq $currentArtifactUrl) { # We found the channel that corresponds to the currently set artifact URL # This means that the app is on a known channel $knownChannel = $true $artifactPlatform = $platform.Key $artifactChannel = $channel.Key } } } if (-not $knownChannel) { Write-Verbose "Current artifact URL $currentArtifactUrl does not correspond to any known channel. Assume manual update" return $false } if ($artifactPlatform -match $appPlatform) { # We are on the right platform on a known channel, nothing to do Write-Verbose "Current artifact URL $currentArtifactUrl corresponds to channel ""$artifactChannel"" on platform ""$artifactPlatform"", which matches the actual platform $appPlatform. No need to switch the artifact URL." return $true } else { Write-Information "Current artifact URL $currentArtifactUrl corresponds to channel ""$artifactChannel"" on platform ""$artifactPlatform"", which does not match the actual platform ""$appPlatform""" $intendedPlatformKey = if ($artifactPlatform -match '_alternative') { $appPlatform + "_alternative" } else { $appPlatform } $intendedArtifactUrl = $ChannelArtifacts[$intendedPlatformKey][$artifactChannel] # Accessing the hashtable on wrong keys should throw already, but we check for null or whitespace just to be sure if ([string]::IsNullOrWhiteSpace($intendedArtifactUrl)) { Write-Warning "Could not determine correct artifact URL for platform ""$artifactPlatform"" and channel ""$artifactChannel"". Expected to find it in ChannelArtifacts with key ""$intendedPlatformKey"" and channel key ""$artifactChannel"". Please check the configuration of ChannelArtifacts." return $false } Write-Information "Switching artifact URL to $intendedArtifactUrl to match the platform ""$appPlatform"" of the ""$artifactChannel"" channel" if ($PSCmdlet.ShouldProcess($AppServiceName, ("Switching artifact URL to match the platform {0}" -f $appPlatform))) { $null = ExecuteAzCommandRobustly -azCommand @("webapp", "config", "appsettings", "set", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--settings", "WEBSITE_RUN_FROM_PACKAGE=$intendedArtifactUrl") -callAzNatively return $true } # If we should not process, we return true anyway, as we are already on a known channel, just not the intended one for the platform. The user can then decide to switch manually or to stay on the current channel. return $true } } function SetAppSettings($AppServiceName, $ResourceGroup, $Settings, $Slot = $null, $AsSlotSettings = $false) { $totalSettingsCount = $Settings.Count $processedSettingsCount = 0 foreach ($oneSetting in $Settings) { $settingName = $oneSetting.name $settingValueEscaped = $oneSetting.value.ToString().Replace('"','\"') if ($settingName.Contains("=")) { Write-Warning "Setting name $settingName contains at least one equal sign (=), which is unsupported. Skipping this setting." continue } $isAppServiceLinux = IsAppServiceLinux -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup if ($isAppServiceLinux) { if ($settingName.Contains("-")) { Write-Warning "Setting name $settingName contains at least one dash (-), which is unsupported on Linux. Skipping this setting." continue } $settingName = $settingName.Replace(":", "__") } Write-Verbose "Setting app setting $settingName of app $AppServiceName in slot [$Slot]" Write-Debug "Setting $settingName to $settingValueEscaped" # there could be cases where this is a secret, so we do not use Write-Verbose if ($PSVersionTable.PSVersion.Major -eq 5 -or -not $PSVersionTable.OS.StartsWith("Microsoft Windows")) { $settingAssignment = "$settingName=$settingValueEscaped" } else { $settingAssignment = "`"$settingName`"=`"$settingValueEscaped`"" } $command = @('webapp', 'config', 'appsettings', 'set', '--name', $AppServiceName, '--resource-group', $ResourceGroup) if ($AsSlotSettings) { # $command += @('--slot-settings', $settingAssignment) Write-Verbose "Not marking setting $settingName as slot setting, as az cli 2.79 cannot create slot settings correctly. Using normal setting instead." $command += @('--settings', $settingAssignment) } else { $command += @('--settings', $settingAssignment) } if (-not [String]::IsNullOrEmpty($Slot)) { $command += @('--slot', $Slot) } $null = ExecuteAzCommandRobustly -callAzNatively -azCommand $command $processedSettingsCount++ Write-Progress -Activity "Setting app settings" -Status "Processed $processedSettingsCount of $totalSettingsCount settings" -PercentComplete (($processedSettingsCount / $totalSettingsCount) * 100) } Write-Progress -Activity "Setting app settings" -Completed -Status "Processed $processedSettingsCount of $totalSettingsCount settings" -PercentComplete 100 # The following does not work, as equal signs split this into incomprehensible gibberish: #$null = az webapp config appsettings set --name $AppServiceName --resource-group $ResourceGroup --settings (ConvertTo-Json($Settings) -Compress).Replace('"','\"') } function RemoveAppSettings($AppServiceName, $ResourceGroup, $SettingNames, $Slot = $null) { # Base command to remove app settings $command = @("webapp", "config", "appsettings", "delete", "--name", $AppServiceName, "--resource-group", $ResourceGroup) $isAppServiceLinux = IsAppServiceLinux -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup $SettingsToRemove = Foreach($settingName in $SettingNames) { if ($isAppServiceLinux) { if ($settingName.Contains("-")) { Write-Warning "Setting name $settingName contains at least one dash (-), which is unsupported on Linux. Skipping this setting." continue } $settingName = $settingName.Replace(":", "__") } Write-Output $settingName } Write-Verbose "Removing app settings $($SettingsToRemove -join ",") from app $AppServiceName in slot [$Slot]" $command += @("--setting-names") $command += $SettingsToRemove if (-not [String]::IsNullOrEmpty($Slot)) { $command += @('--slot', $Slot) } $null = Invoke-Az $command } function ReadAppSettings($AppServiceName, $ResourceGroup) { $slotSettings = ExecuteAzCommandRobustly -azCommand "az webapp config appsettings list --name $AppServiceName --resource-group $ResourceGroup --query `"[?slotSetting]`"" | Convert-LinesToObject $unboundSettings = ExecuteAzCommandRobustly -azCommand "az webapp config appsettings list --name $AppServiceName --resource-group $ResourceGroup --query `"[?!slotSetting]`"" | Convert-LinesToObject Write-Information "Read $($slotSettings.Count) slot settings and $($unboundSettings.Count) other settings from app $AppServiceName" return @{ slotSettings = $slotSettings settings = $unboundSettings } } function ReadAppSetting($AppServiceName, $ResourceGroup, $SettingName, $Slot = $null) { $isAppServiceLinux = IsAppServiceLinux -AppServiceName $AppServiceName -ResourceGroup $ResourceGroup if ($isAppServiceLinux) { $SettingName = $SettingName.Replace(":", "__") } $azCommand = @("webapp", "config", "appsettings", "list", "--name", $AppServiceName, "--resource-group", $ResourceGroup, "--query", "[?name=='$SettingName'].value | [0]") if (-not [string]::IsNullOrEmpty($Slot)) { $azCommand += @("--slot", $Slot) } $settingValue = ExecuteAzCommandRobustly -callAzNatively -azCommand $azCommand -noSecretLeakageWarning if(![string]::IsNullOrEmpty($settingValue)) { return $settingValue.Trim('"') } else { return $settingValue } } # SIG # Begin signature block # MIIv6wYJKoZIhvcNAQcCoIIv3DCCL9gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDNiNF4ciLHJNIi # mSYpOsrbX+gwHJBQdwU9bNQyv+v7VaCCFA4wggVyMIIDWqADAgECAhB2U/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/kMw4h3mKJSAfa9TCCB6gwggWQoAMCAQICDF3VjaKN # us83AvC1UTANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQ # R2xvYmFsU2lnbiBudi1zYTEyMDAGA1UEAxMpR2xvYmFsU2lnbiBHQ0MgUjQ1IEVW # IENvZGVTaWduaW5nIENBIDIwMjAwHhcNMjUxMTI3MTcwNDI2WhcNMjcxMTI4MTcw # NDI2WjCCAQwxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRIwEAYDVQQF # EwlIUkIgMTIzODExEzARBgsrBgEEAYI3PAIBAxMCREUxFzAVBgsrBgEEAYI3PAIB # AhMGSGVzc2VuMSIwIAYLKwYBBAGCNzwCAQETEU9mZmVuYmFjaCBhbSBNYWluMQsw # CQYDVQQGEwJERTEPMA0GA1UECBMGSGVzc2VuMRowGAYDVQQHExFPZmZlbmJhY2gg # YW0gTWFpbjEZMBcGA1UECQwQS2Fpc2Vyc3RyYcOfZSAzOTEXMBUGA1UEChMOZ2x1 # ZWNra2FuamEgQUcxFzAVBgNVBAMTDmdsdWVja2thbmphIEFHMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAkQoXi0dUFVttodx+Ydj1O6EZZqgDdlSGDA/6 # x1UCkMrWNVEW+LdbUGU8KW7fUcKPCAcDJNrXfXxZeBht2G4pPvhaMz/kBdSK6bI1 # sqo1WSN//beapdUefQpq/wgnUneq13tEJQAke6EWdLyidObcogBSp9wCXBbMWsTO # utgCONjyu8AilmzRY+94lO7VwUA2LGGPX8FRAEt5AMzifsEo2lIEKiDou2H8HUUC # PibiChiuT3oGIDYYnCA/RzS44E0cAuAzlD3NQNCeIDzfoFiUD8mAC1gYU6i8yIej # jUGl8+kpbpBYjgzwbsiCBn0rDhrlpJ3MHkZCrp82kzWK0l7c3ukNvdlGcU4tKdXk # AHgpJecdYUDvz9iaYFvYEivF+Jg+Tc8ZnzsP5/q3KKw4g0QiJ+MXgvwJx8OSvAKW # tkwkLxgE9oxufs3Y8xsmwyWqxWDBcyzzvs6yISnUaeTtGmyB8BsEbahDFrxHhV6U # nwxNpJ+iM+j08J1tNIW0AXjY6ojGOIC8IIL+EiK34MXJ6Jxy22mntMnc6ztK6c7H # IKiRHIPX4jXtg7IYRS/k5muuIt/xKzN7qtF9xJbaZi8jRE6fgWDwszLJUMHSLthh # yKTsUEvuqZ79WnSHErg26EPQYirAY/IFt7Z7+3SDW2WI8uG2qY6hkpE0hm+/F3uS # M+s98jUCAwEAAaOCAbYwggGyMA4GA1UdDwEB/wQEAwIHgDCBnwYIKwYBBQUHAQEE # gZIwgY8wTAYIKwYBBQUHMAKGQGh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20v # Y2FjZXJ0L2dzZ2NjcjQ1ZXZjb2Rlc2lnbmNhMjAyMC5jcnQwPwYIKwYBBQUHMAGG # M2h0dHA6Ly9vY3NwLmdsb2JhbHNpZ24uY29tL2dzZ2NjcjQ1ZXZjb2Rlc2lnbmNh # MjAyMDBVBgNVHSAETjBMMEEGCSsGAQQBoDIBAjA0MDIGCCsGAQUFBwIBFiZodHRw # czovL3d3dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5LzAHBgVngQwBAzAJBgNV # HRMEAjAAMEcGA1UdHwRAMD4wPKA6oDiGNmh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5j # b20vZ3NnY2NyNDVldmNvZGVzaWduY2EyMDIwLmNybDATBgNVHSUEDDAKBggrBgEF # BQcDAzAfBgNVHSMEGDAWgBQlndD8WQmGY8Xs87ETO1ccA5I2ETAdBgNVHQ4EFgQU # q/cn5ijjtp0mG1yoiF02hg4dx4IwDQYJKoZIhvcNAQELBQADggIBAJ1TZv/rvy2w # jANcL/kb6rTk+/6L7l49UghLghUKFVfrdEEc+21iexA7zlkvhM0TrhdiFU7TjDky # InPctzsDlqwUhawEx4PT8ZkZkZzm25YWaqtZH44st/Fz59KiG+85NUdRd+0cL3Y8 # NR66z3xfI6K3W/nrIcE6RHm/opOM+L02Hd2MBligLnoFYcTvR3NPCA21A6+IOaYM # n5YZzNKFXWry8ZHpWjnE4u9mxHYpS1zu2aIkwL8mfYM5moYoh0PAcp9XA5Sm4KrV # LeIzZ3HIy4EzLCbFBP+OGFpkqq8pTtmYItG+g1rYEg5a8egrY83zJMHazaTFBgRI # MNXCgeMZhC8O6NsAtbj3FSbiYKg1hNwZzHYL+uL3jcPZjuUoOpmvXu67xWs4ZfdT # Mluy5E7FyWwtnOjr/04EXWyKATYMDIkd47Wqam/ZB7umF5T5YPnmTlv18ArEXuVQ # EEpS/cN90DtRz2OGruu+V9bg3fk6NKDJLve8detDOTTBN0C/bFGxI5YLHmwVAdaq # pz3t14ShRjVcxP7aN0bEL3YOuQvjnjQGe29H6n/MPf8UG4WYMd+a8qIP4HROLJq0 # YJylzYBglqoQeQC/OG+PtWTvL9oByPVYNc+llAuap/xmWSLZgAqPbi+PAfow69Lg # bppHUCnJhNkXD/mJ4qB0KvPG+bzL19dEMYIbMzCCGy8CAQEwbDBcMQswCQYDVQQG # EwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEyMDAGA1UEAxMpR2xvYmFs # U2lnbiBHQ0MgUjQ1IEVWIENvZGVTaWduaW5nIENBIDIwMjACDF3VjaKNus83AvC1 # UTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkG # CSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEE # AYI3AgEVMC8GCSqGSIb3DQEJBDEiBCCNs4FH3wmWRv08EtLdCjLnXsmcNpyUxe+7 # ZDRCtdZABDANBgkqhkiG9w0BAQEFAASCAgBSVE9QvEC/92jD8GpE6lsFF3WqR0es # ont0pvqY/lGEa6t+5iYisu1ayuJebLhjyD4r5FkK24JbEonfH1/lVJLZ12cgXj83 # 4lMSvssNcAMN8ya+OP7Q5rinBJyzMzAQ4Kdv5IlGp/M8/mUi4RA3YgiN/A68OXVz # YP09bGf2+z1U5xaJMJxOeVYKOXUj4cwurYq7qrGIY9PcRLymuo1lrJ7rbl+golj+ # vPATNhHXeuP7eKz9eBtravI6pZr31w3ceLTsHIcWaumSEIBBtKkwgWWmriOWfWNW # mqMQfJA5K56bb1mN3JqMv49K9ESNk6HAkO+y9JlXmhGzb3c+ED3RbyU9vAQ7VYoT # uA++CJ0wuQIUUkeUbn7+Ooip4bNvHKF/jFLRPB2PTBN7rSisYnRCaxqAHwglOF9G # 5UUPZpELstJA0LCeEvwUB0Uv+G6Fa0NoCLmi6j+rJpjmADWDqqHCBq2w8Z5m/JyO # HhnK1T0l0tBZfii+xNVrbQO8AjvBY5cIZj6gHTxqXhoZmEjF6WMNs7P7+He5lDdt # vjuxucMWFO5p5uApmLj37Lmo+KrckVx8Y3/gCWLaaa0Zj5M7sibozffswKvJwruf # D4wL18/69brtdP65K2jDAHcQWmUD94whIT6XOna6aY8Fkk7VYp0EV83SZJAzzEgS # k6jK5L/IZv/bYKGCGBEwghgNBgorBgEEAYI3AwMBMYIX/TCCF/kGCSqGSIb3DQEH # AqCCF+owghfmAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFiBgsqhkiG9w0BCRABBKCC # AVEEggFNMIIBSQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCDtdYHI # LEg3/WKTCB4RLXa9m/JUzh2wg3XL979TTjXkBAIGagxE8CugGBMyMDI2MDYwMTEz # MTg0MS40ODZaMASAAgH0oIHhpIHeMIHbMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRp # b25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046NzgwMC0wNUUwLUQ5NDcxNTAz # BgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9y # aXR5oIIPITCCB4IwggVqoAMCAQICEzMAAAAF5c8P/2YuyYcAAAAAAAUwDQYJKoZI # hvcNAQEMBQAwdzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjFIMEYGA1UEAxM/TWljcm9zb2Z0IElkZW50aXR5IFZlcmlmaWNhdGlv # biBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDIwMB4XDTIwMTExOTIwMzIz # MVoXDTM1MTExOTIwNDIzMVowYTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0Eg # VGltZXN0YW1waW5nIENBIDIwMjAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK # AoICAQCefOdSY/3gxZ8FfWO1BiKjHB7X55cz0RMFvWVGR3eRwV1wb3+yq0OXDEqh # UhxqoNv6iYWKjkMcLhEFxvJAeNcLAyT+XdM5i2CgGPGcb95WJLiw7HzLiBKrxmDj # 1EQB/mG5eEiRBEp7dDGzxKCnTYocDOcRr9KxqHydajmEkzXHOeRGwU+7qt8Md5l4 # bVZrXAhK+WSk5CihNQsWbzT1nRliVDwunuLkX1hyIWXIArCfrKM3+RHh+Sq5RZ8a # Yyik2r8HxT+l2hmRllBvE2Wok6IEaAJanHr24qoqFM9WLeBUSudz+qL51HwDYyID # PSQ3SeHtKog0ZubDk4hELQSxnfVYXdTGncaBnB60QrEuazvcob9n4yR65pUNBCF5 # qeA4QwYnilBkfnmeAjRN3LVuLr0g0FXkqfYdUmj1fFFhH8k8YBozrEaXnsSL3kdT # D01X+4LfIWOuFzTzuoslBrBILfHNj8RfOxPgjuwNvE6YzauXi4orp4Sm6tF245Da # FOSYbWFK5ZgG6cUY2/bUq3g3bQAqZt65KcaewEJ3ZyNEobv35Nf6xN6FrA6jF944 # 7+NHvCjeWLCQZ3M8lgeCcnnhTFtyQX3XgCoc6IRXvFOcPVrr3D9RPHCMS6Ckg8wg # gTrtIVnY8yjbvGOUsAdZbeXUIQAWMs0d3cRDv09SvwVRd61evQIDAQABo4ICGzCC # AhcwDgYDVR0PAQH/BAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRr # aSg6NS9IY0DPe9ivSek+2T3bITBUBgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9z # aXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoA # UwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUyH7SaoUqG8oZ # mAQHJ89QEE9oqKIwgYQGA1UdHwR9MHsweaB3oHWGc2h0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZp # Y2F0aW9uJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5j # cmwwgZQGCCsGAQUFBwEBBIGHMIGEMIGBBggrBgEFBQcwAoZ1aHR0cDovL3d3dy5t # aWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBJZGVudGl0eSUy # MFZlcmlmaWNhdGlvbiUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUy # MDIwMjAuY3J0MA0GCSqGSIb3DQEBDAUAA4ICAQBfiHbHfm21WhV150x4aPpO4dhE # mSUVpbixNDmv6TvuIHv1xIs174bNGO/ilWMm+Jx5boAXrJxagRhHQtiFprSjMktT # liL4sKZyt2i+SXncM23gRezzsoOiBhv14YSd1Klnlkzvgs29XNjT+c8hIfPRe9rv # VCMPiH7zPZcw5nNjthDQ+zD563I1nUJ6y59TbXWsuyUsqw7wXZoGzZwijWT5oc6G # vD3HDokJY401uhnj3ubBhbkR83RbfMvmzdp3he2bvIUztSOuFzRqrLfEvsPkVHYn # vH1wtYyrt5vShiKheGpXa2AWpsod4OJyT4/y0dggWi8g/tgbhmQlZqDUf3UqUQsZ # aLdIu/XSjgoZqDjamzCPJtOLi2hBwL+KsCh0Nbwc21f5xvPSwym0Ukr4o5sCcMUc # Sy6TEP7uMV8RX0eH/4JLEpGyae6Ki8JYg5v4fsNGif1OXHJ2IWG+7zyjTDfkmQ1s # nFOTgyEX8qBpefQbF0fx6URrYiarjmBprwP6ZObwtZXJ23jK3Fg/9uqM3j0P01nz # VygTppBabzxPAh/hHhhls6kwo3QLJ6No803jUsZcd4JQxiYHHc+Q/wAMcPUnYKv/ # q2O444LO1+n6j01z5mggCSlRwD9faBIySAcA9S8h22hIAcRQqIGEjolCK9F6nK9Z # yX4lhthsGHumaABdWzCCB5cwggV/oAMCAQICEzMAAABXJNOV4KLpyTEAAAAAAFcw # DQYJKoZIhvcNAQEMBQAwYTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGlt # ZXN0YW1waW5nIENBIDIwMjAwHhcNMjUxMDIzMjA0NjUzWhcNMjYxMDIyMjA0NjUz # WjCB2zELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UE # CxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVs # ZCBUU1MgRVNOOjc4MDAtMDVFMC1EOTQ3MTUwMwYDVQQDEyxNaWNyb3NvZnQgUHVi # bGljIFJTQSBUaW1lIFN0YW1waW5nIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEB # BQADggIPADCCAgoCggIBALFspQqTCH24syS2NZD1ztnJl9h0Vr0WwJnikmeXse/4 # wspnVexGqfiHNoqkbVg5CinuYC+iVfNMLZ+QtqhySz8VGBSjRt1JB5ACNtTKAjfm # Fp4U/Cv2Lj4m+vuve9I3W3hSiImTFsHeYZ6V/Sd43rXrhHV26fw3xQSteSbg9yTs # 1rhdrLkAj4KmI0D5P4KavtygirVyUW10gkifWLSE1NiB8Jn3RO5dj32deeMNONaa # Pnw3k49ICTs3Ffyb+ekNDPsNfYwCqPyOTxM6y1dSD0J5j+KK9V+EWyV5PDjV8jjn # 1zsStlS6TcYJJStcgHs2xT9rs6ooWl5FtYfRkCxhDShEp3s8IHUWizTWmLZvAE/6 # WR2Cd+ZmVapGXTCHJKUByZPxdX0i8gynirR+EwuHHNxEilDICLatO2WZu+CQrH4Z # q0NYo1TQ4tUpZ/kAWpoAu1r4mW5EJ3HkEavQ2PuoQDcDq2rAGVIla9pD7o9Yxwzl # 81BuDvUEyu9D/6F0qmQDdaE791HxfCUxpgMYPpdWTzs+dDGPehwQ8P92yP8ARjby # 5Ony1Z68RjeQebpxf5WL441myFHcgT1UJzzil7tPEkR22NfTNR6Fl+jzWb/r80nq # lXllhynSowtxo1Y22xqYviS24smikUsBKqOPbSS77uvXEO3VrG5LGouE1EZ1Y9pj # AgMBAAGjggHLMIIBxzAdBgNVHQ4EFgQUjoPJXi01DgIJSGfm416Yg+0SkqcwHwYD # VR0jBBgwFoAUa2koOjUvSGNAz3vYr0npPtk92yEwbAYDVR0fBGUwYzBhoF+gXYZb # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIw # UHVibGljJTIwUlNBJTIwVGltZXN0YW1waW5nJTIwQ0ElMjAyMDIwLmNybDB5Bggr # BgEFBQcBAQRtMGswaQYIKwYBBQUHMAKGXWh0dHA6Ly93d3cubWljcm9zb2Z0LmNv # bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwUHVibGljJTIwUlNBJTIwVGltZXN0 # YW1waW5nJTIwQ0ElMjAyMDIwLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM # MAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDBmBgNVHSAEXzBdMFEGDCsGAQQB # gjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v # cGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wCAYGZ4EMAQQCMA0GCSqGSIb3DQEB # DAUAA4ICAQBydcB2POmZOUlAQz2NuXf7vWCVWmjWu9bsY1+HMjv1yeLjxDQkjsJE # U5zaIDy8Uw9BYN8+ExX/9k/9CBUsXbVlbU44c65/liyJ83kWsFIUwhVazwSShFlb # IZviIO/5weyWyTfPPpbSJgWy+ZE9UrQS3xulJLAHA2zUkMMPdAlF4RrngcZZ0r45 # AF9aIYjdestWwdrNK70MfArHqZdgrgXn03w6zBs1v7czceWGitg/DlsHqk1mXBpS # TuGI2TSPN3E60IIXx5f/AFzh4/HFi98BBZbUELNsXkWAG9ynZ5e6CFiil1mgWCWO # T90D7Igvg0zKe3o3WCk629/en94K/sC/zLOf2d7yFmTySb9fKjcONH1Db3kZ8MzE # J8fHTNmxrl10Gecuz/Gl0+ByTKN+PambZ+F0MIlBPww6fvjFC9JII73fw3qO169+ # 9TxTz2G+E26GYY1dcffsAhw6DqTQgbflbl1O/MrSXSs0NSb9nBD9RfR/f8Ei7DA1 # L1jBO7vZhhJTjw2TzFa/ALgRLi3W00hHWi8LGQaZc8SwXIMYWfwrN9MgYbhN0Iak # 9WA2dqWuekXsTwNkmrD3E6E+oCYCehNOgZmds0Ezb1jo7OV0Kh22Ll3KHg3MHtlG # guxAzhg/BpixPS4qrULLkAjO7+yNsUfrD2U9gMf/OR4yJDPtzM0ytTGCB0Mwggc/ # AgEBMHgwYTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5n # IENBIDIwMjACEzMAAABXJNOV4KLpyTEAAAAAAFcwDQYJYIZIAWUDBAIBBQCgggSc # MBEGCyqGSIb3DQEJEAIPMQIFADAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQw # HAYJKoZIhvcNAQkFMQ8XDTI2MDYwMTEzMTg0MVowLwYJKoZIhvcNAQkEMSIEIFdL # 8d7m4yWTOjHA6U8MzQ4UedkPFIfV08C9cCLgCNqdMIG5BgsqhkiG9w0BCRACLzGB # qTCBpjCBozCBoAQg9TyfZLUFbkxliGyizuH9VVDpVFNvQEQhKQ2ZhUx421IwfDBl # pGMwYTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENB # IDIwMjACEzMAAABXJNOV4KLpyTEAAAAAAFcwggNeBgsqhkiG9w0BCRACEjGCA00w # ggNJoYIDRTCCA0EwggIpAgEBMIIBCaGB4aSB3jCB2zELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg # T3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjc4MDAtMDVFMC1E # OTQ3MTUwMwYDVQQDEyxNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lIFN0YW1waW5n # IEF1dGhvcml0eaIjCgEBMAcGBSsOAwIaAxUA/S8xOZxCUQFBNkrN8Wiij1x5y8Og # ZzBlpGMwYTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5n # IENBIDIwMjAwDQYJKoZIhvcNAQELBQACBQDtx+bPMCIYDzIwMjYwNjAxMTEwOTAz # WhgPMjAyNjA2MDIxMTA5MDNaMHQwOgYKKwYBBAGEWQoEATEsMCowCgIFAO3H5s8C # AQAwBwIBAAICP3IwBwIBAAICEzAwCgIFAO3JOE8CAQAwNgYKKwYBBAGEWQoEAjEo # MCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG # 9w0BAQsFAAOCAQEAkQ6ePXwUOKhfMLpgEBLmOP/2pj5SAVnyfmi0jrlyrriUrxV5 # C6fQ0wFmao/pwwEH6YJVME6+qRFg3wirncaa/v8KmzcRFRPzypU2glkfmwKCltCa # se8zm8g7uuMBdun1brU9W3anqubxgKBR+FukWpYKppod96ZxFgTPOsOVEMySIGfA # hzhok0rsRhwMXIS1RlWOkFtP3tYXS0d1lx8yT6ck4N+KOz4iL8SrtgMf81oE7ujE # TIZHd8HIdandAtL7yWgM8suigS6GkRWo3cY2R5L5HiOlxCRwQbkwC5uW28dtEyS2 # b8SVusNBT4DSAwQIQt++PbI5IrxUPq4pCwLvnTANBgkqhkiG9w0BAQEFAASCAgAb # o+i1+NnBO4+VAnnQmy9FQRhtOpTRS7yZDjRLS1ySU4oWFioCgQ41o/QBAdDwQ4M6 # 5jcpIJII/UZso5LpK5iRUCZHkLgolKQImOIuw6qlDTmALgn6P1AiuGBQdeyvhQGm # ipropDRUFaBL0SmGBWT0qZr4ei77ONCCM2XHM6bEfFZ2CpkWQ2xHQxj7kHZr98uw # M4JbGbYLEVpxrx97uuz1Nj/SKoCMJfOQFpgMOKwHbyizLoa3Sw9Mo6WV+D4NN0JN # JIlqrQIiYQHA7iLe2edaOogtI/FbeRaTQ29o8fXQPnMhYJCrvLOonORpFfVEvpAQ # KoI3gB+mdBZ2VnrG05mqvULTlvOV7EFjPEA+QeUT7Z1sL82yoOgsv8LvhQnE1TBw # FM+DurC16RtxWfGDQqYaR1+dCMu6UAD5G3TAZ370Bbf4cKMOReYwMe06r7RX2vqz # s/LECprlts1v883Px7zg7yztxCJsaEk8Xu2GGZb1GKdzuIYOprZDiWLyX9KZfqju # nvFD/c+qWbTlp8wJnoIQ2isWzhVfcANNtfQw+D49nWwHVdy7Y66Lcw6JlIFPMVh2 # JhaWp/jqK39cy66BlpTJoW+s7LgG24dIHimFpSKsYwADuLvpt1DnOpM6g61nQEnn # dgbTVYJpePpN17RzKUOnUvseY3Lm4cdhadK/Jhj7aw== # SIG # End signature block |