From 59a6db97848fdc11abc22cc864a0f7e0dca99c24 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 21 Feb 2022 23:55:48 -0800 Subject: [PATCH 01/34] Preliminary changes to publisher check --- src/code/InstallHelper.cs | 104 ++++++++++++++++++++++++++++++++++ src/code/InstallPSResource.cs | 4 +- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 8d75ae518..b5362f344 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -22,6 +22,9 @@ using System.Text.RegularExpressions; using System.Threading; +using Microsoft.PowerShell.Commands; +using System.Collections.ObjectModel; + namespace Microsoft.PowerShell.PowerShellGet.Cmdlets { /// @@ -449,6 +452,12 @@ private List InstallPackage( _cmdletPassedIn.WriteVerbose(string.Format("Successfully able to download package from source to: '{0}'", tempInstallPath)); + // Run authenticode validation + if (!SkipPublisherCheck && !PublisherValidation(new string[]{ pkg.Name }, _versionRange, _pathsToSearch)) + { + + } + // pkgIdentity.Version.Version gets the version without metadata or release labels. string newVersion = pkgIdentity.Version.ToNormalizedString(); string normalizedVersionNoPrerelease = newVersion; @@ -586,6 +595,101 @@ private List InstallPackage( return pkgsSuccessfullyInstalled; } + private bool PublisherValidation(String[] pkgName, VersionRange versionRange, List pathsToSearch) + { + // 1) See if the current module that is trying to be installed is already installed + // call Get-PSResource + GetHelper getHelper = new GetHelper(this); + var pkgVersionsAlreadyInstalled = getHelper.GetPackagesFromPath(pkgName, versionRange, pathsToSearch, _prerelease); + + + + // 2) If the module is already installed (an earlier version of the module, or same version being reinstalled, get the authenticode signature + Collection authenticodeSignature = new Collection(); + try + { + authenticodeSignature = this.InvokeCommand.InvokeScript( + script: $"param ([string] $signedFilePath) Get-AuthenticodeSignature -FilePaath $signedFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { signedFilePath }); + } + catch { } + + Signature signature = (authenticodeSignature.Any() 0 && authenticodeSignature[0] != null) ? (Signature)authenticodeSignature[0].BaseObject : null; + + if (signature == null) + { + return false; + } + + // 2b) If you're able to get the authenticode signature, get the module details + + + + // 3) Validate the catalog signature for the current module being installed + Collection catalogAuthenticodeSignature = new Collection(); + try + { + catalogAuthenticodeSignature = this.InvokeCommand.InvokeScript( + script: $"param ([string] $catalogFilePath) Get-AuthenticodeSignature -FilePaath $catalogFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { catalogFilePath }); + } + catch { } + + Signature catalogSignature = (catalogAuthenticodeSignature.Any() && catalogAuthenticodeSignature[0] != null) ? (Signature)catalogAuthenticodeSignature[0].BaseObject : null; + + if (catalogSignature == null || !catalogSignature.Status.Equals(SignatureStatus.Valid)) + { + return false; + } + + // Run catalog validation + //Collection TestFileCatalogResult = new Collection(); + try + { + var TestFileCatalogResult = this.InvokeCommand.InvokeScript( + script: $"param ([string] $catalogFilePath) Test-FileCatalog -Path $moduleBasePath" + + $" -CatalogFilePath $CatalogFilePath" + + $" -FilesToSkip $script: PSGetItemInfoFileName,'*.cat','*.nupkg','*.nuspec'" + + $" -Detailed" + + $" -ErrorAction SilentlyContinue", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { catalogFilePath }); + + var catalogValidation = (TestFileCatalogResult.Any() && TestFileCatalogResult[0] != null) ? (Signature)TestFileCatalogResult[0].BaseObject : null; + + } + catch { } + + //catalogValidation = (TestFileCatalogResult.Any() && TestFileCatalogResult[0] != null) ? (CatalogInformation)TestFileCatalogResult[0].BaseObject : null; + ` + if (catalogValidation == null || !catalogValidation.Status.Equals(SignatureStatus.Valid) || !catalogValidation.Signature.Status.Equals(SignatureStatus.Valid)) + { + return false; + } ` + + + + + // 5) if there is an installed module, and we have the info for the current module, + // test these scenarios: + // $InstalledModuleAuthenticodePublisher == $InstalledModuleDetails.Publisher + // $InstalledModuleRootCA = $InstalledModuleDetails.RootCertificateAuthority + // ???? $IsInstalledModuleSignedByMicrosoft = $InstalledModuleDetails.IsMicrosoftCertificate + // $InstalledModuleVersion = $InstalledModuleDetails.Version + + + + return true; + } + private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string tempInstallPath, string newVersion) { var requireLicenseAcceptance = false; diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 687d89a7c..1d4b800bd 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -267,8 +267,8 @@ protected override void ProcessRecord() using (StreamReader sr = new StreamReader(_requiredResourceFile)) { requiredResourceFileStream = sr.ReadToEnd(); - } - + } + Hashtable pkgsInJsonFile = null; try { From 62b29a7bc668f8b392c87ab856f9ec3f25187e29 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 21 Apr 2022 17:55:17 -0700 Subject: [PATCH 02/34] Add cataloginformation file --- src/code/CatalogInformation.cs | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/code/CatalogInformation.cs diff --git a/src/code/CatalogInformation.cs b/src/code/CatalogInformation.cs new file mode 100644 index 000000000..cbe9f5f75 --- /dev/null +++ b/src/code/CatalogInformation.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; + + +namespace Microsoft.PowerShell.PowerShellGet.UtilClasses +{ + #region Enums + + public enum CatalogValidationStatus + { + Valid, + ValidationFailed + } + + #endregion + + #region CatalogInformation + + public sealed class CatalogInformation + { + #region Properties + + public CatalogValidationStatus Status { get; } + public Signature Signature { get; } + public string HashAlgorithm { get; } + public Dictionary CatalogItems { get; } + public Dictionary PathItems { get; } + + #endregion + + #region Constructors + + private CatalogInformation() { } + + private CatalogInformation( + CatalogValidationStatus status, + Signature signature, + string hashAlgorithm, + Dictionary catalogItems, + Dictionary pathItems) + { + Status = status; + Signature = signature; + HashAlgorithm = hashAlgorithm ?? string.Empty; + CatalogItems = catalogItems ?? new Dictionary(); + pathItems = pathItems ?? new Dictionary(); + } + + #endregion + } + + #endregion +} From 0ea564f27fd005925f31dc1c87e42eccc5eac986 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 21 Apr 2022 17:59:58 -0700 Subject: [PATCH 03/34] Add SkipPublisherCheck set to true for Update and Save --- src/code/SavePSResource.cs | 1 + src/code/UpdatePSResource.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index a7e17e3ab..5791edba4 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -259,6 +259,7 @@ private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] p asNupkg: AsNupkg, includeXML: IncludeXML, skipDependencyCheck: SkipDependencyCheck, + skipPublisherCheck: true, savePkg: true, pathsToInstallPkg: new List { _path }); diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index 9913b4468..7839d48d2 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -177,6 +177,7 @@ protected override void ProcessRecord() asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, + skipPublisherCheck: true, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); From 89ff82f61f2a8da64f3c6b0021debf6ad12c5864 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 21 Apr 2022 18:00:32 -0700 Subject: [PATCH 04/34] Add SkipPublisherCheck parameter to Install-PSResource --- src/code/InstallPSResource.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 1d4b800bd..98c1b9c8a 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -103,6 +103,13 @@ class InstallPSResource : PSCmdlet /// [Parameter] public SwitchParameter SkipDependencyCheck { get; set; } + + /// + /// Skips the check for resource dependencies, so that only found resources are installed, + /// and not any resources the found resource depends on. + /// + [Parameter] + public SwitchParameter SkipPublisherCheck { get; set; } /// /// Passes the resource installed to the console. @@ -267,8 +274,8 @@ protected override void ProcessRecord() using (StreamReader sr = new StreamReader(_requiredResourceFile)) { requiredResourceFileStream = sr.ReadToEnd(); - } - + } + Hashtable pkgsInJsonFile = null; try { @@ -459,6 +466,7 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, + skipPublisherCheck: SkipPublisherCheck, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); From 4352534676becf4f7a5d7654a439d20ae33ffcce Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 21 Apr 2022 18:18:16 -0700 Subject: [PATCH 05/34] Add finished functionality for publisher check --- src/code/InstallHelper.cs | 2143 +++++++++++++++++++++---------------- 1 file changed, 1194 insertions(+), 949 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index b5362f344..5fd89828d 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -1,623 +1,697 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.PowerShell.PowerShellGet.UtilClasses; -using MoreLinq.Extensions; -using NuGet.Common; -using NuGet.Configuration; -using NuGet.Packaging; -using NuGet.Packaging.Core; -using NuGet.Packaging.PackageExtraction; -using NuGet.Protocol; -using NuGet.Protocol.Core.Types; -using NuGet.Versioning; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Net; -using System.Text.RegularExpressions; -using System.Threading; - -using Microsoft.PowerShell.Commands; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.Commands; +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using Microsoft.Win32.SafeHandles; +using MoreLinq.Extensions; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Packaging.PackageExtraction; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; +using System; +using System.Collections; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using System.Threading; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + /// + /// Install helper class + /// + internal class InstallHelper : PSCmdlet + { + #region Members + + private const string MsgRepositoryNotTrusted = "Untrusted repository"; + private const string MsgInstallUntrustedPackage = "You are installing the modules from an untrusted repository. If you trust this repository, change its Trusted value by running the Set-PSResourceRepository cmdlet. Are you sure you want to install the PSresource from '{0}' ?"; + private readonly string[] certStoreLocations = { "cert:\\LocalMachine\\Root", "cert:\\LocalMachine\\AuthRoot", "cert:\\CurrentUser\\Root", "cert:\\CurrentUser\\AuthRoot" }; + + private CancellationToken _cancellationToken; + private readonly PSCmdlet _cmdletPassedIn; + private List _pathsToInstallPkg; + private VersionRange _versionRange; + private bool _prerelease; + private bool _acceptLicense; + private bool _quiet; + private bool _reinstall; + private bool _force; + private bool _trustRepository; + private PSCredential _credential; + private bool _asNupkg; + private bool _includeXML; + private bool _noClobber; + private bool _skipPublisherCheck; + private bool _savePkg; + List _pathsToSearch; + List _pkgNamesToInstall; + + #endregion + + + #region Enums + + public struct CERT_CHAIN_POLICY_PARA + { + public CERT_CHAIN_POLICY_PARA(int size) + { + cbSize = (uint)size; + dwFlags = 0; + pvExtraPolicyPara = IntPtr.Zero; + } + public uint cbSize; + public uint dwFlags; + public IntPtr pvExtraPolicyPara; + } + + public struct CERT_CHAIN_POLICY_STATUS + { + public CERT_CHAIN_POLICY_STATUS(int size) + { + cbSize = (uint)size; + dwError = 0; + lChainIndex = IntPtr.Zero; + lElementIndex = IntPtr.Zero; + pvExtraPolicyStatus = IntPtr.Zero; + } + public uint cbSize; + public uint dwError; + public IntPtr lChainIndex; + public IntPtr lElementIndex; + public IntPtr pvExtraPolicyStatus; + } + + #endregion + + + #region Public methods + + public InstallHelper(PSCmdlet cmdletPassedIn) + { + CancellationTokenSource source = new CancellationTokenSource(); + _cancellationToken = source.Token; + _cmdletPassedIn = cmdletPassedIn; + } + + public List InstallPackages( + string[] names, + VersionRange versionRange, + bool prerelease, + string[] repository, + bool acceptLicense, + bool quiet, + bool reinstall, + bool force, + bool trustRepository, + bool noClobber, + PSCredential credential, + bool asNupkg, + bool includeXML, + bool skipDependencyCheck, + bool skipPublisherCheck, + bool savePkg, + List pathsToInstallPkg) + { + _cmdletPassedIn.WriteVerbose(string.Format("Parameters passed in >>> Name: '{0}'; Version: '{1}'; Prerelease: '{2}'; Repository: '{3}'; " + + "AcceptLicense: '{4}'; Quiet: '{5}'; Reinstall: '{6}'; TrustRepository: '{7}'; NoClobber: '{8}'; AsNupkg: '{9}'; IncludeXML '{10}'; SavePackage '{11}'", + string.Join(",", names), + versionRange != null ? (versionRange.OriginalString != null ? versionRange.OriginalString : string.Empty) : string.Empty, + prerelease.ToString(), + repository != null ? string.Join(",", repository) : string.Empty, + acceptLicense.ToString(), + quiet.ToString(), + reinstall.ToString(), + trustRepository.ToString(), + noClobber.ToString(), + asNupkg.ToString(), + includeXML.ToString(), + savePkg.ToString())); + + _versionRange = versionRange; + _prerelease = prerelease; + _acceptLicense = acceptLicense || force; + _skipPublisherCheck = skipPublisherCheck || force; + _quiet = quiet; + _reinstall = reinstall; + _force = force; + _trustRepository = trustRepository || force; + _noClobber = noClobber; + _credential = credential; + _asNupkg = asNupkg; + _includeXML = includeXML; + _savePkg = savePkg; + _pathsToInstallPkg = pathsToInstallPkg; + + // Create list of installation paths to search. + _pathsToSearch = new List(); + _pkgNamesToInstall = names.ToList(); + + // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) + // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations + // e.g.: + // ./InstallPackagePath1/PackageA + // ./InstallPackagePath1/PackageB + // ./InstallPackagePath2/PackageC + // ./InstallPackagePath3/PackageD + foreach (var path in _pathsToInstallPkg) + { + _pathsToSearch.AddRange(Utils.GetSubDirectories(path)); + } + + // Go through the repositories and see which is the first repository to have the pkg version available + return ProcessRepositories( + repository: repository, + trustRepository: _trustRepository, + credential: _credential, + skipDependencyCheck: skipDependencyCheck); + } + + #endregion + + #region Private methods + + // This method calls iterates through repositories (by priority order) to search for the pkgs to install + private List ProcessRepositories( + string[] repository, + bool trustRepository, + PSCredential credential, + bool skipDependencyCheck) + { + var listOfRepositories = RepositorySettings.Read(repository, out string[] _); + var yesToAll = false; + var noToAll = false; + + var findHelper = new FindHelper(_cancellationToken, _cmdletPassedIn); + List allPkgsInstalled = new List(); + + foreach (var repo in listOfRepositories) + { + // If no more packages to install, then return + if (!_pkgNamesToInstall.Any()) return allPkgsInstalled; + + string repoName = repo.Name; + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to search for packages in '{0}'", repoName)); + + // Source is only trusted if it's set at the repository level to be trusted, -TrustRepository flag is true, -Force flag is true + // OR the user issues trust interactively via console. + var sourceTrusted = true; + if (repo.Trusted == false && !trustRepository && !_force) + { + _cmdletPassedIn.WriteVerbose("Checking if untrusted repository should be used"); + + if (!(yesToAll || noToAll)) + { + // Prompt for installation of package from untrusted repository + var message = string.Format(CultureInfo.InvariantCulture, MsgInstallUntrustedPackage, repoName); + sourceTrusted = _cmdletPassedIn.ShouldContinue(message, MsgRepositoryNotTrusted, true, ref yesToAll, ref noToAll); + } + } + + if (!sourceTrusted && !yesToAll) + { + continue; + } + + _cmdletPassedIn.WriteVerbose("Untrusted repository accepted as trusted source."); + + // If it can't find the pkg in one repository, it'll look for it in the next repo in the list + var isLocalRepo = repo.Uri.AbsoluteUri.StartsWith(Uri.UriSchemeFile + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase); + + // Finds parent packages and dependencies + IEnumerable pkgsFromRepoToInstall = findHelper.FindByResourceName( + name: _pkgNamesToInstall.ToArray(), + type: ResourceType.None, + version: _versionRange != null ? _versionRange.OriginalString : null, + prerelease: _prerelease, + tag: null, + repository: new string[] { repoName }, + credential: credential, + includeDependencies: !skipDependencyCheck); + + if (!pkgsFromRepoToInstall.Any()) + { + _cmdletPassedIn.WriteVerbose(string.Format("None of the specified resources were found in the '{0}' repository.", repoName)); + // Check in the next repository + continue; + } + + // Select the first package from each name group, which is guaranteed to be the latest version. + // We should only have one version returned for each package name + // e.g.: + // PackageA (version 1.0) + // PackageB (version 2.0) + // PackageC (version 1.0) + pkgsFromRepoToInstall = pkgsFromRepoToInstall.GroupBy( + m => new { m.Name }).Select( + group => group.First()).ToList(); + + // Check to see if the pkgs (including dependencies) are already installed (ie the pkg is installed and the version satisfies the version range provided via param) + if (!_reinstall) + { + pkgsFromRepoToInstall = FilterByInstalledPkgs(pkgsFromRepoToInstall); + } + + if (!pkgsFromRepoToInstall.Any()) + { + continue; + } + + List pkgsInstalled = InstallPackage( + pkgsFromRepoToInstall, + repoName, + repo.Uri.AbsoluteUri, + repo.CredentialInfo, + credential, + isLocalRepo); + + foreach (PSResourceInfo pkg in pkgsInstalled) + { + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + } + + allPkgsInstalled.AddRange(pkgsInstalled); + } + + // At this only package names left were those which could not be found in registered repositories + foreach (string pkgName in _pkgNamesToInstall) + { + var message = String.Format("Package '{0}' with requested version range {1} could not be installed as it was not found in any registered repositories", + pkgName, + _versionRange.ToString()); + var ex = new ArgumentException(message); + var ResourceNotFoundError = new ErrorRecord(ex, "ResourceNotFoundError", ErrorCategory.ObjectNotFound, null); + _cmdletPassedIn.WriteError(ResourceNotFoundError); + } + + return allPkgsInstalled; + } + + // Check if any of the pkg versions are already installed, if they are we'll remove them from the list of packages to install + private IEnumerable FilterByInstalledPkgs(IEnumerable packages) + { + // Create list of installation paths to search. + List _pathsToSearch = new List(); + // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) + // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations + // e.g.: + // ./InstallPackagePath1/PackageA + // ./InstallPackagePath1/PackageB + // ./InstallPackagePath2/PackageC + // ./InstallPackagePath3/PackageD + foreach (var path in _pathsToInstallPkg) + { + _pathsToSearch.AddRange(Utils.GetSubDirectories(path)); + } + + var filteredPackages = new Dictionary(); + foreach (var pkg in packages) + { + filteredPackages.Add(pkg.Name, pkg); + } + + GetHelper getHelper = new GetHelper(_cmdletPassedIn); + // Get currently installed packages. + // selectPrereleaseOnly is false because even if Prerelease is true we want to include both stable and prerelease, never select prerelease only. + IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( + name: filteredPackages.Keys.ToArray(), + versionRange: _versionRange, + pathsToSearch: _pathsToSearch, + selectPrereleaseOnly: false); + if (!pkgsAlreadyInstalled.Any()) + { + return packages; + } + + // Remove from list package versions that are already installed. + foreach (PSResourceInfo pkg in pkgsAlreadyInstalled) + { + _cmdletPassedIn.WriteWarning( + string.Format("Resource '{0}' with version '{1}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter", + pkg.Name, + pkg.Version)); + + filteredPackages.Remove(pkg.Name); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + } + + return filteredPackages.Values.ToArray(); + } + + private List InstallPackage( + IEnumerable pkgsToInstall, // those found to be required to be installed (includes Dependency packages as well) + string repoName, + string repoUri, + PSCredentialInfo repoCredentialInfo, + PSCredential credential, + bool isLocalRepo) + { + List pkgsSuccessfullyInstalled = new List(); + int totalPkgs = pkgsToInstall.Count(); + + // Counters for tracking current package out of total + int totalInstalledPkgCount = 0; + foreach (PSResourceInfo pkg in pkgsToInstall) + { + totalInstalledPkgCount++; + var tempInstallPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + // Create a temp directory to install to + var dir = Directory.CreateDirectory(tempInstallPath); // should check it gets created properly + // To delete file attributes from the existing ones get the current file attributes first and use AND (&) operator + // with a mask (bitwise complement of desired attributes combination). + // TODO: check the attributes and if it's read only then set it + // attribute may be inherited from the parent + // TODO: are there Linux accommodations we need to consider here? + dir.Attributes &= ~FileAttributes.ReadOnly; + + _cmdletPassedIn.WriteVerbose(string.Format("Begin installing package: '{0}'", pkg.Name)); + + if (!_quiet) + { + int activityId = 0; + int percentComplete = ((totalInstalledPkgCount * 100) / totalPkgs); + string activity = string.Format("Installing {0}...", pkg.Name); + string statusDescription = string.Format("{0}% Complete", percentComplete); + _cmdletPassedIn.WriteProgress( + new ProgressRecord(activityId, activity, statusDescription)); + } + + // Create PackageIdentity in order to download + string createFullVersion = pkg.Version.ToString(); + if (pkg.IsPrerelease) + { + createFullVersion = pkg.Version.ToString() + "-" + pkg.Prerelease; + } + + if (!NuGetVersion.TryParse(createFullVersion, out NuGetVersion pkgVersion)) + { + var message = String.Format("{0} package could not be installed with error: could not parse package '{0}' version '{1} into a NuGetVersion", + pkg.Name, + pkg.Version.ToString()); + var ex = new ArgumentException(message); + var packageIdentityVersionParseError = new ErrorRecord(ex, "psdataFileNotExistError", ErrorCategory.ReadError, null); + _cmdletPassedIn.WriteError(packageIdentityVersionParseError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + continue; + } + + var pkgIdentity = new PackageIdentity(pkg.Name, pkgVersion); + var cacheContext = new SourceCacheContext(); + + if (isLocalRepo) + { + /* Download from a local repository -- this is slightly different process than from a server */ + var localResource = new FindLocalPackagesResourceV2(repoUri); + var resource = new LocalDownloadResource(repoUri, localResource); + + // Actually downloading the .nupkg from a local repo + var result = resource.GetDownloadResourceResultAsync( + identity: pkgIdentity, + downloadContext: new PackageDownloadContext(cacheContext), + globalPackagesFolder: tempInstallPath, + logger: NullLogger.Instance, + token: _cancellationToken).GetAwaiter().GetResult(); + + // Create the package extraction context + PackageExtractionContext packageExtractionContext = new PackageExtractionContext( + packageSaveMode: PackageSaveMode.Nupkg, + xmlDocFileSaveMode: PackageExtractionBehavior.XmlDocFileSaveMode, + clientPolicyContext: null, + logger: NullLogger.Instance); + + // Extracting from .nupkg and placing files into tempInstallPath + result.PackageReader.CopyFiles( + destination: tempInstallPath, + packageFiles: result.PackageReader.GetFiles(), + extractFile: new PackageFileExtractor( + result.PackageReader.GetFiles(), + packageExtractionContext.XmlDocFileSaveMode).ExtractPackageFile, + logger: NullLogger.Instance, + token: _cancellationToken); + result.Dispose(); + } + else + { + /* Download from a non-local repository */ + // Set up NuGet API resource for download + PackageSource source = new PackageSource(repoUri); + + // Explicitly passed in Credential takes precedence over repository CredentialInfo + if (credential != null) + { + string password = new NetworkCredential(string.Empty, credential.Password).Password; + source.Credentials = PackageSourceCredential.FromUserInput(repoUri, credential.UserName, password, true, null); + } + else if (repoCredentialInfo != null) + { + PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( + repoName, + repoCredentialInfo, + _cmdletPassedIn); + + string password = new NetworkCredential(string.Empty, repoCredential.Password).Password; + source.Credentials = PackageSourceCredential.FromUserInput(repoUri, repoCredential.UserName, password, true, null); + } + var provider = FactoryExtensionsV3.GetCoreV3(NuGet.Protocol.Core.Types.Repository.Provider); + SourceRepository repository = new SourceRepository(source, provider); + + /* Download from a non-local repository -- ie server */ + var downloadResource = repository.GetResourceAsync().GetAwaiter().GetResult(); + DownloadResourceResult result = null; + try + { + result = downloadResource.GetDownloadResourceResultAsync( + identity: pkgIdentity, + downloadContext: new PackageDownloadContext(cacheContext), + globalPackagesFolder: tempInstallPath, + logger: NullLogger.Instance, + token: _cancellationToken).GetAwaiter().GetResult(); + } + catch (Exception e) + { + _cmdletPassedIn.WriteVerbose(string.Format("Error attempting download: '{0}'", e.Message)); + } + finally + { + // Need to close the .nupkg + if (result != null) result.Dispose(); + } + } + + _cmdletPassedIn.WriteVerbose(string.Format("Successfully able to download package from source to: '{0}'", tempInstallPath)); + + // pkgIdentity.Version.Version gets the version without metadata or release labels. + string newVersion = pkgIdentity.Version.ToNormalizedString(); + string normalizedVersionNoPrerelease = newVersion; + if (pkgIdentity.Version.IsPrerelease) + { + // eg: 2.0.2 + normalizedVersionNoPrerelease = pkgIdentity.Version.ToNormalizedString().Substring(0, pkgIdentity.Version.ToNormalizedString().IndexOf('-')); + } + + string tempDirNameVersion = isLocalRepo ? tempInstallPath : Path.Combine(tempInstallPath, pkgIdentity.Id.ToLower(), newVersion); + var version4digitNoPrerelease = pkgIdentity.Version.Version.ToString(); + string moduleManifestVersion = string.Empty; + var scriptPath = Path.Combine(tempDirNameVersion, pkg.Name + ".ps1"); + var modulePath = Path.Combine(tempDirNameVersion, pkg.Name + ".psd1"); + // Check if the package is a module or a script + var isModule = File.Exists(modulePath); + + string installPath; + if (_savePkg) + { + // For save the installation path is what is passed in via -Path + installPath = _pathsToInstallPkg.FirstOrDefault(); + + // If saving as nupkg simply copy the nupkg and move onto next iteration of loop + // asNupkg functionality only applies to Save-PSResource + if (_asNupkg) + { + var nupkgFile = pkgIdentity.ToString().ToLower() + ".nupkg"; + File.Copy(Path.Combine(tempDirNameVersion, nupkgFile), Path.Combine(installPath, nupkgFile)); + + _cmdletPassedIn.WriteVerbose(string.Format("'{0}' moved into file path '{1}'", nupkgFile, installPath)); + pkgsSuccessfullyInstalled.Add(pkg); -namespace Microsoft.PowerShell.PowerShellGet.Cmdlets -{ - /// - /// Install helper class - /// - internal class InstallHelper : PSCmdlet - { - #region Members - - private const string MsgRepositoryNotTrusted = "Untrusted repository"; - private const string MsgInstallUntrustedPackage = "You are installing the modules from an untrusted repository. If you trust this repository, change its Trusted value by running the Set-PSResourceRepository cmdlet. Are you sure you want to install the PSresource from '{0}' ?"; - - private CancellationToken _cancellationToken; - private readonly PSCmdlet _cmdletPassedIn; - private List _pathsToInstallPkg; - private VersionRange _versionRange; - private bool _prerelease; - private bool _acceptLicense; - private bool _quiet; - private bool _reinstall; - private bool _force; - private bool _trustRepository; - private PSCredential _credential; - private bool _asNupkg; - private bool _includeXML; - private bool _noClobber; - private bool _savePkg; - List _pathsToSearch; - List _pkgNamesToInstall; - - #endregion - - #region Public methods - - public InstallHelper(PSCmdlet cmdletPassedIn) - { - CancellationTokenSource source = new CancellationTokenSource(); - _cancellationToken = source.Token; - _cmdletPassedIn = cmdletPassedIn; - } - - public List InstallPackages( - string[] names, - VersionRange versionRange, - bool prerelease, - string[] repository, - bool acceptLicense, - bool quiet, - bool reinstall, - bool force, - bool trustRepository, - bool noClobber, - PSCredential credential, - bool asNupkg, - bool includeXML, - bool skipDependencyCheck, - bool savePkg, - List pathsToInstallPkg) - { - _cmdletPassedIn.WriteVerbose(string.Format("Parameters passed in >>> Name: '{0}'; Version: '{1}'; Prerelease: '{2}'; Repository: '{3}'; " + - "AcceptLicense: '{4}'; Quiet: '{5}'; Reinstall: '{6}'; TrustRepository: '{7}'; NoClobber: '{8}'; AsNupkg: '{9}'; IncludeXML '{10}'; SavePackage '{11}'", - string.Join(",", names), - versionRange != null ? (versionRange.OriginalString != null ? versionRange.OriginalString : string.Empty) : string.Empty, - prerelease.ToString(), - repository != null ? string.Join(",", repository) : string.Empty, - acceptLicense.ToString(), - quiet.ToString(), - reinstall.ToString(), - trustRepository.ToString(), - noClobber.ToString(), - asNupkg.ToString(), - includeXML.ToString(), - savePkg.ToString())); - - _versionRange = versionRange; - _prerelease = prerelease; - _acceptLicense = acceptLicense || force; - _quiet = quiet; - _reinstall = reinstall; - _force = force; - _trustRepository = trustRepository || force; - _noClobber = noClobber; - _credential = credential; - _asNupkg = asNupkg; - _includeXML = includeXML; - _savePkg = savePkg; - _pathsToInstallPkg = pathsToInstallPkg; - - // Create list of installation paths to search. - _pathsToSearch = new List(); - _pkgNamesToInstall = names.ToList(); - - // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) - // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations - // e.g.: - // ./InstallPackagePath1/PackageA - // ./InstallPackagePath1/PackageB - // ./InstallPackagePath2/PackageC - // ./InstallPackagePath3/PackageD - foreach (var path in _pathsToInstallPkg) - { - _pathsToSearch.AddRange(Utils.GetSubDirectories(path)); - } - - // Go through the repositories and see which is the first repository to have the pkg version available - return ProcessRepositories( - repository: repository, - trustRepository: _trustRepository, - credential: _credential, - skipDependencyCheck: skipDependencyCheck); - } - - #endregion - - #region Private methods - - // This method calls iterates through repositories (by priority order) to search for the pkgs to install - private List ProcessRepositories( - string[] repository, - bool trustRepository, - PSCredential credential, - bool skipDependencyCheck) - { - var listOfRepositories = RepositorySettings.Read(repository, out string[] _); - var yesToAll = false; - var noToAll = false; - - var findHelper = new FindHelper(_cancellationToken, _cmdletPassedIn); - List allPkgsInstalled = new List(); - - foreach (var repo in listOfRepositories) - { - // If no more packages to install, then return - if (!_pkgNamesToInstall.Any()) return allPkgsInstalled; - - string repoName = repo.Name; - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to search for packages in '{0}'", repoName)); - - // Source is only trusted if it's set at the repository level to be trusted, -TrustRepository flag is true, -Force flag is true - // OR the user issues trust interactively via console. - var sourceTrusted = true; - if (repo.Trusted == false && !trustRepository && !_force) - { - _cmdletPassedIn.WriteVerbose("Checking if untrusted repository should be used"); - - if (!(yesToAll || noToAll)) - { - // Prompt for installation of package from untrusted repository - var message = string.Format(CultureInfo.InvariantCulture, MsgInstallUntrustedPackage, repoName); - sourceTrusted = _cmdletPassedIn.ShouldContinue(message, MsgRepositoryNotTrusted, true, ref yesToAll, ref noToAll); - } - } - - if (!sourceTrusted && !yesToAll) - { - continue; - } - - _cmdletPassedIn.WriteVerbose("Untrusted repository accepted as trusted source."); - - // If it can't find the pkg in one repository, it'll look for it in the next repo in the list - var isLocalRepo = repo.Uri.AbsoluteUri.StartsWith(Uri.UriSchemeFile + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase); - - // Finds parent packages and dependencies - IEnumerable pkgsFromRepoToInstall = findHelper.FindByResourceName( - name: _pkgNamesToInstall.ToArray(), - type: ResourceType.None, - version: _versionRange != null ? _versionRange.OriginalString : null, - prerelease: _prerelease, - tag: null, - repository: new string[] { repoName }, - credential: credential, - includeDependencies: !skipDependencyCheck); - - if (!pkgsFromRepoToInstall.Any()) - { - _cmdletPassedIn.WriteVerbose(string.Format("None of the specified resources were found in the '{0}' repository.", repoName)); - // Check in the next repository - continue; - } - - // Select the first package from each name group, which is guaranteed to be the latest version. - // We should only have one version returned for each package name - // e.g.: - // PackageA (version 1.0) - // PackageB (version 2.0) - // PackageC (version 1.0) - pkgsFromRepoToInstall = pkgsFromRepoToInstall.GroupBy( - m => new { m.Name }).Select( - group => group.First()).ToList(); - - // Check to see if the pkgs (including dependencies) are already installed (ie the pkg is installed and the version satisfies the version range provided via param) - if (!_reinstall) - { - pkgsFromRepoToInstall = FilterByInstalledPkgs(pkgsFromRepoToInstall); - } - - if (!pkgsFromRepoToInstall.Any()) - { - continue; - } - - List pkgsInstalled = InstallPackage( - pkgsFromRepoToInstall, - repoName, - repo.Uri.AbsoluteUri, - repo.CredentialInfo, - credential, - isLocalRepo); - - foreach (PSResourceInfo pkg in pkgsInstalled) - { - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - - allPkgsInstalled.AddRange(pkgsInstalled); - } - - // At this only package names left were those which could not be found in registered repositories - foreach (string pkgName in _pkgNamesToInstall) - { - var message = String.Format("Package '{0}' with requested version range {1} could not be installed as it was not found in any registered repositories", - pkgName, - _versionRange.ToString()); - var ex = new ArgumentException(message); - var ResourceNotFoundError = new ErrorRecord(ex, "ResourceNotFoundError", ErrorCategory.ObjectNotFound, null); - _cmdletPassedIn.WriteError(ResourceNotFoundError); - } - - return allPkgsInstalled; - } - - // Check if any of the pkg versions are already installed, if they are we'll remove them from the list of packages to install - private IEnumerable FilterByInstalledPkgs(IEnumerable packages) - { - // Create list of installation paths to search. - List _pathsToSearch = new List(); - // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) - // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations - // e.g.: - // ./InstallPackagePath1/PackageA - // ./InstallPackagePath1/PackageB - // ./InstallPackagePath2/PackageC - // ./InstallPackagePath3/PackageD - foreach (var path in _pathsToInstallPkg) - { - _pathsToSearch.AddRange(Utils.GetSubDirectories(path)); - } - - var filteredPackages = new Dictionary(); - foreach (var pkg in packages) - { - filteredPackages.Add(pkg.Name, pkg); - } - - GetHelper getHelper = new GetHelper(_cmdletPassedIn); - // Get currently installed packages. - // selectPrereleaseOnly is false because even if Prerelease is true we want to include both stable and prerelease, never select prerelease only. - IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( - name: filteredPackages.Keys.ToArray(), - versionRange: _versionRange, - pathsToSearch: _pathsToSearch, - selectPrereleaseOnly: false); - if (!pkgsAlreadyInstalled.Any()) - { - return packages; - } - - // Remove from list package versions that are already installed. - foreach (PSResourceInfo pkg in pkgsAlreadyInstalled) - { - _cmdletPassedIn.WriteWarning( - string.Format("Resource '{0}' with version '{1}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter", - pkg.Name, - pkg.Version)); - - filteredPackages.Remove(pkg.Name); - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - - return filteredPackages.Values.ToArray(); - } - - private List InstallPackage( - IEnumerable pkgsToInstall, // those found to be required to be installed (includes Dependency packages as well) - string repoName, - string repoUri, - PSCredentialInfo repoCredentialInfo, - PSCredential credential, - bool isLocalRepo) - { - List pkgsSuccessfullyInstalled = new List(); - int totalPkgs = pkgsToInstall.Count(); - - // Counters for tracking current package out of total - int totalInstalledPkgCount = 0; - foreach (PSResourceInfo pkg in pkgsToInstall) - { - totalInstalledPkgCount++; - var tempInstallPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - // Create a temp directory to install to - var dir = Directory.CreateDirectory(tempInstallPath); // should check it gets created properly - // To delete file attributes from the existing ones get the current file attributes first and use AND (&) operator - // with a mask (bitwise complement of desired attributes combination). - // TODO: check the attributes and if it's read only then set it - // attribute may be inherited from the parent - // TODO: are there Linux accommodations we need to consider here? - dir.Attributes &= ~FileAttributes.ReadOnly; - - _cmdletPassedIn.WriteVerbose(string.Format("Begin installing package: '{0}'", pkg.Name)); - - if (!_quiet) - { - int activityId = 0; - int percentComplete = ((totalInstalledPkgCount * 100) / totalPkgs); - string activity = string.Format("Installing {0}...", pkg.Name); - string statusDescription = string.Format("{0}% Complete", percentComplete); - _cmdletPassedIn.WriteProgress( - new ProgressRecord(activityId, activity, statusDescription)); - } - - // Create PackageIdentity in order to download - string createFullVersion = pkg.Version.ToString(); - if (pkg.IsPrerelease) - { - createFullVersion = pkg.Version.ToString() + "-" + pkg.Prerelease; - } - - if (!NuGetVersion.TryParse(createFullVersion, out NuGetVersion pkgVersion)) - { - var message = String.Format("{0} package could not be installed with error: could not parse package '{0}' version '{1} into a NuGetVersion", - pkg.Name, - pkg.Version.ToString()); - var ex = new ArgumentException(message); - var packageIdentityVersionParseError = new ErrorRecord(ex, "psdataFileNotExistError", ErrorCategory.ReadError, null); - _cmdletPassedIn.WriteError(packageIdentityVersionParseError); - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - continue; - } - - var pkgIdentity = new PackageIdentity(pkg.Name, pkgVersion); - var cacheContext = new SourceCacheContext(); - - if (isLocalRepo) - { - /* Download from a local repository -- this is slightly different process than from a server */ - var localResource = new FindLocalPackagesResourceV2(repoUri); - var resource = new LocalDownloadResource(repoUri, localResource); - - // Actually downloading the .nupkg from a local repo - var result = resource.GetDownloadResourceResultAsync( - identity: pkgIdentity, - downloadContext: new PackageDownloadContext(cacheContext), - globalPackagesFolder: tempInstallPath, - logger: NullLogger.Instance, - token: _cancellationToken).GetAwaiter().GetResult(); - - // Create the package extraction context - PackageExtractionContext packageExtractionContext = new PackageExtractionContext( - packageSaveMode: PackageSaveMode.Nupkg, - xmlDocFileSaveMode: PackageExtractionBehavior.XmlDocFileSaveMode, - clientPolicyContext: null, - logger: NullLogger.Instance); - - // Extracting from .nupkg and placing files into tempInstallPath - result.PackageReader.CopyFiles( - destination: tempInstallPath, - packageFiles: result.PackageReader.GetFiles(), - extractFile: new PackageFileExtractor( - result.PackageReader.GetFiles(), - packageExtractionContext.XmlDocFileSaveMode).ExtractPackageFile, - logger: NullLogger.Instance, - token: _cancellationToken); - result.Dispose(); - } - else - { - /* Download from a non-local repository */ - // Set up NuGet API resource for download - PackageSource source = new PackageSource(repoUri); - - // Explicitly passed in Credential takes precedence over repository CredentialInfo - if (credential != null) - { - string password = new NetworkCredential(string.Empty, credential.Password).Password; - source.Credentials = PackageSourceCredential.FromUserInput(repoUri, credential.UserName, password, true, null); - } - else if (repoCredentialInfo != null) - { - PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( - repoName, - repoCredentialInfo, - _cmdletPassedIn); - - string password = new NetworkCredential(string.Empty, repoCredential.Password).Password; - source.Credentials = PackageSourceCredential.FromUserInput(repoUri, repoCredential.UserName, password, true, null); - } - var provider = FactoryExtensionsV3.GetCoreV3(NuGet.Protocol.Core.Types.Repository.Provider); - SourceRepository repository = new SourceRepository(source, provider); - - /* Download from a non-local repository -- ie server */ - var downloadResource = repository.GetResourceAsync().GetAwaiter().GetResult(); - DownloadResourceResult result = null; - try - { - result = downloadResource.GetDownloadResourceResultAsync( - identity: pkgIdentity, - downloadContext: new PackageDownloadContext(cacheContext), - globalPackagesFolder: tempInstallPath, - logger: NullLogger.Instance, - token: _cancellationToken).GetAwaiter().GetResult(); - } - catch (Exception e) - { - _cmdletPassedIn.WriteVerbose(string.Format("Error attempting download: '{0}'", e.Message)); - } - finally - { - // Need to close the .nupkg - if (result != null) result.Dispose(); - } - } - - _cmdletPassedIn.WriteVerbose(string.Format("Successfully able to download package from source to: '{0}'", tempInstallPath)); - - // Run authenticode validation - if (!SkipPublisherCheck && !PublisherValidation(new string[]{ pkg.Name }, _versionRange, _pathsToSearch)) + continue; + } + } + else { + // PSModules: + /// ./Modules + /// ./Scripts + /// _pathsToInstallPkg is sorted by desirability, Find will pick the pick the first Script or Modules path found in the list + installPath = isModule ? _pathsToInstallPkg.Find(path => path.EndsWith("Modules", StringComparison.InvariantCultureIgnoreCase)) + : _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); + } - } - - // pkgIdentity.Version.Version gets the version without metadata or release labels. - string newVersion = pkgIdentity.Version.ToNormalizedString(); - string normalizedVersionNoPrerelease = newVersion; - if (pkgIdentity.Version.IsPrerelease) - { - // eg: 2.0.2 - normalizedVersionNoPrerelease = pkgIdentity.Version.ToNormalizedString().Substring(0, pkgIdentity.Version.ToNormalizedString().IndexOf('-')); - } - - string tempDirNameVersion = isLocalRepo ? tempInstallPath : Path.Combine(tempInstallPath, pkgIdentity.Id.ToLower(), newVersion); - var version4digitNoPrerelease = pkgIdentity.Version.Version.ToString(); - string moduleManifestVersion = string.Empty; - var scriptPath = Path.Combine(tempDirNameVersion, pkg.Name + ".ps1"); - var modulePath = Path.Combine(tempDirNameVersion, pkg.Name + ".psd1"); - // Check if the package is a module or a script - var isModule = File.Exists(modulePath); - - string installPath; - if (_savePkg) - { - // For save the installation path is what is passed in via -Path - installPath = _pathsToInstallPkg.FirstOrDefault(); - - // If saving as nupkg simply copy the nupkg and move onto next iteration of loop - // asNupkg functionality only applies to Save-PSResource - if (_asNupkg) - { - var nupkgFile = pkgIdentity.ToString().ToLower() + ".nupkg"; - File.Copy(Path.Combine(tempDirNameVersion, nupkgFile), Path.Combine(installPath, nupkgFile)); - - _cmdletPassedIn.WriteVerbose(string.Format("'{0}' moved into file path '{1}'", nupkgFile, installPath)); - pkgsSuccessfullyInstalled.Add(pkg); - - continue; - } - } - else - { - // PSModules: - /// ./Modules - /// ./Scripts - /// _pathsToInstallPkg is sorted by desirability, Find will pick the pick the first Script or Modules path found in the list - installPath = isModule ? _pathsToInstallPkg.Find(path => path.EndsWith("Modules", StringComparison.InvariantCultureIgnoreCase)) - : _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); - } - - if (isModule) - { - var moduleManifest = Path.Combine(tempDirNameVersion, pkgIdentity.Id + ".psd1"); - if (!File.Exists(moduleManifest)) - { - var message = String.Format("{0} package could not be installed with error: Module manifest file: {1} does not exist. This is not a valid PowerShell module.", pkgIdentity.Id, moduleManifest); - - var ex = new ArgumentException(message); - var psdataFileDoesNotExistError = new ErrorRecord(ex, "psdataFileNotExistError", ErrorCategory.ReadError, null); - _cmdletPassedIn.WriteError(psdataFileDoesNotExistError); - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - continue; - } - - if (!Utils.TryParseModuleManifest(moduleManifest, _cmdletPassedIn, out Hashtable parsedMetadataHashtable)) - { - // Ran into errors parsing the module manifest file which was found in Utils.ParseModuleManifest() and written. - continue; - } - - moduleManifestVersion = parsedMetadataHashtable["ModuleVersion"] as string; - - // Accept License verification - if (!_savePkg && !CallAcceptLicense(pkg, moduleManifest, tempInstallPath, newVersion)) - { - continue; - } - - // If NoClobber is specified, ensure command clobbering does not happen - if (_noClobber && !DetectClobber(pkg.Name, parsedMetadataHashtable)) - { - continue; - } - } - - // Delete the extra nupkg related files that are not needed and not part of the module/script - DeleteExtraneousFiles(pkgIdentity, tempDirNameVersion); - - if (_includeXML) - { - CreateMetadataXMLFile(tempDirNameVersion, installPath, pkg, isModule); - } - - MoveFilesIntoInstallPath( - pkg, - isModule, - isLocalRepo, - tempDirNameVersion, - tempInstallPath, - installPath, - newVersion, - moduleManifestVersion, - scriptPath); - - _cmdletPassedIn.WriteVerbose(String.Format("Successfully installed package '{0}' to location '{1}'", pkg.Name, installPath)); - pkgsSuccessfullyInstalled.Add(pkg); - } - catch (Exception e) - { - _cmdletPassedIn.WriteError( - new ErrorRecord( - new PSInvalidOperationException( - message: $"Unable to successfully install package '{pkg.Name}': '{e.Message}'", - innerException: e), - "InstallPackageFailed", - ErrorCategory.InvalidOperation, - _cmdletPassedIn)); - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - finally - { - // Delete the temp directory and all its contents - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to delete '{0}'", tempInstallPath)); - - if (Directory.Exists(tempInstallPath)) - { - if (!TryDeleteDirectory(tempInstallPath, out ErrorRecord errorMsg)) - { - _cmdletPassedIn.WriteError(errorMsg); - } - else - { - _cmdletPassedIn.WriteVerbose(String.Format("Successfully deleted '{0}'", tempInstallPath)); - } - } - } - } - - return pkgsSuccessfullyInstalled; - } - - private bool PublisherValidation(String[] pkgName, VersionRange versionRange, List pathsToSearch) + // Run authenticode validation // ismodule + if (!_skipPublisherCheck && true && !PublisherValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath)) + { + _cmdletPassedIn.WriteVerbose("Publisher validation failed."); + ThrowTerminatingError( + new ErrorRecord( + new PSInvalidOperationException( + message: $"Install-PSResource publisher validation is invalid."), + "InstallPSResourcePublisherValidation", + ErrorCategory.InvalidResult, + _cmdletPassedIn)); + } + + + if (isModule) + { + var moduleManifest = Path.Combine(tempDirNameVersion, pkgIdentity.Id + ".psd1"); + if (!File.Exists(moduleManifest)) + { + var message = String.Format("{0} package could not be installed with error: Module manifest file: {1} does not exist. This is not a valid PowerShell module.", pkgIdentity.Id, moduleManifest); + + var ex = new ArgumentException(message); + var psdataFileDoesNotExistError = new ErrorRecord(ex, "psdataFileNotExistError", ErrorCategory.ReadError, null); + _cmdletPassedIn.WriteError(psdataFileDoesNotExistError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + continue; + } + + if (!Utils.TryParseModuleManifest(moduleManifest, _cmdletPassedIn, out Hashtable parsedMetadataHashtable)) + { + // Ran into errors parsing the module manifest file which was found in Utils.ParseModuleManifest() and written. + continue; + } + + moduleManifestVersion = parsedMetadataHashtable["ModuleVersion"] as string; + + // Accept License verification + if (!_savePkg && !CallAcceptLicense(pkg, moduleManifest, tempInstallPath, newVersion)) + { + continue; + } + + // If NoClobber is specified, ensure command clobbering does not happen + if (_noClobber && !DetectClobber(pkg.Name, parsedMetadataHashtable)) + { + continue; + } + } + + // Delete the extra nupkg related files that are not needed and not part of the module/script + DeleteExtraneousFiles(pkgIdentity, tempDirNameVersion); + + if (_includeXML) + { + CreateMetadataXMLFile(tempDirNameVersion, installPath, newVersion, pkg, isModule); + } + + MoveFilesIntoInstallPath( + pkg, + isModule, + isLocalRepo, + tempDirNameVersion, + tempInstallPath, + installPath, + newVersion, + moduleManifestVersion, + scriptPath); + + _cmdletPassedIn.WriteVerbose(String.Format("Successfully installed package '{0}' to location '{1}'", pkg.Name, installPath)); + pkgsSuccessfullyInstalled.Add(pkg); + } + catch (Exception e) + { + _cmdletPassedIn.WriteError( + new ErrorRecord( + new PSInvalidOperationException( + message: $"Unable to successfully install package '{pkg.Name}': '{e.Message}'", + innerException: e), + "InstallPackageFailed", + ErrorCategory.InvalidOperation, + _cmdletPassedIn)); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + } + finally + { + // Delete the temp directory and all its contents + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to delete '{0}'", tempInstallPath)); + + if (Directory.Exists(tempInstallPath)) + { + if (!TryDeleteDirectory(tempInstallPath, out ErrorRecord errorMsg)) + { + _cmdletPassedIn.WriteError(errorMsg); + } + else + { + _cmdletPassedIn.WriteVerbose(String.Format("Successfully deleted '{0}'", tempInstallPath)); + } + } + } + } + + return pkgsSuccessfullyInstalled; + } + + private bool PublisherValidation(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath) { // 1) See if the current module that is trying to be installed is already installed - // call Get-PSResource - GetHelper getHelper = new GetHelper(this); - var pkgVersionsAlreadyInstalled = getHelper.GetPackagesFromPath(pkgName, versionRange, pathsToSearch, _prerelease); + GetHelper getHelper = new GetHelper(_cmdletPassedIn); + var moduleInstallPath = Path.Combine(installPath, pkgName); + var pkgVersionsAlreadyInstalled = getHelper.GetPackagesFromPath(new string[] { pkgName }, VersionRange.All, new List { moduleInstallPath }, _prerelease); + + string signedFilePath = string.Empty; + var catalogFileName = pkgName + ".cat"; + string moduleBasePath = string.Empty; + + PSResourceInfo resourceObj = null; + if (pkgVersionsAlreadyInstalled != null) // && pkgVersionsAlreadyInstalled.FirstOrDefault() != null) + { + + resourceObj = pkgVersionsAlreadyInstalled.FirstOrDefault(); + if (resourceObj == null) + { + return true; + } + signedFilePath = Path.Combine(resourceObj.InstalledLocation, catalogFileName); + if (!File.Exists(signedFilePath)) { + + return true; + } + } + // 2) If the module is already installed (an earlier version of the module, or same version being reinstalled, get the authenticode signature Collection authenticodeSignature = new Collection(); - try - { - authenticodeSignature = this.InvokeCommand.InvokeScript( - script: $"param ([string] $signedFilePath) Get-AuthenticodeSignature -FilePaath $signedFilePath", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { signedFilePath }); - } - catch { } + try + { + authenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $signedFilePath) Get-AuthenticodeSignature -FilePath $signedFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { signedFilePath }); + } + catch (Exception e){ + + _cmdletPassedIn.WriteVerbose(e.Message); + } - Signature signature = (authenticodeSignature.Any() 0 && authenticodeSignature[0] != null) ? (Signature)authenticodeSignature[0].BaseObject : null; + Signature signature = (authenticodeSignature.Any() && authenticodeSignature[0] != null) ? (Signature)authenticodeSignature[0].BaseObject : null; if (signature == null) { @@ -625,20 +699,39 @@ private bool PublisherValidation(String[] pkgName, VersionRange versionRange, Li } // 2b) If you're able to get the authenticode signature, get the module details + Hashtable moduleDetails = new Hashtable(); + + moduleDetails.Add("AuthenticodeSignature", signature); + moduleDetails.Add("Version", resourceObj.Version.ToString()); + moduleDetails.Add("ModuleBase", moduleBasePath); + + // Microsoft cert check is working well! + var isMicrosoftCert = IsMicrosoftCert(signature); + moduleDetails.Add("IsMicrosoftCertificate", isMicrosoftCert); + + /// UP TO HERE LOOKS GOOD! + + var publisherDetails = GetAuthenticodePublisher(signature, pkgName); + if (publisherDetails.Count == 2) + { + moduleDetails.Add("Publisher", publisherDetails["Publisher"]); + moduleDetails.Add("RootCertificateAuthority", publisherDetails["PublisherRootCA"]); + } // 3) Validate the catalog signature for the current module being installed + string catalogFilePath = Path.Combine(tempDirNameVersion, catalogFileName); Collection catalogAuthenticodeSignature = new Collection(); - try - { - catalogAuthenticodeSignature = this.InvokeCommand.InvokeScript( - script: $"param ([string] $catalogFilePath) Get-AuthenticodeSignature -FilePaath $catalogFilePath", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { catalogFilePath }); - } + try + { + catalogAuthenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $catalogFilePath) Get-AuthenticodeSignature -FilePath $catalogFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { catalogFilePath }); + } catch { } Signature catalogSignature = (catalogAuthenticodeSignature.Any() && catalogAuthenticodeSignature[0] != null) ? (Signature)catalogAuthenticodeSignature[0].BaseObject : null; @@ -649,352 +742,504 @@ private bool PublisherValidation(String[] pkgName, VersionRange versionRange, Li } // Run catalog validation - //Collection TestFileCatalogResult = new Collection(); - try - { - var TestFileCatalogResult = this.InvokeCommand.InvokeScript( - script: $"param ([string] $catalogFilePath) Test-FileCatalog -Path $moduleBasePath" + + Collection TestFileCatalogResult = new Collection(); + CatalogInformation catalogValidation = null; + try + { + TestFileCatalogResult = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $moduleBasePath, [string] $catalogFilePath) Test-FileCatalog -Path $moduleBasePath" + $" -CatalogFilePath $CatalogFilePath" + $" -FilesToSkip $script: PSGetItemInfoFileName,'*.cat','*.nupkg','*.nuspec'" + $" -Detailed" + - $" -ErrorAction SilentlyContinue", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { catalogFilePath }); + $" -ErrorAction SilentlyContinue", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { moduleBasePath, catalogFilePath }); - var catalogValidation = (TestFileCatalogResult.Any() && TestFileCatalogResult[0] != null) ? (Signature)TestFileCatalogResult[0].BaseObject : null; - - } + catalogValidation = (TestFileCatalogResult.Any() && TestFileCatalogResult[0] != null) ? (CatalogInformation)TestFileCatalogResult[0].BaseObject : null; + } catch { } - //catalogValidation = (TestFileCatalogResult.Any() && TestFileCatalogResult[0] != null) ? (CatalogInformation)TestFileCatalogResult[0].BaseObject : null; - ` - if (catalogValidation == null || !catalogValidation.Status.Equals(SignatureStatus.Valid) || !catalogValidation.Signature.Status.Equals(SignatureStatus.Valid)) + if (catalogValidation == null || !catalogValidation.Status.Equals(SignatureStatus.Valid) + || (catalogValidation.Signature != null && !catalogValidation.Signature.Status.Equals(SignatureStatus.Valid))) { return false; - } ` - - - + } // 5) if there is an installed module, and we have the info for the current module, // test these scenarios: // $InstalledModuleAuthenticodePublisher == $InstalledModuleDetails.Publisher + // $InstalledModuleVersion = $InstalledModuleDetails.Version + // $InstalledModuleRootCA = $InstalledModuleDetails.RootCertificateAuthority // ???? $IsInstalledModuleSignedByMicrosoft = $InstalledModuleDetails.IsMicrosoftCertificate - // $InstalledModuleVersion = $InstalledModuleDetails.Version - + return true; + } + + private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string tempInstallPath, string newVersion) + { + var requireLicenseAcceptance = false; + var success = true; + + if (File.Exists(moduleManifest)) + { + using (StreamReader sr = new StreamReader(moduleManifest)) + { + var text = sr.ReadToEnd(); + + var pattern = "RequireLicenseAcceptance\\s*=\\s*\\$true"; + var patternToSkip1 = "#\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; + var patternToSkip2 = "\\*\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; + + Regex rgx = new Regex(pattern); + Regex rgxComment1 = new Regex(patternToSkip1); + Regex rgxComment2 = new Regex(patternToSkip2); + if (rgx.IsMatch(text) && !rgxComment1.IsMatch(text) && !rgxComment2.IsMatch(text)) + { + requireLicenseAcceptance = true; + } + } + + // Licesnse agreement processing + if (requireLicenseAcceptance) + { + // If module requires license acceptance and -AcceptLicense is not passed in, display prompt + if (!_acceptLicense) + { + var PkgTempInstallPath = Path.Combine(tempInstallPath, p.Name, newVersion); + var LicenseFilePath = Path.Combine(PkgTempInstallPath, "License.txt"); + + if (!File.Exists(LicenseFilePath)) + { + var exMessage = String.Format("{0} package could not be installed with error: License.txt not found. License.txt must be provided when user license acceptance is required.", p.Name); + var ex = new ArgumentException(exMessage); + var acceptLicenseError = new ErrorRecord(ex, "LicenseTxtNotFound", ErrorCategory.ObjectNotFound, null); + + _cmdletPassedIn.WriteError(acceptLicenseError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase)); + success = false; + } + + // Otherwise read LicenseFile + string licenseText = System.IO.File.ReadAllText(LicenseFilePath); + var acceptanceLicenseQuery = $"Do you accept the license terms for module '{p.Name}'."; + var message = licenseText + "`r`n" + acceptanceLicenseQuery; + + var title = "License Acceptance"; + var yesToAll = false; + var noToAll = false; + var shouldContinueResult = _cmdletPassedIn.ShouldContinue(message, title, true, ref yesToAll, ref noToAll); + + if (shouldContinueResult || yesToAll) + { + _acceptLicense = true; + } + } + + // Check if user agreed to license terms, if they didn't then throw error, otherwise continue to install + if (!_acceptLicense) + { + var message = String.Format("{0} package could not be installed with error: License Acceptance is required for module '{0}'. Please specify '-AcceptLicense' to perform this operation.", p.Name); + var ex = new ArgumentException(message); + var acceptLicenseError = new ErrorRecord(ex, "ForceAcceptLicense", ErrorCategory.InvalidArgument, null); + + _cmdletPassedIn.WriteError(acceptLicenseError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase)); + success = false; + } + } + } + + return success; + } + + private bool DetectClobber(string pkgName, Hashtable parsedMetadataHashtable) + { + // Get installed modules, then get all possible paths + bool foundClobber = false; + GetHelper getHelper = new GetHelper(_cmdletPassedIn); + // selectPrereleaseOnly is false because even if Prerelease is true we want to include both stable and prerelease, never select prerelease only. + IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( + name: new string[] { "*" }, + versionRange: VersionRange.All, + pathsToSearch: _pathsToSearch, + selectPrereleaseOnly: false); + // user parsed metadata hash + List listOfCmdlets = new List(); + foreach (var cmdletName in parsedMetadataHashtable["CmdletsToExport"] as object[]) + { + listOfCmdlets.Add(cmdletName as string); + + } + + foreach (var pkg in pkgsAlreadyInstalled) + { + List duplicateCmdlets = new List(); + List duplicateCmds = new List(); + // See if any of the cmdlets or commands in the pkg we're trying to install exist within a package that's already installed + if (pkg.Includes.Cmdlet != null && pkg.Includes.Cmdlet.Any()) + { + duplicateCmdlets = listOfCmdlets.Where(cmdlet => pkg.Includes.Cmdlet.Contains(cmdlet)).ToList(); + + } + + if (pkg.Includes.Command != null && pkg.Includes.Command.Any()) + { + duplicateCmds = listOfCmdlets.Where(commands => pkg.Includes.Command.Contains(commands, StringComparer.InvariantCultureIgnoreCase)).ToList(); + } + + if (duplicateCmdlets.Any() || duplicateCmds.Any()) + { + + duplicateCmdlets.AddRange(duplicateCmds); + + var errMessage = string.Format( + "{1} package could not be installed with error: The following commands are already available on this system: '{0}'. This module '{1}' may override the existing commands. If you still want to install this module '{1}', remove the -NoClobber parameter.", + String.Join(", ", duplicateCmdlets), pkgName); + + var ex = new ArgumentException(errMessage); + var noClobberError = new ErrorRecord(ex, "CommandAlreadyExists", ErrorCategory.ResourceExists, null); + + _cmdletPassedIn.WriteError(noClobberError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + foundClobber = true; + + return foundClobber; + } + } + + return foundClobber; + } + + private void CreateMetadataXMLFile(string dirNameVersion, string installPath, string pkgVersion, PSResourceInfo pkg, bool isModule) + { + // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" + // Modules will have the metadata file: "PSGetModuleInfo.xml" + var metadataXMLPath = isModule ? Path.Combine(dirNameVersion, "PSGetModuleInfo.xml") + : Path.Combine(dirNameVersion, (pkg.Name + "_InstalledScriptInfo.xml")); + + pkg.InstalledDate = DateTime.Now; + pkg.InstalledLocation = Path.Combine(installPath, pkg.Name, pkgVersion); + + // Write all metadata into metadataXMLPath + if (!pkg.TryWrite(metadataXMLPath, out string error)) + { + var message = string.Format("{0} package could not be installed with error: Error parsing metadata into XML: '{1}'", pkg.Name, error); + var ex = new ArgumentException(message); + var ErrorParsingMetadata = new ErrorRecord(ex, "ErrorParsingMetadata", ErrorCategory.ParserError, null); + + _cmdletPassedIn.WriteError(ErrorParsingMetadata); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + } + } + + private void DeleteExtraneousFiles(PackageIdentity pkgIdentity, string dirNameVersion) + { + // Deleting .nupkg SHA file, .nuspec, and .nupkg after unpacking the module + var pkgIdString = pkgIdentity.ToString(); + var nupkgSHAToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg.sha512"); + var nuspecToDelete = Path.Combine(dirNameVersion, pkgIdentity.Id + ".nuspec"); + var nupkgToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg"); + var nupkgMetadataToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg.metadata"); + var contentTypesToDelete = Path.Combine(dirNameVersion, "[Content_Types].xml"); + var relsDirToDelete = Path.Combine(dirNameVersion, "_rels"); + var packageDirToDelete = Path.Combine(dirNameVersion, "package"); + + // Unforunately have to check if each file exists because it may or may not be there + if (File.Exists(nupkgSHAToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nupkgSHAToDelete)); + File.Delete(nupkgSHAToDelete); + } + if (File.Exists(nuspecToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nuspecToDelete)); + File.Delete(nuspecToDelete); + } + if (File.Exists(nupkgToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nupkgToDelete)); + File.Delete(nupkgToDelete); + } + if (File.Exists(nupkgMetadataToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nupkgMetadataToDelete)); + File.Delete(nupkgMetadataToDelete); + } + if (File.Exists(contentTypesToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", contentTypesToDelete)); + File.Delete(contentTypesToDelete); + } + if (Directory.Exists(relsDirToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", relsDirToDelete)); + Utils.DeleteDirectory(relsDirToDelete); + } + if (Directory.Exists(packageDirToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", packageDirToDelete)); + Utils.DeleteDirectory(packageDirToDelete); + } + } + + private bool TryDeleteDirectory( + string tempInstallPath, + out ErrorRecord errorMsg) + { + errorMsg = null; + + try + { + Utils.DeleteDirectory(tempInstallPath); + } + catch (Exception e) + { + var TempDirCouldNotBeDeletedError = new ErrorRecord(e, "errorDeletingTempInstallPath", ErrorCategory.InvalidResult, null); + errorMsg = TempDirCouldNotBeDeletedError; + return false; + } return true; - } - - private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string tempInstallPath, string newVersion) - { - var requireLicenseAcceptance = false; - var success = true; - - if (File.Exists(moduleManifest)) - { - using (StreamReader sr = new StreamReader(moduleManifest)) - { - var text = sr.ReadToEnd(); - - var pattern = "RequireLicenseAcceptance\\s*=\\s*\\$true"; - var patternToSkip1 = "#\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; - var patternToSkip2 = "\\*\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; - - Regex rgx = new Regex(pattern); - Regex rgxComment1 = new Regex(patternToSkip1); - Regex rgxComment2 = new Regex(patternToSkip2); - if (rgx.IsMatch(text) && !rgxComment1.IsMatch(text) && !rgxComment2.IsMatch(text)) - { - requireLicenseAcceptance = true; - } - } - - // Licesnse agreement processing - if (requireLicenseAcceptance) - { - // If module requires license acceptance and -AcceptLicense is not passed in, display prompt - if (!_acceptLicense) - { - var PkgTempInstallPath = Path.Combine(tempInstallPath, p.Name, newVersion); - var LicenseFilePath = Path.Combine(PkgTempInstallPath, "License.txt"); - - if (!File.Exists(LicenseFilePath)) - { - var exMessage = String.Format("{0} package could not be installed with error: License.txt not found. License.txt must be provided when user license acceptance is required.", p.Name); - var ex = new ArgumentException(exMessage); - var acceptLicenseError = new ErrorRecord(ex, "LicenseTxtNotFound", ErrorCategory.ObjectNotFound, null); - - _cmdletPassedIn.WriteError(acceptLicenseError); - _pkgNamesToInstall.RemoveAll(x => x.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase)); - success = false; - } - - // Otherwise read LicenseFile - string licenseText = System.IO.File.ReadAllText(LicenseFilePath); - var acceptanceLicenseQuery = $"Do you accept the license terms for module '{p.Name}'."; - var message = licenseText + "`r`n" + acceptanceLicenseQuery; - - var title = "License Acceptance"; - var yesToAll = false; - var noToAll = false; - var shouldContinueResult = _cmdletPassedIn.ShouldContinue(message, title, true, ref yesToAll, ref noToAll); - - if (shouldContinueResult || yesToAll) - { - _acceptLicense = true; - } - } - - // Check if user agreed to license terms, if they didn't then throw error, otherwise continue to install - if (!_acceptLicense) - { - var message = String.Format("{0} package could not be installed with error: License Acceptance is required for module '{0}'. Please specify '-AcceptLicense' to perform this operation.", p.Name); - var ex = new ArgumentException(message); - var acceptLicenseError = new ErrorRecord(ex, "ForceAcceptLicense", ErrorCategory.InvalidArgument, null); - - _cmdletPassedIn.WriteError(acceptLicenseError); - _pkgNamesToInstall.RemoveAll(x => x.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase)); - success = false; - } - } - } - - return success; - } - - private bool DetectClobber(string pkgName, Hashtable parsedMetadataHashtable) - { - // Get installed modules, then get all possible paths - bool foundClobber = false; - GetHelper getHelper = new GetHelper(_cmdletPassedIn); - // selectPrereleaseOnly is false because even if Prerelease is true we want to include both stable and prerelease, never select prerelease only. - IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( - name: new string[] { "*" }, - versionRange: VersionRange.All, - pathsToSearch: _pathsToSearch, - selectPrereleaseOnly: false); - // user parsed metadata hash - List listOfCmdlets = new List(); - foreach (var cmdletName in parsedMetadataHashtable["CmdletsToExport"] as object[]) - { - listOfCmdlets.Add(cmdletName as string); - - } - - foreach (var pkg in pkgsAlreadyInstalled) - { - List duplicateCmdlets = new List(); - List duplicateCmds = new List(); - // See if any of the cmdlets or commands in the pkg we're trying to install exist within a package that's already installed - if (pkg.Includes.Cmdlet != null && pkg.Includes.Cmdlet.Any()) - { - duplicateCmdlets = listOfCmdlets.Where(cmdlet => pkg.Includes.Cmdlet.Contains(cmdlet)).ToList(); - - } - - if (pkg.Includes.Command != null && pkg.Includes.Command.Any()) - { - duplicateCmds = listOfCmdlets.Where(commands => pkg.Includes.Command.Contains(commands, StringComparer.InvariantCultureIgnoreCase)).ToList(); - } - - if (duplicateCmdlets.Any() || duplicateCmds.Any()) - { - - duplicateCmdlets.AddRange(duplicateCmds); - - var errMessage = string.Format( - "{1} package could not be installed with error: The following commands are already available on this system: '{0}'. This module '{1}' may override the existing commands. If you still want to install this module '{1}', remove the -NoClobber parameter.", - String.Join(", ", duplicateCmdlets), pkgName); - - var ex = new ArgumentException(errMessage); - var noClobberError = new ErrorRecord(ex, "CommandAlreadyExists", ErrorCategory.ResourceExists, null); - - _cmdletPassedIn.WriteError(noClobberError); - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); - foundClobber = true; - - return foundClobber; - } - } - - return foundClobber; - } - - private void CreateMetadataXMLFile(string dirNameVersion, string installPath, PSResourceInfo pkg, bool isModule) - { - // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" - // Modules will have the metadata file: "PSGetModuleInfo.xml" - var metadataXMLPath = isModule ? Path.Combine(dirNameVersion, "PSGetModuleInfo.xml") - : Path.Combine(dirNameVersion, (pkg.Name + "_InstalledScriptInfo.xml")); - - pkg.InstalledDate = DateTime.Now; - pkg.InstalledLocation = installPath; - - // Write all metadata into metadataXMLPath - if (!pkg.TryWrite(metadataXMLPath, out string error)) - { - var message = string.Format("{0} package could not be installed with error: Error parsing metadata into XML: '{1}'", pkg.Name, error); - var ex = new ArgumentException(message); - var ErrorParsingMetadata = new ErrorRecord(ex, "ErrorParsingMetadata", ErrorCategory.ParserError, null); - - _cmdletPassedIn.WriteError(ErrorParsingMetadata); - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - } - - private void DeleteExtraneousFiles(PackageIdentity pkgIdentity, string dirNameVersion) - { - // Deleting .nupkg SHA file, .nuspec, and .nupkg after unpacking the module - var pkgIdString = pkgIdentity.ToString(); - var nupkgSHAToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg.sha512"); - var nuspecToDelete = Path.Combine(dirNameVersion, pkgIdentity.Id + ".nuspec"); - var nupkgToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg"); - var nupkgMetadataToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg.metadata"); - var contentTypesToDelete = Path.Combine(dirNameVersion, "[Content_Types].xml"); - var relsDirToDelete = Path.Combine(dirNameVersion, "_rels"); - var packageDirToDelete = Path.Combine(dirNameVersion, "package"); - - // Unforunately have to check if each file exists because it may or may not be there - if (File.Exists(nupkgSHAToDelete)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nupkgSHAToDelete)); - File.Delete(nupkgSHAToDelete); - } - if (File.Exists(nuspecToDelete)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nuspecToDelete)); - File.Delete(nuspecToDelete); - } - if (File.Exists(nupkgToDelete)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nupkgToDelete)); - File.Delete(nupkgToDelete); - } - if (File.Exists(nupkgMetadataToDelete)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nupkgMetadataToDelete)); - File.Delete(nupkgMetadataToDelete); - } - if (File.Exists(contentTypesToDelete)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", contentTypesToDelete)); - File.Delete(contentTypesToDelete); - } - if (Directory.Exists(relsDirToDelete)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", relsDirToDelete)); - Utils.DeleteDirectory(relsDirToDelete); - } - if (Directory.Exists(packageDirToDelete)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", packageDirToDelete)); - Utils.DeleteDirectory(packageDirToDelete); - } - } - - private bool TryDeleteDirectory( - string tempInstallPath, - out ErrorRecord errorMsg) - { - errorMsg = null; - - try - { - Utils.DeleteDirectory(tempInstallPath); - } - catch (Exception e) - { - var TempDirCouldNotBeDeletedError = new ErrorRecord(e, "errorDeletingTempInstallPath", ErrorCategory.InvalidResult, null); - errorMsg = TempDirCouldNotBeDeletedError; - return false; - } - - return true; - } - - private void MoveFilesIntoInstallPath( - PSResourceInfo pkgInfo, - bool isModule, - bool isLocalRepo, - string dirNameVersion, - string tempInstallPath, - string installPath, - string newVersion, - string moduleManifestVersion, - string scriptPath) - { - // Creating the proper installation path depending on whether pkg is a module or script - var newPathParent = isModule ? Path.Combine(installPath, pkgInfo.Name) : installPath; - var finalModuleVersionDir = isModule ? Path.Combine(installPath, pkgInfo.Name, moduleManifestVersion) : installPath; - - // If script, just move the files over, if module, move the version directory over - var tempModuleVersionDir = (!isModule || isLocalRepo) ? dirNameVersion - : Path.Combine(tempInstallPath, pkgInfo.Name.ToLower(), newVersion); - - _cmdletPassedIn.WriteVerbose(string.Format("Installation source path is: '{0}'", tempModuleVersionDir)); - _cmdletPassedIn.WriteVerbose(string.Format("Installation destination path is: '{0}'", finalModuleVersionDir)); - - if (isModule) - { - // If new path does not exist - if (!Directory.Exists(newPathParent)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); - Directory.CreateDirectory(newPathParent); - Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); - } - else - { - _cmdletPassedIn.WriteVerbose(string.Format("Temporary module version directory is: '{0}'", tempModuleVersionDir)); - - if (Directory.Exists(finalModuleVersionDir)) - { - // Delete the directory path before replacing it with the new module. - // If deletion fails (usually due to binary file in use), then attempt restore so that the currently - // installed module is not corrupted. - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to delete with restore on failure.'{0}'", finalModuleVersionDir)); - Utils.DeleteDirectoryWithRestore(finalModuleVersionDir); - } - - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); - Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); - } - } - else - { - if (!_savePkg) - { - // Need to delete old xml files because there can only be 1 per script - var scriptXML = pkgInfo.Name + "_InstalledScriptInfo.xml"; - _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)))); - if (File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML))) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting script metadata XML")); - File.Delete(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); - } - - _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML))); - Utils.MoveFiles(Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); - - // Need to delete old script file, if that exists - _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1")))); - if (File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1"))) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting script file")); - File.Delete(Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1")); - } - } - - _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1"))); - Utils.MoveFiles(scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1")); - } - } - - #endregion - } -} + } + + private void MoveFilesIntoInstallPath( + PSResourceInfo pkgInfo, + bool isModule, + bool isLocalRepo, + string dirNameVersion, + string tempInstallPath, + string installPath, + string newVersion, + string moduleManifestVersion, + string scriptPath) + { + // Creating the proper installation path depending on whether pkg is a module or script + var newPathParent = isModule ? Path.Combine(installPath, pkgInfo.Name) : installPath; + var finalModuleVersionDir = isModule ? Path.Combine(installPath, pkgInfo.Name, moduleManifestVersion) : installPath; + + // If script, just move the files over, if module, move the version directory over + var tempModuleVersionDir = (!isModule || isLocalRepo) ? dirNameVersion + : Path.Combine(tempInstallPath, pkgInfo.Name.ToLower(), newVersion); + + _cmdletPassedIn.WriteVerbose(string.Format("Installation source path is: '{0}'", tempModuleVersionDir)); + _cmdletPassedIn.WriteVerbose(string.Format("Installation destination path is: '{0}'", finalModuleVersionDir)); + + if (isModule) + { + // If new path does not exist + if (!Directory.Exists(newPathParent)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); + Directory.CreateDirectory(newPathParent); + Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); + } + else + { + _cmdletPassedIn.WriteVerbose(string.Format("Temporary module version directory is: '{0}'", tempModuleVersionDir)); + + if (Directory.Exists(finalModuleVersionDir)) + { + // Delete the directory path before replacing it with the new module. + // If deletion fails (usually due to binary file in use), then attempt restore so that the currently + // installed module is not corrupted. + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to delete with restore on failure.'{0}'", finalModuleVersionDir)); + Utils.DeleteDirectoryWithRestore(finalModuleVersionDir); + } + + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); + Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); + } + } + else + { + if (!_savePkg) + { + // Need to delete old xml files because there can only be 1 per script + var scriptXML = pkgInfo.Name + "_InstalledScriptInfo.xml"; + _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)))); + if (File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML))) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting script metadata XML")); + File.Delete(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); + } + + _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML))); + Utils.MoveFiles(Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); + + // Need to delete old script file, if that exists + _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1")))); + if (File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1"))) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting script file")); + File.Delete(Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1")); + } + } + + _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1"))); + Utils.MoveFiles(scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + ".ps1")); + } + } + + private Hashtable GetAuthenticodePublisher(Signature authenticodeSignature, string pkgName) + { + Hashtable publisherInfo = new Hashtable(); + if (authenticodeSignature.SignerCertificate != null) + { + X509Chain chain = new X509Chain(); + chain.Build(authenticodeSignature.SignerCertificate); + + foreach (X509ChainElement element in chain.ChainElements) + { + foreach (var certStoreLocation in certStoreLocations) + { + // TODO: come back and update this + var results = PowerShellInvoker.InvokeScriptWithHost( + cmdlet: _cmdletPassedIn, + script: @" + param ([string] $certStoreLocation ) + + Microsoft.PowerShell.Management\Get-ChildItem -Path $certStoreLocation | + Microsoft.PowerShell.Core\Where-Object { ($_.Subject -eq $element.Certificate.Subject) -and ($_.thumbprint -eq $element.Certificate.Thumbprint) } + ", + args: new object[] { certStoreLocation, element }, + out Exception terminatingError); + + + if (terminatingError != null) + { + ThrowTerminatingError( + new ErrorRecord( + new PSInvalidOperationException( + message: $"Install-PSResource encountered an error while authenticating certificate for \"{pkgName}\" from certificate store \"{certStoreLocation}\".", + innerException: terminatingError), + "InstallPSResourceCannotReadCertFromStore", + ErrorCategory.InvalidResult, + this)); + } + + X509Certificate2 rootCertificateAuthority = results.Any() ? (X509Certificate2)results.FirstOrDefault() : null; + + if (rootCertificateAuthority != null) + { + publisherInfo.Add("Publisher", authenticodeSignature.SignerCertificate.Subject); + publisherInfo.Add("PublisherRootCA", rootCertificateAuthority); + + return publisherInfo; + } + } + } + } + + return publisherInfo; + } + + private bool IsMicrosoftCert(Signature authenticodeSignature) + { + bool isMicrosoftCert = false; + if (authenticodeSignature.SignerCertificate != null) + { + SafeX509ChainHandle safex509ChainHandle = null; + try + { + X509Chain chain = new X509Chain(); + chain.Build(authenticodeSignature.SignerCertificate); + + // safehandle is available with dotnet api https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles?view=net-6.0 + safex509ChainHandle = chain.SafeHandle; + + isMicrosoftCert = IsMicrosoftCertificateHelper(safex509ChainHandle); + + } + catch (Exception e) + { + // throw + + } + + if (safex509ChainHandle != null) + { + safex509ChainHandle.Dispose(); + } + } + + return isMicrosoftCert; + } + + public static bool IsMicrosoftCertificateHelper(SafeX509ChainHandle pChainContext) + { + //------------------------------------------------------------------------- + // CERT_CHAIN_POLICY_MICROSOFT_ROOT + // + // Checks if the last element of the first simple chain contains a + // Microsoft root public key. If it doesn't contain a Microsoft root + // public key, dwError is set to CERT_E_UNTRUSTEDROOT. + // + // pPolicyPara is optional. However, + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG can be set in + // the dwFlags in pPolicyPara to also check for the Microsoft Test Roots. + // + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can be set + // in the dwFlags in pPolicyPara to check for the Microsoft root for + // application signing instead of the Microsoft product root. This flag + // explicitly checks for the application root only and cannot be combined + // with the test root flag. + // + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG can be set + // in the dwFlags in pPolicyPara to always disable the Flight root. + // + // pvExtraPolicyPara and pvExtraPolicyStatus aren't used and must be set + // to NULL. + //-------------------------------------------------------------------------- + const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG = 0x00010000; + const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG = 0x00020000; + //const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG = 0x00040000; + CERT_CHAIN_POLICY_PARA PolicyPara = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); + CERT_CHAIN_POLICY_STATUS PolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); + int CERT_CHAIN_POLICY_MICROSOFT_ROOT = 7; + PolicyPara.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG; + bool isMicrosoftRoot = false; + if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), + pChainContext, + ref PolicyPara, + ref PolicyStatus)) + { + isMicrosoftRoot = (PolicyStatus.dwError == 0); + } + // Also check for the Microsoft root for application signing if the Microsoft product root verification is unsuccessful. + if (!isMicrosoftRoot) + { + // Some Microsoft modules can be signed with Microsoft Application Root instead of Microsoft Product Root, + // So we need to use the MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG for the certificate verification. + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can not be used + // with MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG, + // so additional CertVerifyCertificateChainPolicy call is required to verify the given certificate is in Microsoft Application Root. + // + CERT_CHAIN_POLICY_PARA PolicyPara2 = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); + CERT_CHAIN_POLICY_STATUS PolicyStatus2 = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); + PolicyPara2.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; + if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), + pChainContext, + ref PolicyPara2, + ref PolicyStatus2)) + { + isMicrosoftRoot = (PolicyStatus2.dwError == 0); + } + } + return isMicrosoftRoot; + } + + [DllImport("Crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)] + public extern static + bool CertVerifyCertificateChainPolicy( + IntPtr pszPolicyOID, + SafeX509ChainHandle pChainContext, + ref CERT_CHAIN_POLICY_PARA pPolicyPara, + ref CERT_CHAIN_POLICY_STATUS pPolicyStatus); + + + #endregion + } +} From 7c25ceef7d10772578de57626bd9708bb1808066 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 21 Apr 2022 19:01:12 -0700 Subject: [PATCH 06/34] Fix changes made after merge conflict --- src/code/InstallHelper.cs | 358 +++++++++++++++++++++++++++++++++++++- 1 file changed, 353 insertions(+), 5 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 1d727abd6..4236edfdc 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.PowerShell.Commands; using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using Microsoft.Win32.SafeHandles; using MoreLinq.Extensions; using NuGet.Common; using NuGet.Configuration; @@ -14,11 +16,14 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Management.Automation; using System.Net; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using System.Threading; @@ -35,6 +40,7 @@ internal class InstallHelper : PSCmdlet public const string PSScriptFileExt = ".ps1"; private const string MsgRepositoryNotTrusted = "Untrusted repository"; private const string MsgInstallUntrustedPackage = "You are installing the modules from an untrusted repository. If you trust this repository, change its Trusted value by running the Set-PSResourceRepository cmdlet. Are you sure you want to install the PSresource from '{0}' ?"; + private readonly string[] certStoreLocations = { "cert:\\LocalMachine\\Root", "cert:\\LocalMachine\\AuthRoot", "cert:\\CurrentUser\\Root", "cert:\\CurrentUser\\AuthRoot" }; private CancellationToken _cancellationToken; private readonly PSCmdlet _cmdletPassedIn; @@ -50,12 +56,49 @@ internal class InstallHelper : PSCmdlet private bool _asNupkg; private bool _includeXML; private bool _noClobber; + private bool _skipPublisherCheck; private bool _savePkg; List _pathsToSearch; List _pkgNamesToInstall; #endregion + + #region Enums + + public struct CERT_CHAIN_POLICY_PARA + { + public CERT_CHAIN_POLICY_PARA(int size) + { + cbSize = (uint)size; + dwFlags = 0; + pvExtraPolicyPara = IntPtr.Zero; + } + public uint cbSize; + public uint dwFlags; + public IntPtr pvExtraPolicyPara; + } + + public struct CERT_CHAIN_POLICY_STATUS + { + public CERT_CHAIN_POLICY_STATUS(int size) + { + cbSize = (uint)size; + dwError = 0; + lChainIndex = IntPtr.Zero; + lElementIndex = IntPtr.Zero; + pvExtraPolicyStatus = IntPtr.Zero; + } + public uint cbSize; + public uint dwError; + public IntPtr lChainIndex; + public IntPtr lElementIndex; + public IntPtr pvExtraPolicyStatus; + } + + #endregion + + #region Public methods public InstallHelper(PSCmdlet cmdletPassedIn) @@ -80,6 +123,7 @@ public List InstallPackages( bool asNupkg, bool includeXML, bool skipDependencyCheck, + bool skipPublisherCheck, bool savePkg, List pathsToInstallPkg) { @@ -101,6 +145,7 @@ public List InstallPackages( _versionRange = versionRange; _prerelease = prerelease; _acceptLicense = acceptLicense || force; + _skipPublisherCheck = skipPublisherCheck || force; _quiet = quiet; _reinstall = reinstall; _force = force; @@ -463,8 +508,8 @@ private List InstallPackage( string tempDirNameVersion = isLocalRepo ? tempInstallPath : Path.Combine(tempInstallPath, pkgIdentity.Id.ToLower(), newVersion); var version4digitNoPrerelease = pkgIdentity.Version.Version.ToString(); string moduleManifestVersion = string.Empty; - var scriptPath = Path.Combine(tempDirNameVersion, pkg.Name + PSScriptFileExt); - var modulePath = Path.Combine(tempDirNameVersion, pkg.Name + PSDataFileExt); + var scriptPath = Path.Combine(tempDirNameVersion, pkg.Name + ".ps1"); + var modulePath = Path.Combine(tempDirNameVersion, pkg.Name + ".psd1"); // Check if the package is a module or a script var isModule = File.Exists(modulePath); @@ -499,6 +544,19 @@ private List InstallPackage( if (isModule) { + + if (!_skipPublisherCheck && !PublisherValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath)) + { + _cmdletPassedIn.WriteVerbose("Publisher validation failed."); + ThrowTerminatingError( + new ErrorRecord( + new PSInvalidOperationException( + message: $"Install-PSResource publisher validation is invalid."), + "InstallPSResourcePublisherValidation", + ErrorCategory.InvalidResult, + _cmdletPassedIn)); + } + var moduleManifest = Path.Combine(tempDirNameVersion, pkgIdentity.Id + PSDataFileExt); if (!File.Exists(moduleManifest)) { @@ -537,7 +595,7 @@ private List InstallPackage( if (_includeXML) { - CreateMetadataXMLFile(tempDirNameVersion, installPath, pkg, isModule); + CreateMetadataXMLFile(tempDirNameVersion, installPath, newVersion, pkg, isModule); } MoveFilesIntoInstallPath( @@ -588,6 +646,139 @@ private List InstallPackage( return pkgsSuccessfullyInstalled; } + private bool PublisherValidation(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath) + { + // 1) See if the current module that is trying to be installed is already installed + GetHelper getHelper = new GetHelper(_cmdletPassedIn); + var moduleInstallPath = Path.Combine(installPath, pkgName); + var pkgVersionsAlreadyInstalled = getHelper.GetPackagesFromPath(new string[] { pkgName }, VersionRange.All, new List { moduleInstallPath }, _prerelease); + + string signedFilePath = string.Empty; + var catalogFileName = pkgName + ".cat"; + string moduleBasePath = string.Empty; + + PSResourceInfo resourceObj = null; + if (pkgVersionsAlreadyInstalled != null) // && pkgVersionsAlreadyInstalled.FirstOrDefault() != null) + { + + resourceObj = pkgVersionsAlreadyInstalled.FirstOrDefault(); + + if (resourceObj == null) + { + return true; + } + + signedFilePath = Path.Combine(resourceObj.InstalledLocation, catalogFileName); + + if (!File.Exists(signedFilePath)) { + + return true; + } + } + + // 2) If the module is already installed (an earlier version of the module, or same version being reinstalled, get the authenticode signature + Collection authenticodeSignature = new Collection(); + try + { + authenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $signedFilePath) Get-AuthenticodeSignature -FilePath $signedFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { signedFilePath }); + } + catch (Exception e){ + + _cmdletPassedIn.WriteVerbose(e.Message); + } + + Signature signature = (authenticodeSignature.Any() && authenticodeSignature[0] != null) ? (Signature)authenticodeSignature[0].BaseObject : null; + + if (signature == null) + { + return false; + } + + // 2b) If you're able to get the authenticode signature, get the module details + Hashtable moduleDetails = new Hashtable(); + + moduleDetails.Add("AuthenticodeSignature", signature); + moduleDetails.Add("Version", resourceObj.Version.ToString()); + moduleDetails.Add("ModuleBase", moduleBasePath); + + // Microsoft cert check is working well! + var isMicrosoftCert = IsMicrosoftCert(signature); + moduleDetails.Add("IsMicrosoftCertificate", isMicrosoftCert); + + /// UP TO HERE LOOKS GOOD! + + var publisherDetails = GetAuthenticodePublisher(signature, pkgName); + if (publisherDetails.Count == 2) + { + moduleDetails.Add("Publisher", publisherDetails["Publisher"]); + moduleDetails.Add("RootCertificateAuthority", publisherDetails["PublisherRootCA"]); + } + + + + // 3) Validate the catalog signature for the current module being installed + string catalogFilePath = Path.Combine(tempDirNameVersion, catalogFileName); + Collection catalogAuthenticodeSignature = new Collection(); + try + { + catalogAuthenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $catalogFilePath) Get-AuthenticodeSignature -FilePath $catalogFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { catalogFilePath }); + } + catch { } + + Signature catalogSignature = (catalogAuthenticodeSignature.Any() && catalogAuthenticodeSignature[0] != null) ? (Signature)catalogAuthenticodeSignature[0].BaseObject : null; + + if (catalogSignature == null || !catalogSignature.Status.Equals(SignatureStatus.Valid)) + { + return false; + } + + // Run catalog validation + Collection TestFileCatalogResult = new Collection(); + CatalogInformation catalogValidation = null; + try + { + TestFileCatalogResult = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $moduleBasePath, [string] $catalogFilePath) Test-FileCatalog -Path $moduleBasePath" + + $" -CatalogFilePath $CatalogFilePath" + + $" -FilesToSkip $script: PSGetItemInfoFileName,'*.cat','*.nupkg','*.nuspec'" + + $" -Detailed" + + $" -ErrorAction SilentlyContinue", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { moduleBasePath, catalogFilePath }); + + catalogValidation = (TestFileCatalogResult.Any() && TestFileCatalogResult[0] != null) ? (CatalogInformation)TestFileCatalogResult[0].BaseObject : null; + } + catch { } + + if (catalogValidation == null || !catalogValidation.Status.Equals(SignatureStatus.Valid) + || (catalogValidation.Signature != null && !catalogValidation.Signature.Status.Equals(SignatureStatus.Valid))) + { + return false; + } + + // 5) if there is an installed module, and we have the info for the current module, + // test these scenarios: + // $InstalledModuleAuthenticodePublisher == $InstalledModuleDetails.Publisher + // $InstalledModuleVersion = $InstalledModuleDetails.Version + + // $InstalledModuleRootCA = $InstalledModuleDetails.RootCertificateAuthority + // ???? $IsInstalledModuleSignedByMicrosoft = $InstalledModuleDetails.IsMicrosoftCertificate + + return true; + } + private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string tempInstallPath, string newVersion) { var requireLicenseAcceptance = false; @@ -723,7 +914,7 @@ private bool DetectClobber(string pkgName, Hashtable parsedMetadataHashtable) return foundClobber; } - private void CreateMetadataXMLFile(string dirNameVersion, string installPath, PSResourceInfo pkg, bool isModule) + private void CreateMetadataXMLFile(string dirNameVersion, string installPath, string pkgVersion, PSResourceInfo pkg, bool isModule) { // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" // Modules will have the metadata file: "PSGetModuleInfo.xml" @@ -731,7 +922,7 @@ private void CreateMetadataXMLFile(string dirNameVersion, string installPath, PS : Path.Combine(dirNameVersion, (pkg.Name + "_InstalledScriptInfo.xml")); pkg.InstalledDate = DateTime.Now; - pkg.InstalledLocation = installPath; + pkg.InstalledLocation = Path.Combine(installPath, pkg.Name, pkgVersion); // Write all metadata into metadataXMLPath if (!pkg.TryWrite(metadataXMLPath, out string error)) @@ -893,6 +1084,163 @@ private void MoveFilesIntoInstallPath( } } + private Hashtable GetAuthenticodePublisher(Signature authenticodeSignature, string pkgName) + { + Hashtable publisherInfo = new Hashtable(); + if (authenticodeSignature.SignerCertificate != null) + { + X509Chain chain = new X509Chain(); + chain.Build(authenticodeSignature.SignerCertificate); + + foreach (X509ChainElement element in chain.ChainElements) + { + foreach (var certStoreLocation in certStoreLocations) + { + // TODO: come back and update this + var results = PowerShellInvoker.InvokeScriptWithHost( + cmdlet: _cmdletPassedIn, + script: @" + param ([string] $certStoreLocation ) + + Microsoft.PowerShell.Management\Get-ChildItem -Path $certStoreLocation | + Microsoft.PowerShell.Core\Where-Object { ($_.Subject -eq $element.Certificate.Subject) -and ($_.thumbprint -eq $element.Certificate.Thumbprint) } + ", + args: new object[] { certStoreLocation, element }, + out Exception terminatingError); + + + if (terminatingError != null) + { + ThrowTerminatingError( + new ErrorRecord( + new PSInvalidOperationException( + message: $"Install-PSResource encountered an error while authenticating certificate for \"{pkgName}\" from certificate store \"{certStoreLocation}\".", + innerException: terminatingError), + "InstallPSResourceCannotReadCertFromStore", + ErrorCategory.InvalidResult, + this)); + } + + X509Certificate2 rootCertificateAuthority = results.Any() ? (X509Certificate2)results.FirstOrDefault() : null; + + if (rootCertificateAuthority != null) + { + publisherInfo.Add("Publisher", authenticodeSignature.SignerCertificate.Subject); + publisherInfo.Add("PublisherRootCA", rootCertificateAuthority); + + return publisherInfo; + } + } + } + } + + return publisherInfo; + } + + private bool IsMicrosoftCert(Signature authenticodeSignature) + { + bool isMicrosoftCert = false; + if (authenticodeSignature.SignerCertificate != null) + { + SafeX509ChainHandle safex509ChainHandle = null; + try + { + X509Chain chain = new X509Chain(); + chain.Build(authenticodeSignature.SignerCertificate); + + // safehandle is available with dotnet api https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles?view=net-6.0 + safex509ChainHandle = chain.SafeHandle; + + isMicrosoftCert = IsMicrosoftCertificateHelper(safex509ChainHandle); + + } + catch (Exception e) + { + // throw + + } + + if (safex509ChainHandle != null) + { + safex509ChainHandle.Dispose(); + } + } + + return isMicrosoftCert; + } + + public static bool IsMicrosoftCertificateHelper(SafeX509ChainHandle pChainContext) + { + //------------------------------------------------------------------------- + // CERT_CHAIN_POLICY_MICROSOFT_ROOT + // + // Checks if the last element of the first simple chain contains a + // Microsoft root public key. If it doesn't contain a Microsoft root + // public key, dwError is set to CERT_E_UNTRUSTEDROOT. + // + // pPolicyPara is optional. However, + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG can be set in + // the dwFlags in pPolicyPara to also check for the Microsoft Test Roots. + // + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can be set + // in the dwFlags in pPolicyPara to check for the Microsoft root for + // application signing instead of the Microsoft product root. This flag + // explicitly checks for the application root only and cannot be combined + // with the test root flag. + // + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG can be set + // in the dwFlags in pPolicyPara to always disable the Flight root. + // + // pvExtraPolicyPara and pvExtraPolicyStatus aren't used and must be set + // to NULL. + //-------------------------------------------------------------------------- + const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG = 0x00010000; + const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG = 0x00020000; + //const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG = 0x00040000; + CERT_CHAIN_POLICY_PARA PolicyPara = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); + CERT_CHAIN_POLICY_STATUS PolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); + int CERT_CHAIN_POLICY_MICROSOFT_ROOT = 7; + PolicyPara.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG; + bool isMicrosoftRoot = false; + if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), + pChainContext, + ref PolicyPara, + ref PolicyStatus)) + { + isMicrosoftRoot = (PolicyStatus.dwError == 0); + } + // Also check for the Microsoft root for application signing if the Microsoft product root verification is unsuccessful. + if (!isMicrosoftRoot) + { + // Some Microsoft modules can be signed with Microsoft Application Root instead of Microsoft Product Root, + // So we need to use the MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG for the certificate verification. + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can not be used + // with MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG, + // so additional CertVerifyCertificateChainPolicy call is required to verify the given certificate is in Microsoft Application Root. + // + CERT_CHAIN_POLICY_PARA PolicyPara2 = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); + CERT_CHAIN_POLICY_STATUS PolicyStatus2 = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); + PolicyPara2.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; + if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), + pChainContext, + ref PolicyPara2, + ref PolicyStatus2)) + { + isMicrosoftRoot = (PolicyStatus2.dwError == 0); + } + } + return isMicrosoftRoot; + } + + [DllImport("Crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)] + public extern static + bool CertVerifyCertificateChainPolicy( + IntPtr pszPolicyOID, + SafeX509ChainHandle pChainContext, + ref CERT_CHAIN_POLICY_PARA pPolicyPara, + ref CERT_CHAIN_POLICY_STATUS pPolicyStatus); + + #endregion } } From d2021cb8f754e2ac3fa3a8d18beb12447aea0b60 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 21 Apr 2022 19:24:57 -0700 Subject: [PATCH 07/34] Attempt to resolve diff issues shows in GitHub UI --- src/code/InstallHelper.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 4236edfdc..d19e733dd 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -64,7 +64,7 @@ internal class InstallHelper : PSCmdlet #endregion - #region Enums + #region Enums public struct CERT_CHAIN_POLICY_PARA { @@ -544,7 +544,6 @@ private List InstallPackage( if (isModule) { - if (!_skipPublisherCheck && !PublisherValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath)) { _cmdletPassedIn.WriteVerbose("Publisher validation failed."); @@ -670,12 +669,13 @@ private bool PublisherValidation(string pkgName, string tempDirNameVersion, Vers signedFilePath = Path.Combine(resourceObj.InstalledLocation, catalogFileName); - if (!File.Exists(signedFilePath)) { + if (!File.Exists(signedFilePath)) + { - return true; + return true; } } - + // 2) If the module is already installed (an earlier version of the module, or same version being reinstalled, get the authenticode signature Collection authenticodeSignature = new Collection(); try @@ -687,7 +687,8 @@ private bool PublisherValidation(string pkgName, string tempDirNameVersion, Vers input: null, args: new object[] { signedFilePath }); } - catch (Exception e){ + catch (Exception e) + { _cmdletPassedIn.WriteVerbose(e.Message); } @@ -1232,7 +1233,7 @@ public static bool IsMicrosoftCertificateHelper(SafeX509ChainHandle pChainContex return isMicrosoftRoot; } - [DllImport("Crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)] + [DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] public extern static bool CertVerifyCertificateChainPolicy( IntPtr pszPolicyOID, From 814b548ff3d84d02688c6a24399446bfeec13aa4 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 21 Apr 2022 19:44:45 -0700 Subject: [PATCH 08/34] Update exceptions in InstallHelper --- src/code/InstallHelper.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index d19e733dd..f30c681e5 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -1119,7 +1119,7 @@ private Hashtable GetAuthenticodePublisher(Signature authenticodeSignature, stri innerException: terminatingError), "InstallPSResourceCannotReadCertFromStore", ErrorCategory.InvalidResult, - this)); + _cmdletPassedIn)); } X509Certificate2 rootCertificateAuthority = results.Any() ? (X509Certificate2)results.FirstOrDefault() : null; @@ -1157,8 +1157,14 @@ private bool IsMicrosoftCert(Signature authenticodeSignature) } catch (Exception e) { - // throw - + ThrowTerminatingError( + new ErrorRecord( + new PSInvalidOperationException( + message: $"Install-PSResource encountered an error while checking if the module uses a valid Microsoft certificate.", + innerException: e.InnerException), + "InstallPSResourceFailedToCheckIfMicrosoftCert", + ErrorCategory.InvalidResult, + _cmdletPassedIn)); } if (safex509ChainHandle != null) From ecd10064a8be7513c4189aa6281fe02219952693 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Fri, 22 Apr 2022 12:47:32 -0700 Subject: [PATCH 09/34] Add publisher validation tests --- test/InstallPSResource.Tests.ps1 | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index c6f173483..ff9500a46 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -12,6 +12,7 @@ Describe 'Test Install-PSResource for Module' { $testModuleName = "test_module" $testModuleName2 = "TestModule99" $testScriptName = "test_script" + $PackageManagement = "PackageManagement" $RequiredResourceJSONFileName = "TestRequiredResourceFile.json" $RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1" Get-NewPSResourceRepositoryFile @@ -415,6 +416,68 @@ Describe 'Test Install-PSResource for Module' { $res3.Name | Should -Be $testModuleName2 $res3.Version | Should -Be "0.0.93.0" } + + # First install module 1.4.3 (with catalog file) + # Then install module 1.4.4 (with catalog file) + # Should install both successfully + It "Install modules using publisher validation" { + Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.3" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.3" + + Install-PSResource -Name $PackageManagement -Version "1.4.4" -Repository $PSGalleryName -TrustRepository + + $res2 = Get-PSResource $PackageManagement -Version "1.4.4" + $res2.Name | Should -Be $PackageManagement + $res2.Version | Should -Be "1.4.4" + } + + # First install module 1.4.7 (with NO catalog file) + # Then install install 1.4.3 (with catalog file) + # Should install both successfully + It "Install modules using publisher validation" { + Install-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.7" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.7" + + Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository + + $res2 = Get-PSResource $PackageManagement -Version "1.4.3" + $res2.Name | Should -Be $PackageManagement + $res2.Version | Should -Be "1.4.3" + } + + # First install module 1.4.3 (with catalog file) + # Then try to install 1.4.7 (with NO catalog file) + # Should install the first module successfully, then FAIL to install the second module + It "Install modules using publisher validation" { + Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.3" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.3" + + Install-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + $Error[0].FullyQualifiedErrorId | Should -be "CommandAlreadyExists,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + # First install module 1.4.3 (with catalog file) + # Then try to install 1.4.4.1 (with incorrect catalog file) + # Should install the first module successfully, then FAIL to install the second module + It "Install modules using publisher validation" { + Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.3" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.3" + + Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + $Error[0].FullyQualifiedErrorId | Should -be "InstallPSResourcePublisherValidation,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } } <# Temporarily commented until -Tag is implemented for this Describe block From 78b25f30cf4b6d83be1fd26f1108c9ccba7e69cc Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Sat, 23 Apr 2022 06:24:35 -0700 Subject: [PATCH 10/34] Update tests --- test/InstallPSResource.Tests.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index ff9500a46..a4a8cd876 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -425,13 +425,13 @@ Describe 'Test Install-PSResource for Module' { $res1 = Get-PSResource $PackageManagement -Version "1.4.3" $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.3" + $res1.Version | Should -Be "1.4.3.0" Install-PSResource -Name $PackageManagement -Version "1.4.4" -Repository $PSGalleryName -TrustRepository $res2 = Get-PSResource $PackageManagement -Version "1.4.4" $res2.Name | Should -Be $PackageManagement - $res2.Version | Should -Be "1.4.4" + $res2.Version | Should -Be "1.4.4.0" } # First install module 1.4.7 (with NO catalog file) @@ -442,13 +442,13 @@ Describe 'Test Install-PSResource for Module' { $res1 = Get-PSResource $PackageManagement -Version "1.4.7" $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.7" + $res1.Version | Should -Be "1.4.7.0" Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository $res2 = Get-PSResource $PackageManagement -Version "1.4.3" $res2.Name | Should -Be $PackageManagement - $res2.Version | Should -Be "1.4.3" + $res2.Version | Should -Be "1.4.3.0" } # First install module 1.4.3 (with catalog file) @@ -459,7 +459,7 @@ Describe 'Test Install-PSResource for Module' { $res1 = Get-PSResource $PackageManagement -Version "1.4.3" $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.3" + $res1.Version | Should -Be "1.4.3.0" Install-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue $Error[0].FullyQualifiedErrorId | Should -be "CommandAlreadyExists,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" @@ -473,7 +473,7 @@ Describe 'Test Install-PSResource for Module' { $res1 = Get-PSResource $PackageManagement -Version "1.4.3" $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.3" + $res1.Version | Should -Be "1.4.3.0" Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue $Error[0].FullyQualifiedErrorId | Should -be "InstallPSResourcePublisherValidation,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" From 811360db1c15fbe8b5e58e7164ef1bc0130627ce Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 25 Apr 2022 03:21:26 -0700 Subject: [PATCH 11/34] Update tests --- test/InstallPSResource.Tests.ps1 | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index a4a8cd876..c4ee66e9e 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -20,7 +20,7 @@ Describe 'Test Install-PSResource for Module' { } 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 { @@ -420,7 +420,7 @@ Describe 'Test Install-PSResource for Module' { # First install module 1.4.3 (with catalog file) # Then install module 1.4.4 (with catalog file) # Should install both successfully - It "Install modules using publisher validation" { + It "Install modules with catalog file using publisher validation" { Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.3" @@ -437,7 +437,7 @@ Describe 'Test Install-PSResource for Module' { # First install module 1.4.7 (with NO catalog file) # Then install install 1.4.3 (with catalog file) # Should install both successfully - It "Install modules using publisher validation" { + It "Install module with catalog file over module with no catalog file" { Install-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.7" @@ -453,22 +453,25 @@ Describe 'Test Install-PSResource for Module' { # First install module 1.4.3 (with catalog file) # Then try to install 1.4.7 (with NO catalog file) - # Should install the first module successfully, then FAIL to install the second module - It "Install modules using publisher validation" { + # Should install both successfully + It "Install module with no catalog file, should" { Install-PSResource -Name $PackageManagement -Version "1.4.3" -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-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue - $Error[0].FullyQualifiedErrorId | Should -be "CommandAlreadyExists,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + + Install-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.7" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.7.0" } # First install module 1.4.3 (with catalog file) # Then try to install 1.4.4.1 (with incorrect catalog file) # Should install the first module successfully, then FAIL to install the second module - It "Install modules using publisher validation" { + It "Install module with incorrect catalog file" { Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.3" @@ -476,7 +479,7 @@ Describe 'Test Install-PSResource for Module' { $res1.Version | Should -Be "1.4.3.0" Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue - $Error[0].FullyQualifiedErrorId | Should -be "InstallPSResourcePublisherValidation,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" } } From 0354781a1f78dfcda403f2a408b238d0078dfe55 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 25 Apr 2022 03:21:47 -0700 Subject: [PATCH 12/34] Remove CatalogInformation calss --- src/code/CatalogInformation.cs | 58 ---------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 src/code/CatalogInformation.cs diff --git a/src/code/CatalogInformation.cs b/src/code/CatalogInformation.cs deleted file mode 100644 index cbe9f5f75..000000000 --- a/src/code/CatalogInformation.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Management.Automation; - - -namespace Microsoft.PowerShell.PowerShellGet.UtilClasses -{ - #region Enums - - public enum CatalogValidationStatus - { - Valid, - ValidationFailed - } - - #endregion - - #region CatalogInformation - - public sealed class CatalogInformation - { - #region Properties - - public CatalogValidationStatus Status { get; } - public Signature Signature { get; } - public string HashAlgorithm { get; } - public Dictionary CatalogItems { get; } - public Dictionary PathItems { get; } - - #endregion - - #region Constructors - - private CatalogInformation() { } - - private CatalogInformation( - CatalogValidationStatus status, - Signature signature, - string hashAlgorithm, - Dictionary catalogItems, - Dictionary pathItems) - { - Status = status; - Signature = signature; - HashAlgorithm = hashAlgorithm ?? string.Empty; - CatalogItems = catalogItems ?? new Dictionary(); - pathItems = pathItems ?? new Dictionary(); - } - - #endregion - } - - #endregion -} From 83ad396e41d8f00c069b3fa5bbe11af3a166bdbe Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 25 Apr 2022 09:29:42 -0700 Subject: [PATCH 13/34] Refactor publisher verification --- src/code/InstallHelper.cs | 203 ++++++++++++++++++++++++++------------ 1 file changed, 138 insertions(+), 65 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index f30c681e5..9fb2f050d 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -653,30 +653,66 @@ private bool PublisherValidation(string pkgName, string tempDirNameVersion, Vers var pkgVersionsAlreadyInstalled = getHelper.GetPackagesFromPath(new string[] { pkgName }, VersionRange.All, new List { moduleInstallPath }, _prerelease); string signedFilePath = string.Empty; - var catalogFileName = pkgName + ".cat"; - string moduleBasePath = string.Empty; + var installedModuleManifest = pkgName + ".psd1"; + Signature installedSignature = null; PSResourceInfo resourceObj = null; - if (pkgVersionsAlreadyInstalled != null) // && pkgVersionsAlreadyInstalled.FirstOrDefault() != null) - { - - resourceObj = pkgVersionsAlreadyInstalled.FirstOrDefault(); + Hashtable installedModuleDetails = null; - if (resourceObj == null) + resourceObj = pkgVersionsAlreadyInstalled.FirstOrDefault(); + if (resourceObj != null) + { + // If there is no version of this package already installed, just validate that any signatures are valid. + // 2a) If the module is already installed (an earlier version of the module, or same version being reinstalled) get the authenticode signature of that module + // Use the module manifest to validate that the signature is valid + signedFilePath = Path.Combine(resourceObj.InstalledLocation, installedModuleManifest); + if (!File.Exists(signedFilePath)) { return true; } - signedFilePath = Path.Combine(resourceObj.InstalledLocation, catalogFileName); + Collection installedAuthenticodeSignature = new Collection(); + try + { + installedAuthenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $signedFilePath) Get-AuthenticodeSignature -FilePath $signedFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { signedFilePath }); + } + catch (Exception e) + { + _cmdletPassedIn.WriteVerbose(e.Message); + //eror? + } - if (!File.Exists(signedFilePath)) + installedSignature = (installedAuthenticodeSignature.Any() && installedAuthenticodeSignature[0] != null) ? (Signature)installedAuthenticodeSignature[0].BaseObject : null; + + // 2b) If you're able to get the authenticode signature, get the module details for the previously installed module + if (installedSignature != null) { - return true; + installedModuleDetails = new Hashtable(); + + installedModuleDetails.Add("AuthenticodeSignature", installedSignature); + + var installedIsMicrosoftCert = IsMicrosoftCert(installedSignature); + installedModuleDetails.Add("IsMicrosoftCertificate", installedIsMicrosoftCert); + + var installedPublisherDetails = GetAuthenticodePublisher(installedSignature, pkgName); + if (installedPublisherDetails.Count == 2) + { + installedModuleDetails.Add("Publisher", installedPublisherDetails["Publisher"]); + installedModuleDetails.Add("RootCertificateAuthority", installedPublisherDetails["PublisherRootCA"]); + } } + + // All done validating previously installed module } - // 2) If the module is already installed (an earlier version of the module, or same version being reinstalled, get the authenticode signature + // 3) Validate the authenticode signature for the current module being installed + signedFilePath = Path.Combine(tempDirNameVersion, installedModuleManifest); Collection authenticodeSignature = new Collection(); try { @@ -689,30 +725,89 @@ private bool PublisherValidation(string pkgName, string tempDirNameVersion, Vers } catch (Exception e) { - _cmdletPassedIn.WriteVerbose(e.Message); } Signature signature = (authenticodeSignature.Any() && authenticodeSignature[0] != null) ? (Signature)authenticodeSignature[0].BaseObject : null; - if (signature == null) + // If the authenticode signature is not valid, return false + if (signature == null || (!signature.Status.Equals(SignatureStatus.Valid) && !signature.Status.Equals(SignatureStatus.NotSigned))) { return false; } - // 2b) If you're able to get the authenticode signature, get the module details + // Check that the catalog file is signed properly + string catalogFilePath = Path.Combine(tempDirNameVersion, pkgName + ".cat"); + if (File.Exists(catalogFilePath)) + { + Collection catalogAuthenticodeSignature = new Collection(); + try + { + catalogAuthenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: $"param ([string] $catalogFilePath) Get-AuthenticodeSignature -FilePath $catalogFilePath", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { catalogFilePath }); + } + catch + { + + } + + Signature catalogSignature = (catalogAuthenticodeSignature.Any() && catalogAuthenticodeSignature[0] != null) ? (Signature)catalogAuthenticodeSignature[0].BaseObject : null; + + if (catalogSignature == null || !catalogSignature.Status.Equals(SignatureStatus.Valid)) + { + return false; + } + + // Run catalog validation + Collection TestFileCatalogResult = new Collection(); + string moduleBasePath = tempDirNameVersion; + try + { + TestFileCatalogResult = _cmdletPassedIn.InvokeCommand.InvokeScript( + script: @"param ( + [string] $moduleBasePath, + [string] $catalogFilePath + ) + $catalogValidation = Test-FileCatalog -Path $moduleBasePath -CatalogFilePath $CatalogFilePath ` + -FilesToSkip '*.cat','*.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) + { + _cmdletPassedIn.WriteVerbose(e.Message); + } + + + bool catalogValidation = (TestFileCatalogResult[0] != null) ? (bool)TestFileCatalogResult[0].BaseObject : false; + + if (!catalogValidation) + { + return false; + } + } + Hashtable moduleDetails = new Hashtable(); moduleDetails.Add("AuthenticodeSignature", signature); - moduleDetails.Add("Version", resourceObj.Version.ToString()); - moduleDetails.Add("ModuleBase", moduleBasePath); - - // Microsoft cert check is working well! var isMicrosoftCert = IsMicrosoftCert(signature); moduleDetails.Add("IsMicrosoftCertificate", isMicrosoftCert); - /// UP TO HERE LOOKS GOOD! - var publisherDetails = GetAuthenticodePublisher(signature, pkgName); if (publisherDetails.Count == 2) { @@ -720,62 +815,41 @@ private bool PublisherValidation(string pkgName, string tempDirNameVersion, Vers moduleDetails.Add("RootCertificateAuthority", publisherDetails["PublisherRootCA"]); } - - - // 3) Validate the catalog signature for the current module being installed - string catalogFilePath = Path.Combine(tempDirNameVersion, catalogFileName); - Collection catalogAuthenticodeSignature = new Collection(); - try + // 5) if there is an installed module, and we have the info for the current module, test these scenarios: + if (!signature.Status.Equals(SignatureStatus.Valid) && !signature.Status.Equals(SignatureStatus.NotSigned)) { - catalogAuthenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( - script: $"param ([string] $catalogFilePath) Get-AuthenticodeSignature -FilePath $catalogFilePath", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { catalogFilePath }); + return false; } - catch { } - Signature catalogSignature = (catalogAuthenticodeSignature.Any() && catalogAuthenticodeSignature[0] != null) ? (Signature)catalogAuthenticodeSignature[0].BaseObject : null; - - if (catalogSignature == null || !catalogSignature.Status.Equals(SignatureStatus.Valid)) + // Issuer is the name of the certificate authority that issued the certificate + if (installedSignature != null && signature != null && !signature.SignerCertificate.Issuer.Equals(installedSignature.SignerCertificate.Issuer)) { return false; } - // Run catalog validation - Collection TestFileCatalogResult = new Collection(); - CatalogInformation catalogValidation = null; - try + // A) Signed by Microsoft is a match? if the installed version was signed by ms and the new one isn't throw error + if (installedModuleDetails != null && moduleDetails != null && (bool)installedModuleDetails["IsMicrosoftCertificate"] && (bool)moduleDetails["IsMicrosoftCertificate"]) { - TestFileCatalogResult = _cmdletPassedIn.InvokeCommand.InvokeScript( - script: $"param ([string] $moduleBasePath, [string] $catalogFilePath) Test-FileCatalog -Path $moduleBasePath" + - $" -CatalogFilePath $CatalogFilePath" + - $" -FilesToSkip $script: PSGetItemInfoFileName,'*.cat','*.nupkg','*.nuspec'" + - $" -Detailed" + - $" -ErrorAction SilentlyContinue", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { moduleBasePath, catalogFilePath }); - - catalogValidation = (TestFileCatalogResult.Any() && TestFileCatalogResult[0] != null) ? (CatalogInformation)TestFileCatalogResult[0].BaseObject : null; + //Throw } - catch { } - if (catalogValidation == null || !catalogValidation.Status.Equals(SignatureStatus.Valid) - || (catalogValidation.Signature != null && !catalogValidation.Signature.Status.Equals(SignatureStatus.Valid))) - { - return false; - } - // 5) if there is an installed module, and we have the info for the current module, - // test these scenarios: - // $InstalledModuleAuthenticodePublisher == $InstalledModuleDetails.Publisher - // $InstalledModuleVersion = $InstalledModuleDetails.Version - // $InstalledModuleRootCA = $InstalledModuleDetails.RootCertificateAuthority - // ???? $IsInstalledModuleSignedByMicrosoft = $InstalledModuleDetails.IsMicrosoftCertificate + + // B) Authenticode publisher is a match + if (installedModuleDetails != null && publisherDetails != null) + { + if (!publisherDetails["Publisher"].Equals(installedModuleDetails["Publisher"])) + { + return false; + } + + // C) RootCertificateAuthority is a match + if (!publisherDetails["PublisherRootCA"].Equals(installedModuleDetails["PublisherRootCA"])) + { + return false; + } + } return true; } @@ -1097,7 +1171,6 @@ private Hashtable GetAuthenticodePublisher(Signature authenticodeSignature, stri { foreach (var certStoreLocation in certStoreLocations) { - // TODO: come back and update this var results = PowerShellInvoker.InvokeScriptWithHost( cmdlet: _cmdletPassedIn, script: @" From 3407a11f16f24a205ee4d8cae078a937c4b26827 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Tue, 26 Apr 2022 16:54:58 -0700 Subject: [PATCH 14/34] Refactor 'packagevalidation' code in installhelper --- src/code/InstallHelper.cs | 221 +++++++++++--------------------------- 1 file changed, 62 insertions(+), 159 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 9fb2f050d..533c8aad5 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -542,20 +542,13 @@ private List InstallPackage( : _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); } - if (isModule) + if (!_skipPublisherCheck && !PackageValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, out ErrorRecord errorRecord)) { - if (!_skipPublisherCheck && !PublisherValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath)) - { - _cmdletPassedIn.WriteVerbose("Publisher validation failed."); - ThrowTerminatingError( - new ErrorRecord( - new PSInvalidOperationException( - message: $"Install-PSResource publisher validation is invalid."), - "InstallPSResourcePublisherValidation", - ErrorCategory.InvalidResult, - _cmdletPassedIn)); - } + ThrowTerminatingError(errorRecord); + } + if (isModule) + { var moduleManifest = Path.Combine(tempDirNameVersion, pkgIdentity.Id + PSDataFileExt); if (!File.Exists(moduleManifest)) { @@ -645,135 +638,33 @@ private List InstallPackage( return pkgsSuccessfullyInstalled; } - private bool PublisherValidation(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath) + private bool PackageValidation(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath, out ErrorRecord errorRecord) { - // 1) See if the current module that is trying to be installed is already installed - GetHelper getHelper = new GetHelper(_cmdletPassedIn); - var moduleInstallPath = Path.Combine(installPath, pkgName); - var pkgVersionsAlreadyInstalled = getHelper.GetPackagesFromPath(new string[] { pkgName }, VersionRange.All, new List { moduleInstallPath }, _prerelease); - - string signedFilePath = string.Empty; - var installedModuleManifest = pkgName + ".psd1"; - - Signature installedSignature = null; - PSResourceInfo resourceObj = null; - Hashtable installedModuleDetails = null; - - resourceObj = pkgVersionsAlreadyInstalled.FirstOrDefault(); - if (resourceObj != null) - { - // If there is no version of this package already installed, just validate that any signatures are valid. - // 2a) If the module is already installed (an earlier version of the module, or same version being reinstalled) get the authenticode signature of that module - // Use the module manifest to validate that the signature is valid - signedFilePath = Path.Combine(resourceObj.InstalledLocation, installedModuleManifest); - if (!File.Exists(signedFilePath)) - { - return true; - } - - Collection installedAuthenticodeSignature = new Collection(); - try - { - installedAuthenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( - script: $"param ([string] $signedFilePath) Get-AuthenticodeSignature -FilePath $signedFilePath", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { signedFilePath }); - } - catch (Exception e) - { - _cmdletPassedIn.WriteVerbose(e.Message); - //eror? - } - - installedSignature = (installedAuthenticodeSignature.Any() && installedAuthenticodeSignature[0] != null) ? (Signature)installedAuthenticodeSignature[0].BaseObject : null; - - // 2b) If you're able to get the authenticode signature, get the module details for the previously installed module - if (installedSignature != null) - { - - installedModuleDetails = new Hashtable(); - - installedModuleDetails.Add("AuthenticodeSignature", installedSignature); - - var installedIsMicrosoftCert = IsMicrosoftCert(installedSignature); - installedModuleDetails.Add("IsMicrosoftCertificate", installedIsMicrosoftCert); - - var installedPublisherDetails = GetAuthenticodePublisher(installedSignature, pkgName); - if (installedPublisherDetails.Count == 2) - { - installedModuleDetails.Add("Publisher", installedPublisherDetails["Publisher"]); - installedModuleDetails.Add("RootCertificateAuthority", installedPublisherDetails["PublisherRootCA"]); - } - } - - // All done validating previously installed module - } - - // 3) Validate the authenticode signature for the current module being installed - signedFilePath = Path.Combine(tempDirNameVersion, installedModuleManifest); - Collection authenticodeSignature = new Collection(); - try - { - authenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( - script: $"param ([string] $signedFilePath) Get-AuthenticodeSignature -FilePath $signedFilePath", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { signedFilePath }); - } - catch (Exception e) - { - _cmdletPassedIn.WriteVerbose(e.Message); - } + errorRecord = null; - Signature signature = (authenticodeSignature.Any() && authenticodeSignature[0] != null) ? (Signature)authenticodeSignature[0].BaseObject : null; - - // If the authenticode signature is not valid, return false - if (signature == null || (!signature.Status.Equals(SignatureStatus.Valid) && !signature.Status.Equals(SignatureStatus.NotSigned))) + // 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 false; + return true; } - + // Check that the catalog file is signed properly string catalogFilePath = Path.Combine(tempDirNameVersion, pkgName + ".cat"); if (File.Exists(catalogFilePath)) { - Collection catalogAuthenticodeSignature = new Collection(); - try - { - catalogAuthenticodeSignature = _cmdletPassedIn.InvokeCommand.InvokeScript( - script: $"param ([string] $catalogFilePath) Get-AuthenticodeSignature -FilePath $catalogFilePath", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { catalogFilePath }); - } - catch - { - - } - - Signature catalogSignature = (catalogAuthenticodeSignature.Any() && catalogAuthenticodeSignature[0] != null) ? (Signature)catalogAuthenticodeSignature[0].BaseObject : null; - - if (catalogSignature == null || !catalogSignature.Status.Equals(SignatureStatus.Valid)) - { - return false; - } - // Run catalog validation Collection TestFileCatalogResult = new Collection(); 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 '*.cat','*.nupkg','*.nuspec', '*.nupkg.metadata', '*.nupkg.sha512' ` + -FilesToSkip '*.nupkg','*.nuspec', '*.nupkg.metadata', '*.nupkg.sha512' ` -Detailed -ErrorAction SilentlyContinue if ($catalogValidation.Status.ToString() -eq 'valid' -and $catalogValidation.Signature.Status -eq 'valid') { @@ -786,68 +677,80 @@ private bool PublisherValidation(string pkgName, string tempDirNameVersion, Vers useNewScope: true, writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, input: null, - args: new object[] { moduleBasePath, catalogFilePath }); + args: new object[] { moduleBasePath, catalogFilePath }); } catch (Exception e) { - _cmdletPassedIn.WriteVerbose(e.Message); + errorRecord = new ErrorRecord(new ArgumentException(e.Message), "TestFileCatalogError", ErrorCategory.InvalidResult, null); } - 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, null); return false; } } - Hashtable moduleDetails = new Hashtable(); - moduleDetails.Add("AuthenticodeSignature", signature); - var isMicrosoftCert = IsMicrosoftCert(signature); - moduleDetails.Add("IsMicrosoftCertificate", isMicrosoftCert); - - var publisherDetails = GetAuthenticodePublisher(signature, pkgName); - if (publisherDetails.Count == 2) - { - moduleDetails.Add("Publisher", publisherDetails["Publisher"]); - moduleDetails.Add("RootCertificateAuthority", publisherDetails["PublisherRootCA"]); - } - - // 5) if there is an installed module, and we have the info for the current module, test these scenarios: - if (!signature.Status.Equals(SignatureStatus.Valid) && !signature.Status.Equals(SignatureStatus.NotSigned)) + // TODO: validate all files (need to figure out what all files are (ie what files shouldn't be validated) : list of extensions + Collection authenticodeSignature = new Collection(); + try { - return false; + 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 }); } - - // Issuer is the name of the certificate authority that issued the certificate - if (installedSignature != null && signature != null && !signature.SignerCertificate.Issuer.Equals(installedSignature.SignerCertificate.Issuer)) + catch (Exception e) { - return false; + errorRecord = new ErrorRecord(new ArgumentException(e.Message), "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, null); } - // A) Signed by Microsoft is a match? if the installed version was signed by ms and the new one isn't throw error - if (installedModuleDetails != null && moduleDetails != null && (bool)installedModuleDetails["IsMicrosoftCertificate"] && (bool)moduleDetails["IsMicrosoftCertificate"]) + // If the authenticode signature is not valid, return false + if (authenticodeSignature.Any() && authenticodeSignature[0] != null) { - //Throw - } - - + foreach (var signature in authenticodeSignature) + { + Signature sign = (Signature) signature.BaseObject; + if (!sign.Status.Equals(SignatureStatus.Valid)) + { + var exMessage = String.Format("The signature for '{0}' is '{1}.", pkgName, sign.Status.ToString()); + var ex = new ArgumentException(exMessage); + errorRecord = new ErrorRecord(ex, "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, null); + return false; + } + } - // B) Authenticode publisher is a match - if (installedModuleDetails != null && publisherDetails != null) - { - if (!publisherDetails["Publisher"].Equals(installedModuleDetails["Publisher"])) + var isMicrosoftCert = IsMicrosoftCert((Signature)authenticodeSignature[0].BaseObject); + if (isMicrosoftCert) { - return false; + _cmdletPassedIn.WriteVerbose(string.Format("Package '{0}' is signed by a Microsoft certificate.", pkgName)); } - // C) RootCertificateAuthority is a match - if (!publisherDetails["PublisherRootCA"].Equals(installedModuleDetails["PublisherRootCA"])) + var publisherDetails = GetAuthenticodePublisher((Signature)authenticodeSignature[0].BaseObject, pkgName); + if (publisherDetails.Count == 2) { - return false; + if (!string.IsNullOrEmpty(publisherDetails["Publisher"].ToString())) + { + _cmdletPassedIn.WriteVerbose(string.Format("Package '{0}' is published by publisher '{1}'.", pkgName, publisherDetails["Publisher"].ToString())); + } + + if (!string.IsNullOrEmpty(publisherDetails["PublisherRootCA"].ToString())) + { + _cmdletPassedIn.WriteVerbose(string.Format("Package '{0}' has the publisher root certificate authority of '{1}'.", pkgName, publisherDetails["PublisherRootCA"].ToString())); + } } } From a1cb378bf103afe3df43b52c860dce403683929b Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Tue, 26 Apr 2022 16:55:10 -0700 Subject: [PATCH 15/34] Update install-psresource tests --- test/InstallPSResource.Tests.ps1 | 85 +++++++++++++++++--------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index c4ee66e9e..3d1dba631 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -417,70 +417,75 @@ Describe 'Test Install-PSResource for Module' { $res3.Version | Should -Be "0.0.93.0" } - # First install module 1.4.3 (with catalog file) - # Then install module 1.4.4 (with catalog file) - # Should install both successfully + # Install module 1.4.3 (is authenticode signed and has catalog file) + # Should install successfully It "Install modules with catalog file using publisher validation" { Install-PSResource -Name $PackageManagement -Version "1.4.3" -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-PSResource -Name $PackageManagement -Version "1.4.4" -Repository $PSGalleryName -TrustRepository - - $res2 = Get-PSResource $PackageManagement -Version "1.4.4" - $res2.Name | Should -Be $PackageManagement - $res2.Version | Should -Be "1.4.4.0" } - # First install module 1.4.7 (with NO catalog file) - # Then install install 1.4.3 (with catalog file) - # Should install both successfully - It "Install module with catalog file over module with no catalog file" { + # Install module 1.4.7 (is authenticode signed and has NO catalog file) + # Should not install successfully + It "Install module with no catalog file" { Install-PSResource -Name $PackageManagement -Version "1.4.7" -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-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository - - $res2 = Get-PSResource $PackageManagement -Version "1.4.3" - $res2.Name | Should -Be $PackageManagement - $res2.Version | Should -Be "1.4.3.0" + $res1.Version | Should -Be "1.4.7.0" } - # First install module 1.4.3 (with catalog file) - # Then try to install 1.4.7 (with NO catalog file) - # Should install both successfully - It "Install module with no catalog file, should" { - Install-PSResource -Name $PackageManagement -Version "1.4.3" -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-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository + # Install module 1.4.3 (with NO catalog file) + # Should install successfully + It "Install module with no catalog file and with -SkipPackageValidation" { + Install-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPublisherCheck -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.7" $res1.Name | Should -Be $PackageManagement $res1.Version | Should -Be "1.4.7.0" } - # First install module 1.4.3 (with catalog file) - # Then try to install 1.4.4.1 (with incorrect catalog file) - # Should install the first module successfully, then FAIL to install the second module + # Install module that is not authenticode signed + # Should FAIL to install the module + It "Install module that is not authenticode signed" { + Install-PSResource -Name $testModuleName -Version "5.0.0" -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" { - Install-PSResource -Name $PackageManagement -Version "1.4.3" -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-PSResource -Name $PackageManagement -Version "1.4.4.1" -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" { + Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -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 signed + # Should install successfully + It "Install script that is not authenticode signed with -SkipPublisherCheck" { + Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPublisherCheck -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource "TestTestScript" -Version "1.3.1.1" + $res1.Name | Should -Be "TestTestScript" + $res1.Version | Should -Be "1.3.1.1" + } + + # Install script that is not signed + # Should throw + It "Install script that is not signed" { + Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -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 From 4a6e6db9ee0b146a2bb573e43692ea5aef75c8d5 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Tue, 26 Apr 2022 16:57:30 -0700 Subject: [PATCH 16/34] Update parameter for Install-PSResource --- src/code/InstallPSResource.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index d907a97de..2829b0b31 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -105,11 +105,10 @@ class InstallPSResource : PSCmdlet public SwitchParameter SkipDependencyCheck { get; set; } /// - /// Skips the check for resource dependencies, so that only found resources are installed, - /// and not any resources the found resource depends on. + /// Skips the check for package validation. /// [Parameter] - public SwitchParameter SkipPublisherCheck { get; set; } + public SwitchParameter SkipPackageValidation { get; set; } /// /// Passes the resource installed to the console. @@ -520,7 +519,7 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, - skipPublisherCheck: SkipPublisherCheck, + skipPackageValidation: SkipPackageValidation, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); From a99d7cb4adcfad6bdcf431ef383334ee7698f303 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Tue, 26 Apr 2022 17:28:07 -0700 Subject: [PATCH 17/34] Update InstallHelper and InstallPSResource tests --- src/code/InstallHelper.cs | 8 ++++---- test/InstallPSResource.Tests.ps1 | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 533c8aad5..10aca329a 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -56,7 +56,7 @@ internal class InstallHelper : PSCmdlet private bool _asNupkg; private bool _includeXML; private bool _noClobber; - private bool _skipPublisherCheck; + private bool _skipPackageValidation; private bool _savePkg; List _pathsToSearch; List _pkgNamesToInstall; @@ -123,7 +123,7 @@ public List InstallPackages( bool asNupkg, bool includeXML, bool skipDependencyCheck, - bool skipPublisherCheck, + bool skipPackageValidation, bool savePkg, List pathsToInstallPkg) { @@ -145,7 +145,7 @@ public List InstallPackages( _versionRange = versionRange; _prerelease = prerelease; _acceptLicense = acceptLicense || force; - _skipPublisherCheck = skipPublisherCheck || force; + _skipPackageValidation = skipPackageValidation || force; _quiet = quiet; _reinstall = reinstall; _force = force; @@ -542,7 +542,7 @@ private List InstallPackage( : _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); } - if (!_skipPublisherCheck && !PackageValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, out ErrorRecord errorRecord)) + if (!_skipPackageValidation && !PackageValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, out ErrorRecord errorRecord)) { ThrowTerminatingError(errorRecord); } diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index 3d1dba631..bed2dd902 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -30,7 +30,7 @@ Describe 'Test Install-PSResource for Module' { $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, @{Name="Test_Module*"; ErrorId="NameContainsWildcard"}, @{Name="Test?Module","Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} - +<# It "Should not install resource with wildcard in name" -TestCases $testCases { param($Name, $ErrorId) Install-PSResource -Name $Name -ErrorVariable err -ErrorAction SilentlyContinue @@ -416,7 +416,7 @@ 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" { @@ -440,7 +440,7 @@ Describe 'Test Install-PSResource for Module' { # Install module 1.4.3 (with NO catalog file) # Should install successfully It "Install module with no catalog file and with -SkipPackageValidation" { - Install-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPublisherCheck -Repository $PSGalleryName -TrustRepository + Install-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.7" $res1.Name | Should -Be $PackageManagement @@ -473,7 +473,7 @@ Describe 'Test Install-PSResource for Module' { # Install script that is signed # Should install successfully It "Install script that is not authenticode signed with -SkipPublisherCheck" { - Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPublisherCheck -Repository $PSGalleryName -TrustRepository + Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource "TestTestScript" -Version "1.3.1.1" $res1.Name | Should -Be "TestTestScript" From b952add42c1388bc009593038feb0e5c241fca24 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Tue, 26 Apr 2022 18:10:43 -0700 Subject: [PATCH 18/34] Add Update-PSResource -SkipPackageValidation parameters and tests --- src/code/UpdatePSResource.cs | 8 +++- test/UpdatePSResource.Tests.ps1 | 74 ++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index 7839d48d2..9854f48c9 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -110,6 +110,12 @@ public sealed class UpdatePSResource : PSCmdlet [Parameter] public SwitchParameter SkipDependencyCheck { get; set; } + /// + /// Skips the check for package validation. + /// + [Parameter] + public SwitchParameter SkipPackageValidation { get; set; } + #endregion #region Override Methods @@ -177,7 +183,7 @@ protected override void ProcessRecord() asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, - skipPublisherCheck: true, + skipPackageValidation: SkipPackageValidation, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); diff --git a/test/UpdatePSResource.Tests.ps1 b/test/UpdatePSResource.Tests.ps1 index 05b2bbf7b..d5c8f0ac6 100644 --- a/test/UpdatePSResource.Tests.ps1 +++ b/test/UpdatePSResource.Tests.ps1 @@ -13,12 +13,13 @@ Describe 'Test Update-PSResource' { $testModuleName = "test_module" $testModuleName2 = "test_module2" $testModuleName3 = "TestModule99" + $PackageManagement = "PackageManagement" Get-NewPSResourceRepositoryFile Get-PSResourceRepository } AfterEach { - Uninstall-PSResource "test_module", "TestModule99", "TestModuleWithLicense", "test_module2", "test_script" + Uninstall-PSResource "test_module", "TestModule99", "TestModuleWithLicense", "test_module2", "test_script", "PackaeManagement" -Version "*" } AfterAll { @@ -333,4 +334,75 @@ Describe 'Test Update-PSResource' { $res.Name | Should -Contain $testModuleName $res.Version | Should -Contain "3.0.0.0" } + + # Update to module 1.4.3 (is authenticode signed and has catalog file) + # Should update successfully + It "Update module with catalog file using publisher validation" { + Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.3" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.3.0" + } + + # Update to module 1.4.7 (is authenticode signed and has NO catalog file) + # Should update successfully + It "Install module with no catalog file" { + Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.7" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.7.0" + } + + # Update to module 1.4.3 (with NO catalog file) + # Should update successfully + It "Update module with no catalog file and with -SkipPackageValidation" { + Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.7" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.7.0" + } + + # Update to module 1.4.4.1 (with incorrect catalog file) + # Should FAIL to update the module + It "Update module with incorrect catalog file" { + Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name $PackageManagement -Version "1.4.4.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" + } + + # Update script that is signed + # Should update successfully + It "Update script that is authenticode signed" { + Install-PSResource -Name "Install-VSCode" -Version "1.4.1" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name "Install-VSCode" -Version "1.4.2" -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" + } + + # Update script that is not signed + # Should update successfully + It "Update script that is not authenticode signed with -SkipPackageValidation" { + Install-PSResource -Name "TestTestScript" -Version "1.0" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource "TestTestScript" -Version "1.3.1.1" + $res1.Name | Should -Be "TestTestScript" + $res1.Version | Should -Be "1.3.1.1" + } + + # Update script that is not signed + # Should throw + It "Update script that is not signed" { + Install-PSResource -Name "TestTestScript" -Version "1.0" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" + } } From 30c1db9253c724b0423695a7f13bd97499b4d14a Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Tue, 26 Apr 2022 23:39:41 -0700 Subject: [PATCH 19/34] Add parameter and tests for Save-PSResource --- src/code/SavePSResource.cs | 8 +++- test/InstallPSResource.Tests.ps1 | 4 +- test/SavePSResource.Tests.ps1 | 78 ++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index 5791edba4..064c46c4d 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -131,6 +131,12 @@ public string Path [Parameter] public SwitchParameter SkipDependencyCheck { get; set; } + /// + /// Skips the check for package validation. + /// + [Parameter] + public SwitchParameter SkipPackageValidation { get; set; } + /// /// Suppresses progress information. /// @@ -259,7 +265,7 @@ private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] p asNupkg: AsNupkg, includeXML: IncludeXML, skipDependencyCheck: SkipDependencyCheck, - skipPublisherCheck: true, + skipPackageValidation: SkipPackageValidation, savePkg: true, pathsToInstallPkg: new List { _path }); diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index bed2dd902..09e16dd86 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -30,7 +30,7 @@ Describe 'Test Install-PSResource for Module' { $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, @{Name="Test_Module*"; ErrorId="NameContainsWildcard"}, @{Name="Test?Module","Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} -<# + It "Should not install resource with wildcard in name" -TestCases $testCases { param($Name, $ErrorId) Install-PSResource -Name $Name -ErrorVariable err -ErrorAction SilentlyContinue @@ -416,7 +416,7 @@ 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" { diff --git a/test/SavePSResource.Tests.ps1 b/test/SavePSResource.Tests.ps1 index 8160144bc..27bbba0c0 100644 --- a/test/SavePSResource.Tests.ps1 +++ b/test/SavePSResource.Tests.ps1 @@ -12,6 +12,7 @@ Describe 'Test Save-PSResource for PSResources' { $testModuleName = "test_module" $testScriptName = "test_script" $testModuleName2 = "testmodule99" + $PackageManagement = "PackageManagement" Get-NewPSResourceRepositoryFile Register-LocalRepos @@ -217,6 +218,83 @@ 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" { + Save-PSResource -Name $PackageManagement -Version "1.4.3" -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" { + Save-PSResource -Name $PackageManagement -Version "1.4.7" -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 1.4.3 (with NO catalog file) + # Should save successfully + It "Save module with no catalog file and with -SkipPackageValidation" { + Save-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPackageValidation -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" { + Save-PSResource -Name $testModuleName -Version "5.0.0" -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" { + Save-PSResource -Name $PackageManagement -Version "1.4.4.1" -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 "FAILIN-- Save script that is authenticode signed" { + Save-PSResource -Name "Install-VSCode" -Version "1.4.2" -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 signed + # Should save successfully + It "FAILING-- ssSave script that is not authenticode signed with -SkipPublisherCheck" { + Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository -Path $SaveDir + + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "TestTestScript.ps1" + $pkgDir | Should -Not -BeNullOrEmpty + $pkgName = Get-ChildItem -Path $pkgDir.FullName + $pkgName.Name | Should -Be "TestTestScript.ps1" + } + + # Save script that is not signed + # Should throw + It "Save script that is not signed" { + Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -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" { From 419c2cb4554525314325b93868f49558e3311983 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Tue, 3 May 2022 11:40:03 -0700 Subject: [PATCH 20/34] Change 'packagevalidation' to 'authenticodecheck' --- src/code/InstallHelper.cs | 10 +++++----- src/code/InstallPSResource.cs | 4 ++-- src/code/SavePSResource.cs | 12 ++++++------ src/code/UpdatePSResource.cs | 12 ++++++------ test/InstallPSResource.Tests.ps1 | 6 +++--- test/SavePSResource.Tests.ps1 | 6 +++--- test/UpdatePSResource.Tests.ps1 | 12 ++++++------ 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 10aca329a..aed9dd4d6 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -56,7 +56,7 @@ internal class InstallHelper : PSCmdlet private bool _asNupkg; private bool _includeXML; private bool _noClobber; - private bool _skipPackageValidation; + private bool _authenticodeCheck; private bool _savePkg; List _pathsToSearch; List _pkgNamesToInstall; @@ -123,7 +123,7 @@ public List InstallPackages( bool asNupkg, bool includeXML, bool skipDependencyCheck, - bool skipPackageValidation, + bool authenticodeCheck, bool savePkg, List pathsToInstallPkg) { @@ -145,7 +145,7 @@ public List InstallPackages( _versionRange = versionRange; _prerelease = prerelease; _acceptLicense = acceptLicense || force; - _skipPackageValidation = skipPackageValidation || force; + _authenticodeCheck = authenticodeCheck; _quiet = quiet; _reinstall = reinstall; _force = force; @@ -542,7 +542,7 @@ private List InstallPackage( : _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); } - if (!_skipPackageValidation && !PackageValidation(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, out ErrorRecord errorRecord)) + if (_authenticodeCheck && !CheckAuthenticodeSignature(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, out ErrorRecord errorRecord)) { ThrowTerminatingError(errorRecord); } @@ -638,7 +638,7 @@ private List InstallPackage( return pkgsSuccessfullyInstalled; } - private bool PackageValidation(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath, out ErrorRecord errorRecord) + private bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath, out ErrorRecord errorRecord) { errorRecord = null; diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 2829b0b31..9f8cebd8a 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -108,7 +108,7 @@ class InstallPSResource : PSCmdlet /// Skips the check for package validation. /// [Parameter] - public SwitchParameter SkipPackageValidation { get; set; } + public SwitchParameter AuthenticodeCheck { get; set; } /// /// Passes the resource installed to the console. @@ -519,7 +519,7 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, - skipPackageValidation: SkipPackageValidation, + AuthenticodeCheck: AuthenticodeCheck, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index 064c46c4d..7de94c18f 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -131,11 +131,11 @@ public string Path [Parameter] public SwitchParameter SkipDependencyCheck { get; set; } - /// - /// Skips the check for package validation. - /// - [Parameter] - public SwitchParameter SkipPackageValidation { get; set; } + /// + /// Skips the check for package validation. + /// + [Parameter] + public SwitchParameter AuthenticodeCheck { get; set; } /// /// Suppresses progress information. @@ -265,7 +265,7 @@ private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] p asNupkg: AsNupkg, includeXML: IncludeXML, skipDependencyCheck: SkipDependencyCheck, - skipPackageValidation: SkipPackageValidation, + AuthenticodeCheck: AuthenticodeCheck, savePkg: true, pathsToInstallPkg: new List { _path }); diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index 9854f48c9..9098b0b00 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -110,11 +110,11 @@ public sealed class UpdatePSResource : PSCmdlet [Parameter] public SwitchParameter SkipDependencyCheck { get; set; } - /// - /// Skips the check for package validation. - /// - [Parameter] - public SwitchParameter SkipPackageValidation { get; set; } + /// + /// Skips the check for package validation. + /// + [Parameter] + public SwitchParameter AuthenticodeCheck { get; set; } #endregion @@ -183,7 +183,7 @@ protected override void ProcessRecord() asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, - skipPackageValidation: SkipPackageValidation, + AuthenticodeCheck: AuthenticodeCheck, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index 09e16dd86..a1174a1fa 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -439,8 +439,8 @@ Describe 'Test Install-PSResource for Module' { # Install module 1.4.3 (with NO catalog file) # Should install successfully - It "Install module with no catalog file and with -SkipPackageValidation" { - Install-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + It "Install module with no catalog file and with -AuthenticodeCheck" { + 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 @@ -473,7 +473,7 @@ Describe 'Test Install-PSResource for Module' { # Install script that is signed # Should install successfully It "Install script that is not authenticode signed with -SkipPublisherCheck" { - Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource "TestTestScript" -Version "1.3.1.1" $res1.Name | Should -Be "TestTestScript" diff --git a/test/SavePSResource.Tests.ps1 b/test/SavePSResource.Tests.ps1 index 27bbba0c0..e5a68c6ba 100644 --- a/test/SavePSResource.Tests.ps1 +++ b/test/SavePSResource.Tests.ps1 @@ -243,8 +243,8 @@ Describe 'Test Save-PSResource for PSResources' { # Save module 1.4.3 (with NO catalog file) # Should save successfully - It "Save module with no catalog file and with -SkipPackageValidation" { - Save-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository -Path $SaveDir + It "Save module with no catalog file and with -AuthenticodeCheck" { + 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 @@ -280,7 +280,7 @@ Describe 'Test Save-PSResource for PSResources' { # Save script that is signed # Should save successfully It "FAILING-- ssSave script that is not authenticode signed with -SkipPublisherCheck" { - Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository -Path $SaveDir + Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "TestTestScript.ps1" $pkgDir | Should -Not -BeNullOrEmpty diff --git a/test/UpdatePSResource.Tests.ps1 b/test/UpdatePSResource.Tests.ps1 index d5c8f0ac6..71c23bfe2 100644 --- a/test/UpdatePSResource.Tests.ps1 +++ b/test/UpdatePSResource.Tests.ps1 @@ -359,9 +359,9 @@ Describe 'Test Update-PSResource' { # Update to module 1.4.3 (with NO catalog file) # Should update successfully - It "Update module with no catalog file and with -SkipPackageValidation" { + It "Update module with no catalog file and with -AuthenticodeCheck" { Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name $PackageManagement -Version "1.4.7" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.7" $res1.Name | Should -Be $PackageManagement @@ -389,9 +389,9 @@ Describe 'Test Update-PSResource' { # Update script that is not signed # Should update successfully - It "Update script that is not authenticode signed with -SkipPackageValidation" { - Install-PSResource -Name "TestTestScript" -Version "1.0" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + It "Update script that is not authenticode signed with -AuthenticodeCheck" { + Install-PSResource -Name "TestTestScript" -Version "1.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource "TestTestScript" -Version "1.3.1.1" $res1.Name | Should -Be "TestTestScript" @@ -401,7 +401,7 @@ Describe 'Test Update-PSResource' { # Update script that is not signed # Should throw It "Update script that is not signed" { - Install-PSResource -Name "TestTestScript" -Version "1.0" -SkipPackageValidation -Repository $PSGalleryName -TrustRepository + Install-PSResource -Name "TestTestScript" -Version "1.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" } From 3da66f83a7899502d5c8395eee043b3d1d732979 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 9 May 2022 21:15:52 -0700 Subject: [PATCH 21/34] Change parameter for call to InstallPackages to 'authenticodeCheck' --- src/code/InstallPSResource.cs | 2 +- src/code/SavePSResource.cs | 2 +- src/code/UpdatePSResource.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 9f8cebd8a..5655ca0f8 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -519,7 +519,7 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, - AuthenticodeCheck: AuthenticodeCheck, + authenticodeCheck: AuthenticodeCheck, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index 7de94c18f..141bf0a99 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -265,7 +265,7 @@ private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] p asNupkg: AsNupkg, includeXML: IncludeXML, skipDependencyCheck: SkipDependencyCheck, - AuthenticodeCheck: AuthenticodeCheck, + authenticodeCheck: AuthenticodeCheck, savePkg: true, pathsToInstallPkg: new List { _path }); diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index 9098b0b00..52053db23 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -183,7 +183,7 @@ protected override void ProcessRecord() asNupkg: false, includeXML: true, skipDependencyCheck: SkipDependencyCheck, - AuthenticodeCheck: AuthenticodeCheck, + authenticodeCheck: AuthenticodeCheck, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg); From 23fa53a7916d0a6d60e5071959e653954e04bb06 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 9 May 2022 21:29:51 -0700 Subject: [PATCH 22/34] Update tests to reflect parameter name change --- test/InstallPSResource.Tests.ps1 | 38 +++++++++----------------------- test/SavePSResource.Tests.ps1 | 38 +++++++++----------------------- test/UpdatePSResource.Tests.ps1 | 32 +++++---------------------- 3 files changed, 26 insertions(+), 82 deletions(-) diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index a1174a1fa..88e16588c 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -417,73 +417,57 @@ Describe 'Test Install-PSResource for Module' { $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" { - Install-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository + 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) + # Install module 1.4.7 (is authenticode signed and has no catalog file) # Should not install successfully It "Install module with no catalog file" { - Install-PSResource -Name $PackageManagement -Version "1.4.7" -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 1.4.3 (with NO catalog file) - # Should install successfully - It "Install module with no catalog file and with -AuthenticodeCheck" { 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" + $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" { - Install-PSResource -Name $testModuleName -Version "5.0.0" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + 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" { - Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + 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" { - Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + 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 signed - # Should install successfully - It "Install script that is not authenticode signed with -SkipPublisherCheck" { - Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository - - $res1 = Get-PSResource "TestTestScript" -Version "1.3.1.1" - $res1.Name | Should -Be "TestTestScript" - $res1.Version | Should -Be "1.3.1.1" - } - # Install script that is not signed # Should throw It "Install script that is not signed" { - Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + 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" } } diff --git a/test/SavePSResource.Tests.ps1 b/test/SavePSResource.Tests.ps1 index e5a68c6ba..d5d0685b0 100644 --- a/test/SavePSResource.Tests.ps1 +++ b/test/SavePSResource.Tests.ps1 @@ -219,10 +219,14 @@ Describe 'Test Save-PSResource for PSResources' { $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" { - Save-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository -Path $SaveDir + 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 @@ -233,17 +237,6 @@ Describe 'Test Save-PSResource for PSResources' { # Save module 1.4.7 (is authenticode signed and has NO catalog file) # Should save successfully It "Save module with no catalog file" { - Save-PSResource -Name $PackageManagement -Version "1.4.7" -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 1.4.3 (with NO catalog file) - # Should save successfully - It "Save module with no catalog file and with -AuthenticodeCheck" { Save-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $PackageManagement @@ -255,21 +248,21 @@ Describe 'Test Save-PSResource for PSResources' { # Save module that is not authenticode signed # Should FAIL to save the module It "Save module that is not authenticode signed" { - Save-PSResource -Name $testModuleName -Version "5.0.0" -Repository $PSGalleryName -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue + 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" { - Save-PSResource -Name $PackageManagement -Version "1.4.4.1" -Repository $PSGalleryName -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue + 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 "FAILIN-- Save script that is authenticode signed" { - Save-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository -Path $SaveDir + It "Save script that is authenticode signed" { + 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 @@ -277,21 +270,10 @@ Describe 'Test Save-PSResource for PSResources' { $pkgName.Name | Should -Be "Install-VSCode.ps1" } - # Save script that is signed - # Should save successfully - It "FAILING-- ssSave script that is not authenticode signed with -SkipPublisherCheck" { - Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir - - $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "TestTestScript.ps1" - $pkgDir | Should -Not -BeNullOrEmpty - $pkgName = Get-ChildItem -Path $pkgDir.FullName - $pkgName.Name | Should -Be "TestTestScript.ps1" - } - # Save script that is not signed # Should throw It "Save script that is not signed" { - Save-PSResource -Name "TestTestScript" -Version "1.3.1.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + 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" } diff --git a/test/UpdatePSResource.Tests.ps1 b/test/UpdatePSResource.Tests.ps1 index 71c23bfe2..0ec52fdbf 100644 --- a/test/UpdatePSResource.Tests.ps1 +++ b/test/UpdatePSResource.Tests.ps1 @@ -339,7 +339,7 @@ Describe 'Test Update-PSResource' { # Should update successfully It "Update module with catalog file using publisher validation" { Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name $PackageManagement -Version "1.4.3" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.3" $res1.Name | Should -Be $PackageManagement @@ -349,17 +349,6 @@ Describe 'Test Update-PSResource' { # Update to module 1.4.7 (is authenticode signed and has NO catalog file) # Should update successfully It "Install module with no catalog file" { - Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name $PackageManagement -Version "1.4.7" -Repository $PSGalleryName -TrustRepository - - $res1 = Get-PSResource $PackageManagement -Version "1.4.7" - $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.7.0" - } - - # Update to module 1.4.3 (with NO catalog file) - # Should update successfully - It "Update module with no catalog file and with -AuthenticodeCheck" { Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository @@ -372,7 +361,7 @@ Describe 'Test Update-PSResource' { # Should FAIL to update the module It "Update module with incorrect catalog file" { Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name $PackageManagement -Version "1.4.4.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + Update-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" } @@ -380,29 +369,18 @@ Describe 'Test Update-PSResource' { # Should update successfully It "Update script that is authenticode signed" { Install-PSResource -Name "Install-VSCode" -Version "1.4.1" -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + Update-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" } - # Update script that is not signed - # Should update successfully - It "Update script that is not authenticode signed with -AuthenticodeCheck" { - Install-PSResource -Name "TestTestScript" -Version "1.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository - - $res1 = Get-PSResource "TestTestScript" -Version "1.3.1.1" - $res1.Name | Should -Be "TestTestScript" - $res1.Version | Should -Be "1.3.1.1" - } - # Update script that is not signed # Should throw It "Update script that is not signed" { - Install-PSResource -Name "TestTestScript" -Version "1.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + Install-PSResource -Name "TestTestScript" -Version "1.0" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" } } From 48751558579548ca81b2c66ea194b0487ee5a8d6 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 9 May 2022 22:11:04 -0700 Subject: [PATCH 23/34] Run -AuthenticodeCheck tests on windows only --- test/InstallPSResource.Tests.ps1 | 16 ++++++---------- test/SavePSResource.Tests.ps1 | 16 ++++++---------- test/UpdatePSResource.Tests.ps1 | 10 +++++----- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index 88e16588c..06414a7f9 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -417,13 +417,9 @@ Describe 'Test Install-PSResource for Module' { $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" { + 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" @@ -433,7 +429,7 @@ Describe 'Test Install-PSResource for Module' { # Install module 1.4.7 (is authenticode signed and has no catalog file) # Should not install successfully - It "Install module with no catalog file" { + 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" @@ -443,20 +439,20 @@ Describe 'Test Install-PSResource for Module' { # Install module that is not authenticode signed # Should FAIL to install the module - It "Install module that is not authenticode signed" { + 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" { + 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" { + 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" @@ -466,7 +462,7 @@ Describe 'Test Install-PSResource for Module' { # Install script that is not signed # Should throw - It "Install script that is not signed" { + 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" } diff --git a/test/SavePSResource.Tests.ps1 b/test/SavePSResource.Tests.ps1 index d5d0685b0..1c3b612e1 100644 --- a/test/SavePSResource.Tests.ps1 +++ b/test/SavePSResource.Tests.ps1 @@ -219,13 +219,9 @@ Describe 'Test Save-PSResource for PSResources' { $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" { + 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 @@ -236,7 +232,7 @@ Describe 'Test Save-PSResource for PSResources' { # Save module 1.4.7 (is authenticode signed and has NO catalog file) # Should save successfully - It "Save module with no catalog file" { + 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 @@ -247,21 +243,21 @@ Describe 'Test Save-PSResource for PSResources' { # Save module that is not authenticode signed # Should FAIL to save the module - It "Save module that is not authenticode signed" { + 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" { + 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" { + 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" @@ -272,7 +268,7 @@ Describe 'Test Save-PSResource for PSResources' { # Save script that is not signed # Should throw - It "Save script that is not signed" { + 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" } diff --git a/test/UpdatePSResource.Tests.ps1 b/test/UpdatePSResource.Tests.ps1 index 0ec52fdbf..3aac0b2a1 100644 --- a/test/UpdatePSResource.Tests.ps1 +++ b/test/UpdatePSResource.Tests.ps1 @@ -337,7 +337,7 @@ Describe 'Test Update-PSResource' { # Update to module 1.4.3 (is authenticode signed and has catalog file) # Should update successfully - It "Update module with catalog file using publisher validation" { + It "Update module with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) { Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository @@ -348,7 +348,7 @@ Describe 'Test Update-PSResource' { # Update to module 1.4.7 (is authenticode signed and has NO catalog file) # Should update successfully - It "Install module with no catalog file" { + It "Install module with no catalog file" -Skip:(!(Get-IsWindows)) { Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository @@ -359,7 +359,7 @@ Describe 'Test Update-PSResource' { # Update to module 1.4.4.1 (with incorrect catalog file) # Should FAIL to update the module - It "Update module with incorrect catalog file" { + It "Update module with incorrect catalog file" -Skip:(!(Get-IsWindows)) { Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" @@ -367,7 +367,7 @@ Describe 'Test Update-PSResource' { # Update script that is signed # Should update successfully - It "Update script that is authenticode signed" { + It "Update script that is authenticode signed" -Skip:(!(Get-IsWindows)) { Install-PSResource -Name "Install-VSCode" -Version "1.4.1" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository @@ -378,7 +378,7 @@ Describe 'Test Update-PSResource' { # Update script that is not signed # Should throw - It "Update script that is not signed" { + It "Update script that is not signed" -Skip:(!(Get-IsWindows)) { Install-PSResource -Name "TestTestScript" -Version "1.0" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue $Error[0].FullyQualifiedErrorId | Should -be "InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" From 0800859b5d573cac43cd31953c39a35dca492dc1 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 May 2022 10:45:23 -0700 Subject: [PATCH 24/34] Update src/code/InstallPSResource.cs Co-authored-by: Anam Navied --- src/code/InstallPSResource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 5655ca0f8..98c64a346 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -105,7 +105,7 @@ class InstallPSResource : PSCmdlet public SwitchParameter SkipDependencyCheck { get; set; } /// - /// Skips the check for package validation. + /// Check validation for signed and catalog files /// [Parameter] public SwitchParameter AuthenticodeCheck { get; set; } From 9ef8bc09322f1bf63b1f08e14bebefd607ef5966 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 May 2022 10:45:30 -0700 Subject: [PATCH 25/34] Update src/code/UpdatePSResource.cs Co-authored-by: Anam Navied --- src/code/UpdatePSResource.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index 52053db23..1035b3ecf 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -111,7 +111,8 @@ public sealed class UpdatePSResource : PSCmdlet public SwitchParameter SkipDependencyCheck { get; set; } /// - /// Skips the check for package validation. + /// Check validation for signed and catalog files + /// [Parameter] public SwitchParameter AuthenticodeCheck { get; set; } From e357a0f34a0b2d29661b6e45845e68e328ae8e64 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 May 2022 10:48:07 -0700 Subject: [PATCH 26/34] Update src/code/InstallHelper.cs Co-authored-by: Anam Navied --- src/code/InstallHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index aed9dd4d6..994ddff5d 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -230,7 +230,7 @@ private List ProcessRepositories( _cmdletPassedIn.WriteVerbose("Untrusted repository accepted as trusted source."); // If it can't find the pkg in one repository, it'll look for it in the next repo in the list - var isLocalRepo = repo.Uri.AbsoluteUri.StartsWith(Uri.UriSchemeFile + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase); + var isLocalRepo = repo.Uri.Scheme == Uri.UriSchemeFile; // Finds parent packages and dependencies IEnumerable pkgsFromRepoToInstall = findHelper.FindByResourceName( From eb0f5d1d96ff4693e2937c3a6c92338474ba8eff Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 May 2022 10:48:17 -0700 Subject: [PATCH 27/34] Update src/code/SavePSResource.cs Co-authored-by: Anam Navied --- src/code/SavePSResource.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index 141bf0a99..73b6d1c19 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -132,7 +132,8 @@ public string Path public SwitchParameter SkipDependencyCheck { get; set; } /// - /// Skips the check for package validation. + /// Check validation for signed and catalog files + /// [Parameter] public SwitchParameter AuthenticodeCheck { get; set; } From b15ef89ced7b64af75c3147f0dda95ad136004d8 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 23 May 2022 10:50:10 -0700 Subject: [PATCH 28/34] Remove old comment --- src/code/InstallHelper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index aed9dd4d6..5a531517d 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -695,8 +695,6 @@ private bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersio } } - - // TODO: validate all files (need to figure out what all files are (ie what files shouldn't be validated) : list of extensions Collection authenticodeSignature = new Collection(); try { From c78a37453a72911121b3aef006a35bcedb6c2097 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 May 2022 21:14:55 -0700 Subject: [PATCH 29/34] Update src/code/InstallHelper.cs Co-authored-by: Paul Higinbotham --- src/code/InstallHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 50ac79876..35b578db1 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -40,7 +40,8 @@ internal class InstallHelper : PSCmdlet public const string PSScriptFileExt = ".ps1"; private const string MsgRepositoryNotTrusted = "Untrusted repository"; private const string MsgInstallUntrustedPackage = "You are installing the modules from an untrusted repository. If you trust this repository, change its Trusted value by running the Set-PSResourceRepository cmdlet. Are you sure you want to install the PSresource from '{0}' ?"; - private readonly string[] certStoreLocations = { "cert:\\LocalMachine\\Root", "cert:\\LocalMachine\\AuthRoot", "cert:\\CurrentUser\\Root", "cert:\\CurrentUser\\AuthRoot" }; + private readonly string[] CertStoreLocations = { "cert:\\LocalMachine\\Root", "cert:\\LocalMachine\\AuthRoot", "cert:\\CurrentUser\\Root", "cert:\\CurrentUser\\AuthRoot" }; + private CancellationToken _cancellationToken; private readonly PSCmdlet _cmdletPassedIn; From a6a4730ee7e4ef2865b360ff5f250df2bd01c711 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Wed, 25 May 2022 11:30:25 -0700 Subject: [PATCH 30/34] Refactor authenticode methods, incorporate code review feedback --- src/code/InstallHelper.cs | 323 +-------------------------------- src/code/Utils.cs | 370 +++++++++++++++++++++++++++++++++++++- 2 files changed, 369 insertions(+), 324 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 35b578db1..01250047c 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -3,7 +3,6 @@ using Microsoft.PowerShell.Commands; using Microsoft.PowerShell.PowerShellGet.UtilClasses; -using Microsoft.Win32.SafeHandles; using MoreLinq.Extensions; using NuGet.Common; using NuGet.Configuration; @@ -22,8 +21,6 @@ using System.Linq; using System.Management.Automation; using System.Net; -using System.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using System.Threading; @@ -40,9 +37,6 @@ internal class InstallHelper : PSCmdlet public const string PSScriptFileExt = ".ps1"; private const string MsgRepositoryNotTrusted = "Untrusted repository"; private const string MsgInstallUntrustedPackage = "You are installing the modules from an untrusted repository. If you trust this repository, change its Trusted value by running the Set-PSResourceRepository cmdlet. Are you sure you want to install the PSresource from '{0}' ?"; - private readonly string[] CertStoreLocations = { "cert:\\LocalMachine\\Root", "cert:\\LocalMachine\\AuthRoot", "cert:\\CurrentUser\\Root", "cert:\\CurrentUser\\AuthRoot" }; - - private CancellationToken _cancellationToken; private readonly PSCmdlet _cmdletPassedIn; private List _pathsToInstallPkg; @@ -64,42 +58,6 @@ internal class InstallHelper : PSCmdlet #endregion - - #region Enums - - public struct CERT_CHAIN_POLICY_PARA - { - public CERT_CHAIN_POLICY_PARA(int size) - { - cbSize = (uint)size; - dwFlags = 0; - pvExtraPolicyPara = IntPtr.Zero; - } - public uint cbSize; - public uint dwFlags; - public IntPtr pvExtraPolicyPara; - } - - public struct CERT_CHAIN_POLICY_STATUS - { - public CERT_CHAIN_POLICY_STATUS(int size) - { - cbSize = (uint)size; - dwError = 0; - lChainIndex = IntPtr.Zero; - lElementIndex = IntPtr.Zero; - pvExtraPolicyStatus = IntPtr.Zero; - } - public uint cbSize; - public uint dwError; - public IntPtr lChainIndex; - public IntPtr lElementIndex; - public IntPtr pvExtraPolicyStatus; - } - - #endregion - - #region Public methods public InstallHelper(PSCmdlet cmdletPassedIn) @@ -543,7 +501,7 @@ private List InstallPackage( : _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); } - if (_authenticodeCheck && !CheckAuthenticodeSignature(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, out ErrorRecord errorRecord)) + if (_authenticodeCheck && !AuthenticodeSignature.CheckAuthenticodeSignature(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, this, out ErrorRecord errorRecord)) { ThrowTerminatingError(errorRecord); } @@ -639,123 +597,6 @@ private List InstallPackage( return pkgsSuccessfullyInstalled; } - private bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath, 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 TestFileCatalogResult = new Collection(); - 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, null); - } - - 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, null); - return false; - } - } - - Collection authenticodeSignature = new Collection(); - 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, null); - } - - // If the authenticode signature is not valid, return false - if (authenticodeSignature.Any() && authenticodeSignature[0] != null) - { - foreach (var signature in authenticodeSignature) - { - Signature sign = (Signature) signature.BaseObject; - if (!sign.Status.Equals(SignatureStatus.Valid)) - { - var exMessage = String.Format("The signature for '{0}' is '{1}.", pkgName, sign.Status.ToString()); - var ex = new ArgumentException(exMessage); - errorRecord = new ErrorRecord(ex, "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, null); - - return false; - } - } - - var isMicrosoftCert = IsMicrosoftCert((Signature)authenticodeSignature[0].BaseObject); - if (isMicrosoftCert) - { - _cmdletPassedIn.WriteVerbose(string.Format("Package '{0}' is signed by a Microsoft certificate.", pkgName)); - } - - var publisherDetails = GetAuthenticodePublisher((Signature)authenticodeSignature[0].BaseObject, pkgName); - if (publisherDetails.Count == 2) - { - if (!string.IsNullOrEmpty(publisherDetails["Publisher"].ToString())) - { - _cmdletPassedIn.WriteVerbose(string.Format("Package '{0}' is published by publisher '{1}'.", pkgName, publisherDetails["Publisher"].ToString())); - } - - if (!string.IsNullOrEmpty(publisherDetails["PublisherRootCA"].ToString())) - { - _cmdletPassedIn.WriteVerbose(string.Format("Package '{0}' has the publisher root certificate authority of '{1}'.", pkgName, publisherDetails["PublisherRootCA"].ToString())); - } - } - } - - return true; - } - private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string tempInstallPath, string newVersion) { var requireLicenseAcceptance = false; @@ -1061,168 +902,6 @@ private void MoveFilesIntoInstallPath( } } - private Hashtable GetAuthenticodePublisher(Signature authenticodeSignature, string pkgName) - { - Hashtable publisherInfo = new Hashtable(); - if (authenticodeSignature.SignerCertificate != null) - { - X509Chain chain = new X509Chain(); - chain.Build(authenticodeSignature.SignerCertificate); - - foreach (X509ChainElement element in chain.ChainElements) - { - foreach (var certStoreLocation in certStoreLocations) - { - var results = PowerShellInvoker.InvokeScriptWithHost( - cmdlet: _cmdletPassedIn, - script: @" - param ([string] $certStoreLocation ) - - Microsoft.PowerShell.Management\Get-ChildItem -Path $certStoreLocation | - Microsoft.PowerShell.Core\Where-Object { ($_.Subject -eq $element.Certificate.Subject) -and ($_.thumbprint -eq $element.Certificate.Thumbprint) } - ", - args: new object[] { certStoreLocation, element }, - out Exception terminatingError); - - - if (terminatingError != null) - { - ThrowTerminatingError( - new ErrorRecord( - new PSInvalidOperationException( - message: $"Install-PSResource encountered an error while authenticating certificate for \"{pkgName}\" from certificate store \"{certStoreLocation}\".", - innerException: terminatingError), - "InstallPSResourceCannotReadCertFromStore", - ErrorCategory.InvalidResult, - _cmdletPassedIn)); - } - - X509Certificate2 rootCertificateAuthority = results.Any() ? (X509Certificate2)results.FirstOrDefault() : null; - - if (rootCertificateAuthority != null) - { - publisherInfo.Add("Publisher", authenticodeSignature.SignerCertificate.Subject); - publisherInfo.Add("PublisherRootCA", rootCertificateAuthority); - - return publisherInfo; - } - } - } - } - - return publisherInfo; - } - - private bool IsMicrosoftCert(Signature authenticodeSignature) - { - bool isMicrosoftCert = false; - if (authenticodeSignature.SignerCertificate != null) - { - SafeX509ChainHandle safex509ChainHandle = null; - try - { - X509Chain chain = new X509Chain(); - chain.Build(authenticodeSignature.SignerCertificate); - - // safehandle is available with dotnet api https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles?view=net-6.0 - safex509ChainHandle = chain.SafeHandle; - - isMicrosoftCert = IsMicrosoftCertificateHelper(safex509ChainHandle); - - } - catch (Exception e) - { - ThrowTerminatingError( - new ErrorRecord( - new PSInvalidOperationException( - message: $"Install-PSResource encountered an error while checking if the module uses a valid Microsoft certificate.", - innerException: e.InnerException), - "InstallPSResourceFailedToCheckIfMicrosoftCert", - ErrorCategory.InvalidResult, - _cmdletPassedIn)); - } - - if (safex509ChainHandle != null) - { - safex509ChainHandle.Dispose(); - } - } - - return isMicrosoftCert; - } - - public static bool IsMicrosoftCertificateHelper(SafeX509ChainHandle pChainContext) - { - //------------------------------------------------------------------------- - // CERT_CHAIN_POLICY_MICROSOFT_ROOT - // - // Checks if the last element of the first simple chain contains a - // Microsoft root public key. If it doesn't contain a Microsoft root - // public key, dwError is set to CERT_E_UNTRUSTEDROOT. - // - // pPolicyPara is optional. However, - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG can be set in - // the dwFlags in pPolicyPara to also check for the Microsoft Test Roots. - // - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can be set - // in the dwFlags in pPolicyPara to check for the Microsoft root for - // application signing instead of the Microsoft product root. This flag - // explicitly checks for the application root only and cannot be combined - // with the test root flag. - // - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG can be set - // in the dwFlags in pPolicyPara to always disable the Flight root. - // - // pvExtraPolicyPara and pvExtraPolicyStatus aren't used and must be set - // to NULL. - //-------------------------------------------------------------------------- - const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG = 0x00010000; - const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG = 0x00020000; - //const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG = 0x00040000; - CERT_CHAIN_POLICY_PARA PolicyPara = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); - CERT_CHAIN_POLICY_STATUS PolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); - int CERT_CHAIN_POLICY_MICROSOFT_ROOT = 7; - PolicyPara.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG; - bool isMicrosoftRoot = false; - if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), - pChainContext, - ref PolicyPara, - ref PolicyStatus)) - { - isMicrosoftRoot = (PolicyStatus.dwError == 0); - } - // Also check for the Microsoft root for application signing if the Microsoft product root verification is unsuccessful. - if (!isMicrosoftRoot) - { - // Some Microsoft modules can be signed with Microsoft Application Root instead of Microsoft Product Root, - // So we need to use the MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG for the certificate verification. - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can not be used - // with MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG, - // so additional CertVerifyCertificateChainPolicy call is required to verify the given certificate is in Microsoft Application Root. - // - CERT_CHAIN_POLICY_PARA PolicyPara2 = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); - CERT_CHAIN_POLICY_STATUS PolicyStatus2 = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); - PolicyPara2.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; - if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), - pChainContext, - ref PolicyPara2, - ref PolicyStatus2)) - { - isMicrosoftRoot = (PolicyStatus2.dwError == 0); - } - } - return isMicrosoftRoot; - } - - [DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public extern static - bool CertVerifyCertificateChainPolicy( - IntPtr pszPolicyOID, - SafeX509ChainHandle pChainContext, - ref CERT_CHAIN_POLICY_PARA pPolicyPara, - ref CERT_CHAIN_POLICY_STATUS pPolicyStatus); - - #endregion } } diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 07c74fc12..4103cf515 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -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; @@ -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 { @@ -1126,7 +1128,371 @@ public static Collection InvokeScriptWithHost( } #endregion Methods - } - + } + + #endregion + + #region AuthenticodeSignature + + internal static class AuthenticodeSignature + { + // TODO: ADDD COMMENTS + #region Members + static readonly string[] CertStoreLocations = { "cert:\\LocalMachine\\Root", "cert:\\LocalMachine\\AuthRoot", "cert:\\CurrentUser\\Root", "cert:\\CurrentUser\\AuthRoot" }; + #endregion + + + #region Enums + + public struct CERT_CHAIN_POLICY_PARA + { + public CERT_CHAIN_POLICY_PARA(int size) + { + cbSize = (uint)size; + dwFlags = 0; + pvExtraPolicyPara = IntPtr.Zero; + } + public uint cbSize; + public uint dwFlags; + public IntPtr pvExtraPolicyPara; + } + + public struct CERT_CHAIN_POLICY_STATUS + { + public CERT_CHAIN_POLICY_STATUS(int size) + { + cbSize = (uint)size; + dwError = 0; + lChainIndex = IntPtr.Zero; + lElementIndex = IntPtr.Zero; + pvExtraPolicyStatus = IntPtr.Zero; + } + public uint cbSize; + public uint dwError; + public IntPtr lChainIndex; + public IntPtr lElementIndex; + public IntPtr pvExtraPolicyStatus; + } + + #endregion + + #region Methods + + internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List 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 TestFileCatalogResult = new Collection(); + 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 authenticodeSignature = new Collection(); + 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) + { + // Create a cache for checking if a cert is signed by Microsoft + Dictionary isMSCertCache = new Dictionary { }; + + 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; + } + + string fileName = new FileInfo(signature.Path).Name; + + bool isMicrosoftCert; + if (isMSCertCache.ContainsKey(signature.SignerCertificate.Thumbprint)) + { + isMicrosoftCert = isMSCertCache[signature.SignerCertificate.Thumbprint]; + } + else + { + isMicrosoftCert = IsMicrosoftCert(signature, cmdletPassedIn, errorRecord); + + // Add new information to the cache + isMSCertCache.Add(signature.SignerCertificate.Thumbprint, isMicrosoftCert); + } + + if (isMicrosoftCert) + { + cmdletPassedIn.WriteVerbose(string.Format("File '{0}' from package '{1}' is signed by a Microsoft certificate.", fileName, pkgName)); + } + + // Create a cache for retrieving the publisher details of a cert + Dictionary publisherDetailsCache = new Dictionary { }; + + Hashtable publisherDetails; + if (publisherDetailsCache.ContainsKey(signature.SignerCertificate.Thumbprint)) + { + publisherDetails = publisherDetailsCache[signature.SignerCertificate.Thumbprint]; + } + else + { + publisherDetails = GetAuthenticodePublisher(signature, pkgName, cmdletPassedIn, errorRecord); + + // Add new information to the cache + publisherDetailsCache.Add(signature.SignerCertificate.Thumbprint, publisherDetails); + } + + if (publisherDetails.Count > 0) + { + if (publisherDetails.ContainsKey("Publisher") && !string.IsNullOrEmpty(publisherDetails["Publisher"].ToString())) + { + cmdletPassedIn.WriteVerbose(string.Format("File '{0}' from package '{1}' is published by publisher '{2}'.", fileName, pkgName, publisherDetails["Publisher"].ToString())); + } + + if (publisherDetails.ContainsKey("PublisherRootCA") && !string.IsNullOrEmpty(publisherDetails["PublisherRootCA"].ToString())) + { + cmdletPassedIn.WriteVerbose(string.Format("File '[0}' from package '{1}' has the publisher root certificate authority of '{2}'.", fileName, pkgName, publisherDetails["PublisherRootCA"].ToString())); + } + } + } + + } + + return true; + } + + internal static Hashtable GetAuthenticodePublisher(Signature authenticodeSignature, string pkgName, PSCmdlet cmdletPassedIn, ErrorRecord errorRecord) + { + Hashtable publisherInfo = new Hashtable(); + if (authenticodeSignature.SignerCertificate != null) + { + X509Chain chain = new X509Chain(); + chain.Build(authenticodeSignature.SignerCertificate); + + foreach (X509ChainElement element in chain.ChainElements) + { + foreach (var certStoreLocation in CertStoreLocations) + { + var results = PowerShellInvoker.InvokeScriptWithHost( + cmdlet: cmdletPassedIn, + script: @" + param ([string] $certStoreLocation ) + + Microsoft.PowerShell.Management\Get-ChildItem -Path $certStoreLocation | + Microsoft.PowerShell.Core\Where-Object { ($_.Subject -eq $element.Certificate.Subject) -and ($_.thumbprint -eq $element.Certificate.Thumbprint) } + ", + args: new object[] { certStoreLocation, element }, + out Exception terminatingError); + + if (terminatingError != null) + { + errorRecord = new ErrorRecord( + new PSInvalidOperationException( + message: $"Install-PSResource encountered an error while authenticating certificate for \"{pkgName}\" from certificate store \"{certStoreLocation}\".", + innerException: terminatingError), + "InstallPSResourceCannotReadCertFromStore", + ErrorCategory.InvalidResult, + cmdletPassedIn); + + return publisherInfo; + } + + X509Certificate2 rootCertificateAuthority = results.Count > 0 ? (X509Certificate2)results.FirstOrDefault() : null; + + if (rootCertificateAuthority != null) + { + publisherInfo.Add("Publisher", authenticodeSignature.SignerCertificate.Subject); + publisherInfo.Add("PublisherRootCA", rootCertificateAuthority); + + return publisherInfo; + } + } + } + } + + return publisherInfo; + } + + internal static bool IsMicrosoftCert(Signature authenticodeSignature, PSCmdlet cmdletPassedIn, ErrorRecord errorRecord) + { + bool isMicrosoftCert = false; + if (authenticodeSignature.SignerCertificate != null) + { + SafeX509ChainHandle safex509ChainHandle = null; + try + { + X509Chain chain = new X509Chain(); + chain.Build(authenticodeSignature.SignerCertificate); + + // safehandle is available with dotnet api https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles?view=net-6.0 + safex509ChainHandle = chain.SafeHandle; + + isMicrosoftCert = IsValidatedMicrosoftCert(safex509ChainHandle); + + } + catch (Exception e) + { + errorRecord = new ErrorRecord( + new PSInvalidOperationException( + message: $"Install-PSResource encountered an error while checking if the module uses a valid Microsoft certificate.", + innerException: e.InnerException), + "InstallPSResourceFailedToCheckIfMicrosoftCert", + ErrorCategory.InvalidResult, + cmdletPassedIn); + + return false; + } + + if (safex509ChainHandle != null) + { + safex509ChainHandle.Dispose(); + } + } + + return isMicrosoftCert; + } + + internal static bool IsValidatedMicrosoftCert(SafeX509ChainHandle pChainContext) + { + //------------------------------------------------------------------------- + // CERT_CHAIN_POLICY_MICROSOFT_ROOT + // + // Checks if the last element of the first simple chain contains a + // Microsoft root public key. If it doesn't contain a Microsoft root + // public key, dwError is set to CERT_E_UNTRUSTEDROOT. + // + // pPolicyPara is optional. However, + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG can be set in + // the dwFlags in pPolicyPara to also check for the Microsoft Test Roots. + // + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can be set + // in the dwFlags in pPolicyPara to check for the Microsoft root for + // application signing instead of the Microsoft product root. This flag + // explicitly checks for the application root only and cannot be combined + // with the test root flag. + // + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG can be set + // in the dwFlags in pPolicyPara to always disable the Flight root. + // + // pvExtraPolicyPara and pvExtraPolicyStatus aren't used and must be set + // to NULL. + //-------------------------------------------------------------------------- + const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG = 0x00010000; + const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG = 0x00020000; + //const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG = 0x00040000; + CERT_CHAIN_POLICY_PARA PolicyPara = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); + CERT_CHAIN_POLICY_STATUS PolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); + int CERT_CHAIN_POLICY_MICROSOFT_ROOT = 7; + PolicyPara.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG; + bool isMicrosoftRoot = false; + if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), + pChainContext, + ref PolicyPara, + ref PolicyStatus)) + { + isMicrosoftRoot = (PolicyStatus.dwError == 0); + } + // Also check for the Microsoft root for application signing if the Microsoft product root verification is unsuccessful. + if (!isMicrosoftRoot) + { + // Some Microsoft modules can be signed with Microsoft Application Root instead of Microsoft Product Root, + // So we need to use the MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG for the certificate verification. + // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can not be used + // with MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG, + // so additional CertVerifyCertificateChainPolicy call is required to verify the given certificate is in Microsoft Application Root. + // + CERT_CHAIN_POLICY_PARA PolicyPara2 = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); + CERT_CHAIN_POLICY_STATUS PolicyStatus2 = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); + PolicyPara2.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; + if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), + pChainContext, + ref PolicyPara2, + ref PolicyStatus2)) + { + isMicrosoftRoot = (PolicyStatus2.dwError == 0); + } + } + return isMicrosoftRoot; + } + + [DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public extern static bool CertVerifyCertificateChainPolicy( + IntPtr pszPolicyOID, + SafeX509ChainHandle pChainContext, + ref CERT_CHAIN_POLICY_PARA pPolicyPara, + ref CERT_CHAIN_POLICY_STATUS pPolicyStatus); + + #endregion + } + #endregion } From 04c60de29c4ca724ab9c632576fb74416adfc09d Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 26 May 2022 12:13:12 -0700 Subject: [PATCH 31/34] Remove certificate checks --- src/code/Utils.cs | 93 ----------------------------------------------- 1 file changed, 93 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 4103cf515..5408d85a3 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1136,46 +1136,6 @@ public static Collection InvokeScriptWithHost( internal static class AuthenticodeSignature { - // TODO: ADDD COMMENTS - #region Members - static readonly string[] CertStoreLocations = { "cert:\\LocalMachine\\Root", "cert:\\LocalMachine\\AuthRoot", "cert:\\CurrentUser\\Root", "cert:\\CurrentUser\\AuthRoot" }; - #endregion - - - #region Enums - - public struct CERT_CHAIN_POLICY_PARA - { - public CERT_CHAIN_POLICY_PARA(int size) - { - cbSize = (uint)size; - dwFlags = 0; - pvExtraPolicyPara = IntPtr.Zero; - } - public uint cbSize; - public uint dwFlags; - public IntPtr pvExtraPolicyPara; - } - - public struct CERT_CHAIN_POLICY_STATUS - { - public CERT_CHAIN_POLICY_STATUS(int size) - { - cbSize = (uint)size; - dwError = 0; - lChainIndex = IntPtr.Zero; - lElementIndex = IntPtr.Zero; - pvExtraPolicyStatus = IntPtr.Zero; - } - public uint cbSize; - public uint dwError; - public IntPtr lChainIndex; - public IntPtr lElementIndex; - public IntPtr pvExtraPolicyStatus; - } - - #endregion - #region Methods internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath, PSCmdlet cmdletPassedIn, out ErrorRecord errorRecord) @@ -1260,9 +1220,6 @@ internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNa // If the authenticode signature is not valid, return false if (authenticodeSignature.Any() && authenticodeSignature[0] != null) { - // Create a cache for checking if a cert is signed by Microsoft - Dictionary isMSCertCache = new Dictionary { }; - foreach (var sign in authenticodeSignature) { Signature signature = (Signature)sign.BaseObject; @@ -1274,57 +1231,7 @@ internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNa return false; } - - string fileName = new FileInfo(signature.Path).Name; - - bool isMicrosoftCert; - if (isMSCertCache.ContainsKey(signature.SignerCertificate.Thumbprint)) - { - isMicrosoftCert = isMSCertCache[signature.SignerCertificate.Thumbprint]; - } - else - { - isMicrosoftCert = IsMicrosoftCert(signature, cmdletPassedIn, errorRecord); - - // Add new information to the cache - isMSCertCache.Add(signature.SignerCertificate.Thumbprint, isMicrosoftCert); - } - - if (isMicrosoftCert) - { - cmdletPassedIn.WriteVerbose(string.Format("File '{0}' from package '{1}' is signed by a Microsoft certificate.", fileName, pkgName)); - } - - // Create a cache for retrieving the publisher details of a cert - Dictionary publisherDetailsCache = new Dictionary { }; - - Hashtable publisherDetails; - if (publisherDetailsCache.ContainsKey(signature.SignerCertificate.Thumbprint)) - { - publisherDetails = publisherDetailsCache[signature.SignerCertificate.Thumbprint]; - } - else - { - publisherDetails = GetAuthenticodePublisher(signature, pkgName, cmdletPassedIn, errorRecord); - - // Add new information to the cache - publisherDetailsCache.Add(signature.SignerCertificate.Thumbprint, publisherDetails); - } - - if (publisherDetails.Count > 0) - { - if (publisherDetails.ContainsKey("Publisher") && !string.IsNullOrEmpty(publisherDetails["Publisher"].ToString())) - { - cmdletPassedIn.WriteVerbose(string.Format("File '{0}' from package '{1}' is published by publisher '{2}'.", fileName, pkgName, publisherDetails["Publisher"].ToString())); - } - - if (publisherDetails.ContainsKey("PublisherRootCA") && !string.IsNullOrEmpty(publisherDetails["PublisherRootCA"].ToString())) - { - cmdletPassedIn.WriteVerbose(string.Format("File '[0}' from package '{1}' has the publisher root certificate authority of '{2}'.", fileName, pkgName, publisherDetails["PublisherRootCA"].ToString())); - } - } } - } return true; From 3b3a552eb1c0e95330336489a2be4beab17f4943 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 26 May 2022 12:18:34 -0700 Subject: [PATCH 32/34] Revert some changes that got made --- src/code/InstallHelper.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 01250047c..16400c09b 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -189,7 +189,7 @@ private List ProcessRepositories( _cmdletPassedIn.WriteVerbose("Untrusted repository accepted as trusted source."); // If it can't find the pkg in one repository, it'll look for it in the next repo in the list - var isLocalRepo = repo.Uri.Scheme == Uri.UriSchemeFile; + var isLocalRepo = repo.Uri.AbsoluteUri.StartsWith(Uri.UriSchemeFile + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase); // Finds parent packages and dependencies IEnumerable pkgsFromRepoToInstall = findHelper.FindByResourceName( @@ -467,8 +467,8 @@ private List InstallPackage( string tempDirNameVersion = isLocalRepo ? tempInstallPath : Path.Combine(tempInstallPath, pkgIdentity.Id.ToLower(), newVersion); var version4digitNoPrerelease = pkgIdentity.Version.Version.ToString(); string moduleManifestVersion = string.Empty; - var scriptPath = Path.Combine(tempDirNameVersion, pkg.Name + ".ps1"); - var modulePath = Path.Combine(tempDirNameVersion, pkg.Name + ".psd1"); + var scriptPath = Path.Combine(tempDirNameVersion, pkg.Name + PSScriptFileExt); + var modulePath = Path.Combine(tempDirNameVersion, pkg.Name + PSDataFileExt); // Check if the package is a module or a script var isModule = File.Exists(modulePath); @@ -546,7 +546,7 @@ private List InstallPackage( if (_includeXML) { - CreateMetadataXMLFile(tempDirNameVersion, installPath, newVersion, pkg, isModule); + CreateMetadataXMLFile(tempDirNameVersion, installPath, pkg, isModule); } MoveFilesIntoInstallPath( @@ -732,7 +732,7 @@ private bool DetectClobber(string pkgName, Hashtable parsedMetadataHashtable) return foundClobber; } - private void CreateMetadataXMLFile(string dirNameVersion, string installPath, string pkgVersion, PSResourceInfo pkg, bool isModule) + private void CreateMetadataXMLFile(string dirNameVersion, string installPath, PSResourceInfo pkg, bool isModule) { // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" // Modules will have the metadata file: "PSGetModuleInfo.xml" @@ -740,7 +740,7 @@ private void CreateMetadataXMLFile(string dirNameVersion, string installPath, st : Path.Combine(dirNameVersion, (pkg.Name + "_InstalledScriptInfo.xml")); pkg.InstalledDate = DateTime.Now; - pkg.InstalledLocation = Path.Combine(installPath, pkg.Name, pkgVersion); + pkg.InstalledLocation = installPath; // Write all metadata into metadataXMLPath if (!pkg.TryWrite(metadataXMLPath, out string error)) From a346d02345b94521ac7d969f250c5e55e2afe6cf Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 26 May 2022 12:55:13 -0700 Subject: [PATCH 33/34] Remove 'IsMicrosoftCert' and 'GetAuthenticodePublisher' methods --- src/code/Utils.cs | 163 +--------------------------------------------- 1 file changed, 1 insertion(+), 162 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 5408d85a3..9ab09b3ce 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1236,168 +1236,7 @@ internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNa return true; } - - internal static Hashtable GetAuthenticodePublisher(Signature authenticodeSignature, string pkgName, PSCmdlet cmdletPassedIn, ErrorRecord errorRecord) - { - Hashtable publisherInfo = new Hashtable(); - if (authenticodeSignature.SignerCertificate != null) - { - X509Chain chain = new X509Chain(); - chain.Build(authenticodeSignature.SignerCertificate); - - foreach (X509ChainElement element in chain.ChainElements) - { - foreach (var certStoreLocation in CertStoreLocations) - { - var results = PowerShellInvoker.InvokeScriptWithHost( - cmdlet: cmdletPassedIn, - script: @" - param ([string] $certStoreLocation ) - - Microsoft.PowerShell.Management\Get-ChildItem -Path $certStoreLocation | - Microsoft.PowerShell.Core\Where-Object { ($_.Subject -eq $element.Certificate.Subject) -and ($_.thumbprint -eq $element.Certificate.Thumbprint) } - ", - args: new object[] { certStoreLocation, element }, - out Exception terminatingError); - - if (terminatingError != null) - { - errorRecord = new ErrorRecord( - new PSInvalidOperationException( - message: $"Install-PSResource encountered an error while authenticating certificate for \"{pkgName}\" from certificate store \"{certStoreLocation}\".", - innerException: terminatingError), - "InstallPSResourceCannotReadCertFromStore", - ErrorCategory.InvalidResult, - cmdletPassedIn); - - return publisherInfo; - } - - X509Certificate2 rootCertificateAuthority = results.Count > 0 ? (X509Certificate2)results.FirstOrDefault() : null; - - if (rootCertificateAuthority != null) - { - publisherInfo.Add("Publisher", authenticodeSignature.SignerCertificate.Subject); - publisherInfo.Add("PublisherRootCA", rootCertificateAuthority); - - return publisherInfo; - } - } - } - } - - return publisherInfo; - } - - internal static bool IsMicrosoftCert(Signature authenticodeSignature, PSCmdlet cmdletPassedIn, ErrorRecord errorRecord) - { - bool isMicrosoftCert = false; - if (authenticodeSignature.SignerCertificate != null) - { - SafeX509ChainHandle safex509ChainHandle = null; - try - { - X509Chain chain = new X509Chain(); - chain.Build(authenticodeSignature.SignerCertificate); - - // safehandle is available with dotnet api https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles?view=net-6.0 - safex509ChainHandle = chain.SafeHandle; - - isMicrosoftCert = IsValidatedMicrosoftCert(safex509ChainHandle); - - } - catch (Exception e) - { - errorRecord = new ErrorRecord( - new PSInvalidOperationException( - message: $"Install-PSResource encountered an error while checking if the module uses a valid Microsoft certificate.", - innerException: e.InnerException), - "InstallPSResourceFailedToCheckIfMicrosoftCert", - ErrorCategory.InvalidResult, - cmdletPassedIn); - - return false; - } - - if (safex509ChainHandle != null) - { - safex509ChainHandle.Dispose(); - } - } - - return isMicrosoftCert; - } - - internal static bool IsValidatedMicrosoftCert(SafeX509ChainHandle pChainContext) - { - //------------------------------------------------------------------------- - // CERT_CHAIN_POLICY_MICROSOFT_ROOT - // - // Checks if the last element of the first simple chain contains a - // Microsoft root public key. If it doesn't contain a Microsoft root - // public key, dwError is set to CERT_E_UNTRUSTEDROOT. - // - // pPolicyPara is optional. However, - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG can be set in - // the dwFlags in pPolicyPara to also check for the Microsoft Test Roots. - // - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can be set - // in the dwFlags in pPolicyPara to check for the Microsoft root for - // application signing instead of the Microsoft product root. This flag - // explicitly checks for the application root only and cannot be combined - // with the test root flag. - // - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG can be set - // in the dwFlags in pPolicyPara to always disable the Flight root. - // - // pvExtraPolicyPara and pvExtraPolicyStatus aren't used and must be set - // to NULL. - //-------------------------------------------------------------------------- - const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG = 0x00010000; - const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG = 0x00020000; - //const uint MICROSOFT_ROOT_CERT_CHAIN_POLICY_DISABLE_FLIGHT_ROOT_FLAG = 0x00040000; - CERT_CHAIN_POLICY_PARA PolicyPara = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); - CERT_CHAIN_POLICY_STATUS PolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); - int CERT_CHAIN_POLICY_MICROSOFT_ROOT = 7; - PolicyPara.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG; - bool isMicrosoftRoot = false; - if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), - pChainContext, - ref PolicyPara, - ref PolicyStatus)) - { - isMicrosoftRoot = (PolicyStatus.dwError == 0); - } - // Also check for the Microsoft root for application signing if the Microsoft product root verification is unsuccessful. - if (!isMicrosoftRoot) - { - // Some Microsoft modules can be signed with Microsoft Application Root instead of Microsoft Product Root, - // So we need to use the MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG for the certificate verification. - // MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG can not be used - // with MICROSOFT_ROOT_CERT_CHAIN_POLICY_ENABLE_TEST_ROOT_FLAG, - // so additional CertVerifyCertificateChainPolicy call is required to verify the given certificate is in Microsoft Application Root. - // - CERT_CHAIN_POLICY_PARA PolicyPara2 = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); - CERT_CHAIN_POLICY_STATUS PolicyStatus2 = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS))); - PolicyPara2.dwFlags = (uint)MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; - if (CertVerifyCertificateChainPolicy(new IntPtr(CERT_CHAIN_POLICY_MICROSOFT_ROOT), - pChainContext, - ref PolicyPara2, - ref PolicyStatus2)) - { - isMicrosoftRoot = (PolicyStatus2.dwError == 0); - } - } - return isMicrosoftRoot; - } - - [DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public extern static bool CertVerifyCertificateChainPolicy( - IntPtr pszPolicyOID, - SafeX509ChainHandle pChainContext, - ref CERT_CHAIN_POLICY_PARA pPolicyPara, - ref CERT_CHAIN_POLICY_STATUS pPolicyStatus); - + #endregion } From 2d9c4accfcee678eab34b3669adeb0a470e6f547 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Wed, 1 Jun 2022 14:08:40 -0700 Subject: [PATCH 34/34] Change reference to 'this' to _cmdletPassedIn --- src/code/InstallHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 16400c09b..17e9e7583 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -501,7 +501,7 @@ private List InstallPackage( : _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); } - if (_authenticodeCheck && !AuthenticodeSignature.CheckAuthenticodeSignature(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, this, out ErrorRecord errorRecord)) + if (_authenticodeCheck && !AuthenticodeSignature.CheckAuthenticodeSignature(pkg.Name, tempDirNameVersion, _versionRange, _pathsToSearch, installPath, _cmdletPassedIn, out ErrorRecord errorRecord)) { ThrowTerminatingError(errorRecord); }