Skip to content

Commit 8d0db7f

Browse files
authored
Add authenticode validation to Install-PSResource (#632)
1 parent 98c08e6 commit 8d0db7f

File tree

8 files changed

+1202
-901
lines changed

8 files changed

+1202
-901
lines changed

src/code/InstallHelper.cs

Lines changed: 905 additions & 896 deletions
Large diffs are not rendered by default.

src/code/InstallPSResource.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ class InstallPSResource : PSCmdlet
103103
/// </summary>
104104
[Parameter]
105105
public SwitchParameter SkipDependencyCheck { get; set; }
106+
107+
/// <summary>
108+
/// Check validation for signed and catalog files
109+
/// </summary>
110+
[Parameter]
111+
public SwitchParameter AuthenticodeCheck { get; set; }
106112

107113
/// <summary>
108114
/// Passes the resource installed to the console.
@@ -310,7 +316,7 @@ protected override void ProcessRecord()
310316
{
311317
requiredResourceFileStream = sr.ReadToEnd();
312318
}
313-
319+
314320
Hashtable pkgsInFile = null;
315321
try
316322
{
@@ -513,6 +519,7 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo
513519
asNupkg: false,
514520
includeXML: true,
515521
skipDependencyCheck: SkipDependencyCheck,
522+
authenticodeCheck: AuthenticodeCheck,
516523
savePkg: false,
517524
pathsToInstallPkg: _pathsToInstallPkg);
518525

src/code/SavePSResource.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ public string Path
131131
[Parameter]
132132
public SwitchParameter SkipDependencyCheck { get; set; }
133133

134+
/// <summary>
135+
/// Check validation for signed and catalog files
136+
137+
/// </summary>
138+
[Parameter]
139+
public SwitchParameter AuthenticodeCheck { get; set; }
140+
134141
/// <summary>
135142
/// Suppresses progress information.
136143
/// </summary>
@@ -259,6 +266,7 @@ private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] p
259266
asNupkg: AsNupkg,
260267
includeXML: IncludeXML,
261268
skipDependencyCheck: SkipDependencyCheck,
269+
authenticodeCheck: AuthenticodeCheck,
262270
savePkg: true,
263271
pathsToInstallPkg: new List<string> { _path });
264272

src/code/UpdatePSResource.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ public sealed class UpdatePSResource : PSCmdlet
111111
[Parameter]
112112
public SwitchParameter SkipDependencyCheck { get; set; }
113113

114+
/// <summary>
115+
/// Check validation for signed and catalog files
116+
117+
/// </summary>
118+
[Parameter]
119+
public SwitchParameter AuthenticodeCheck { get; set; }
120+
114121
#endregion
115122

116123
#region Override Methods
@@ -178,6 +185,7 @@ protected override void ProcessRecord()
178185
asNupkg: false,
179186
includeXML: true,
180187
skipDependencyCheck: SkipDependencyCheck,
188+
authenticodeCheck: AuthenticodeCheck,
181189
savePkg: false,
182190
pathsToInstallPkg: _pathsToInstallPkg);
183191

src/code/Utils.cs

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using Microsoft.Win32.SafeHandles;
45
using NuGet.Versioning;
56
using System;
67
using System.Collections;
@@ -14,6 +15,7 @@
1415
using System.Management.Automation.Runspaces;
1516
using System.Runtime.InteropServices;
1617
using System.Security;
18+
using System.Security.Cryptography.X509Certificates;
1719

