Obs/bin/ObsDep/content/Powershell/Roles/Common/Servicing/Scripts/Modules/FolderHash.psm1
#region C# code $FolderHashSource = @" using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace Microsoft.Solutions.IO { [Serializable] public class FolderHashEntry : IEquatable<FolderHashEntry> { public string Path { get; private set; } public long LastWriteTimeUTCTicks { get; private set; } public int Size { get; private set; } public string Hash { get; private set; } public FolderHashEntry(string path, long lastWriteTimeUTCTicks, int size, string hash) { Path = path; LastWriteTimeUTCTicks = lastWriteTimeUTCTicks; Size = size; Hash = hash; } public void UpdateHash(string hash) { Hash = hash; } public override int GetHashCode() { return Path.GetHashCode(); } public override bool Equals(object obj) { if (obj == null) return false; FolderHashEntry fhe = obj as FolderHashEntry; if (fhe == null) return false; else return Equals(fhe); } public bool Equals(FolderHashEntry other) { if (other == null) { return false; } return (this.Path.Equals(other.Path)); } } [Serializable] public class FolderHash { public string Algorithm { get; private set; } public List<FolderHashEntry> Entries { get; private set; } public FolderHash(string algorithm) { Algorithm = algorithm; Entries = new List<FolderHashEntry>(); } } } "@ Add-Type -TypeDefinition $FolderHashSource -Language CSharp #endregion C# code <# .Synopsis Perform a deep copy on an object #> function Invoke-DeepCopy ($object) { $ms = $null $newObject = $null try { # Serialize and Deserialize data using BinaryFormatter $ms = New-Object System.IO.MemoryStream $bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter $bf.Serialize($ms, $object) $ms.Position = 0 $newObject = $bf.Deserialize($ms) } finally { if ($ms) { $ms.Dispose() $ms = $null } } $newObject } function Get-HashProviderFromString { [CmdletBinding()] param ( [ValidateSet('SHA1','SHA256', 'SHA384', 'SHA512')] [string] $Algorithm = 'SHA256' ) $sha = $null switch ($Algorithm) { 'SHA1' { $sha = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider } 'SHA256' { $sha = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider } 'SHA384' { $sha = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider } 'SHA512' { $sha = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider } } return $sha } <# .Synopsis For a given Path and Algorithm, returns a hash for the file. We write our own function because build machines are v3.0 and Get-FileHash in PowerShell is v4.0. #> function Get-FileHash { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string[]] $Path, [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')] [string] $Algorithm = 'SHA256' ) $sha = Get-HashProviderFromString -Algorithm $Algorithm $file = [System.IO.File]::Open($Path,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read) [System.BitConverter]::ToString($sha.ComputeHash($file)) -replace "-","" $file.Close() } <# .Synopsis For a given path, returns a Microsoft.Solutions.IO.FolderHash object with an entry for each file found. #> function Get-FolderHash { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string[]] $Path, [string] $Filter, [string[]] $Exclude, [string[]] $Include, [switch] $Recurse, [switch] $NoHash, [switch] $AllowSkippedFiles, [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')] [string] $Algorithm = 'SHA256' ) $folderHash = New-Object Microsoft.Solutions.IO.FolderHash -ArgumentList $Algorithm if (-not (Test-Path $Path)) { throw "Path $Path does not exist or access denied." } $args = @{ 'Path' = $Path 'Filter' = $Filter 'Exclude' = $Exclude 'Include' = $Include 'Recurse' = $Recurse 'ErrorAction' = 'SilentlyContinue' } $files = Get-ChildItem @args -File foreach ($file in $files) { $fullName = $file.FullName Write-Verbose "Processing: $fullName." try { # remove $Path from name so we store only the relative path $relativePath = $fullName -replace [regex]::Escape($($Path)) $lastWriteTimeUTCTicks = $file.LastWriteTimeUtc.Ticks $size = $file.Length if ($NoHash) { $hash = [Guid]::Empty } else { $hash = Get-FileHash -Path $fullName -Algorithm $Algorithm -ErrorAction Stop } $entry = New-Object Microsoft.Solutions.IO.FolderHashEntry -ArgumentList $relativePath.TrimStart("\"), $lastWriteTimeUTCTicks, $size, $hash.Hash $folderHash.Entries.Add($entry) } catch [Microsoft.PowerShell.Commands.WriteErrorException] { if (-not $AllowSkippedFiles) { Write-Warning "WriteErrorException: $fullName." $ex = $_ | Select-Object * Write-Warning $ex throw } Write-Verbose "Skipping $fullName." } catch { Write-Warning "Error processing: $fullName." $ex = $_ | Select-Object * Write-Warning $ex throw } } $folderHash } <# .Synopsis For a given path and Microsoft.Solutions.IO.FolderHash.Entries object or JSON file containing Entries, test each file and report files that are same, different, missing, extra files from the Entries list. Errors will be reported as skipped unless critical which will throw. #> function Test-FolderHash { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $Path, [string] $Filter, [string[]] $Exclude, [string[]] $Include, [switch] $Recurse, [switch] $NoHash, [switch] $AllowSkippedFiles, [Parameter(ParameterSetName='Entry', Mandatory=$true)] [Microsoft.Solutions.IO.FolderHash] $FolderHash, [Parameter(ParameterSetName='JSON', Mandatory=$true)] [string] $JsonFile ) if ($JsonFile) { Write-Verbose "Processing JSON: $JsonFile." $fh = Get-Content $JsonFile -Raw | ConvertFrom-Json $folderHashSrc = New-Object Microsoft.Solutions.IO.FolderHash -ArgumentList $fh.Algorithm $entries = $fh.Entries foreach ($entry in $entries) { $entry = New-Object Microsoft.Solutions.IO.FolderHashEntry -ArgumentList $entry.Path, $entry.LastWriteTimeUTCTicks, $entry.Size, $entry.Hash $folderHashSrc.Entries.Add($entry) } } else { Write-Verbose "Processing Microsoft.Solutions.IO.FolderHash object." # use a clone so we do not change the original object $folderHashSrc = Invoke-DeepCopy $FolderHash } Write-Verbose "Found $($folderHashSrc.Entries.Count) entries." Write-Verbose "Reading files from: $Path" # always call with -NoHash first for performance reasons, so we don't hash files with different sizes or LastWriteTimeUTCTicks $args = @{ 'Path' = $Path 'Filter' = $Filter 'Exclude' = $Exclude 'Include' = $Include 'Recurse' = $Recurse 'NoHash' = $true 'Algorithm' = $folderHashSrc.Algorithm } $folderHashDst = Get-FolderHash @args Write-Verbose "Found $($folderHashDst.Entries.Count) entries." Write-Verbose "Starting test against $Path." # create new lists for each category we are tracking by moving entries from the original lists $same = New-Object System.Collections.Generic.List[Object] $different = New-Object System.Collections.Generic.List[Object] # could not process $skipped = New-Object System.Collections.Generic.List[Object] # in src, not in dst $missing = New-Object System.Collections.Generic.List[Object] # not in src, in dst $extra = New-Object System.Collections.Generic.List[Object] while ($folderHashSrc.Entries.Count) { $srcEntry = $folderHashSrc.Entries[0] Write-Verbose "Finding: $($srcEntry.Path)." $dstEntry = $folderHashDst.Entries | Where-Object {$_.Path -eq $srcEntry.Path} if ($dstEntry) { Write-Verbose "Found: $($dstEntry.Path)." # we save ticks which are in 100-ns units. we are not guaranteed this granularity when storing files # on unc paths, locally, etc. Use granularity to the second to optimize not calculating the hash. $srcSeconds = [long]($srcEntry.LastWriteTimeUTCTicks / 10000000) * 10000000 $dstSeconds = [long]($dstEntry.LastWriteTimeUTCTicks / 10000000) * 10000000 if ($srcSeconds -ne $dstSeconds) { # ok to report LastWriteTimeUTCTicks here Write-Verbose "LastWriteTimeUTCTicks mismatch: $($srcEntry.LastWriteTimeUTCTicks) -ne $($dstEntry.LastWriteTimeUTCTicks)." # different $different.Add($srcEntry) } elseif ($srcEntry.Size -ne $dstEntry.Size) { Write-Verbose "Size mismatch: $($srcEntry.Size) -ne $($dstEntry.Size)." # different $different.Add($srcEntry) } else { # try to get hash now $fullName = Join-Path $Path $dstEntry.Path try { $hash = (Get-FileHash -Path $fullName -Algorithm $folderHashSrc.Algorithm -ErrorAction Stop).Hash $dstEntry.UpdateHash($hash) if ($srcEntry.Hash -ne $dstEntry.Hash) { Write-Verbose "Hash mismatch: $($srcEntry.Hash) -ne $($dstEntry.Hash)." # different $different.Add($srcEntry) } else { Write-Verbose "Same: $($srcEntry.Path)." # same $same.Add($srcEntry) } } catch [Microsoft.PowerShell.Commands.WriteErrorException] { if (-not $AllowSkippedFiles) { Write-Warning "WriteErrorException: $fullName." $ex = $_ | Select-Object * Write-Warning $ex throw } Write-Verbose "Skipping $fullName." # skipped $skipped.Add($srcEntry) } catch { Write-Warning "Error processing: $fullName." $ex = $_ | Select-Object * Write-Warning $ex throw } } if (-not ($folderHashDst.Entries.Remove($dstEntry))) { throw "Error removing dstEntry from folderHashDst.Entries." } } else { Write-Verbose "Missing $($srcEntry.Path)." # missing $missing.Add($srcEntry) } if (-not ($folderHashSrc.Entries.Remove($srcEntry))) { throw "Error removing srcEntry from folderHashSrc.Entries." } } # anything left in the dst list is extra $extra.AddRange($folderHashDst.Entries) $results = [ordered]@{} $results.Add('NumFailures', ($different.Count + $skipped.Count + $missing.Count + $extra.Count)) $results.Add('Same', $same) $results.Add('Different', $different) $results.Add('Skipped', $skipped) $results.Add('Missing', $missing) $results.Add('Extra', $extra) Write-Verbose "Test-FolderHash results:" Write-Verbose " Same: $($same.Count)" Write-Verbose " Different: $($different.Count)" Write-Verbose " Skipped: $($skipped.Count)" Write-Verbose " Missing: $($missing.Count)" Write-Verbose " Extra: $($extra.Count)" $results } function Get-FolderContentSummaryHash { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $Path, [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')] [string] $Algorithm = 'SHA256' ) $folderHash = New-Object Microsoft.Solutions.IO.FolderHash -ArgumentList $Algorithm if (-not (Test-Path $Path)) { throw "Path $Path does not exist or access denied." } $args = @{ 'Path' = $Path 'Recurse' = $true 'ErrorAction' = 'SilentlyContinue' } $files = Get-ChildItem @args -File foreach ($file in $files) { $fullName = $file.FullName Write-Verbose "Processing: $fullName." try { # Remove $Path from name so we store only the relative path $relativePath = $fullName -replace [regex]::Escape($($Path)) $lastWriteTimeUTCTicks = $file.LastWriteTimeUtc.Ticks $size = $file.Length $hash = Get-FileHash -Path $fullName -Algorithm $Algorithm -ErrorAction Stop $entry = New-Object Microsoft.Solutions.IO.FolderHashEntry -ArgumentList $relativePath.TrimStart("\"), $lastWriteTimeUTCTicks, $size, $hash.Hash $folderHash.Entries.Add($entry) } catch [Microsoft.PowerShell.Commands.WriteErrorException] { Write-Warning "WriteErrorException: $fullName." $ex = $_ | Select-Object * Write-Warning $ex throw } catch { Write-Warning "Error processing: $fullName." $ex = $_ | Select-Object * Write-Warning $ex throw } } # Sort the file list by name and then create a byte stream for hashing based on the names and hashes $fileNameAndHashBytes = new-object System.Collections.Generic.List[byte] foreach ($entry in $folderHash.Entries) { $fileNameAndHashBytes.AddRange([System.Text.Encoding]::UTF8.GetBytes($entry.Path)) $fileNameAndHashBytes.AddRange([System.Text.Encoding]::UTF8.GetBytes($entry.Hash)) } $sha = Get-HashProviderFromString -Algorithm $Algorithm return [System.BitConverter]::ToString($sha.ComputeHash($fileNameAndHashBytes.ToArray())) -replace "-","" } #region Exports Export-ModuleMember Get-FolderContentSummaryHash Export-ModuleMember Get-FolderHash Export-ModuleMember Test-FolderHash #endregion Exports # SIG # Begin signature block # MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBzh2j+pjx1Jn5O # 2VuDyNN5fl8LCWpODT0gGlEJPNFZyqCCDXYwggX0MIID3KADAgECAhMzAAADrzBA # DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA # hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG # 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN # xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL # go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB # tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd # mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ # 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY # 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp # XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn # TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT # e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG # OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O # PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk # ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx # HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt # CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEICOU4zc8snhm8wfpMSot7Zq3 # zUgclC+dSX4dQPrpBkjNMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAbx0cWnQr8ILejMQXAUeGB0MKGVct4OzPIOwfpU7slXQWyl+6+dXX8zlZ # 4Q/jKGJ1NdzfhuZvyMnhfx24qvhXkIE44G0EPe7dd5c1VhC3atYrlbwCgaCL/0Oh # VytrHMPboTBJsnd7SuUoMI+OTXo6FxAiR7BCJXLkefiA9ez0ZeJHPZ3FGaLMB0mc # 239COHG5sMw5Y+JhfenZ/4nch1QHNhhZO89dI8FCu4tOow3BqEjRO7e957G+XtJN # Zo2UBOajZxI9y+6763DxUXwXmNXW2KNforiO0N+tdjZIdsujDOiPBta1ymgbFszS # zN3LT5QkvKv5SaiJHTZ/yhJJ9rxXKqGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC # F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq # hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCCAUi/OXIb3dDQuVm0OXPrRQoVrbYcWdRzXgH2mk0EisgIGZr4F/8S1 # GBMyMDI0MDgxOTE3Mjc0Mi41NjJaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l # cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0w # NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg # ghHqMIIHIDCCBQigAwIBAgITMwAAAfGzRfUn6MAW1gABAAAB8TANBgkqhkiG9w0B # AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1 # NTVaFw0yNTAzMDUxODQ1NTVaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z # MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0wNUUwLUQ5NDcxJTAjBgNV # BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCxulCZttIf8X97rW9/J+Q4Vg9PiugB1ya1/DRxxLW2 # hwy4QgtU3j5fV75ZKa6XTTQhW5ClkGl6gp1nd5VBsx4Jb+oU4PsMA2foe8gP9bQN # PVxIHMJu6TYcrrn39Hddet2xkdqUhzzySXaPFqFMk2VifEfj+HR6JheNs2LLzm8F # DJm+pBddPDLag/R+APIWHyftq9itwM0WP5Z0dfQyI4WlVeUS+votsPbWm+RKsH4F # QNhzb0t/D4iutcfCK3/LK+xLmS6dmAh7AMKuEUl8i2kdWBDRcc+JWa21SCefx5SP # hJEFgYhdGPAop3G1l8T33cqrbLtcFJqww4TQiYiCkdysCcnIF0ZqSNAHcfI9SAv3 # gfkyxqQNJJ3sTsg5GPRF95mqgbfQbkFnU17iYbRIPJqwgSLhyB833ZDgmzxbKmJm # dDabbzS0yGhngHa6+gwVaOUqcHf9w6kwxMo+OqG3QZIcwd5wHECs5rAJZ6PIyFM7 # Ad2hRUFHRTi353I7V4xEgYGuZb6qFx6Pf44i7AjXbptUolDcVzYEdgLQSWiuFajS # 6Xg3k7Cy8TiM5HPUK9LZInloTxuULSxJmJ7nTjUjOj5xwRmC7x2S/mxql8nvHSCN # 1OED2/wECOot6MEe9bL3nzoKwO8TNlEStq5scd25GA0gMQO+qNXV/xTDOBTJ8zBc # GQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFLy2xe59sCE0SjycqE5Erb4YrS1gMB8G # A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG # Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy # MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w # XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy # dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG # A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD # AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDhSEjSBFSCbJyl3U/QmFMW2eLPBknnlsfI # D/7gTMvANEnhq08I9HHbbqiwqDEHSvARvKtL7j0znICYBbMrVSmvgDxU8jAGqMyi # LoM80788So3+T6IZV//UZRJqBl4oM3bCIQgFGo0VTeQ6RzYL+t1zCUXmmpPmM4xc # ScVFATXj5Tx7By4ShWUC7Vhm7picDiU5igGjuivRhxPvbpflbh/bsiE5tx5cuOJE # JSG+uWcqByR7TC4cGvuavHSjk1iRXT/QjaOEeJoOnfesbOdvJrJdbm+leYLRI67N # 3cd8B/suU21tRdgwOnTk2hOuZKs/kLwaX6NsAbUy9pKsDmTyoWnGmyTWBPiTb2rp # 5ogo8Y8hMU1YQs7rHR5hqilEq88jF+9H8Kccb/1ismJTGnBnRMv68Ud2l5LFhOZ4 # nRtl4lHri+N1L8EBg7aE8EvPe8Ca9gz8sh2F4COTYd1PHce1ugLvvWW1+aOSpd8N # nwEid4zgD79ZQxisJqyO4lMWMzAgEeFhUm40FshtzXudAsX5LoCil4rLbHfwYtGO # pw9DVX3jXAV90tG9iRbcqjtt3vhW9T+L3fAZlMeraWfh7eUmPltMU8lEQOMelo/1 # ehkIGO7YZOHxUqeKpmF9QaW8LXTT090AHZ4k6g+tdpZFfCMotyG+E4XqN6ZWtKEB # QiE3xL27BDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI # hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy # MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV # BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp # bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg # M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF # dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6 # GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp # Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu # yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E # XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0 # lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q # GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ # +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA # PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw # EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG # NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV # MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK # BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC # AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX # zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v # cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI # KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG # 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x # M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC # VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449 # xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM # nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS # PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d # Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn # GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs # QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL # jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL # 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN # MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjg2MDMtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQD7 # n7Bk4gsM2tbU/i+M3BtRnLj096CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6m3KJjAiGA8yMDI0MDgxOTEzNDE1 # OFoYDzIwMjQwODIwMTM0MTU4WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDqbcom # AgEAMAcCAQACAhHZMAcCAQACAhPAMAoCBQDqbxumAgEAMDYGCisGAQQBhFkKBAIx # KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI # hvcNAQELBQADggEBAHbW0zNEXtwJRAsWDT/wgZKu2s0Z/shz2CgeImpP30YSXR2D # TyWSQoP71bpz6h3+H9sOcH9EfUJY0kTuP1tGWbQjSFEt8XoZIggICakAuxzeBw4R # Jq5qXh3kjF5v7IqaAxw3sSFrSD61HzFJ4w8mYrOvi6QHsQqcwa3cKluuE9EgyWvi # fkV1FgUZhQOPmdiAYZT1sOv8aGVyS3AwZXySFLT1ApsRJOm29sTg2ymTqvlyHonT # OyJ/zpg2VfdnbWDbq68CRFLfoML+yh6o8VD0aVK3zz9MSRTDkxzgdOYl9a1LnNki # jQ7IOUvIPbJoixknhl9sIV6hpFboiUGC7DdHAEAxggQNMIIECQIBATCBkzB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAfGzRfUn6MAW1gABAAAB8TAN # BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G # CSqGSIb3DQEJBDEiBCA3v+gJOvUOVYXdDLerDdbiYr1gpMJZVed2QkA7sE99nzCB # +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EINV3/T5hS7ijwao466RosB7wwEib # t0a1P5EqIwEj9hF4MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAHxs0X1J+jAFtYAAQAAAfEwIgQgFs1zRH4TLFpVqquJOl9k4WDGrTQS # Lavdhaaf4tUwl1kwDQYJKoZIhvcNAQELBQAEggIAeEisuhcdRXd2PkElil0VH67h # 1klF+8RqRSj+e3laOmFbVj2/+/Vb6WT0lBm01XeADZdhYWvIW6CB59QUBqipdywv # cmTOriqY5niQFd439/uA+B140WIRvbFQjmYrh/2moCTAoJdIkCUpkx/asU+i2jLv # LuT1XsRMCOhj9kddPak4NjPn8Qs/tWK2qAZsznuNNeqCaks3gfO8oypuRLtwMdv3 # 537WLfsoroHWAfyLC6Rkc6QL75sWHF8hz9bAx3ODt7/suPIYQxEkn+emAoJH244C # bBMSr8omJFYhSzZ54H6iiTKaV3CKApd6xOIwJgdGrrcaMv97WS1IVZIROA/vPSiA # 6ayZw0D1YjtULVvujOkUGPnp583u35oDSkH5Njc738VKLTlLZVpr1LQMRUv3w2xH # ewQs6UQx5hW4vLXpJu+M1N1bb7k/nwGTCEh53JRV36VI/qkiiUrRUnSCxGNJ8dYm # ckUOiE4bJkbvHnWoj+Cl2KlMVrV+58SLCJO2wlJ5LUcpacR2pWRrVRPwZEPqD5PJ # XYqyzmcpQfZdN4+MeV0ANDaK84vEUnu2V7bn/bck7iQJ/d08fbp1u60T8ibga+52 # cgjTvJwdUVcHnVvcWeuqdW5T3kC2dkpqg1KOuyax+TqFmeGqBRtYOdVcJJySn2yn # 8P8H1Thr0O04BHcSm8A= # SIG # End signature block |