Skip to content

Commit 4aa8444

Browse files
authored
Merge pull request #3525 from vexx32/assert-checksum
(#3477) Replace Get-ChecksumValid with Assert-ValidChecksum cmdlet.
2 parents 46fa7bf + 69c0cac commit 4aa8444

19 files changed

+904
-450
lines changed

src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,18 @@
5858
</Reference>
5959
</ItemGroup>
6060
<ItemGroup>
61+
<Compile Include="Commands\AssertValidChecksumCommand.cs" />
62+
<Compile Include="Shared\ChecksumExeNotFoundException.cs" />
63+
<Compile Include="Shared\ChecksumVerificationFailedException.cs" />
64+
<Compile Include="Shared\ChecksumMissingException.cs" />
6165
<Compile Include="Commands\GetEnvironmentVariableCommand.cs" />
6266
<Compile Include="Commands\GetEnvironmentVariableNamesCommand.cs" />
6367
<Compile Include="Commands\InstallChocolateyPathCommand.cs" />
6468
<Compile Include="Commands\SetEnvironmentVariableCommand.cs" />
6569
<Compile Include="Commands\TestProcessAdminRightsCommand.cs" />
6670
<Compile Include="Commands\UninstallChocolateyPathCommand.cs" />
6771
<Compile Include="Commands\UpdateSessionEnvironmentCommand.cs" />
72+
<Compile Include="Helpers\ChecksumValidator.cs" />
6873
<Compile Include="Helpers\Elevation.cs" />
6974
<Compile Include="Helpers\EnvironmentHelper.cs" />
7075
<Compile Include="Helpers\Paths.cs" />
@@ -74,6 +79,7 @@
7479
<Compile Include="..\SolutionVersion.cs">
7580
<Link>Properties\SolutionVersion.cs</Link>
7681
</Compile>
82+
<Compile Include="Shared\ChecksumType.cs" />
7783
<Compile Include="Shared\ChocolateyCmdlet.cs" />
7884
<Compile Include="Shared\EnvironmentVariables.cs" />
7985
<Compile Include="Win32\NativeMethods.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright © 2017 - 2024 Chocolatey Software, Inc
2+
// Copyright © 2011 - 2017 RealDimensions Software, LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
//
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
using System;
18+
using System.IO;
19+
using System.Management.Automation;
20+
using Chocolatey.PowerShell.Helpers;
21+
using Chocolatey.PowerShell.Shared;
22+
23+
namespace Chocolatey.PowerShell.Commands
24+
{
25+
[Cmdlet(VerbsLifecycle.Assert, "ValidChecksum")]
26+
[OutputType(typeof(void))]
27+
public class AssertValidChecksumCommand : ChocolateyCmdlet
28+
{
29+
[Parameter(Mandatory = true, Position = 0)]
30+
[Alias("File", "FilePath")]
31+
public string Path { get; set; }
32+
33+
[Parameter(Position = 1)]
34+
public string Checksum { get; set; } = string.Empty;
35+
36+
[Parameter(Position = 2)]
37+
public ChecksumType ChecksumType { get; set; } = ChecksumType.Md5;
38+
39+
[Parameter(Position = 3)]
40+
[Alias("OriginalUrl")]
41+
public string Url { get; set; } = string.Empty;
42+
43+
protected override void End()
44+
{
45+
try
46+
{
47+
ChecksumValidator.AssertChecksumValid(this, Path, Checksum, ChecksumType, Url);
48+
}
49+
catch (ChecksumMissingException error)
50+
{
51+
ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.ChecksumMissing", ErrorCategory.MetadataError, Checksum));
52+
}
53+
catch (ChecksumVerificationFailedException error)
54+
{
55+
ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.BadChecksum", ErrorCategory.InvalidResult, Checksum));
56+
}
57+
catch (FileNotFoundException error)
58+
{
59+
ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.FileNotFound", ErrorCategory.ObjectNotFound, Path));
60+
}
61+
catch (ChecksumExeNotFoundException error)
62+
{
63+
ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.ChecksumExeNotFound", ErrorCategory.ObjectNotFound, targetObject: null));
64+
}
65+
catch (Exception error)
66+
{
67+
ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.Unknown", ErrorCategory.NotSpecified, Path));
68+
}
69+
}
70+
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright © 2017 - 2024 Chocolatey Software, Inc
2+
// Copyright © 2011 - 2017 RealDimensions Software, LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
//
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
18+
using System;
19+
using System.Collections.ObjectModel;
20+
using System.Diagnostics;
21+
using System.IO;
22+
using System.Management.Automation;
23+
using System.Management.Automation.Host;
24+
using Chocolatey.PowerShell.Shared;
25+
using static Chocolatey.PowerShell.Helpers.PSHelper;
26+
27+
namespace Chocolatey.PowerShell.Helpers
28+
{
29+
/// <summary>
30+
/// Helper class to validate checksums. Used by <see cref="Commands.AssertValidChecksumCommand"/>, and any other commands that need to validate checksums.
31+
/// </summary>
32+
public static class ChecksumValidator
33+
{
34+
/// <summary>
35+
/// Tests whether a given <paramref name="checksum"/> matches the checksum of a given file.
36+
/// </summary>
37+
/// <param name="cmdlet">The cmdlet calling the method.</param>
38+
/// <param name="path">The path to the file to verify the checksum of.</param>
39+
/// <param name="checksum">The checksum value to validate against.</param>
40+
/// <param name="checksumType">The type of the checksum.</param>
41+
/// <param name="url">The original url that the file was downloaded from, if any.</param>
42+
/// <param name="error">If this method returns false, this will contain an exception that can be raised if needed.</param>
43+
/// <returns>True if the actual checksum of the file matches the given checksum, otherwise False.</returns>
44+
public static bool IsValid(PSCmdlet cmdlet, string path, string checksum, ChecksumType? checksumType, string url, out Exception error)
45+
{
46+
if (checksumType is null)
47+
{
48+
checksumType = ChecksumType.Md5;
49+
}
50+
51+
if (IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyIgnoreChecksums), "true"))
52+
{
53+
cmdlet.WriteWarning("Ignoring checksums due to feature checksumFiles turned off or option --ignore-checksums set.");
54+
error = null;
55+
return true;
56+
}
57+
58+
if (string.IsNullOrWhiteSpace(checksum))
59+
{
60+
if (IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyAllowEmptyChecksums), "true"))
61+
{
62+
cmdlet.WriteDebug("Empty checksums are allowed due to allowEmptyChecksums feature or option.");
63+
error = null;
64+
return true;
65+
}
66+
67+
var isHttpsUrl = !string.IsNullOrEmpty(url) && url.ToLower().StartsWith("https");
68+
69+
if (isHttpsUrl && IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyAllowEmptyChecksumsSecure), "true"))
70+
{
71+
cmdlet.WriteDebug("Download from HTTPS source with feature 'allowEmptyChecksumsSecure' enabled.");
72+
error = null;
73+
return true;
74+
}
75+
76+
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.");
77+
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.");
78+
79+
if (GetPSVersion().Major >= 4)
80+
{
81+
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.");
82+
}
83+
84+
if (IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPowerShellHost), "true")
85+
&& !(cmdlet.Host is null))
86+
{
87+
const string prompt = "Do you wish to allow the install to continue (not recommended)?";
88+
var info = string.Format(
89+
"The integrity of the file '{0}'{1} has not been verified by a checksum in the package scripts",
90+
GetFileName(path),
91+
string.IsNullOrWhiteSpace(url) ? string.Empty : $" from '{url}'");
92+
93+
var choices = new Collection<ChoiceDescription>
94+
{
95+
new ChoiceDescription("&Yes"),
96+
new ChoiceDescription("&No"),
97+
};
98+
99+
var selection = cmdlet.Host.UI.PromptForChoice(info, prompt, choices, defaultChoice: 1);
100+
101+
if (selection == 0)
102+
{
103+
error = null;
104+
return true;
105+
}
106+
}
107+
108+
var errorMessage = isHttpsUrl
109+
? "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)."
110+
: "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.";
111+
112+
error = new ChecksumMissingException(errorMessage);
113+
return false;
114+
}
115+
116+
if (!FileExists(cmdlet, path))
117+
{
118+
error = new FileNotFoundException($"Unable to checksum a file that doesn't exist - Could not find file '{path}'", path);
119+
return false;
120+
}
121+
122+
var checksumExe = CombinePaths(cmdlet, GetInstallLocation(cmdlet), "tools", "checksum.exe");
123+
if (!FileExists(cmdlet, checksumExe))
124+
{
125+
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);
126+
return false;
127+
}
128+
129+
cmdlet.WriteDebug($"checksum.exe found at '{checksumExe}'");
130+
var arguments = string.Format(
131+
"-c=\"{0}\" -t=\"{1}\" -f=\"{2}\"",
132+
checksum,
133+
checksumType.ToString().ToLower(),
134+
path);
135+
136+
cmdlet.WriteDebug($"Executing command ['{checksumExe}' {arguments}]");
137+
138+
var process = new Process
139+
{
140+
StartInfo = new ProcessStartInfo(checksumExe, arguments)
141+
{
142+
UseShellExecute = false,
143+
WindowStyle = ProcessWindowStyle.Hidden,
144+
},
145+
};
146+
147+
process.Start();
148+
process.WaitForExit();
149+
150+
var exitCode = process.ExitCode;
151+
process.Dispose();
152+
153+
cmdlet.WriteDebug($"Command ['{checksumExe}' {arguments}] exited with '{exitCode}'");
154+
155+
if (exitCode != 0)
156+
{
157+
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);
158+
return false;
159+
}
160+
161+
error = null;
162+
return true;
163+
}
164+
165+
/// <summary>
166+
/// Validate the checksum of a file against an expected <paramref name="checksum"/> and throw if the checksum does not match.
167+
/// </summary>
168+
/// <param name="cmdlet">The cmdlet calling the method.</param>
169+
/// <param name="path">The path to the file to verify the checksum for.</param>
170+
/// <param name="checksum">The expected checksum value.</param>
171+
/// <param name="checksumType">The type of the checksum to look for.</param>
172+
/// <param name="url">The url the file was downloaded from originally, if any.</param>
173+
public static void AssertChecksumValid(PSCmdlet cmdlet, string path, string checksum, ChecksumType? checksumType, string url)
174+
{
175+
if (!IsValid(cmdlet, path, checksum, checksumType, url, out var exception))
176+
{
177+
throw exception;
178+
}
179+
}
180+
}
181+
}