1820
namespace Microsoft.PowerShell.PowerShellGet.UtilClasses
1921
{
@@ -1126,7 +1128,117 @@ public static Collection<T> InvokeScriptWithHost<T>(
11261128
}
11271129

11281130
#endregion Methods
1129-
}
1130-
1131+
}
1132+
1133+
#endregion
1134+
1135+
#region AuthenticodeSignature
1136+
1137+
internal static class AuthenticodeSignature
1138+
{
1139+
#region Methods
1140+
1141+
internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List<string> pathsToSearch, string installPath, PSCmdlet cmdletPassedIn, out ErrorRecord errorRecord)
1142+
{
1143+
errorRecord = null;
1144+
1145+
// Because authenticode and catalog verifications are only applicable on Windows, we allow all packages by default to be installed on unix systems.
1146+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
1147+
{
1148+
return true;
1149+
}
1150+
1151+
// Check that the catalog file is signed properly
1152+
string catalogFilePath = Path.Combine(tempDirNameVersion, pkgName + ".cat");
1153+
if (File.Exists(catalogFilePath))
1154+
{
1155+
// Run catalog validation
1156+
Collection<PSObject> TestFileCatalogResult = new Collection<PSObject>();
1157+
string moduleBasePath = tempDirNameVersion;
1158+
try
1159+
{
1160+
// By default "Test-FileCatalog will look through all files in the provided directory, -FilesToSkip allows us to ignore specific files
1161+
TestFileCatalogResult = cmdletPassedIn.InvokeCommand.InvokeScript(
1162+
script: @"param (
1163+
[string] $moduleBasePath,
1164+
[string] $catalogFilePath
1165+
)
1166+
$catalogValidation = Test-FileCatalog -Path $moduleBasePath -CatalogFilePath $CatalogFilePath `
1167+
-FilesToSkip '*.nupkg','*.nuspec', '*.nupkg.metadata', '*.nupkg.sha512' `
1168+
-Detailed -ErrorAction SilentlyContinue
1169+
1170+
if ($catalogValidation.Status.ToString() -eq 'valid' -and $catalogValidation.Signature.Status -eq 'valid') {
1171+
return $true
1172+
}
1173+
else {
1174+
return $false
1175+
}
1176+
",
1177+
useNewScope: true,
1178+
writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None,
1179+
input: null,
1180+
args: new object[] { moduleBasePath, catalogFilePath });
1181+
}
1182+
catch (Exception e)
1183+
{
1184+
errorRecord = new ErrorRecord(new ArgumentException(e.Message), "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn);
1185+
return false;
1186+
}
1187+
1188+
bool catalogValidation = (TestFileCatalogResult[0] != null) ? (bool)TestFileCatalogResult[0].BaseObject : false;
1189+
if (!catalogValidation)
1190+
{
1191+
var exMessage = String.Format("The catalog file '{0}' is invalid.", pkgName + ".cat");
1192+
var ex = new ArgumentException(exMessage);
1193+
1194+
errorRecord = new ErrorRecord(ex, "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn);
1195+
return false;
1196+
}
1197+
}
1198+
1199+
Collection<PSObject> authenticodeSignature = new Collection<PSObject>();
1200+
try
1201+
{
1202+
string[] listOfExtensions = { "*.ps1", "*.psd1", "*.psm1", "*.mof", "*.cat", "*.ps1xml" };
1203+
authenticodeSignature = cmdletPassedIn.InvokeCommand.InvokeScript(
1204+
script: @"param (
1205+
[string] $tempDirNameVersion,
1206+
[string[]] $listOfExtensions
1207+
)
1208+
Get-ChildItem $tempDirNameVersion -Recurse -Include $listOfExtensions | Get-AuthenticodeSignature -ErrorAction SilentlyContinue",
1209+
useNewScope: true,
1210+
writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None,
1211+
input: null,
1212+
args: new object[] { tempDirNameVersion, listOfExtensions });
1213+
}
1214+
catch (Exception e)
1215+
{
1216+
errorRecord = new ErrorRecord(new ArgumentException(e.Message), "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn);
1217+
return false;
1218+
}
1219+
1220+
// If the authenticode signature is not valid, return false
1221+
if (authenticodeSignature.Any() && authenticodeSignature[0] != null)
1222+
{
1223+
foreach (var sign in authenticodeSignature)
1224+
{
1225+
Signature signature = (Signature)sign.BaseObject;
1226+
if (!signature.Status.Equals(SignatureStatus.Valid))
1227+
{
1228+
var exMessage = String.Format("The signature for '{0}' is '{1}.", pkgName, signature.Status.ToString());
1229+
var ex = new ArgumentException(exMessage);
1230+
errorRecord = new ErrorRecord(ex, "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn);
1231+
1232+
return false;
1233+
}
1234+
}
1235+
}
1236+
1237+
return true;
1238+
}
1239+
1240+
#endregion
1241+
}
1242+
11311243
#endregion
11321244
}

test/InstallPSResource.Tests.ps1

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ Describe 'Test Install-PSResource for Module' {
1212
$testModuleName = "test_module"
1313
$testModuleName2 = "TestModule99"
1414
$testScriptName = "test_script"
15+
$PackageManagement = "PackageManagement"
1516
$RequiredResourceJSONFileName = "TestRequiredResourceFile.json"
1617
$RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1"
1718
Get-NewPSResourceRepositoryFile
1819
Register-LocalRepos
1920
}
2021

2122
AfterEach {
22-
Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule","ClobberTestModule1", "ClobberTestModule2" -SkipDependencyCheck -ErrorAction SilentlyContinue
23+
Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule","ClobberTestModule1", "ClobberTestModule2", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue
2324
}
2425

