ozo-ad-windows-enable-bitlocker.ps1

#Requires -Version 5.1 -RunAsAdministrator

<#PSScriptInfo
    .VERSION 1.0.0
    .GUID c253d6ab-fb10-4d07-b03c-f2b630a39fa1
    .AUTHOR Andy Lievertz <alievertz@onezeroone.dev>
    .COMPANYNAME One Zero One
    .COPYRIGHT This script is released under the terms of the GNU General Public License ("GPL") version 2.0.
    .TAGS
    .LICENSEURI https://github.com/onezeroone-dev/OZO-AD-Windows-Enable-BitLocker/blob/main/LICENSE
    .PROJECTURI https://github.com/onezeroone-dev/OZO-AD-Windows-Enable-BitLocker
    .ICONURI
    .EXTERNALMODULEDEPENDENCIES
    .REQUIREDSCRIPTS
    .EXTERNALSCRIPTDEPENDENCIES
    .RELEASENOTES https://github.com/onezeroone-dev/OZO-AD-Windows-Enable-BitLocker/blob/main/CHANGELOG.md
#>


<#
    .DESCRIPTION
    Enables BitLocker on all local fixed volumes after verifying that the endpoint meets the BitLocker prerequisites and has successfully recorded a recovery password in Active Directory.
    .PARAMETER GPOName
    The name of the group policy containing the BitLocker settings. The script will proceed only if this GPO is applied.
    .PARAMETER Restart
    Restarts the computer after enabling BitLocker to perform the hardware test and begin encrypting.
    .PARAMETER SkipSecureBootCheck
    By default, the script will proceed only if Secure Boot is enabled, however, Secure Boot is not a strict requirement for enabling BitLocker. Specifying this parameter will allow the script to continue even if Secure Boot if off.
    .EXAMPLE
    powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File ozo-ad-windows-enable-bitlocker.ps1 -GPOName "OZO BitLocker Settings Policy" -SkipSecureBootCheck
    .EXAMPLE
    ozo-ad-windows-enable-bitlocker -GPOName "All Workstations Settings" -Restart
    .LINK
    https://github.com/onezeroone-dev/OZO-AD-Windows-Enable-BitLocker/blob/main/README.md
    .NOTES
    Messages are written to the Windows Event Viewer "One Zero One" provider. When this provider is not available, messages are written to the Microsoft-Windows-PowerShell provider with Event ID 4100.
#>


# PARAMETERS
[CmdletBinding(SupportsShouldProcess = $true)] Param (
    [Parameter(Mandatory=$true,HelpMessage="BitLocker settings GPO name")][String]$GPOName,
    [Parameter(Mandatory=$false,HelpMessage="Restarts the computer after enabling BitLocker")][Switch]$Restart,
    [Parameter(Mandatory=$false,HelpMessage="Disables checking if Secure Boot is on")][Switch]$SkipSecureBootCheck
)

