From d4692bf4660c45a7a9c29f442431b71fa36803c6 Mon Sep 17 00:00:00 2001 From: Rain Sallow Date: Mon, 30 Sep 2024 11:31:06 -0400 Subject: [PATCH] (#3477) Add Assert-ValidChecksum cmdlet This command replaces the Get-ChecksumValid helper function, adding an alias for compatibility purposes. --- .../Chocolatey.PowerShell.csproj | 6 + .../Commands/AssertValidChecksumCommand.cs | 71 +++ .../Helpers/ChecksumValidator.cs | 181 ++++++++ src/Chocolatey.PowerShell/Helpers/PSHelper.cs | 250 +++++++++- .../Shared/ChecksumExeNotFoundException.cs | 41 ++ .../Shared/ChecksumMissingException.cs | 41 ++ .../Shared/ChecksumType.cs | 29 ++ .../ChecksumVerificationFailedException.cs | 50 ++ .../Shared/ChocolateyCmdlet.cs | 12 + .../Shared/EnvironmentVariables.cs | 19 + .../chocolatey.resources.csproj | 1 - .../helpers/chocolateyInstaller.psm1 | 2 + .../helpers/functions/Get-CheckSumValid.ps1 | 434 ------------------ 13 files changed, 701 insertions(+), 436 deletions(-) create mode 100644 src/Chocolatey.PowerShell/Commands/AssertValidChecksumCommand.cs create mode 100644 src/Chocolatey.PowerShell/Helpers/ChecksumValidator.cs create mode 100644 src/Chocolatey.PowerShell/Shared/ChecksumExeNotFoundException.cs create mode 100644 src/Chocolatey.PowerShell/Shared/ChecksumMissingException.cs create mode 100644 src/Chocolatey.PowerShell/Shared/ChecksumType.cs create mode 100644 src/Chocolatey.PowerShell/Shared/ChecksumVerificationFailedException.cs delete mode 100644 src/chocolatey.resources/helpers/functions/Get-CheckSumValid.ps1 diff --git a/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj b/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj index 982d5c1a4..39975693f 100644 --- a/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj +++ b/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj @@ -58,6 +58,10 @@ + + + + @@ -65,6 +69,7 @@ + @@ -74,6 +79,7 @@ Properties\SolutionVersion.cs + diff --git a/src/Chocolatey.PowerShell/Commands/AssertValidChecksumCommand.cs b/src/Chocolatey.PowerShell/Commands/AssertValidChecksumCommand.cs new file mode 100644 index 000000000..b39a6d145 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/AssertValidChecksumCommand.cs @@ -0,0 +1,71 @@ +// Copyright © 2017 - 2024 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.IO; +using System.Management.Automation; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Assert, "ValidChecksum")] + public class AssertValidChecksumCommand : ChocolateyCmdlet + { + [Parameter(Mandatory = true, Position = 0)] + [Alias("File", "FilePath")] + public string Path { get; set; } + + [Parameter(Position = 1)] + public string Checksum { get; set; } = string.Empty; + + [Parameter(Position = 2)] + public ChecksumType ChecksumType { get; set; } = ChecksumType.Md5; + + [Parameter(Position = 3)] + [Alias("OriginalUrl")] + public string Url { get; set; } = string.Empty; + + protected override void End() + { + try + { + ChecksumValidator.AssertChecksumValid(this, Path, Checksum, ChecksumType, Url); + } + catch (ChecksumMissingException error) + { + ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.ChecksumMissing", ErrorCategory.MetadataError, Checksum)); + } + catch (ChecksumVerificationFailedException error) + { + ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.BadChecksum", ErrorCategory.InvalidResult, Checksum)); + } + catch (FileNotFoundException error) + { + ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.FileNotFound", ErrorCategory.ObjectNotFound, Path)); + } + catch (ChecksumExeNotFoundException error) + { + ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.ChecksumExeNotFound", ErrorCategory.ObjectNotFound, targetObject: null)); + } + catch (Exception error) + { + ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.Unknown", ErrorCategory.NotSpecified, Path)); + } + } + + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/ChecksumValidator.cs b/src/Chocolatey.PowerShell/Helpers/ChecksumValidator.cs new file mode 100644 index 000000000..3e02f9bcd --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/ChecksumValidator.cs @@ -0,0 +1,181 @@ +// Copyright © 2017 - 2024 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Host; +using Chocolatey.PowerShell.Shared; +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Helpers +{ + /// + /// Helper class to validate checksums. Used by , and any other commands that need to validate checksums. + /// + public static class ChecksumValidator + { + /// + /// Tests whether a given matches the checksum of a given file. + /// + /// The cmdlet calling the method. + /// The path to the file to verify the checksum of. + /// The checksum value to validate against. + /// The type of the checksum. + /// The original url that the file was downloaded from, if any. + /// If this method returns false, this will contain an exception that can be raised if needed. + /// True if the actual checksum of the file matches the given checksum, otherwise False. + public static bool IsValid(PSCmdlet cmdlet, string path, string checksum, ChecksumType? checksumType, string url, out Exception error) + { + if (checksumType is null) + { + checksumType = ChecksumType.Md5; + } + + if (IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyIgnoreChecksums), "true")) + { + cmdlet.WriteWarning("Ignoring checksums due to feature checksumFiles turned off or option --ignore-checksums set."); + error = null; + return true; + } + + if (string.IsNullOrWhiteSpace(checksum)) + { + if (IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyAllowEmptyChecksums), "true")) + { + cmdlet.WriteDebug("Empty checksums are allowed due to allowEmptyChecksums feature or option."); + error = null; + return true; + } + + var isHttpsUrl = !string.IsNullOrEmpty(url) && url.ToLower().StartsWith("https"); + + if (isHttpsUrl && IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyAllowEmptyChecksumsSecure), "true")) + { + cmdlet.WriteDebug("Download from HTTPS source with feature 'allowEmptyChecksumsSecure' enabled."); + error = null; + return true; + } + + cmdlet.WriteWarning("Missing package checksums are not allowed (by default for HTTP/FTP, \n HTTPS when feature 'allowEmptyChecksumsSecure' is disabled) for \n safety and security reasons. Although we strongly advise against it, \n if you need this functionality, please set the feature \n 'allowEmptyChecksums' ('choco feature enable -n \n allowEmptyChecksums') \n or pass in the option '--allow-empty-checksums'. You can also pass \n checksums at runtime (recommended). See `choco install -?` for details."); + cmdlet.WriteDebug("If you are a maintainer attempting to determine the checksum for packaging purposes, please run \n 'choco install checksum' and run 'checksum -t sha256 -f $file' \n Ensure you do this for all remote resources."); + + if (GetPSVersion().Major >= 4) + { + cmdlet.WriteDebug("Because you are running PowerShell with a major version of v4 or greater, you could also opt to run \n '(Get-FileHash -Path $file -Algorithm SHA256).Hash' \n rather than install a separate tool."); + } + + if (IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPowerShellHost), "true") + && !(cmdlet.Host is null)) + { + const string prompt = "Do you wish to allow the install to continue (not recommended)?"; + var info = string.Format( + "The integrity of the file '{0}'{1} has not been verified by a checksum in the package scripts", + GetFileName(path), + string.IsNullOrWhiteSpace(url) ? string.Empty : $" from '{url}'"); + + var choices = new Collection + { + new ChoiceDescription("&Yes"), + new ChoiceDescription("&No"), + }; + + var selection = cmdlet.Host.UI.PromptForChoice(info, prompt, choices, defaultChoice: 1); + + if (selection == 0) + { + error = null; + return true; + } + } + + var errorMessage = isHttpsUrl + ? "This package downloads over HTTPS but does not yet have package checksums to verify the package. We recommend asking the maintainer to add checksums to this package. In the meantime if you need this package to work correctly, please enable the feature allowEmptyChecksumsSecure, provide the runtime switch '--allow-empty-checksums-secure', or pass in checksums at runtime (recommended - see 'choco install -?' / 'choco upgrade -?' for details)." + : "Empty checksums are no longer allowed by default for non-secure sources. Please ask the maintainer to add checksums to this package. In the meantime if you need this package to work correctly, please enable the feature allowEmptyChecksums, provide the runtime switch '--allow-empty-checksums', or pass in checksums at runtime (recommended - see 'choco install -?' / 'choco upgrade -?' for details). It is strongly advised against allowing empty checksums for non-internal HTTP/FTP sources."; + + error = new ChecksumMissingException(errorMessage); + return false; + } + + if (!FileExists(cmdlet, path)) + { + error = new FileNotFoundException($"Unable to checksum a file that doesn't exist - Could not find file '{path}'", path); + return false; + } + + var checksumExe = CombinePaths(cmdlet, GetInstallLocation(cmdlet), "tools", "checksum.exe"); + if (!FileExists(cmdlet, checksumExe)) + { + error = new FileNotFoundException("Unable to locate 'checksum.exe', your Chocolatey installation may be incomplete or damaged. Try reinstalling chocolatey with 'choco install chocolatey --force'.", checksumExe); + return false; + } + + cmdlet.WriteDebug($"checksum.exe found at '{checksumExe}'"); + var arguments = string.Format( + "-c=\"{0}\" -t=\"{1}\" -f=\"{2}\"", + checksum, + checksumType.ToString().ToLower(), + path); + + cmdlet.WriteDebug($"Executing command ['{checksumExe}' {arguments}]"); + + var process = new Process + { + StartInfo = new ProcessStartInfo(checksumExe, arguments) + { + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + }, + }; + + process.Start(); + process.WaitForExit(); + + var exitCode = process.ExitCode; + process.Dispose(); + + cmdlet.WriteDebug($"Command ['{checksumExe}' {arguments}] exited with '{exitCode}'"); + + if (exitCode != 0) + { + error = new ChecksumVerificationFailedException($"Checksum for '{path}' did not match '{checksum}' for checksum type '{checksumType}'. Consider passing the actual checksums through with `--checksum --checksum64` once you validate the checksums are appropriate. A less secure option is to pass `--ignore-checksums` if necessary.", checksum, path); + return false; + } + + error = null; + return true; + } + + /// + /// Validate the checksum of a file against an expected and throw if the checksum does not match. + /// + /// The cmdlet calling the method. + /// The path to the file to verify the checksum for. + /// The expected checksum value. + /// The type of the checksum to look for. + /// The url the file was downloaded from originally, if any. + public static void AssertChecksumValid(PSCmdlet cmdlet, string path, string checksum, ChecksumType? checksumType, string url) + { + if (!IsValid(cmdlet, path, checksum, checksumType, url, out var exception)) + { + throw exception; + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/PSHelper.cs b/src/Chocolatey.PowerShell/Helpers/PSHelper.cs index a18bca9fc..3e99bc1b6 100644 --- a/src/Chocolatey.PowerShell/Helpers/PSHelper.cs +++ b/src/Chocolatey.PowerShell/Helpers/PSHelper.cs @@ -14,7 +14,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System; using System.Management.Automation; +using System.Reflection; namespace Chocolatey.PowerShell.Helpers { @@ -23,11 +28,18 @@ namespace Chocolatey.PowerShell.Helpers /// public static class PSHelper { + private static readonly IList _powershellLocations = new List + { + Environment.ExpandEnvironmentVariables("%systemroot%\\SysNative\\WindowsPowerShell\\v1.0\\powershell.exe"), + Environment.ExpandEnvironmentVariables("%systemroot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"), + "powershell.exe" + }; + /// /// Writes objects to the output pipeline of the , enumerating collections. /// /// The cmdlet calling the method. - /// + /// The object to write to the pipeline. public static void WriteObject(PSCmdlet cmdlet, object output) { cmdlet.WriteObject(output, enumerateCollection: true); @@ -49,5 +61,241 @@ public static void WriteHost(PSCmdlet cmdlet, string message) cmdlet.WriteVerbose(message); } } + + /// + /// Gets the location of the Chocolatey install location. + /// + /// The cmdlet calling the method. + /// The path to the Chocolatey folder. + public static string GetInstallLocation(PSCmdlet cmdlet) + { + return GetParentDirectory(cmdlet, GetParentDirectory(cmdlet, typeof(PSHelper).Assembly.Location)); + } + + /// + /// Combine the given path fragments into a single path. + /// + /// The cmdlet calling the method. + /// The parent path to combine child fragments with. + /// One or more child paths to combine the parent path with. + /// The completed path constructed from fragments. + public static string CombinePaths(PSCmdlet cmdlet, string parent, params string[] childPaths) + { + var result = parent; + foreach (var path in childPaths) + { + result = cmdlet.SessionState.Path.Combine(result, path); + } + + return result; + } + + /// + /// Convert the given to the target type , using PowerShell's default conversion semantics. + /// + /// The type to convert the value to. + /// The value to convert. + /// The converted value. + public static T ConvertTo(object value) + { + return (T)LanguagePrimitives.ConvertTo(value, typeof(T)); + } + + /// + /// Checks for the existence of the target , creating it if it doesn't exist. + /// + /// The cmdlet running the method. + /// The directory to look for or create. + public static void EnsureDirectoryExists(PSCmdlet cmdlet, string directory) + { + if (!ContainerExists(cmdlet, directory)) + { + NewDirectory(cmdlet, directory); + } + } + + /// + /// Test the equality of two values, based on PowerShell's equality checks, case insensitive for string values. + /// Equivalent to -eq in PowerShell. + /// + /// The first (LHS) value to compare. + /// The second (RHS) vale to compare. + /// True if PowerShell considers the values equial, false otherwise. + public static bool IsEqual(object first, object second) + { + return LanguagePrimitives.Equals(first, second, ignoreCase: true); + } + + /// + /// Test the equality of two values, based on PowerShell's equality checks, optionally case insensitive. + /// Equivalent to -eq in PowerShell if is true, otherwise equivalent to -ceq. + /// + /// The first (LHS) value to compare. + /// The second (RHS) vale to compare. + /// Whether to ignore case in the comparison for string values. + /// True if PowerShell considers the values equial, false otherwise. + public static bool IsEqual(object first, object second, bool ignoreCase) + { + return LanguagePrimitives.Equals(first, second, ignoreCase); + } + + /// + /// Test whether an item at the given path exists. + /// Equivalent to Test-Path. + /// + /// The cmdlet calling the method. + /// The path to look for an item at. + /// True if the item exists, otherwise false. + public static bool ItemExists(PSCmdlet cmdlet, string path) + { + return cmdlet.InvokeProvider.Item.Exists(path); + } + + /// + /// Test whether a non-container item at the given path exists. + /// Equivalent to Test-Path -PathType Leaf. + /// + /// The cmdlet calling the method. + /// The path to look for a non-container item at. + /// True if a file exists at the given path, otherwise false. + public static bool FileExists(PSCmdlet cmdlet, string path) + { + return ItemExists(cmdlet, path) && !ContainerExists(cmdlet, path); + } + + /// + /// Test whether a container item at the given path exists. + /// Equivalent to Test-Path -PathType Container. + /// + /// The cmdlet calling the method. + /// The path to look for a container item at. + /// True if a container exists at the given path, otherwise false. + public static bool ContainerExists(PSCmdlet cmdlet, string path) + { + return cmdlet.InvokeProvider.Item.IsContainer(path); + } + + /// + /// Gets the parent directory of a given path. + /// + /// The cmdlet calling the method. + /// The path to find the parent container for. + /// The path to the parent container of the provided path. + public static string GetParentDirectory(PSCmdlet cmdlet, string path) + { + return cmdlet.SessionState.Path.ParseParent(GetUnresolvedPath(cmdlet, path), string.Empty); + } + + /// + /// Gets the file name segment of a provided file path. + /// + /// The path to take the file name from. + /// The file name and extension. + public static string GetFileName(string path) + { + return Path.GetFileName(path); + } + + /// + /// Gets the current PowerShell version of the running PowerShell assemblies. + /// Equivalent to $PSVersionTable.PSVersion. + /// + /// The current PowerShell version. + public static Version GetPSVersion() + { + Version result = null; + var assembly = Assembly.GetAssembly(typeof(Cmdlet)); + + // This type is public in PS v6.2+, this reflection will not be needed once we're using newer assemblies. + var psVersionInfo = assembly.GetType("System.Management.Automation.PSVersionInfo"); + var versionProperty = psVersionInfo?.GetProperty("PSVersion", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + + var getter = versionProperty?.GetGetMethod(true); + result = (Version)getter?.Invoke(null, Array.Empty()); + + // Assume absolute minimum version if we can't determine the version. + return result ?? new Version(2, 0); + } + + /// + /// Turns a relative path into a full path based on the current context the is running in, + /// without ensuring that the path actually exists. + /// + /// Similar to Resolve-Path, but will not error on a path that does not exist. + /// + /// The cmdlet running the method. + /// The relative path to transform into a full path. + /// The full path to the item. + public static string GetUnresolvedPath(PSCmdlet cmdlet, string path) + { + return cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path); + } + + /// + /// Creates a new item at the specified . + /// + /// The cmdlet calling the method. + /// The path to the new item. + /// The name for the new item, which may be null if the contains the name already. + /// The name for the item type to create. For the FileSystem provider, this will be File or Directory. + /// A containing the references to the item(s) created. + public static Collection NewItem(PSCmdlet cmdlet, string path, string name, string itemType) + { + return cmdlet.InvokeProvider.Item.New(path, name, itemType, content: string.Empty); + } + + /// + /// Creates a new item at the specified . + /// + /// The cmdlet calling the method. + /// The path to the new item. + /// The name for the new item, which may be null if the contains the name already. + /// The name for the item type to create. For the FileSystem provider, this will be File or Directory. + /// A containing the references to the item(s) created. + public static Collection NewItem(PSCmdlet cmdlet, string path, string itemType) + { + return NewItem(cmdlet, path, name: null, itemType); + } + + /// + /// Creates a new file at the designated . + /// + /// The cmdlet calling the method. + /// The path to the file to be created. + /// A containing the references to the item(s) created. + public static Collection NewFile(PSCmdlet cmdlet, string path) + { + return NewItem(cmdlet, path, itemType: "File"); + } + + /// + /// Creates a new directory at the designated . + /// + /// The cmdlet calling the method. + /// The path to the directory to be created. + /// A containing the references to the item(s) created. + public static Collection NewDirectory(PSCmdlet cmdlet, string path) + { + return NewItem(cmdlet, path, itemType: "Directory"); + } + + /// + /// Gets the path to the location of powershell.exe. + /// + /// The path where powershell.exe is found. + /// Thrown if powershell.exe cannot be located. + public static string GetPowerShellLocation() + { + foreach (var powershellLocation in _powershellLocations) + { + if (File.Exists(powershellLocation)) + { + return powershellLocation; + } + } + + throw new FileNotFoundException(string.Format("Unable to find suitable location for PowerShell. Searched the following locations: '{0}'", string.Join("; ", _powershellLocations))); + } } } + diff --git a/src/Chocolatey.PowerShell/Shared/ChecksumExeNotFoundException.cs b/src/Chocolatey.PowerShell/Shared/ChecksumExeNotFoundException.cs new file mode 100644 index 000000000..5e8dd941f --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ChecksumExeNotFoundException.cs @@ -0,0 +1,41 @@ +// Copyright © 2017 - 2024 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Runtime.Serialization; + +namespace Chocolatey.PowerShell.Shared +{ + [Serializable] + internal class ChecksumExeNotFoundException : Exception + { + public ChecksumExeNotFoundException() + { + } + + public ChecksumExeNotFoundException(string message) : base(message) + { + } + + public ChecksumExeNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } + + protected ChecksumExeNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/src/Chocolatey.PowerShell/Shared/ChecksumMissingException.cs b/src/Chocolatey.PowerShell/Shared/ChecksumMissingException.cs new file mode 100644 index 000000000..cf308e933 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ChecksumMissingException.cs @@ -0,0 +1,41 @@ +// Copyright © 2017 - 2024 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Runtime.Serialization; + +namespace Chocolatey.PowerShell.Shared +{ + [Serializable] + internal class ChecksumMissingException : Exception + { + public ChecksumMissingException() + { + } + + public ChecksumMissingException(string message) : base(message) + { + } + + public ChecksumMissingException(string message, Exception innerException) : base(message, innerException) + { + } + + protected ChecksumMissingException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/src/Chocolatey.PowerShell/Shared/ChecksumType.cs b/src/Chocolatey.PowerShell/Shared/ChecksumType.cs new file mode 100644 index 000000000..15af7e801 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ChecksumType.cs @@ -0,0 +1,29 @@ +// Copyright © 2017 - 2024 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Chocolatey.PowerShell.Shared +{ + /// + /// The supported types of checksums for file integrity verification. + /// + public enum ChecksumType + { + Md5, + Sha1, + Sha256, + Sha512, + } +} diff --git a/src/Chocolatey.PowerShell/Shared/ChecksumVerificationFailedException.cs b/src/Chocolatey.PowerShell/Shared/ChecksumVerificationFailedException.cs new file mode 100644 index 000000000..457e1e6c4 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ChecksumVerificationFailedException.cs @@ -0,0 +1,50 @@ +// Copyright © 2017 - 2024 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Runtime.Serialization; + +namespace Chocolatey.PowerShell.Shared +{ + [Serializable] + internal class ChecksumVerificationFailedException : Exception + { + public string Checksum { get; } + public string FilePath { get; } + + public ChecksumVerificationFailedException() + { + } + + public ChecksumVerificationFailedException(string message, string checksum, string path) : this(message) + { + Checksum = checksum; + FilePath = path; + } + + public ChecksumVerificationFailedException(string message) : base(message) + { + } + + public ChecksumVerificationFailedException(string message, Exception innerException) : base(message, innerException) + { + } + + protected ChecksumVerificationFailedException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs b/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs index 458975cb8..8e29e0add 100644 --- a/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs +++ b/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs @@ -27,6 +27,18 @@ namespace Chocolatey.PowerShell.Shared /// public abstract class ChocolateyCmdlet : PSCmdlet { + /// + /// The canonical error ID for the command to assist with traceability. + /// For more specific error IDs where needed, use "{ErrorId}.EventName". + /// + protected string ErrorId + { + get + { + return GetType().Name + "Error"; + } + } + /// /// For compatibility reasons, we always add the -IgnoredArguments parameter, so that newly added parameters /// won't break things too much if a package is run with an older version of Chocolatey. diff --git a/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs b/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs index 117478640..ffbec6bec 100644 --- a/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs +++ b/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs @@ -14,6 +14,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.ComponentModel; + namespace Chocolatey.PowerShell.Shared { public static class EnvironmentVariables @@ -26,5 +28,22 @@ public static class EnvironmentVariables public const string System = "SYSTEM"; public const string SystemRoot = "SystemRoot"; public const string Username = "USERNAME"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyIgnoreChecksums = nameof(ChocolateyIgnoreChecksums); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyAllowEmptyChecksums = nameof(ChocolateyAllowEmptyChecksums); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyAllowEmptyChecksumsSecure = nameof(ChocolateyAllowEmptyChecksumsSecure); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPowerShellHost = nameof(ChocolateyPowerShellHost); + } } diff --git a/src/chocolatey.resources/chocolatey.resources.csproj b/src/chocolatey.resources/chocolatey.resources.csproj index 207c9f748..078ba7d4a 100644 --- a/src/chocolatey.resources/chocolatey.resources.csproj +++ b/src/chocolatey.resources/chocolatey.resources.csproj @@ -66,7 +66,6 @@ - diff --git a/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 b/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 index d71dfe2ed..a350aedb9 100644 --- a/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 +++ b/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 @@ -134,6 +134,8 @@ if (Test-Path $extensionsPath) { } } +Set-Alias -Name 'Get-CheckSumValid' -Value 'Assert-ValidChecksum' + # Exercise caution and test _thoroughly_ with AND without the licensed extension installed # when making any changes here. And make sure to update this comment if needed when any # changes are being made. diff --git a/src/chocolatey.resources/helpers/functions/Get-CheckSumValid.ps1 b/src/chocolatey.resources/helpers/functions/Get-CheckSumValid.ps1 deleted file mode 100644 index 520658672..000000000 --- a/src/chocolatey.resources/helpers/functions/Get-CheckSumValid.ps1 +++ /dev/null @@ -1,434 +0,0 @@ -# Copyright © 2017 - 2021 Chocolatey Software, Inc. -# Copyright © 2015 - 2017 RealDimensions Software, LLC -# Copyright © 2011 - 2015 RealDimensions Software, LLC & original authors/contributors from https://github.com/chocolatey/chocolatey -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -function Get-ChecksumValid { - <# -.SYNOPSIS -Checks a file's checksum versus a passed checksum and checksum type. - -.DESCRIPTION -Makes a determination if a file meets an expected checksum signature. -This function is usually used when comparing a file that is downloaded -from an official distribution point. If the checksum fails to match the -expected output, this function throws an error. - -Checksums have been used for years as a means of verification. A -checksum hash is a unique value or signature that corresponds to the -contents of a file. File names and extensions can be altered without -changing the checksum signature. However if you changed the contents of -the file, even one character, the checksum will be different. - -Checksums are used to provide as a means of cryptographically ensuring -the contents of a file have not been changed. While some cryptographic -algorithms, including MD5 and SHA1, are no longer considered secure -against attack, the goal of a checksum algorithm is to make it -extremely difficult (near impossible with better algorithms) to alter -the contents of a file (whether by accident or for malicious reasons) -and still result in the same checksum signature. - -When verifying a checksum using a secure algorithm, if the checksum -matches the expected signature, the contents of the file are identical -to what is expected. - -.NOTES -This uses the checksum.exe tool available separately at -https://community.chocolatey.org/packages/checksum. - -Options that affect checksum verification: - -* `--ignore-checksums` - skips checksumming -* `--allow-empty-checksums` - skips checksumming when the package is missing a checksum -* `--allow-empty-checksums-secure` - skips checksumming when the package is missing a checksum for secure (HTTPS) locations -* `--require-checksums` - requires checksums for both non-secure and secure locations -* `--download-checksum`, `--download-checksum-type` - allows user to pass their own checksums -* `--download-checksum-x64`, `--download-checksum-type-x64` - allows user to pass their own checksums - -Features that affect checksum verification: - -* `checksumFiles` - when turned off, skips checksumming -* `allowEmptyChecksums` - when turned on, skips checksumming when the package is missing a checksum -* `allowEmptyChecksumsSecure` - when turned on, skips checksumming when the package is missing a checksum for secure (HTTPS) locations - -.INPUTS -None - -.OUTPUTS -None - -.PARAMETER File -The full path to a binary file that is checksummed and compared to the -passed Checksum parameter value. - -.PARAMETER Checksum -The expected checksum hash value of the File resource. The checksum -type is covered by ChecksumType. - -**NOTE:** Checksums in packages are meant as a measure to validate the -originally intended file that was used in the creation of a package is -the same file that is received at a future date. Since this is used for -other steps in the process related to the community repository, it -ensures that the file a user receives is the same file a maintainer -and a moderator (if applicable), plus any moderation review has -intended for you to receive with this package. If you are looking at a -remote source that uses the same url for updates, you will need to -ensure the package also stays updated in line with those remote -resource updates. You should look into [automatic packaging](https://docs.chocolatey.org/en-us/create/automatic-packages) -to help provide that functionality. - -**NOTE:** To determine checksums, you can get that from the original -site if provided. You can also use the [checksum tool available on -the community feed](https://community.chocolatey.org/packages/checksum) (`choco install checksum`) -and use it e.g. `checksum -t sha256 -f path\to\file`. Ensure you -provide checksums for all remote resources used. - -.PARAMETER ChecksumType -The type of checksum that the file is validated with - 'md5', 'sha1', -'sha256' or 'sha512' - defaults to 'md5'. - -MD5 is not recommended as certain organizations need to use FIPS -compliant algorithms for hashing - see -https://support.microsoft.com/en-us/kb/811833 for more details. - -The recommendation is to use at least SHA256. - -.PARAMETER IgnoredArguments -Allows splatting with arguments that do not apply. Do not use directly. - -.EXAMPLE -Get-ChecksumValid -File $fileFullPath -CheckSum $checksum -ChecksumType $checksumType - -.LINK -Get-ChocolateyWebFile - -.LINK -Install-ChocolateyPackage -#> - param( - [parameter(Mandatory = $true, Position = 0)][string] $file, - [parameter(Mandatory = $false, Position = 1)][string] $checksum = '', - [parameter(Mandatory = $false, Position = 2)][string] $checksumType = 'md5', - [parameter(Mandatory = $false, Position = 3)][string] $originalUrl = '', - [parameter(ValueFromRemainingArguments = $true)][Object[]] $ignoredArguments - ) - - Write-FunctionCallLogMessage -Invocation $MyInvocation -Parameters $PSBoundParameters - - if ($env:ChocolateyIgnoreChecksums -eq 'true') { - Write-Warning "Ignoring checksums due to feature checksumFiles turned off or option --ignore-checksums set." - return - } - - if ($checksum -eq '' -or $checksum -eq $null) { - $allowEmptyChecksums = $env:ChocolateyAllowEmptyChecksums - $allowEmptyChecksumsSecure = $env:ChocolateyAllowEmptyChecksumsSecure - if ($allowEmptyChecksums -eq 'true') { - Write-Debug "Empty checksums are allowed due to allowEmptyChecksums feature or option." - return - } - - if ($originalUrl -ne $null -and $originalUrl.ToLower().StartsWith("https") -and $allowEmptyChecksumsSecure -eq 'true') { - Write-Debug "Download from HTTPS source with feature 'allowEmptyChecksumsSecure' enabled." - return - } - - Write-Warning "Missing package checksums are not allowed (by default for HTTP/FTP, `n HTTPS when feature 'allowEmptyChecksumsSecure' is disabled) for `n safety and security reasons. Although we strongly advise against it, `n if you need this functionality, please set the feature `n 'allowEmptyChecksums' ('choco feature enable -n `n allowEmptyChecksums') `n or pass in the option '--allow-empty-checksums'. You can also pass `n checksums at runtime (recommended). See `choco install -?` for details." - Write-Debug "If you are a maintainer attempting to determine the checksum for packaging purposes, please run `n 'choco install checksum' and run 'checksum -t sha256 -f $file' `n Ensure you do this for all remote resources." - if ($PSVersionTable.PSVersion.Major -ge 4) { - Write-Debug "Because you are running PowerShell with a major version of v4 or greater, you could also opt to run `n '(Get-FileHash -Path $file -Algorithm SHA256).Hash' `n rather than install a separate tool." - } - - if ($env:ChocolateyPowerShellHost -eq 'true') { - $statement = "The integrity of the file '$([System.IO.Path]::GetFileName($file))'" - if ($originalUrl -ne $null -and $originalUrl -ne '') { - $statement += " from '$originalUrl'" - } - $statement += " has not been verified by a checksum in the package scripts." - $question = 'Do you wish to allow the install to continue (not recommended)?' - $choices = New-Object System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription] - $choices.Add((New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes')) - $choices.Add((New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList '&No')) - - $selection = $Host.UI.PromptForChoice($statement, $question, $choices, 1) - - if ($selection -eq 0) { - return - } - } - - if ($originalUrl -ne $null -and $originalUrl.ToLower().StartsWith("https")) { - throw "This package downloads over HTTPS but does not yet have package checksums to verify the package. We recommend asking the maintainer to add checksums to this package. In the meantime if you need this package to work correctly, please enable the feature allowEmptyChecksumsSecure, provide the runtime switch '--allow-empty-checksums-secure', or pass in checksums at runtime (recommended - see 'choco install -?' / 'choco upgrade -?' for details)." - } - else { - throw "Empty checksums are no longer allowed by default for non-secure sources. Please ask the maintainer to add checksums to this package. In the meantime if you need this package to work correctly, please enable the feature allowEmptyChecksums, provide the runtime switch '--allow-empty-checksums', or pass in checksums at runtime (recommended - see 'choco install -?' / 'choco upgrade -?' for details). It is strongly advised against allowing empty checksums for non-internal HTTP/FTP sources." - } - } - - if (!([System.IO.File]::Exists($file))) { - throw "Unable to checksum a file that doesn't exist - Could not find file `'$file`'" - } - - if ($checksumType -eq $null -or $checksumType -eq '') { - $checksumType = 'md5' - } - - if ($checksumType -ne 'sha1' -and $checksumType -ne 'sha256' -and $checksumType -ne 'sha512' -and $checksumType -ne 'md5') { - Write-Debug 'Setting checksumType to md5 due to non-set value or type is not specified correctly.' - throw "Checksum type '$checksumType' is unsupported. This type may be supported in a newer version of Chocolatey." - } - - $checksumExe = Join-Path "$helpersPath" '..\tools\checksum.exe' - if (!([System.IO.File]::Exists($checksumExe))) { - Update-SessionEnvironment - $checksumExe = Join-Path "$env:ChocolateyInstall" 'tools\checksum.exe' - } - Write-Debug "checksum.exe found at `'$checksumExe`'" - - $params = "-c=`"$checksum`" -t=`"$checksumType`" -f=`"$file`"" - - Write-Debug "Executing command ['$checksumExe' $params]" - $process = New-Object System.Diagnostics.Process - $process.StartInfo = New-Object System.Diagnostics.ProcessStartInfo($checksumExe, $params) - $process.StartInfo.UseShellExecute = $false - $process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden - - $process.Start() | Out-Null - $process.WaitForExit() - $exitCode = $process.ExitCode - $process.Dispose() - - Write-Debug "Command [`'$checksumExe`' $params] exited with `'$exitCode`'." - - if ($exitCode -ne 0) { - throw "Checksum for '$file' did not meet '$checksum' for checksum type '$checksumType'. Consider passing the actual checksums through with `--checksum --checksum64` once you validate the checksums are appropriate. A less secure option is to pass `--ignore-checksums` if necessary." - } - - #$fileCheckSumActual = $md5Output.Split(' ')[0] - # if ($fileCheckSumActual -ne $checkSum) { - # throw "CheckSum for `'$file'` did not meet `'$checkSum`'." - # } -} - -# SIG # Begin signature block -# MIInKwYJKoZIhvcNAQcCoIInHDCCJxgCAQExDzANBglghkgBZQMEAgEFADB5Bgor -# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG -# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB2C8DV+23E5keM -# 058E9c67jcShgUMIAFLVjBdLSTJE5aCCIK4wggWNMIIEdaADAgECAhAOmxiO+dAt -# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK -# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV -# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa -# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy -# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD -# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E -# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy -# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF -# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 -# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB -# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR -# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 -# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB -# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S -# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x -# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB -# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP -# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC -# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp -# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv -# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 -# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB -# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc -# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov -# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy -# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW -# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF -# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z -# twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG -# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx -# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy -# dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx -# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy -# RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg -# Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH -# JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf -# UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w -# 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk -# tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb -# qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm -# cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6 -# 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK -# QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo -# 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB -# Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche -# MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB -# /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU -# 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG -# CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j -# c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp -# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig -# NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v -# dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI -# hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd -# 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC -# qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl -# /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC -# RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT -# gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/ -# a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37 -# xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL -# NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0 -# YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ -# RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG -# sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw -# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw -# HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX -# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0 -# ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN -# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr -# PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM -# gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg -# nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC -# EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0 -# p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa -# khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0 -# XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I -# HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2 -# FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH -# X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2 -# 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD -# VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k -# TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD -# AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj -# ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t -# L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0 -# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww -# HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB -# ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j -# fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI -# moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf -# JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx -# oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3 -# LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx -# 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9 -# Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I -# Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug -# 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5 -# Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQ -# BUSv85SdCDmmv9s/X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX -# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 -# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAw -# MDAwMFoXDTM0MTAxMzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp -# Z2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCC -# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X -# 5dLnXaEOCdwvSKOXejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uU -# UI8cIOrHmjsvlmbjaedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa -# 2mq62DvKXd4ZGIX7ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgt -# XkV1lnX+3RChG4PBuOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60 -# pCFkcOvV5aDaY7Mu6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17 -# cz4y7lI0+9S769SgLDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BY -# QfvYsSzhUa+0rRUGFOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9 -# c33u3Qr/eTQQfqZcClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw -# 9/sqhux7UjipmAmhcbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2c -# kpMEtGlwJw1Pt7U20clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhR -# B8qUt+JQofM604qDy0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYD -# VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgG -# BmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxq -# II+eyG8wHQYDVR0OBBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEw -# T6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH -# NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGD -# MIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYB -# BQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 -# ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQEL -# BQADggIBAIEa1t6gqbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF -# 7SaCinEvGN1Ott5s1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrC -# QDifXcigLiV4JZ0qBXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFc -# jGnRuSvExnvPnPp44pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8 -# wWkZus8W8oM3NG6wQSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbF -# KNOt50MAcN7MmJ4ZiQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP -# 4xeR0arAVeOGv6wnLEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VP -# NTwAvb6cKmx5AdzaROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvr -# moI1VygWy2nyMpqy0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2 -# obhDLN9OTH0eaHDAdwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJ -# uEbTbDJ8WC9nR2XlG3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIG7TCCBNWg -# AwIBAgIQBNI793flHTneCMtwLiiYFTANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG -# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 -# IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0Ex -# MB4XDTI0MDUwOTAwMDAwMFoXDTI3MDUxMTIzNTk1OVowdTELMAkGA1UEBhMCVVMx -# DzANBgNVBAgTBkthbnNhczEPMA0GA1UEBxMGVG9wZWthMSEwHwYDVQQKExhDaG9j -# b2xhdGV5IFNvZnR3YXJlLCBJbmMxITAfBgNVBAMTGENob2NvbGF0ZXkgU29mdHdh -# cmUsIEluYzCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAPDJgdZWj0RV -# lBBBniCyGy19FB736U5AahB+dAw3nmafOEeG+syql0m9kzV0gu4bSd4Al587ioAG -# DUPAGhXf0R+y11cx7c1cgdyxvfBvfMEkgD7sOUeF9ggZJc0YZ4qc7Pa6qqMpHDru -# pjshvLmQMSLaGKF68m+w2mJiZkLMYBEotPiAC3+IzI1MQqidCfN6rfQUmtcKyrVz -# 2zCt8CvuR3pSyNCBcQgKZ/+NwBfDqPTt1wKq5JCIQiLnbDZwJ9F5433enzgUGQgh -# KRoIwfp/hap7t7lrNf859Xe1/zHT4qtNgzGqSdJ2Kbz1YAMFjZokYHv/sliyxJN9 -# 7++0BApX2t45JsQaqyQ60TSKxqOH0JIIDeYgwxfJ8YFmuvt7T4zVM8u02Axp/1YV -# nKP2AOVca6FDe9EiccrexAWPGoP+WQi8WFQKrNVKr5XTLI0MNTjadOHfF0XUToyF -# H8FVnZZV1/F1kgd/bYbt/0M/QkS4FGmJoqT8dyRyMkTlTynKul4N3QIDAQABo4IC -# AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE -# FFpfZUilS5A+fjYV80ib5qKkBoczMD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYI -# KwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8E -# BAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZN -# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNp -# Z25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0 -# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5 -# NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw -# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v -# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu -# Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB -# CwUAA4ICAQAW9ANNkR2cF6ulbM+/XUWeWqC7UTqtsRwj7WAo8XTr52JebRchTGDH -# BZP9sDRZsFt+lPcPvBrv41kWoaFBmebTaPMh6YDHaON+uc19CTWXsMh8eog0lzGU -# iA3mKdbVit0udrgNlBUqTIuvMlMFIARWSz90FMeQrCFokLmqoqjp7u0sVPM7ng6T -# 9D8ct/m5LSpIa5TJCjAfyfw75GK0wzTDdTi1MgiAIyX0EedMrEwXjOjSApQ+uhIW -# v/AHDf8ukJzDFTTeiUkYZ1w++z70QZkzLfQTi6eH9vqgyXWcnGCwOxKquqe8RSIe -# M3FdtLstn9nI8S4qeiKdmomG6FAZTzYiGULJdJGsLh6Uii56zZdq3bSre/yrfed4 -# hf/0MqEtWSU7LpkWM8AApRkIKRBZIQ73/7WxwsF9kHoZxqoRMDGTzWt+S7/XrSOa -# QbKf0CxdxMPHKC2A1u3xGNDChtQEwpHxYXf/teD7GeFYFQJg/wn4dC72mZze97+c -# YcpmI4R13Q7owmRthK1hnuq4EOQIcoTPbQXiaRzULbYrcOnJi7EbXcqdeAAnZAyV -# b6zGqAaE9Sw4RYvkosL5IlBgrdIwSFJMbeirBoM2GukIHQ8UaEu3l1PoNQvVbqM1 -# 8zHiN4WA4rp9G9wfcAlZWq9iKF34sA+Xu03qSVaKPKn6YJMl5PfUsDGCBdMwggXP -# AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw -# PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2 -# IFNIQTM4NCAyMDIxIENBMQIQBNI793flHTneCMtwLiiYFTANBglghkgBZQMEAgEF -# AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor -# BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3 -# DQEJBDEiBCAHAWDOsVcr+f5oRXQofamoM1SNFTaQMVfxDaIa2b7w0DANBgkqhkiG -# 9w0BAQEFAASCAYDOJFvkqv0Mkd58W9VkYrs7Bg/4kDvhDNsHpN1qwb3fL1exzjQn -# Hea8vFiHVRQfPs9Y9zg4KUD+yAqOwIDnmYIKj5J/8x8v5bgzu6sIVMJPkAaxmaD5 -# jegjOdkcpk+rOsXr/q/TfE2s8yzqIH5g8a72kjhr7PfjZRybD6f6odkW6+uSGjlH -# ytxe1SoPuYarVspzDoFBp1DbQ0zhyYU9yk9LuGcJrq6SxZGE04H0dIBtxD0443fk -# xGzbxDNNZES6NIPj1m07p8KqwPUO1jKgSWHYgfCKCwW/Fe8Xrf0Zh653IddoL/ZK -# rzUN+STIearprxHluImpULzG7JP3RnJfcTMCePx9myXjF5s07GUNOBG1bMhggEsx -# dXau+kP4c81X+oBLb+o+6MCWxk5eBWn1dfQ0AnagKbkEgYxF8tgjeVfl/ThilL8q -# kU3t4foovvSBwdx6eT2EtzbTpt1UrbvB6NsB4Ow9m10sYhwxvMTImIotBrZp+vtT -# sOx4dwjwAgGLYNGhggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBjMQsw -# CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRp -# Z2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENB -# AhAFRK/zlJ0IOaa/2z9f5WEWMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkD -# MQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjQwNTE2MTYxOTE2WjAvBgkq -# hkiG9w0BCQQxIgQgBPTcF4Ev8PYDniyQvqFYo1Qk3utzA/X59BzQe1q4GCMwDQYJ -# KoZIhvcNAQEBBQAEggIAmWL+Q+mb5WPgF8MP0+pX2q72NCh+if977Z6aTCTqfXpz -# 25fsCGD4d46AtRQBMeS2D/mk7zZjUcyLID+Z18sJDlE/WPQm+Cmc2Jc35Q/VJPr5 -# HBr6R8hFmcVMLSoezd7Aq7fSpD28g/IERoZ9lzPMbQZnFjt9N4IR1qnZq0VhE2cT -# CnmGZp6pZS5k0LdZ8/ntrJF27GY02ZGCiFgCiaT84+VDHWIsXU5x30bPfAyd8zru -# CmjYTBZpgUhbukabNxoCOp2I78U77Kl6cQV4zpjG4QFV8D4p3J7ObwS14+x5Ykp+ -# stLexSVPSqqUnyGHBV+NZuiXqnk9lulC9X1aWBiu+IvhxwvGL4Zzt6fG3sNpwajt -# marVdezZo6d2pJ/ApZO01g+LirLhLoXeNy9b8hP1yKsdwBpSFN5lljr3u4KkjgWw -# UZVWbORQ+YoK75KX47AdcFG1BkwmlaIhyhkgMds9IE4gw0Ve313QuqkMjbWronr6 -# 3y97kPIk3mOZzwf3V/gPJ0JOkTtq+upuEuDEOaoO+mzf8yrXPWFSNzRbSngUFx9l -# LohCtxr5IP85tpR1weQYzFaSoujocWdxGgWlmZfO9AMPiRfOkpYd/HVwuA0HEg+h -# MuOZlRSByKZGpyvKYvkirFtixC3MXEdO+tLX2QybBV0cAtFMDg9zfZsOVwtgqgY= -# SIG # End signature block