2526
AfterAll {
@@ -415,6 +416,56 @@ Describe 'Test Install-PSResource for Module' {
415416
$res3.Name | Should -Be $testModuleName2
416417
$res3.Version | Should -Be "0.0.93.0"
417418
}
419+
420+
# Install module 1.4.3 (is authenticode signed and has catalog file)
421+
# Should install successfully
422+
It "Install modules with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) {
423+
Install-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository
424+
425+
$res1 = Get-PSResource $PackageManagement -Version "1.4.3"
426+
$res1.Name | Should -Be $PackageManagement
427+
$res1.Version | Should -Be "1.4.3.0"
428+
}
429+
430+
# Install module 1.4.7 (is authenticode signed and has no catalog file)
431+
# Should not install successfully
432+
It "Install module with no catalog file" -Skip:(!(Get-IsWindows)) {
433+
Install-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository
434+
435+
$res1 = Get-PSResource $PackageManagement -Version "1.4.7"
436+
$res1.Name | Should -Be $PackageManagement
437+
$res1.Version | Should -Be "1.4.7.0"
438+
}
439+
440+
# Install module that is not authenticode signed
441+
# Should FAIL to install the module
442+
It "Install module that is not authenticode signed" -Skip:(!(Get-IsWindows)) {
443+
Install-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
444+
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource"
445+
}
446+
# Install 1.4.4.1 (with incorrect catalog file)
447+
# Should FAIL to install the module
448+
It "Install module with incorrect catalog file" -Skip:(!(Get-IsWindows)) {
449+
Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
450+
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource"
451+
}
452+
453+
# Install script that is signed
454+
# Should install successfully
455+
It "Install script that is authenticode signed" -Skip:(!(Get-IsWindows)) {
456+
Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository
457+
458+
$res1 = Get-PSResource "Install-VSCode" -Version "1.4.2"
459+
$res1.Name | Should -Be "Install-VSCode"
460+
$res1.Version | Should -Be "1.4.2.0"
461+
}
462+
463+
# Install script that is not signed
464+
# Should throw
465+
It "Install script that is not signed" -Skip:(!(Get-IsWindows)) {
466+
Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
467+
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource"
468+
}
418469
}
419470

420471
<# Temporarily commented until -Tag is implemented for this Describe block

test/SavePSResource.Tests.ps1

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Describe 'Test Save-PSResource for PSResources' {
1212
$testModuleName = "test_module"
1313
$testScriptName = "test_script"
1414
$testModuleName2 = "testmodule99"
15+
$PackageManagement = "PackageManagement"
1516
Get-NewPSResourceRepositoryFile
1617
Register-LocalRepos
1718

@@ -217,6 +218,61 @@ Describe 'Test Save-PSResource for PSResources' {
217218
$res.Name | Should -Be $testModuleName
218219
$res.Version | Should -Be "1.0.0.0"
219220
}
221+
222+
# Save module 1.4.3 (is authenticode signed and has catalog file)
223+
# Should save successfully
224+
It "Save modules with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) {
225+
Save-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir
226+
227+
$pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $PackageManagement
228+
$pkgDir | Should -Not -BeNullOrEmpty
229+
$pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName
230+
$pkgDirVersion.Name | Should -Be "1.4.3"
231+
}
232+
233+
# Save module 1.4.7 (is authenticode signed and has NO catalog file)
234+
# Should save successfully
235+
It "Save module with no catalog file" -Skip:(!(Get-IsWindows)) {
236+
Save-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir
237+
238+
$pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $PackageManagement
239+
$pkgDir | Should -Not -BeNullOrEmpty
240+
$pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName
241+
$pkgDirVersion.Name | Should -Be "1.4.7"
242+
}
243+
244+
# Save module that is not authenticode signed
245+
# Should FAIL to save the module
246+
It "Save module that is not authenticode signed" -Skip:(!(Get-IsWindows)) {
247+
Save-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue
248+
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource"
249+
}
250+
251+
# Save 1.4.4.1 (with incorrect catalog file)
252+
# Should FAIL to save the module
253+
It "Save module with incorrect catalog file" -Skip:(!(Get-IsWindows)) {
254+
Save-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue
255+
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource"
256+
}
257+
258+
# Save script that is signed
259+
# Should save successfully
260+
It "Save script that is authenticode signed" -Skip:(!(Get-IsWindows)) {
261+
Save-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir
262+
263+
$pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "Install-VSCode.ps1"
264+
$pkgDir | Should -Not -BeNullOrEmpty
265+
$pkgName = Get-ChildItem -Path $pkgDir.FullName
266+
$pkgName.Name | Should -Be "Install-VSCode.ps1"
267+
}
268+
269+
# Save script that is not signed
270+
# Should throw
271+
It "Save script that is not signed" -Skip:(!(Get-IsWindows)) {
272+
Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
273+
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource"
274+
}
275+
220276
<#
221277
# Tests should not write to module directory
222278
It "Save specific module resource by name if no -Path param is specifed" {

0 commit comments

Comments
 (0)