src/Chocolatey.PowerShell/Helpers/EnvironmentHelper.cs

+21-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ public static class EnvironmentHelper
2929
private const string MachineEnvironmentRegistryKeyName = @"SYSTEM\CurrentControlSet\Control\Session Manager\Environment\";
3030
private const string UserEnvironmentRegistryKeyName = "Environment";
3131

32+
/// <summary>
33+
/// Get an environment variable from the current process scope by name.
34+
/// </summary>
35+
/// <param name="name">The name of the variable to retrieve.</param>
36+
/// <returns>The value of the environment variable.</returns>
37+
public static string GetVariable(string name)
38+
{
39+
return Environment.GetEnvironmentVariable(name);
40+
}
41+
3242
/// <summary>
3343
/// Gets the value of the environment variable with the target <paramref name="name"/>, expanding environment names that may be present in the value.
3444
/// </summary>
@@ -53,7 +63,7 @@ public static string GetVariable(PSCmdlet cmdlet, string name, EnvironmentVariab
5363
{
5464
if (scope == EnvironmentVariableTarget.Process)
5565
{
56-
return Environment.GetEnvironmentVariable(name, scope);
66+
return GetVariable(name);
5767
}
5868

5969
var value = string.Empty;
@@ -128,6 +138,16 @@ public static string[] GetVariableNames(EnvironmentVariableTarget scope)
128138
}
129139
}
130140

141+
/// <summary>
142+
/// Sets the value of an environment variable for the current process only.
143+
/// </summary>
144+
/// <param name="name">The name of the environment variable to set.</param>
145+
/// <param name="value">The value to set the environment variable to.</param>
146+
public static void SetVariable(string name, string value)
147+
{
148+
Environment.SetEnvironmentVariable(name, value);
149+
}
150+
131151
/// <summary>
132152
/// Sets the value of an environment variable at the target <paramref name="scope"/>, and updates the current session environment.
133153
/// </summary>

0 commit comments

Comments
 (0)