Skip to content

Add authenticode validation to Install-PSResource #632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
59a6db9
Preliminary changes to publisher check
alerickson Feb 22, 2022
62b29a7
Add cataloginformation file
alerickson Apr 22, 2022
0ea564f
Add SkipPublisherCheck set to true for Update and Save
alerickson Apr 22, 2022
89ff82f
Add SkipPublisherCheck parameter to Install-PSResource
alerickson Apr 22, 2022
4352534
Add finished functionality for publisher check
alerickson Apr 22, 2022
0e2e8e9
Merge branch 'master' of https://github.com/PowerShell/PowerShellGet …
alerickson Apr 22, 2022
7c25cee
Fix changes made after merge conflict
alerickson Apr 22, 2022
d2021cb
Attempt to resolve diff issues shows in GitHub UI
alerickson Apr 22, 2022
814b548
Update exceptions in InstallHelper
alerickson Apr 22, 2022
ecd1006
Add publisher validation tests
alerickson Apr 22, 2022
78b25f3
Update tests
alerickson Apr 23, 2022
811360d
Update tests
alerickson Apr 25, 2022
0354781
Remove CatalogInformation calss
alerickson Apr 25, 2022
83ad396
Refactor publisher verification
alerickson Apr 25, 2022
3407a11
Refactor 'packagevalidation' code in installhelper
alerickson Apr 26, 2022
a1cb378
Update install-psresource tests
alerickson Apr 26, 2022
4a6e6db
Update parameter for Install-PSResource
alerickson Apr 26, 2022
a99d7cb
Update InstallHelper and InstallPSResource tests
alerickson Apr 27, 2022
b952add
Add Update-PSResource -SkipPackageValidation parameters and tests
alerickson Apr 27, 2022
30c1db9
Add parameter and tests for Save-PSResource
alerickson Apr 27, 2022
419c2cb
Change 'packagevalidation' to 'authenticodecheck'
alerickson May 3, 2022
3da66f8
Change parameter for call to InstallPackages to 'authenticodeCheck'
alerickson May 10, 2022
23fa53a
Update tests to reflect parameter name change
alerickson May 10, 2022
4875155
Run -AuthenticodeCheck tests on windows only
alerickson May 10, 2022
0800859
Update src/code/InstallPSResource.cs
alerickson May 23, 2022
9ef8bc0
Update src/code/UpdatePSResource.cs
alerickson May 23, 2022
e357a0f
Update src/code/InstallHelper.cs
alerickson May 23, 2022
eb0f5d1
Update src/code/SavePSResource.cs
alerickson May 23, 2022
b15ef89
Remove old comment
alerickson May 23, 2022
19c7d71
Merge branch 'publisherCheck' of https://github.com/alerickson/PowerS…
alerickson May 23, 2022
c78a374
Update src/code/InstallHelper.cs
alerickson May 24, 2022
a6a4730
Refactor authenticode methods, incorporate code review feedback
alerickson May 25, 2022
04c60de
Remove certificate checks
alerickson May 26, 2022
3b3a552
Revert some changes that got made
alerickson May 26, 2022
a346d02
Remove 'IsMicrosoftCert' and 'GetAuthenticodePublisher' methods
alerickson May 26, 2022
a72bb41
Merge branch 'master' of https://github.com/PowerShell/PowerShellGet …
alerickson May 26, 2022
f21455b
Merge branch 'master' of https://github.com/PowerShell/PowerShellGet …
alerickson Jun 1, 2022
2d9c4ac
Change reference to 'this' to _cmdletPassedIn
alerickson Jun 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,801 changes: 905 additions & 896 deletions src/code/InstallHelper.cs

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion src/code/InstallPSResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ class InstallPSResource : PSCmdlet
/// </summary>
[Parameter]
public SwitchParameter SkipDependencyCheck { get; set; }

/// <summary>
/// Check validation for signed and catalog files
/// </summary>
[Parameter]
public SwitchParameter AuthenticodeCheck { get; set; }

/// <summary>
/// Passes the resource installed to the console.
Expand Down Expand Up @@ -310,7 +316,7 @@ protected override void ProcessRecord()
{
requiredResourceFileStream = sr.ReadToEnd();
}

Hashtable pkgsInFile = null;
try
{
Expand Down Expand Up @@ -513,6 +519,7 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo
asNupkg: false,
includeXML: true,
skipDependencyCheck: SkipDependencyCheck,
authenticodeCheck: AuthenticodeCheck,
savePkg: false,
pathsToInstallPkg: _pathsToInstallPkg);

Expand Down
8 changes: 8 additions & 0 deletions src/code/SavePSResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ public string Path
[Parameter]
public SwitchParameter SkipDependencyCheck { get; set; }

/// <summary>
/// Check validation for signed and catalog files

/// </summary>
[Parameter]
public SwitchParameter AuthenticodeCheck { get; set; }