# CLASSES
Class OZOMain {
    # PROPERTIES: Booleans, Strings
    [Boolean] $Restart     = $false
    [Boolean] $skipSBCheck = $false
    [XML]     $rsopXml     = $null
    # PROPERTIES: Lists
    [System.Collections.Generic.List[PSCustomObject]] $blVolumes = @()
    # METHODS: Constructor method
    OZOMain($GPOName,$Restart,$SkipSecureBootCheck) {
        # Set properties
        $this.Restart = $Restart
        # Letting skipSBCheck = $true for now - once we work out how to relaunch in 64-bit with parameters passed, this can be uncommented
        $this.skipSBCheck = $SkipSecureBootCheck
        # Declare ourselves to the world
        Write-OZOProvider -Message "Starting process." -Level "Information"
        # Determine if the environment validates, the GPO is refreshed, the required GPO is adopted, [Secure Boot is enabled], and the TPM is good
        If (($this.ValidateEnvironment() -And $this.GenerateRSoP() -And $this.EvaluateGPOSettings($GPOName) -And $this.EvaluateSecureBoot() -And $this.EvaluateTPM()) -eq $true) {
            # The environment validates, the GPO is refreshed, the required GPO is adopted, [Secure Boot is enabled], and the TPM is good; iterate on the BitLocker volume mount points
            ForEach ($mountPoint in (Get-BitLockerVolume).MountPoint) {
                # Determine if mount point is a letter
                If (($mountPoint -Split(":"))[0] -CMatch "^[A-Z]") {
                    # Create an OZOBLVolume object for this mount point
                    $this.blVolumes.Add(([OZOBLVolume]::new($mountPoint)))
                }
            }
            # Call RestartComputer to determine if a restart is requested and (if yes) execute it
            $this.RestartComputer()
        }
        # Declare ourselves to the world
        Write-OZOProvider -Message "Process complete." -Level "Information"
    }
    # METHODS: Environment validation method
    Hidden [Boolean] ValidateEnvironment() {
        # Control variable
        [Boolean] $Return = $true
        # Try to quietly import the BitLocker module
        Try {
            Import-Module -Name "BitLocker" -ErrorAction Stop *> $null
            # Success
        } Catch {
            # Failure
            Write-OZOProvider -Message "Error importing the BitLocker module." -Level "Error"
            $Return = $false
        }
        # Try to quietly import the TrustedPlatformModule module
        Try {
            Import-Module -Name "TrustedPlatformModule" -ErrorAction Stop *> $null
            # Success
        } Catch {
            # Failure
            Write-OZOProvider -Message "Error importing the TrustedPlatformModule module." -Level "Error"
            $Return = $false
        }
        # Return
        return $Return
    }
    # METHODS: Generate RSoP method
    Hidden [Boolean] GenerateRSoP() {
        # Control variable
        [Boolean] $Return = $true
        # Local variables
        [String]$rsopXmlPath = (Join-Path -Path $Env:windir -ChildPath ("Temp\" + (New-Guid).Guid + "-RSoP.xml"))
        # Try to generate an RSoP
        Try {
            Start-Process -FilePath (Join-Path -Path $Env:windir -ChildPath "System32\gpresult.exe") -ArgumentList "/scope","computer","/X",$rsopXmlPath -NoNewWindow -Wait -ErrorAction Stop
            # Success; try to read in XML
            Try {
                $this.rsopXml = [Xml](Get-Content -Path $rsopXmlPath -ErrorAction Stop)
                # Success; try to remove the file
                Try {
                    Remove-Item -Force -Path $rsopXmlPath -ErrorAction Stop
                    # Success
                } Catch {
                    Write-OZOProvider -Message ("Failed to remove temporary RSoP XML results file, " + $rsopXmlPath + ".") -Level "Warning"
                }
            } Catch {
                # Failure
                Write-OZOProvider -Message ("Failed to read RSoP XML results file, " + $rsopXmlPath + ".") -Level "Warning"
            }
        } Catch {
            # Failure
            Write-OZOProvider -Message ("Failed to generate RSoP XML results file. Error message is: " + $_) -Level "Warning"
            $Return = $false
        }
        # Return
        return $Return
    }
    # METHODS: Evaluate GPO settings method
    Hidden [Boolean] EvaluateGPOSettings($GPOName) {
        # Control variable
        [Boolean] $Return = $true
        # Determine if the GPO name appears in the RSoP XML
        If ($this.rsopXml.Rsop.ComputerResults.GPO.Name -Contains $GPOName) {
            # GPO Name appears in the XML; determine if it is access denied
            If (($this.rsopXml.Rsop.ComputerResults.GPO | Where-Object {$_.Name -eq $GPOName}).AccessDenied -eq $true) {
                # Access is denied
                Write-OZOProvider -Message ("Access is denied to " + $GPOName + ".") -Level "Error"
                $Return = $false
            }
        } Else {
            # GPO Name does not appear in the XML
            Write-OZOProvider -Message ($GPOName + " not found.") -Level "Error"
            $Return = $false
        }
        # Return
        return $Return
    }
    # METHODS: Evaluate SecureBoot method
    Hidden [Boolean] EvaluateSecureBoot() {
        # Control variable
        [Boolean] $Return = $true
        # Determine if we are not skipping Secure Boot check
        If ($this.skipSBCheck -eq $false) {
            # We are not skipping Secure Boot check; determine if SecureBoot is present and enabled
            Try {
                Confirm-SecureBootUEFI -ErrorAction Stop
                # Success
            } Catch {
                # Failure
                Write-OZOProvider -Message "UEFI Secure Boot is present and disabled or not present." -Level "Error"
                $Return = $false
            }
        }
        # Return
        return $return
    }
    # METHODS: Evaluate TPM method
    Hidden [Boolean] EvaluateTPM() {
        # Control variable
        [Boolean] $Return = $true
        # Local variable
        [PSCustomObject] $Tpm = $null
        # Try to get the TPM
        Try {
            $Tpm = (Get-Tpm -ErrorAction Stop)
            # Success; determine if TPM
            If (((Get-WmiObject -class Win32_Tpm -namespace root\CIMV2\Security\MicrosoftTpm).SpecVersion -Split(",")) -Contains "2.0") {
                # TPM 2.0; determine if it does not meet requirements
                If (($Tpm.TpmPresent -And $Tpm.TpmReady -And $Tpm.TpmEnabled -And $Tpm.TpmActivated) -eq $false) {
                    # One or more requirements not met
                    Write-OZOProvider -Message "TPM 2.0 is not present and ready and enabled and activated." -Level "Error"
                    $Return = $false
                }
            } Else {
                # TPM 1.2; determine if it does not meet requirements
                If (($Tpm.TpmPresent -And $Tpm.TpmReady) -eq $false) {
                    # One or more requirements not met
                    Write-OZOProvider -Message "TPM 1.2 is not present and ready." -Level "Error"
                    $Return = $false
                }
            }
        } Catch {
            # Failure
            Write-OZOProvider -Message "TPM not found." -Level "Error"
            $Return = $false
        }
        # Return
        return $Return
    }
    # METHODS: RestartComputer method
    Hidden [Void] RestartComputer() {
        # Determine if the operator requested restart
        If ($this.Restart -eq $true) {
            # Operator requested restart
            Write-OZOProvider -Message "Computer will restart in 30 seconds." -Level "Warning"
            # Sleep for 30 seconds
            Start-Sleep -Seconds 30
            # Force restart
            Restart-Computer -Force
        }
    }
}

Class OZOBLVolume {
    # PROPERTIES: Booleans and Strings
    [Boolean] $Validates  = $false
    [String]  $mountPoint = $null
    # PROPERTIES: System Objects
    [System.Object] $blVolume  = $null
    [System.Object] $phyVolume = $null
    # METHODS: Constructor Method
    OZOBLVolume([String]$MountPoint) {
        # Set properties
        $this.mountPoint = $MountPoint
        # Read the volume
        $this.ReadVolume()
        # Switch on DriveType + VolumeStatus + ProtectionStatus
        Switch -Wildcard ($this.phyVolume.DriveType + " " + $this.blVolume.VolumeStatus + " " + $this.blVolume.ProtectionStatus) {
            ("Removable*") {
                # Removable disk; do nothing
                Write-OZOProvider -Message ($this.blVolume.MountPoint + " is a removable disk; skipping") -Level "Warning"
            }
            ("Fixed FullyEncrypted On") {
                # Fixed disk with BitLocker enabled and Protectionstatus on; determine if protection is not TPM + RecoveryPassword
                If ($this.blVolume.KeyProtector.KeyProtectorType -NotContains "Tpm" -And $this.blVolume.KeyProtector.KeyProtectorType -NotContains "RecoveryPassword") {
                    # Volume does not have the correct protectors; decrypt then encrypt with proper protectors
                    Write-OZOProvider -Message ($this.blVolume.MountPoint + " is encrypted but does not have the correct protectors; decrypting then re-encrypting.") -Level "Warning"
                    $this.DisableBitLocker()
                    $this.EnableBitLocker()
                } Else {
                    # Volume has correct protectors
                    Write-OZOProvider -Message ($this.blVolume.MountPoint + " is encrypted and already has the correct protectors; skipping") -Level "Information"
                }
            }
            ("Fixed FullyEncrypted Off") {
                # Fixed disk with BitLocker enabled but no key protectors; decrypt and encrypt
                Write-OZOProvider -Message ($this.blVolume.MountPoint + " is encrypted but has no protectors; decrypting then re-encrypting.") -Level "Warning"
                $this.DisableBitLocker()
                $this.EnableBitLocker()
            }
            ("Fixed FullyDecrypted Off") {
                # Determine if there are NOT Tpm and RecoveryPassword protectors (Fixed FullyDecrypted Off but with Tpm and RecoveryPassword protectors indicates that BitLocker has been enabled but is pending reboot)
                If ($this.blVolume.KeyProtector.KeyProtectorType -NotContains "Tpm" -And $this.blVolume.KeyProtector.KeyProtectorType -NotContains "RecoveryPassword") {
                    # Fixed disk with no encryption and not pending reboot; encrypt
                    Write-OZOProvider -Message ($this.blVolume.MountPoint + " is not encrypted; encrypting.") -Level "Warning"
                    $this.EnableBitLocker()
                } Else {
                    # Fixed disk with no encryption and pending reboot; skip
                    Write-OZOProvider -Message ($this.blVolume.MountPoint + " is pending reboot; skipping.") -Level "Warning"
                }
            }
            default {
                # Nothing to do
                Write-OZOProvider -Message ($this.blVolume.MountPoint + " meets no criteria (likely already encrypted with correct protectors)") -Level "Information"
            }
        }
        # Reread the volume
        $this.ReadVolume()
    }
    # METHODS: Read volume method
    Hidden [Void] ReadVolume() {
        # Store the BitLocker volume object
        $this.blVolume = (Get-BitLockerVolume -MountPoint $this.mountPoint)
        # Store the physical volume object
        $this.phyVolume = (Get-Volume -DriveLetter ($this.mountPoint -Split(":"))[0])
    }
    # METHODS:
    [Void] EnableBitLocker() {
        # Call ManageTpmProtector, ManageRecoveryPasswordProtector, and BackupKeyProtector to determine if we can enable BitLocker
        If (($this.ManageTpmProtector() -And $this.ManageRecoveryPasswordProtector() -And $this.BackupKeyProtector()) -eq $true) {
            # TPM Protector is managed, RecoveryPassword protector is managed and keys are backed up to AD; enable BitLocker
            Try {
                Enable-BitLocker -MountPoint $this.blVolume.MountPoint -TpmProtector -ErrorAction Stop
                # Success
                Write-OZOProvider -Message ("Enabled Bitlocker on " + $this.blVolume.MountPoint + ".") -Level "Information"
                # Determine if this volume is *not* the OS volume
                If ($this.blVolume.VolumeType -ne "OperatingSystem") {
                    # Volume is not the OS volume; try to enable automagic unlock
                    Try {
                        Enable-BitLockerAutoUnlock -MountPoint $this.blVolume.MountPoint -ErrorAction Stop
                        # Success
                        Write-OZOProvider -Message ("Set auto-unlock for " + $this.blVolume.MountPoint + ".") -Level "Information"
                    } Catch {
                        # Failure
                        Write-OZOProvider -Message ("Error setting auto-unlock on " + $this.blVolume.MountPoint + ". Error message is: " + $_) -Level "Error"
                    }
                }
            } Catch {
                # Failure
                Write-OZOProvider -Message ("Error enabling BitLocker on " + $this.blVolume.MountPoint + ". Error message is: " + $_) -Level "Error"
            }
        }
        # Re-read the volume
        $this.ReadVolume()
    }
    # METHODS: Disable BitLocker method
    Hidden [Boolean] DisableBitLocker() {
        # Control variable
        [Boolean] $Return = $true
        # Try to decrypt
        Try {
            Disable-BitLocker -MountPoint $this.blVolume.MountPoint -ErrorAction Stop
            # Success; wait until decryption is complete
            Do {
                Start-Sleep -Seconds 60
                Write-OZOProvider -Message ("Waiting for decryption to complete on " + $this.blVolume.MountPoint + ".") -Level "Warning"
            } Until ((Get-BitLockerVolume -MountPoint $this.blVolume.MountPoint).VolumeStatus -eq "FullyDecrypted")
            # Decryption complete; log
            Write-OZOProvider -Message ("Decryption of " + $this.blVolume.MountPoint + " is complete.") -Level "Information"
        } Catch {
            # Failure
            Write-OZOProvider -Message ("Decryption of " + $this.blVolume.MountPoint + " failed.") -Level "Error"
            $Return = $false
        }
        # Re-read the volume
        $this.ReadVolume()
        # Return
        return $Return
    }
    # METHODS: Manage TPM Protector method
    Hidden [Boolean] ManageTpmProtector() {
        # Control variable
        $Return = $true
        # Determine if a TPM Protector is defined
        If (($this.blVolume.KeyProtector | Where-Object {$_.KeyprotectorType -eq "Tpm"}).Count -gt 0) {
            # Found existing TPM protector (dealbreaker)
            Write-OZOProvider -Message ("TPM protector for " + $this.blVolume.MountPoint + " already exists; attempting to remove.") -Level "Warning"
            # Try to remove
            ForEach ($keyProtectorID in ($this.blVolume.KeyProtector.KeyProtectorID)) {
                Try {
                    Remove-BitLockerKeyProtector -MountPoint $this.blVolume.MountPoint -KeyProtectorId $keyProtectorID -ErrorAction Stop
                    # Success
                    Write-OZOProvider -Message ("Removed Key Protector ID " + $keyProtectorID + " from " + $this.blVolume.MountPoint + ".") -Level "Information"
                } Catch {
                    # Failure
                    Write-OZOProvider -Message ("Unable to remove Key Protector ID " + $keyProtectorID + " from " + $this.blVolume.MountPoint + "; aborting.") -Level "Error"
                    $Return = $false
                }
            }
        }
        # Re-read the volume
        $this.ReadVolume()
        # Return
        return $Return
    }
    # METHODS: Manage RecoveryPassword Protector method
    Hidden [Boolean] ManageRecoveryPasswordProtector() {
        # Control variable
        $Return = $true
        # Determine if a RecoveryPassword protector exists
        If (($this.blVolume.KeyProtector | Where-Object {$_.KeyProtectorType -eq "RecoveryPassword"}).Count -eq 0) {
            # None found; log
            Write-OZOProvider -Message ("No Recovery Password protector found for " + $this.blVolume.MountPoint + "; attempting to add.") -Level "Information"
            # Try to add a RecoveryPassword protector
            Try {
                Add-BitLockerKeyProtector -MountPoint $this.blVolume.MountPoint -RecoveryPasswordProtector -ErrorAction Stop
                # Success
                Write-OZOProvider -Message ("Added Recovery Password protector for " + $this.blVolume.MountPoint + ".") -Level "Information"
            } Catch {
                # Failure
                Write-OZOProvider -Message ("Error adding Recovery Password protector for " + $this.blVolume.MountPoint + ". Error message is: " + $_) -Level "Error"
                $Return = $false
            }
        }
        # Re-read volume
        $this.ReadVolume()
        # Return
        return $Return
    }
    # METHODS: Backup Key Protector method
    Hidden [Boolean] BackupKeyProtector() {
        # Control variable
        [Boolean] $Return = $true
        # Iterate through all RecoveryPassword protectors and back them up to AD
        ForEach ($keyProtector in ($this.blVolume.KeyProtector | Where-Object {$_.KeyProtectorType -eq "RecoveryPassword"})) {
            # Try to back up the key protector
            Try {
                Backup-BitLockerKeyProtector -MountPoint $this.blVolume.MountPoint -KeyProtectorId $keyProtector.KeyProtectorID -ErrorAction Stop
                # Success
                Write-OZOProvider -Message ("Recovery Password protector for " + $this.blVolume.MountPoint + " backed up to AD.") -Level "Information"
            } Catch {
                # Failure
                Write-OZOProvider -Message ("Error backing up Recovery Password protector for " + $this.blVolume.MountPoint + " to AD. Error message is: " + $_) -Level "Error"
                $Return = $false
            }
        }
        # Re-read volume
        $this.ReadVolume()
        # Return
        return $Return
    }
}

# FUNCTIONS
Function Get-OZOPSArgumentList {
    # Parameters
    Param (
        [Parameter(Mandatory=$true)][System.Object]$InvocationInfo
    )
    # Local variables
    [System.Collections.Generic.List[String]]$ArgumentList = @()
    # Add arguments to list
    $ArgumentList.Add("-ExecutionPolicy")
    $ArgumentList.Add("ByPass")
    $ArgumentList.Add("-WindowStyle")
    $ArgumentList.Add("Hidden")
    $ArgumentList.Add("-NonInteractive")
    $ArgumentList.Add("-File")
    $ArgumentList.Add(('"' + $InvocationInfo.MyCommand.Definition + '"'))
    # Iterate through the bound parameters and add to list
    ForEach ($BoundParameter in $InvocationInfo.BoundParameters.GetEnumerator()) {
        Switch ($BoundParameter.Value.GetType().Name) {
            "String" {
                $ArgumentList.Add("-" + $BoundParameter.Key)
                $ArgumentList.Add('"' + $BoundParameter.Value + '"')
            }
            "SwitchParameter" {
                If ($BoundParameter.Value -eq $true) { $ArgumentList.Add("-" + $BoundParameter.Key) }
            }
            default {
                $ArgumentList.Add("-" + $BoundParameter.Key)
                $ArgumentList.Add($BoundParameter.Value)
            }
        }
    }
    # Return
    return $ArgumentList
}

Function Write-OZOProvider {
    [CmdLetBinding()] Param(
        [Parameter(Mandatory=$true)] [String] $Message,
        [Parameter(Mandatory=$false)][String] $Level = "Information"
    )
    # Initialize variables
    [String] $Source = $null
    [Int32]  $Id     = 1000
    # Switch on level to set Id
    Switch($Level.ToLower()) {
        "information" { $Id = 1000 }
        "warning"     { $Id = 1001 }
        "error"       { $Id = 1002 }
        default       { $Id = 1000 }
    }
    # Set the script name
    If ([String]::IsNullOrEmpty($MyInvocation.ScriptName)) {
        $Source = "Command-line input:"
    } Else {
        $Source = ((Split-Path -Path $MyInvocation.ScriptName -Leaf) + ":")
    }
    # Try to write to the One Zero One log
    Try {
        # Write to One Zero One and suppress output
        New-WinEvent -ProviderName "One Zero One" -Id $Id -Payload $Source,$Message -ErrorAction Stop
        # Success
    } Catch {
        # Failure; write to Microsoft-Windows-PowerShell and suppress output
        New-WinEvent -ProviderName "Microsoft-Windows-PowerShell" -Id 4100 -Payload $Source,"Script output.",$Message
    }
}

# MAIN
# Determine if this is a 64 bit process
If ([Environment]::Is64BitOperatingSystem -eq $true -And [Environment]::Is64BitProcess -eq $false) {
    # Process is not 64-bit; log
    Write-OZOProvider -Message ("Running in 32-bit PowerShell; attempting to re-launch " + $MyInvocation.InvocationName + " in 64-bit PowerShell") -Level "Warning"
    # Re-launch using 64-bit PowerShell
    Start-Process -FilePath (Join-Path -Path $Env:Windir -ChildPath "SysNative\WindowsPowerShell\v1.0\powershell.exe") -ArgumentList (Get-OZOPSArgumentList -InvocationInfo $MyInvocation) -Wait -NoNewWindow -ErrorAction Stop
} Else {
    # OS is 64-bit and PowerShell is 64-bit; log
    Write-OZOProvider -Message "Running in 64-bit PowerShell." -Level "Information"
    # Create a OZOMain object
    [OZOMain]::new($GPOName,$Restart,$SkipSecureBootCheck) | Out-Null
}

# SIG # Begin signature block
# MIIfcgYJKoZIhvcNAQcCoIIfYzCCH18CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCyZsFEKI90k0VO
# nOr/yTc0IzSOx+e7A1xINC4fZLYvaqCCDPgwggZyMIIEWqADAgECAghkM1HTxzif
# CDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
# EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G
# A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe
# Fw0xNjA2MjQyMDQ0MzBaFw0zMTA2MjQyMDQ0MzBaMHgxCzAJBgNVBAYTAlVTMQ4w
# DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENv
# cnAxNDAyBgNVBAMMK1NTTC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBD
# QSBSU0EgUjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCfgxNzqrDG
# bSHL24t6h3TQcdyOl3Ka5LuINLTdgAPGL0WkdJq/Hg9Q6p5tePOf+lEmqT2d0bKU
# Vz77OYkbkStW72fL5gvjDjmMxjX0jD3dJekBrBdCfVgWQNz51ShEHZVkMGE6ZPKX
# 13NMfXsjAm3zdetVPW+qLcSvvnSsXf5qtvzqXHnpD0OctVIFD+8+sbGP0EmtpuNC
# GVQ/8y8Ooct8/hP5IznaJRy4PgBKOm8yMDdkHseudQfYVdIYyQ6KvKNc8HwKp4WB
# wg6vj5lc02AlvINaaRwlE81y9eucgJvcLGfE3ckJmNVz68Qho+Uyjj4vUpjGYDdk
# jLJvSlRyGMwnh/rNdaJjIUy1PWT9K6abVa8mTGC0uVz+q0O9rdATZlAfC9KJpv/X
# gAbxwxECMzNhF/dWH44vO2jnFfF3VkopngPawismYTJboFblSSmNNqf1x1KiVgMg
# Lzh4gL32Bq5BNMuURb2bx4kYHwu6/6muakCZE93vUN8BuvIE1tAx3zQ4XldbyDge
# VtSsSKbt//m4wTvtwiS+RGCnd83VPZhZtEPqqmB9zcLlL/Hr9dQg1Zc0bl0EawUR
# 0tOSjAknRO1PNTFGfnQZBWLsiePqI3CY5NEv1IoTGEaTZeVYc9NMPSd6Ij/D+KNV
# t/nmh4LsRR7Fbjp8sU65q2j3m2PVkUG8qQIDAQABo4H7MIH4MA8GA1UdEwEB/wQF
# MAMBAf8wHwYDVR0jBBgwFoAU3QQJB6L1en1SUxKSle44gCUNplkwMAYIKwYBBQUH
# AQEEJDAiMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTARBgNVHSAE
# CjAIMAYGBFUdIAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwOwYDVR0fBDQwMjAwoC6g
# LIYqaHR0cDovL2NybHMuc3NsLmNvbS9zc2wuY29tLXJzYS1Sb290Q0EuY3JsMB0G
# A1UdDgQWBBRUwv4QlQCTzWr158DX2bJLuI8M4zAOBgNVHQ8BAf8EBAMCAYYwDQYJ
# KoZIhvcNAQELBQADggIBAPUPJodwr5miyvXWyfCNZj05gtOII9iCv49UhCe204MH
# 154niU2EjlTRIO5gQ9tXQjzHsJX2vszqoz2OTwbGK1mGf+tzG8rlQCbgPW/M9r1x
# xs19DiBAOdYF0q+UCL9/wlG3K7V7gyHwY9rlnOFpLnUdTsthHvWlM98CnRXZ7WmT
# V7pGRS6AvGW+5xI+3kf/kJwQrfZWsqTU+tb8LryXIbN2g9KR+gZQ0bGAKID+260P
# Z+34fdzZcFt6umi1s0pmF4/n8OdX3Wn+vF7h1YyfE7uVmhX7eSuF1W0+Z0duGwdc
# +1RFDxYRLhHDsLy1bhwzV5Qe/kI0Ro4xUE7bM1eV+jjk5hLbq1guRbfZIsr0WkdJ
# LCjoT4xCPGRo6eZDrBmRqccTgl/8cQo3t51Qezxd96JSgjXktefTCm9r/o35pNfV
# HUvnfWII+NnXrJlJ27WEQRQu9i5gl1NLmv7xiHp0up516eDap8nMLDt7TAp4z5T3
# NmC2gzyKVMtODWgqlBF1JhTqIDfM63kXdlV4cW3iSTgzN9vkbFnHI2LmvM4uVEv9
# XgMqyN0eS3FE0HU+MWJliymm7STheh2ENH+kF3y0rH0/NVjLw78a3Z9UVm1F5VPz
# iIorMaPKPlDRADTsJwjDZ8Zc6Gi/zy4WZbg8Zv87spWrmo2dzJTw7XhQf+xkR6Od
# MIIGfjCCBGagAwIBAgIQZ2iSsNbwOsjnLExSAX6F6DANBgkqhkiG9w0BAQsFADB4
# MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24x
# ETAPBgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmlu
# ZyBJbnRlcm1lZGlhdGUgQ0EgUlNBIFIxMB4XDTI0MTExNjEwMzUyOFoXDTI1MTEx
# NjEwMzUyOFowZTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCENvbG9yYWRvMQ8wDQYD
# VQQHDAZEZW52ZXIxGDAWBgNVBAoMD0FuZHJldyBMaWV2ZXJ0ejEYMBYGA1UEAwwP
# QW5kcmV3IExpZXZlcnR6MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA
# vIBAQzK0aahepOrPmvCEqfd6dMZC4GvV7kflKwrn4QPJGfqhFmUtadP1e3ange8O
# QZ3/w7UjOTAUNUHfhjbSgUBlKjbS6EWQKZuRFzI3SNkMJkcjTX4uS2P4QsnwM+SW
# IE5me3CTssdjtgue+Iiy53TMgW8JpoxiULVxmm3bhCRUAgxWeT6tzjytR1UyGcMc
# cm/YE6TOgsCHiZoo4X4HJD9iHDrNldArq04Jl6FsADxEswttKyfqpIRJLoAysVl1
# f8CEDBwhszJrEXBnAlWViJFfNY+dKP4jhf7lLqSvPCuADqP2jvM0Ym5I8qDGMz9j
# XPSMLF58MFB4vM4viS7nLRFJ8S1Q98vQvB8W4kk0WPuiZbZTHsROzohE1VSbLnIY
# ag5dDOWI8L6yutAsfdZFYFmSTKcMSiOj5VbK4LhAJUL2G8vPwpTGFgr+cEp0p62F
# P0WXK+/cRfGqodI5S+bg+9rQTD9zf829DwraSRAt5P5zrQk4WPst3JW/vIKNx7cV
# AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFFTC/hCVAJPN
# avXnwNfZsku4jwzjMHoGCCsGAQUFBwEBBG4wbDBIBggrBgEFBQcwAoY8aHR0cDov
# L2NlcnQuc3NsLmNvbS9TU0xjb20tU3ViQ0EtQ29kZVNpZ25pbmctUlNBLTQwOTYt
# UjEuY2VyMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTBRBgNVHSAE
# SjBIMAgGBmeBDAEEATA8BgwrBgEEAYKpMAEDAwEwLDAqBggrBgEFBQcCARYeaHR0
# cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoGCCsGAQUFBwMD
# ME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9jcmxzLnNzbC5jb20vU1NMY29tLVN1
# YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNybDAdBgNVHQ4EFgQUSj8HrSK7
# f/j+Dz31jJFhOF7rJUMwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IC
# AQBf4lcc6FUJ1W/opNz8yjS9qLUy9cQt0s35BhasB5QoTbDaW4jv9xnFGhQVg6n+
# jhL0i94Vsywd/MRBb8lYGpuBZnS/7LHuRZu7qUuud+IMDyRHIyBK6koN5bfyA5VY
# c7bFbNpbe1s1hMWke8di4qgMLZKDfyG/RtA0swf5t4UgQLPP0h+koZ8X8V5+P0V0
# 1HsdXyXd+ojo38EoZyCKfQL2aAwMPwzZfCbmI5SRXNOc6K8oqXzQcendhlKSfVBo
# Zgpi+1updqbD4jmJfYdK5AYPxJ3YH6td6ETtr8owL+bmX8lQjlXPOwVnC11rVlNB
# VjqtaJRUClLtiNiYSTKVfjdmGVJ4+sNov0dWhHc0A9o5NX/05VVYTlImuJpnG5Og
# o7w6kWRdsgE8gM58jWf7XfI6aQS0Np/z2B+ZBj0K93khEHBX7cvvORa92LCHiVeP
# km+zEAMXgxIPs/e8cmcc/o3CORgzEwxlH9Z3UOWCuXSHD3P2RPNDAY+WPdjSHm9f
# JFlGq+f9iKyedxYa/NNjNag/5EbZ+Z2NldtSMNeFdsejGJ/TJHF1PyJd4aXx9J1i
# B/IZBOoJYyh9xpQ3ljZUKE/4otPi7INpuDFwgWiUHZZJVvrGTWwxH1Yhf8P+VpFf
# aNqsBuvklUcUDs3RNE0f1qlgFfcnAepFF+RiBRqmsj29fjGCEdAwghHMAgEBMIGM
# MHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3Rv
# bjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NTTC5jb20gQ29kZSBTaWdu
# aW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjECEGdokrDW8DrI5yxMUgF+hegwDQYJ
# YIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEMMQIwADAZBgkqhkiG9w0BCQMxDAYK
# KwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG
# 9w0BCQQxIgQg3VXrUU18J3mwx1ZuB7AZcWxNQ1rfo8K/xs3gM0viVBwwDQYJKoZI
# hvcNAQEBBQAEggGAVfqKrTzrlSOxAztx2eQEwyI93qP1cdJGuRSxYK67r4orKma9
# WwjIyifPLmnHDC0ByAChlwrK/2/mVk7M8A3Kh4aczOCicf4EpxdXBFB6/vq3YXjw
# 5/Uqb8oLdX2vSE66HbCbLSyd9502mZl+QA4tOW416K1uKqnHu76tDsR90yVuvshB
# kMexIcI71A0jbPX8EmfntH+5zO0LhRTEff7ZZ7BD43mtqDrJwanw+kvn3SRp+ftH
# Yuz18Nz2hS3qW8ZIaccp+TM6egCfPNrgRi6TnvMsTb020RD4lKKYnvLsrzF63253
# 6w3fhIJ+VwvueiUIYaDWLU2kq1MzqDgPH4jAsA0ywg3dQzZfW40sk/z0BfAbwKyR
# YRIXb5NZifvOuvDm60cN2TfmIgdeFe4sz78H+w5s0AkB4lLg/n0aN7fqLewj5H7h
# IwkKmsjw51gWW8uqvB9EgImU/+pfJyGxR7uWTF5QkPs/wdhIzV1pWC32Y1xbXLg0
# W980l5ILRT3vNbQloYIPFjCCDxIGCisGAQQBgjcDAwExgg8CMIIO/gYJKoZIhvcN
# AQcCoIIO7zCCDusCAQMxDTALBglghkgBZQMEAgEwdwYLKoZIhvcNAQkQAQSgaARm
# MGQCAQEGDCsGAQQBgqkwAQMGATAxMA0GCWCGSAFlAwQCAQUABCBCC/mFPXl5vNYm
# sK/BS2wE2lZkVHTW8SfPjW/A42IeyQIIL4qRkGvLzOQYDzIwMjUwNzA0MTcwOTI4
# WjADAgEBoIIMADCCBPwwggLkoAMCAQICEFparOgaNW60YoaNV33gPccwDQYJKoZI
# hvcNAQELBQAwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQH
# DAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNvbSBU
# aW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0EgUjEwHhcNMjQwMjE5MTYxODE5WhcN
# MzQwMjE2MTYxODE4WjBuMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO
# BgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMSowKAYDVQQDDCFTU0wu
# Y29tIFRpbWVzdGFtcGluZyBVbml0IDIwMjQgRTEwWTATBgcqhkjOPQIBBggqhkjO
# PQMBBwNCAASnYXL1MOl6xIMUlgVC49zonduUbdkyb0piy2i8t3JlQEwA74cjK8g9
# mRC8GH1cAAVMIr8M2HdZpVgkV1LXBLB8o4IBWjCCAVYwHwYDVR0jBBgwFoAUDJ0Q
# JY6apxuZh0PPCH7hvYGQ9M8wUQYIKwYBBQUHAQEERTBDMEEGCCsGAQUFBzAChjVo
# dHRwOi8vY2VydC5zc2wuY29tL1NTTC5jb20tdGltZVN0YW1waW5nLUktUlNBLVIx
# LmNlcjBRBgNVHSAESjBIMDwGDCsGAQQBgqkwAQMGATAsMCoGCCsGAQUFBwIBFh5o
# dHRwczovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkwCAYGZ4EMAQQCMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMEYGA1UdHwQ/MD0wO6A5oDeGNWh0dHA6Ly9jcmxzLnNz
# bC5jb20vU1NMLmNvbS10aW1lU3RhbXBpbmctSS1SU0EtUjEuY3JsMB0GA1UdDgQW
# BBRQTySs77U+YxMjCZIm7Lo6luRdIjAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcN
# AQELBQADggIBAJigjwMAkbyrxGRBf0Ih4r+rbCB57lTuwViC6nH2fZSciMogpqSz
# rSeVZ2eIb5vhj9rT7jqWXZn02Fncs4YTrA1QyxJW36yjC4jl5/bsFCaWuXzGXt2Y
# 6Ifp//A3Z0sNTMWTTBobmceM3sqnovdX9ToRFP+29r5yQnPcgRTI2PvrVSqLxY9E
# yk9/0cviM3W29YBl080ENblRcu3Y8RsfzRtVT/2snuDocRxvRYmd0TPaMgIj2xII
# 651QnPp1hiq9xU0AyovLzbsi5wlR5Ip4i/i8+x+HwYJNety5cYtdWJ7uQP6YaZtW
# /jNoHp76qNftq/IlSx6xEYBRjFBxHSq2fzhUQ5oBawk2OsZ2j0wOf7q7AqjCt6t/
# +fbmWjrAWYWZGj/RLjltqdFPBpIKqdhjVIxaGgzVhaE/xHKBg4k4DfFZkBYJ9BWu
# P93Tm+paWBDwXI7Fg3alGsboErWPWlvwMAmpeJUjeKLZY26JPLt9ZWceTVWuIyuj
# erqb5IMmeqLJm5iFq/Qy4YPGyPiolw5w1k9OeO4ErmS2FKvk1ejvw4SWR+S1VyWn
# ktY442WaoStxBCCVWZdMWFeB+EpL8uoQNq1MhSt/sIUjUudkyZLIbMVQjj7b6gPX
# nD6mS8FgWiCAhuM1a/hgA+6o1sJWizHdmcpYDhyNzorf9KVRE6iR7rcmMIIG/DCC
# BOSgAwIBAgIQbVIYcIfoI02FYADQgI+TVjANBgkqhkiG9w0BAQsFADB8MQswCQYD
# VQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xGDAWBgNV
# BAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBSb290IENlcnRp
# ZmljYXRpb24gQXV0aG9yaXR5IFJTQTAeFw0xOTExMTMxODUwMDVaFw0zNDExMTIx
# ODUwMDVaMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwH
# SG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20gVGlt
# ZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEArlEQE9L5PCCgIIXeyVAcZMnh/cXpNP8KfzFI6HJaxV6oYf3x
# h/dRXPu35tDBwhOwPsJjoqgY/Tg6yQGBqt65t94wpx0rAgTVgEGMqGri6vCI6rEt
# SZVy9vagzTDHcGfFDc0Eu71mTAyeNCUhjaYTBkyANqp9m6IRrYEXOKdd/eREsqVD
# mhryd7dBTS9wbipm+mHLTHEFBdrKqKDM3fPYdBOro3bwQ6OmcDZ1qMY+2Jn1o0l4
# N9wORrmPcpuEGTOThFYKPHm8/wfoMocgizTYYeDG/+MbwkwjFZjWKwb4hoHT2WK8
# pvGW/OE0Apkrl9CZSy2ulitWjuqpcCEm2/W1RofOunpCm5Qv10T9tIALtQo73GHI
# lIDU6xhYPH/ACYEDzgnNfwgnWiUmMISaUnYXijp0IBEoDZmGT4RTguiCmjAFF5OV
# NbY03BQoBb7wK17SuGswFlDjtWN33ZXSAS+i45My1AmCTZBV6obAVXDzLgdJ1A1r
# yyXz4prLYyfJReEuhAsVp5VouzhJVcE57dRrUanmPcnb7xi57VPhXnCuw26hw1Hd
# +ulK3jJEgbc3rwHPWqqGT541TI7xaldaWDo85k4lR2bQHPNGwHxXuSy3yczyOg57
# TcqqG6cE3r0KR6jwzfaqjTvN695GsPAPY/h2YksNgF+XBnUD9JBtL4c34AcCAwEA
# AaOCAYEwggF9MBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAU3QQJB6L1
# en1SUxKSle44gCUNplkwgYMGCCsGAQUFBwEBBHcwdTBRBggrBgEFBQcwAoZFaHR0
# cDovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkvU1NMY29tUm9vdENlcnRpZmljYXRp
# b25BdXRob3JpdHlSU0EuY3J0MCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3Ns
# LmNvbTA/BgNVHSAEODA2MDQGBFUdIAAwLDAqBggrBgEFBQcCARYeaHR0cHM6Ly93
# d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoGCCsGAQUFBwMIMDsGA1Ud
# HwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmxzLnNzbC5jb20vc3NsLmNvbS1yc2EtUm9v
# dENBLmNybDAdBgNVHQ4EFgQUDJ0QJY6apxuZh0PPCH7hvYGQ9M8wDgYDVR0PAQH/
# BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQCSGXUNplpCzxkH2fL8lPrAm/AV6USW
# Wi9xM91Q5RN7mZN3D8T7cm1Xy7qmnItFukgdtiUzLbQokDJyFTrF1pyLgGw/2hU3
# FJEywSN8crPsBGo812lyWFgAg0uOwUYw7WJQ1teICycX/Fug0KB94xwxhsvJBiRT
# pQyhu/2Kyu1Bnx7QQBA1XupcmfhbQrK5O3Q/yIi//kN0OkhQEiS0NlyPPYoRboHW
# C++wogzV6yNjBbKUBrMFxABqR7mkA0x1Kfy3Ud08qyLC5Z86C7JFBrMBfyhfPpKV
# lIiiTQuKz1rTa8ZW12ERoHRHcfEjI1EwwpZXXK5J5RcW6h7FZq/cZE9kLRZhvnRK
# tb+X7CCtLx2h61ozDJmifYvuKhiUg9LLWH0Or9D3XU+xKRsRnfOuwHWuhWch8G7k
# EmnTG9CtD9Dgtq+68KgVHtAWjKk2ui1s1iLYAYxnDm13jMZm0KpRM9mLQHBK5Gb4
# dFgAQwxOFPBslf99hXWgLyYE33vTIi9p0gYqGHv4OZh1ElgGsvyKdUUJkAr5hfbD
# X6pYScJI8v9VNYm1JEyFAV9x4MpskL6kE2Sy8rOqS9rQnVnIyPWLi8N9K4GZvPit
# /Oy+8nFL6q5kN2SZbox5d69YYFe+rN1sDD4CpNWwBBTI/q0V4pkgvhL99IV2Xasj
# HZf4peSrHdL4RjGCAlgwggJUAgEBMIGHMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI
# DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAt
# BgNVBAMMJlNTTC5jb20gVGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxAhBa
# WqzoGjVutGKGjVd94D3HMAsGCWCGSAFlAwQCAaCCAWEwGgYJKoZIhvcNAQkDMQ0G
# CyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTA3MDQxNzA5MjhaMCgGCSqG
# SIb3DQEJNDEbMBkwCwYJYIZIAWUDBAIBoQoGCCqGSM49BAMCMC8GCSqGSIb3DQEJ
# BDEiBCB+iPo7CkidsB4as0n23b0ODRVZyvzUzDjkij3WJaX9iTCByQYLKoZIhvcN
# AQkQAi8xgbkwgbYwgbMwgbAEIJ1xf43CN2Wqzl5KsOH1ddeaF9Qc7tj9r+8D/T29
# iUfnMIGLMHekdTBzMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNV
# BAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8wLQYDVQQDDCZTU0wuY29t
# IFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMQIQWlqs6Bo1brRiho1XfeA9
# xzAKBggqhkjOPQQDAgRHMEUCIQCMQjBCqDtVZnIQjJneq4q5Pcp/DIWBxFNhsi7k
# 3U2tMQIgW1I+1LafJxTkqYkK8purvjDu+UJfmHxCSP0D5KYrQs8=
# SIG # End signature block