/// <summary>
/// Suppresses progress information.
/// </summary>
Expand Down Expand Up @@ -259,6 +266,7 @@ private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] p
asNupkg: AsNupkg,
includeXML: IncludeXML,
skipDependencyCheck: SkipDependencyCheck,
authenticodeCheck: AuthenticodeCheck,
savePkg: true,
pathsToInstallPkg: new List<string> { _path });

Expand Down
8 changes: 8 additions & 0 deletions src/code/UpdatePSResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ public sealed class UpdatePSResource : PSCmdlet
[Parameter]
public SwitchParameter SkipDependencyCheck { get; set; }

/// <summary>
/// Check validation for signed and catalog files

/// </summary>
[Parameter]
public SwitchParameter AuthenticodeCheck { get; set; }

#endregion

#region Override Methods
Expand Down Expand Up @@ -178,6 +185,7 @@ protected override void ProcessRecord()
asNupkg: false,
includeXML: true,
skipDependencyCheck: SkipDependencyCheck,
authenticodeCheck: AuthenticodeCheck,
savePkg: false,
pathsToInstallPkg: _pathsToInstallPkg);

Expand Down
116 changes: 114 additions & 2 deletions src/code/Utils.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Win32.SafeHandles;
using NuGet.Versioning;
using System;
using System.Collections;
Expand All @@ -14,6 +15,7 @@
using System.Management.Automation.Runspaces;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.PowerShell.PowerShellGet.UtilClasses
{
Expand Down Expand Up @@ -1126,7 +1128,117 @@ public static Collection<T> InvokeScriptWithHost<T>(
}

#endregion Methods
}

}

#endregion

#region AuthenticodeSignature

internal static class AuthenticodeSignature
{
#region Methods

internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List<string> pathsToSearch, string installPath, PSCmdlet cmdletPassedIn, out ErrorRecord errorRecord)
{
errorRecord = null;

// Because authenticode and catalog verifications are only applicable on Windows, we allow all packages by default to be installed on unix systems.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return true;
}

// Check that the catalog file is signed properly
string catalogFilePath = Path.Combine(tempDirNameVersion, pkgName + ".cat");
if (File.Exists(catalogFilePath))
{
// Run catalog validation
Collection<PSObject> TestFileCatalogResult = new Collection<PSObject>();
string moduleBasePath = tempDirNameVersion;
try
{
// By default "Test-FileCatalog will look through all files in the provided directory, -FilesToSkip allows us to ignore specific files
TestFileCatalogResult = cmdletPassedIn.InvokeCommand.InvokeScript(
script: @"param (
[string] $moduleBasePath,
[string] $catalogFilePath
)
$catalogValidation = Test-FileCatalog -Path $moduleBasePath -CatalogFilePath $CatalogFilePath `
-FilesToSkip '*.nupkg','*.nuspec', '*.nupkg.metadata', '*.nupkg.sha512' `
-Detailed -ErrorAction SilentlyContinue

if ($catalogValidation.Status.ToString() -eq 'valid' -and $catalogValidation.Signature.Status -eq 'valid') {
return $true
}
else {
return $false
}
",
useNewScope: true,
writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None,
input: null,
args: new object[] { moduleBasePath, catalogFilePath });
}
catch (Exception e)
{
errorRecord = new ErrorRecord(new ArgumentException(e.Message), "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn);
return false;
}

bool catalogValidation = (TestFileCatalogResult[0] != null) ? (bool)TestFileCatalogResult[0].BaseObject : false;
if (!catalogValidation)
{
var exMessage = String.Format("The catalog file '{0}' is invalid.", pkgName + ".cat");
var ex = new ArgumentException(exMessage);

errorRecord = new ErrorRecord(ex, "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn);
return false;
}
}

Collection<PSObject> authenticodeSignature = new Collection<PSObject>();
try
{
string[] listOfExtensions = { "*.ps1", "*.psd1", "*.psm1", "*.mof", "*.cat", "*.ps1xml" };
authenticodeSignature = cmdletPassedIn.InvokeCommand.InvokeScript(
script: @"param (
[string] $tempDirNameVersion,
[string[]] $listOfExtensions
)
Get-ChildItem $tempDirNameVersion -Recurse -Include $listOfExtensions | Get-AuthenticodeSignature -ErrorAction SilentlyContinue",
useNewScope: true,
writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None,
input: null,
args: new object[] { tempDirNameVersion, listOfExtensions });
}
catch (Exception e)
{
errorRecord = new ErrorRecord(new ArgumentException(e.Message), "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn);
return false;
}

// If the authenticode signature is not valid, return false
if (authenticodeSignature.Any() && authenticodeSignature[0] != null)
{
foreach (var sign in authenticodeSignature)
{
Signature signature = (Signature)sign.BaseObject;
if (!signature.Status.Equals(SignatureStatus.Valid))
{
var exMessage = String.Format("The signature for '{0}' is '{1}.", pkgName, signature.Status.ToString());
var ex = new ArgumentException(exMessage);
errorRecord = new ErrorRecord(ex, "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn);

return false;
}
}
}

return true;
}

#endregion
}

#endregion
}
53 changes: 52 additions & 1 deletion test/InstallPSResource.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ Describe 'Test Install-PSResource for Module' {
$testModuleName = "test_module"
$testModuleName2 = "TestModule99"
$testScriptName = "test_script"
$PackageManagement = "PackageManagement"
$RequiredResourceJSONFileName = "TestRequiredResourceFile.json"
$RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1"
Get-NewPSResourceRepositoryFile
Register-LocalRepos
}

AfterEach {
Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule","ClobberTestModule1", "ClobberTestModule2" -SkipDependencyCheck -ErrorAction SilentlyContinue
Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule","ClobberTestModule1", "ClobberTestModule2", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue
}

AfterAll {
Expand Down Expand Up @@ -415,6 +416,56 @@ Describe 'Test Install-PSResource for Module' {
$res3.Name | Should -Be $testModuleName2
$res3.Version | Should -Be "0.0.93.0"
}

# Install module 1.4.3 (is authenticode signed and has catalog file)
# Should install successfully
It "Install modules with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) {
Install-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository

$res1 = Get-PSResource $PackageManagement -Version "1.4.3"
$res1.Name | Should -Be $PackageManagement
$res1.Version | Should -Be "1.4.3.0"
}

# Install module 1.4.7 (is authenticode signed and has no catalog file)
# Should not install successfully
It "Install module with no catalog file" -Skip:(!(Get-IsWindows)) {
Install-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository

$res1 = Get-PSResource $PackageManagement -Version "1.4.7"
$res1.Name | Should -Be $PackageManagement
$res1.Version | Should -Be "1.4.7.0"
}

# Install module that is not authenticode signed
# Should FAIL to install the module
It "Install module that is not authenticode signed" -Skip:(!(Get-IsWindows)) {
Install-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource"
}
# Install 1.4.4.1 (with incorrect catalog file)
# Should FAIL to install the module
It "Install module with incorrect catalog file" -Skip:(!(Get-IsWindows)) {
Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource"
}

# Install script that is signed
# Should install successfully
It "Install script that is authenticode signed" -Skip:(!(Get-IsWindows)) {
Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository

$res1 = Get-PSResource "Install-VSCode" -Version "1.4.2"
$res1.Name | Should -Be "Install-VSCode"
$res1.Version | Should -Be "1.4.2.0"
}

# Install script that is not signed
# Should throw
It "Install script that is not signed" -Skip:(!(Get-IsWindows)) {
Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource"
}
}

<# Temporarily commented until -Tag is implemented for this Describe block
Expand Down
56 changes: 56 additions & 0 deletions test/SavePSResource.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Describe 'Test Save-PSResource for PSResources' {
$testModuleName = "test_module"
$testScriptName = "test_script"
$testModuleName2 = "testmodule99"
$PackageManagement = "PackageManagement"
Get-NewPSResourceRepositoryFile
Register-LocalRepos

Expand Down Expand Up @@ -217,6 +218,61 @@ Describe 'Test Save-PSResource for PSResources' {
$res.Name | Should -Be $testModuleName
$res.Version | Should -Be "1.0.0.0"
}

# Save module 1.4.3 (is authenticode signed and has catalog file)
# Should save successfully
It "Save modules with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) {
Save-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir

$pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $PackageManagement
$pkgDir | Should -Not -BeNullOrEmpty
$pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName
$pkgDirVersion.Name | Should -Be "1.4.3"
}

# Save module 1.4.7 (is authenticode signed and has NO catalog file)
# Should save successfully
It "Save module with no catalog file" -Skip:(!(Get-IsWindows)) {
Save-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir

$pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $PackageManagement
$pkgDir | Should -Not -BeNullOrEmpty
$pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName
$pkgDirVersion.Name | Should -Be "1.4.7"
}

# Save module that is not authenticode signed
# Should FAIL to save the module
It "Save module that is not authenticode signed" -Skip:(!(Get-IsWindows)) {
Save-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource"
}

# Save 1.4.4.1 (with incorrect catalog file)
# Should FAIL to save the module
It "Save module with incorrect catalog file" -Skip:(!(Get-IsWindows)) {
Save-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource"
}

# Save script that is signed
# Should save successfully
It "Save script that is authenticode signed" -Skip:(!(Get-IsWindows)) {
Save-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir

$pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "Install-VSCode.ps1"
$pkgDir | Should -Not -BeNullOrEmpty
$pkgName = Get-ChildItem -Path $pkgDir.FullName
$pkgName.Name | Should -Be "Install-VSCode.ps1"
}

# Save script that is not signed
# Should throw
It "Save script that is not signed" -Skip:(!(Get-IsWindows)) {
Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue
$Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource"
}

<#
# Tests should not write to module directory
It "Save specific module resource by name if no -Path param is specifed" {
Expand Down
Loading