mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Remove the code that spins up our own assembly load context. Keep the code that registers our `Resolve` method to the default loader's `Resolving` event, so that we can continue to do special assembly resolution as needed (such as the GAC probing logic needed for consuming FullCLR PS modules). Essentially, the assembly `Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.dll` is not needed anymore, the remaining code should be moved to S.M.A.dll. However, that will break DSC and other native hosts that are hosting powershell. So this assembly is kept for now.
1846 lines
69 KiB
C#
1846 lines
69 KiB
C#
/********************************************************************++
|
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
--********************************************************************/
|
|
|
|
#pragma warning disable 1634, 1691
|
|
#pragma warning disable 56523
|
|
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using Microsoft.PowerShell;
|
|
using Microsoft.PowerShell.Commands;
|
|
using Microsoft.Win32;
|
|
using System.Management.Automation.Internal;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Globalization;
|
|
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Runtime.InteropServices;
|
|
using DWORD = System.UInt32;
|
|
|
|
namespace Microsoft.PowerShell
|
|
{
|
|
/// <summary>
|
|
/// Defines the different Execution Policies supported by the
|
|
/// PSAuthorizationManager class.
|
|
///
|
|
/// </summary>
|
|
public enum ExecutionPolicy
|
|
{
|
|
/// Unrestricted - No files must be signed. If a file originates from the
|
|
/// internet, Monad provides a warning prompt to alert the user. To
|
|
/// suppress this warning message, right-click on the file in File Explorer,
|
|
/// select "Properties," and then "Unblock."
|
|
Unrestricted = 0,
|
|
|
|
/// RemoteSigned - Only .msh and .mshxml files originating from the internet
|
|
/// must be digitally signed. If remote, signed, and executed, Monad
|
|
/// prompts to determine if files from the signing publisher should be
|
|
/// run or not. This is the default setting.
|
|
RemoteSigned = 1,
|
|
|
|
/// AllSigned - All .msh and .mshxml files must be digitally signed. If
|
|
/// signed and executed, Monad prompts to determine if files from the
|
|
/// signing publisher should be run or not.
|
|
AllSigned = 2,
|
|
|
|
/// Restricted - All .msh files are blocked. Mshxml files must be digitally
|
|
/// signed, and by a trusted publisher. If you haven't made a trust decision
|
|
/// on the publisher yet, prompting is done as in AllSigned mode.
|
|
Restricted = 3,
|
|
|
|
/// Bypass - No files must be signed, and internet origin is not verified
|
|
Bypass = 4,
|
|
|
|
/// Undefined - Not specified at this scope
|
|
Undefined = 5,
|
|
|
|
/// <summary>
|
|
/// Default - The most restrictive policy available.
|
|
/// </summary>
|
|
Default = Restricted
|
|
};
|
|
|
|
/// <summary>
|
|
/// Defines the available configuration scopes for an execution
|
|
/// policy. They are in the following priority, with successive
|
|
/// elements overriding the items that precede them:
|
|
/// LocalMachine -> CurrentUser -> Runspace
|
|
///
|
|
/// </summary>
|
|
public enum ExecutionPolicyScope
|
|
{
|
|
/// Execution policy is retrieved from the
|
|
/// PSExecutionPolicyPreference environment variable.
|
|
Process = 0,
|
|
|
|
/// Execution policy is retrieved from the HKEY_CURRENT_USER
|
|
/// registry hive for the current ShellId.
|
|
CurrentUser = 1,
|
|
|
|
/// Execution policy is retrieved from the HKEY_LOCAL_MACHINE
|
|
/// registry hive for the current ShellId.
|
|
LocalMachine = 2,
|
|
|
|
/// Execution policy is retrieved from the current user's
|
|
/// group policy setting.
|
|
UserPolicy = 3,
|
|
|
|
/// Execution policy is retrieved from the machine-wide
|
|
/// group policy setting.
|
|
MachinePolicy = 4
|
|
}
|
|
}
|
|
|
|
namespace System.Management.Automation.Internal
|
|
{
|
|
/// <summary>
|
|
/// The SAFER policy associated with this file
|
|
///
|
|
/// </summary>
|
|
internal enum SaferPolicy
|
|
{
|
|
/// Explicitly allowed through an Allow rule
|
|
ExplicitlyAllowed = 0,
|
|
|
|
/// Allowed because it has not been explicitly disallowed
|
|
Allowed = 1,
|
|
|
|
/// Disallowed by a rule or policy.
|
|
Disallowed = 2
|
|
}
|
|
|
|
/// <summary>
|
|
/// Security Support APIs
|
|
/// </summary>
|
|
public static class SecuritySupport
|
|
{
|
|
#region execution policy
|
|
|
|
internal static ExecutionPolicyScope[] ExecutionPolicyScopePreferences
|
|
{
|
|
get
|
|
{
|
|
return new ExecutionPolicyScope[] {
|
|
ExecutionPolicyScope.MachinePolicy,
|
|
ExecutionPolicyScope.UserPolicy,
|
|
ExecutionPolicyScope.Process,
|
|
ExecutionPolicyScope.CurrentUser,
|
|
ExecutionPolicyScope.LocalMachine
|
|
};
|
|
}
|
|
}
|
|
|
|
internal static void SetExecutionPolicy(ExecutionPolicyScope scope, ExecutionPolicy policy, string shellId)
|
|
{
|
|
#if UNIX
|
|
throw new PlatformNotSupportedException();
|
|
#else
|
|
string executionPolicy = "Restricted";
|
|
string preferenceKey = Utils.GetRegistryConfigurationPath(shellId);
|
|
|
|
switch (policy)
|
|
{
|
|
case ExecutionPolicy.Restricted:
|
|
executionPolicy = "Restricted"; break;
|
|
case ExecutionPolicy.AllSigned:
|
|
executionPolicy = "AllSigned"; break;
|
|
case ExecutionPolicy.RemoteSigned:
|
|
executionPolicy = "RemoteSigned"; break;
|
|
case ExecutionPolicy.Unrestricted:
|
|
executionPolicy = "Unrestricted"; break;
|
|
case ExecutionPolicy.Bypass:
|
|
executionPolicy = "Bypass"; break;
|
|
}
|
|
|
|
// Set the execution policy
|
|
switch (scope)
|
|
{
|
|
case ExecutionPolicyScope.Process:
|
|
{
|
|
if (policy == ExecutionPolicy.Undefined)
|
|
executionPolicy = null;
|
|
|
|
Environment.SetEnvironmentVariable("PSExecutionPolicyPreference", executionPolicy);
|
|
break;
|
|
}
|
|
|
|
case ExecutionPolicyScope.CurrentUser:
|
|
{
|
|
// They want to remove it
|
|
if (policy == ExecutionPolicy.Undefined)
|
|
{
|
|
ConfigPropertyAccessor.Instance.RemoveExecutionPolicy(ConfigPropertyAccessor.PropertyScope.CurrentUser, shellId);
|
|
CleanKeyParents(Registry.CurrentUser, preferenceKey);
|
|
}
|
|
else
|
|
{
|
|
ConfigPropertyAccessor.Instance.SetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.CurrentUser, shellId, executionPolicy);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ExecutionPolicyScope.LocalMachine:
|
|
{
|
|
// They want to remove it
|
|
if (policy == ExecutionPolicy.Undefined)
|
|
{
|
|
ConfigPropertyAccessor.Instance.RemoveExecutionPolicy(ConfigPropertyAccessor.PropertyScope.SystemWide, shellId);
|
|
CleanKeyParents(Registry.LocalMachine, preferenceKey);
|
|
}
|
|
else
|
|
{
|
|
ConfigPropertyAccessor.Instance.SetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.SystemWide, shellId, executionPolicy);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Clean up the parents of a registry key as long as they
|
|
// contain at most a single child
|
|
private static void CleanKeyParents(RegistryKey baseKey, string keyPath)
|
|
{
|
|
#if CORECLR
|
|
if (!Platform.IsInbox)
|
|
return;
|
|
#endif
|
|
using (RegistryKey key = baseKey.OpenSubKey(keyPath, true))
|
|
{
|
|
// Verify the child key has no children
|
|
if ((key == null) || ((key.ValueCount == 0) && (key.SubKeyCount == 0)))
|
|
{
|
|
// If so, split the key into its path elements
|
|
string[] parentKeys = keyPath.Split(Utils.Separators.Backslash);
|
|
|
|
// Verify we aren't traveling into SOFTWARE\Microsoft
|
|
if (parentKeys.Length <= 2)
|
|
return;
|
|
|
|
string currentItem = parentKeys[parentKeys.Length - 1];
|
|
|
|
// Figure out the parent key name
|
|
string parentKeyPath = keyPath.Remove(keyPath.Length - currentItem.Length - 1);
|
|
|
|
// Open the parent, and clear the child key
|
|
if (key != null)
|
|
{
|
|
using (RegistryKey parentKey = baseKey.OpenSubKey(parentKeyPath, true))
|
|
{
|
|
parentKey.DeleteSubKey(currentItem, true);
|
|
}
|
|
}
|
|
|
|
// Now, process the parent key
|
|
CleanKeyParents(baseKey, parentKeyPath);
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static ExecutionPolicy GetExecutionPolicy(string shellId)
|
|
{
|
|
foreach (ExecutionPolicyScope scope in ExecutionPolicyScopePreferences)
|
|
{
|
|
ExecutionPolicy policy = GetExecutionPolicy(shellId, scope);
|
|
if (policy != ExecutionPolicy.Undefined)
|
|
return policy;
|
|
}
|
|
|
|
return ExecutionPolicy.Restricted;
|
|
}
|
|
|
|
|
|
private static bool? _hasGpScriptParent;
|
|
|
|
/// <summary>
|
|
/// A value indicating that the current process was launched by GPScript.exe
|
|
/// Used to determine execution policy when group policies are in effect
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is somewhat expensive to determine and does not change within the lifetime of the current process
|
|
/// </remarks>
|
|
private static bool HasGpScriptParent
|
|
{
|
|
get
|
|
{
|
|
if (!_hasGpScriptParent.HasValue)
|
|
{
|
|
_hasGpScriptParent = IsCurrentProcessLaunchedByGpScript();
|
|
}
|
|
return _hasGpScriptParent.Value;
|
|
}
|
|
}
|
|
|
|
private static bool IsCurrentProcessLaunchedByGpScript()
|
|
{
|
|
Process currentProcess = Process.GetCurrentProcess();
|
|
string gpScriptPath = IO.Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.System),
|
|
"gpscript.exe");
|
|
|
|
bool foundGpScriptParent = false;
|
|
try
|
|
{
|
|
while (currentProcess != null)
|
|
{
|
|
if (String.Equals(gpScriptPath,
|
|
PsUtils.GetMainModule(currentProcess).FileName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foundGpScriptParent = true;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
currentProcess = PsUtils.GetParentProcess(currentProcess);
|
|
}
|
|
}
|
|
}
|
|
catch (System.ComponentModel.Win32Exception)
|
|
{
|
|
// If you attempt to retrieve the MainModule of a 64-bit process
|
|
// from a WOW64 (32-bit) process, the Win32 API has a fatal
|
|
// flaw that causes this to return the error:
|
|
// "Only part of a ReadProcessMemory or WriteProcessMemory
|
|
// request was completed."
|
|
// In this case, we just catch the exception and eat it.
|
|
// The implication is that logon / logoff scripts that somehow
|
|
// launch the Wow64 version of PowerShell will be subject
|
|
// to the execution policy deployed by Group Policy (where
|
|
// our goal here is to not have the Group Policy execution policy
|
|
// affect logon / logoff scripts.
|
|
}
|
|
return foundGpScriptParent;
|
|
}
|
|
|
|
internal static ExecutionPolicy GetExecutionPolicy(string shellId, ExecutionPolicyScope scope)
|
|
{
|
|
#if UNIX
|
|
return ExecutionPolicy.Unrestricted;
|
|
#else
|
|
switch (scope)
|
|
{
|
|
case ExecutionPolicyScope.Process:
|
|
{
|
|
string policy = Environment.GetEnvironmentVariable("PSExecutionPolicyPreference");
|
|
|
|
if (!String.IsNullOrEmpty(policy))
|
|
return ParseExecutionPolicy(policy);
|
|
else
|
|
return ExecutionPolicy.Undefined;
|
|
}
|
|
|
|
case ExecutionPolicyScope.CurrentUser:
|
|
case ExecutionPolicyScope.LocalMachine:
|
|
{
|
|
string policy = GetLocalPreferenceValue(shellId, scope);
|
|
|
|
if (!String.IsNullOrEmpty(policy))
|
|
return ParseExecutionPolicy(policy);
|
|
else
|
|
return ExecutionPolicy.Undefined;
|
|
}
|
|
|
|
// TODO: Group Policy is only supported on Full systems, but !LINUX && CORECLR
|
|
// will run there as well, so I don't think we should remove it.
|
|
case ExecutionPolicyScope.UserPolicy:
|
|
case ExecutionPolicyScope.MachinePolicy:
|
|
{
|
|
string groupPolicyPreference = GetGroupPolicyValue(shellId, scope);
|
|
|
|
// Be sure we aren't being called by Group Policy
|
|
// itself. A group policy should never block a logon /
|
|
// logoff script.
|
|
if (String.IsNullOrEmpty(groupPolicyPreference) || HasGpScriptParent)
|
|
{
|
|
return ExecutionPolicy.Undefined;
|
|
}
|
|
return ParseExecutionPolicy(groupPolicyPreference);
|
|
}
|
|
}
|
|
|
|
return ExecutionPolicy.Restricted;
|
|
#endif
|
|
}
|
|
|
|
internal static ExecutionPolicy ParseExecutionPolicy(string policy)
|
|
{
|
|
if (String.Equals(policy, "Bypass",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ExecutionPolicy.Bypass;
|
|
}
|
|
else if (String.Equals(policy, "Unrestricted",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ExecutionPolicy.Unrestricted;
|
|
}
|
|
else if (String.Equals(policy, "RemoteSigned",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ExecutionPolicy.RemoteSigned;
|
|
}
|
|
else if (String.Equals(policy, "AllSigned",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ExecutionPolicy.AllSigned;
|
|
}
|
|
else if (String.Equals(policy, "Restricted",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ExecutionPolicy.Restricted;
|
|
}
|
|
else
|
|
{
|
|
return ExecutionPolicy.Default;
|
|
}
|
|
}
|
|
|
|
internal static string GetExecutionPolicy(ExecutionPolicy policy)
|
|
{
|
|
switch (policy)
|
|
{
|
|
case ExecutionPolicy.Bypass: return "Bypass";
|
|
case ExecutionPolicy.Unrestricted: return "Unrestricted";
|
|
case ExecutionPolicy.RemoteSigned: return "RemoteSigned";
|
|
case ExecutionPolicy.AllSigned: return "AllSigned";
|
|
case ExecutionPolicy.Restricted: return "Restricted";
|
|
default: return "Restricted";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if file has product binary signature
|
|
/// </summary>
|
|
/// <param name="file">Name of file to check</param>
|
|
/// <returns>True when file has product binary signature</returns>
|
|
public static bool IsProductBinary(string file)
|
|
{
|
|
if (String.IsNullOrEmpty(file) || (!IO.File.Exists(file)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if it is in the product folder, if not, skip checking the catalog
|
|
// and any other checks.
|
|
var isUnderProductFolder = Utils.IsUnderProductFolder(file);
|
|
if (!isUnderProductFolder)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#if UNIX
|
|
// There is no signature support on non-Windows platforms (yet), when
|
|
// execution reaches here, we are sure the file is under product folder
|
|
return true;
|
|
#else
|
|
// Check the file signature
|
|
Signature fileSignature = SignatureHelper.GetSignature(file, null);
|
|
if ((fileSignature != null) && (fileSignature.IsOSBinary))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// WTGetSignatureInfo is used to verify catalog signature.
|
|
// On Win7, catalog API is not available.
|
|
// On OneCore SKUs like NanoServer/IoT, the API has a bug that makes it not able to find the
|
|
// corresponding catalog file for a given product file, so it doesn't work properly.
|
|
// In these cases, we just trust the 'isUnderProductFolder' check.
|
|
if (Signature.CatalogApiAvailable.HasValue && !Signature.CatalogApiAvailable.Value)
|
|
{
|
|
// When execution reaches here, we are sure the file is under product folder
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if !CORECLR
|
|
/// <summary>
|
|
/// Get the pass / fail result of calling the SAFER API
|
|
/// </summary>
|
|
///
|
|
/// <param name="path">The path to the file in question</param>
|
|
/// <param name="handle">A file handle to the file in question, if available.</param>
|
|
[ArchitectureSensitive]
|
|
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods")]
|
|
internal static SaferPolicy GetSaferPolicy(string path, SafeHandle handle)
|
|
{
|
|
SaferPolicy status = SaferPolicy.Allowed;
|
|
|
|
SAFER_CODE_PROPERTIES codeProperties = new SAFER_CODE_PROPERTIES();
|
|
IntPtr hAuthzLevel;
|
|
|
|
// Prepare the code properties struct.
|
|
codeProperties.cbSize = (uint)Marshal.SizeOf(typeof(SAFER_CODE_PROPERTIES));
|
|
codeProperties.dwCheckFlags = (
|
|
NativeConstants.SAFER_CRITERIA_IMAGEPATH |
|
|
NativeConstants.SAFER_CRITERIA_IMAGEHASH |
|
|
NativeConstants.SAFER_CRITERIA_AUTHENTICODE);
|
|
codeProperties.ImagePath = path;
|
|
|
|
if (handle != null)
|
|
{
|
|
codeProperties.hImageFileHandle = handle.DangerousGetHandle();
|
|
}
|
|
|
|
// turn off WinVerifyTrust UI
|
|
codeProperties.dwWVTUIChoice = NativeConstants.WTD_UI_NONE;
|
|
|
|
// Identify the level associated with the code
|
|
if (NativeMethods.SaferIdentifyLevel(1, ref codeProperties, out hAuthzLevel, NativeConstants.SRP_POLICY_SCRIPT))
|
|
{
|
|
// We found an Authorization Level applicable to this application.
|
|
IntPtr hRestrictedToken = IntPtr.Zero;
|
|
try
|
|
{
|
|
if (!NativeMethods.SaferComputeTokenFromLevel(
|
|
hAuthzLevel, // Safer Level
|
|
IntPtr.Zero, // Test current process' token
|
|
ref hRestrictedToken, // target token
|
|
NativeConstants.SAFER_TOKEN_NULL_IF_EQUAL,
|
|
IntPtr.Zero))
|
|
{
|
|
int lastError = Marshal.GetLastWin32Error();
|
|
if ((lastError == NativeConstants.ERROR_ACCESS_DISABLED_BY_POLICY) ||
|
|
(lastError == NativeConstants.ERROR_ACCESS_DISABLED_NO_SAFER_UI_BY_POLICY))
|
|
{
|
|
status = SaferPolicy.Disallowed;
|
|
}
|
|
else
|
|
{
|
|
throw new System.ComponentModel.Win32Exception();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (hRestrictedToken == IntPtr.Zero)
|
|
{
|
|
// This is not necessarily the "fully trusted" level,
|
|
// it means that the thread token is complies with the requested level
|
|
status = SaferPolicy.Allowed;
|
|
}
|
|
else
|
|
{
|
|
status = SaferPolicy.Disallowed;
|
|
NativeMethods.CloseHandle(hRestrictedToken);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
NativeMethods.SaferCloseLevel(hAuthzLevel);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
|
|
return status;
|
|
}
|
|
#endif
|
|
/// <summary>
|
|
/// Returns the value of the Execution Policy as retrieved
|
|
/// from group policy.
|
|
/// </summary>
|
|
/// <returns>NULL if it is not defined at this level</returns>
|
|
private static string GetGroupPolicyValue(string shellId, ExecutionPolicyScope scope)
|
|
{
|
|
RegistryKey[] scopeKey = null;
|
|
|
|
switch (scope)
|
|
{
|
|
case ExecutionPolicyScope.MachinePolicy:
|
|
{
|
|
scopeKey = Utils.RegLocalMachine;
|
|
}; break;
|
|
|
|
case ExecutionPolicyScope.UserPolicy:
|
|
{
|
|
scopeKey = Utils.RegCurrentUser;
|
|
}; break;
|
|
}
|
|
|
|
Dictionary<string, object> groupPolicySettings = Utils.GetGroupPolicySetting(".", scopeKey);
|
|
if (groupPolicySettings == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Object enableScriptsValue = null;
|
|
if (groupPolicySettings.TryGetValue("EnableScripts", out enableScriptsValue))
|
|
{
|
|
if (String.Equals(enableScriptsValue.ToString(), "0", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "Restricted";
|
|
}
|
|
else if (String.Equals(enableScriptsValue.ToString(), "1", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Object executionPolicyValue = null;
|
|
if (groupPolicySettings.TryGetValue("ExecutionPolicy", out executionPolicyValue))
|
|
{
|
|
return executionPolicyValue.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the value of the Execution Policy as retrieved
|
|
/// from the local preference.
|
|
/// </summary>
|
|
/// <returns>NULL if it is not defined at this level</returns>
|
|
private static string GetLocalPreferenceValue(string shellId, ExecutionPolicyScope scope)
|
|
{
|
|
switch (scope)
|
|
{
|
|
// 1: Look up the current-user preference
|
|
case ExecutionPolicyScope.CurrentUser:
|
|
return ConfigPropertyAccessor.Instance.GetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.CurrentUser, shellId);
|
|
|
|
// 2: Look up the system-wide preference
|
|
case ExecutionPolicyScope.LocalMachine:
|
|
return ConfigPropertyAccessor.Instance.GetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.SystemWide, shellId);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion execution policy
|
|
|
|
/// <summary>
|
|
/// throw if file does not exist
|
|
/// </summary>
|
|
///
|
|
/// <param name="filePath"> path to file </param>
|
|
///
|
|
/// <returns> Does not return a value </returns>
|
|
///
|
|
/// <remarks> </remarks>
|
|
///
|
|
internal static void CheckIfFileExists(string filePath)
|
|
{
|
|
if (!File.Exists(filePath))
|
|
{
|
|
throw new FileNotFoundException(filePath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// check to see if the specified cert is suitable to be
|
|
/// used as a code signing cert
|
|
/// </summary>
|
|
///
|
|
/// <param name="c"> certificate object </param>
|
|
///
|
|
/// <returns> true on success, false otherwise </returns>
|
|
///
|
|
/// <remarks> </remarks>
|
|
///
|
|
internal static bool CertIsGoodForSigning(X509Certificate2 c)
|
|
{
|
|
if (!CertHasPrivatekey(c))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return CertHasOid(c, CertificateFilterInfo.CodeSigningOid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// check to see if the specified cert is suitable to be
|
|
/// used as an encryption cert for PKI encryption. Note
|
|
/// that this cert doesn't require the private key.
|
|
/// </summary>
|
|
///
|
|
/// <param name="c"> certificate object </param>
|
|
///
|
|
/// <returns> true on success, false otherwise </returns>
|
|
///
|
|
/// <remarks> </remarks>
|
|
///
|
|
internal static bool CertIsGoodForEncryption(X509Certificate2 c)
|
|
{
|
|
return (
|
|
CertHasOid(c, CertificateFilterInfo.DocumentEncryptionOid) &&
|
|
(CertHasKeyUsage(c, X509KeyUsageFlags.DataEncipherment) ||
|
|
CertHasKeyUsage(c, X509KeyUsageFlags.KeyEncipherment)));
|
|
}
|
|
|
|
private static bool CertHasOid(X509Certificate2 c, string oid)
|
|
{
|
|
Collection<string> ekus = GetCertEKU(c);
|
|
|
|
foreach (string testOid in ekus)
|
|
{
|
|
if (testOid == oid)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool CertHasKeyUsage(X509Certificate2 c, X509KeyUsageFlags keyUsage)
|
|
{
|
|
foreach (X509Extension extension in c.Extensions)
|
|
{
|
|
X509KeyUsageExtension keyUsageExtension = extension as X509KeyUsageExtension;
|
|
if (keyUsageExtension != null)
|
|
{
|
|
if ((keyUsageExtension.KeyUsages & keyUsage) == keyUsage)
|
|
{
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// check if the specified cert has a private key in it
|
|
/// </summary>
|
|
///
|
|
/// <param name="cert"> certificate object </param>
|
|
///
|
|
/// <returns> true on success, false otherwise </returns>
|
|
///
|
|
/// <remarks> </remarks>
|
|
///
|
|
internal static bool CertHasPrivatekey(X509Certificate2 cert)
|
|
{
|
|
return cert.HasPrivateKey;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the EKUs of a cert
|
|
/// </summary>
|
|
///
|
|
/// <param name="cert"> certificate object </param>
|
|
///
|
|
/// <returns> a collection of cert eku strings </returns>
|
|
///
|
|
/// <remarks> </remarks>
|
|
///
|
|
[ArchitectureSensitive]
|
|
internal static Collection<string> GetCertEKU(X509Certificate2 cert)
|
|
{
|
|
Collection<string> ekus = new Collection<string>();
|
|
IntPtr pCert = cert.Handle;
|
|
int structSize = 0;
|
|
IntPtr dummy = IntPtr.Zero;
|
|
|
|
if (Security.NativeMethods.CertGetEnhancedKeyUsage(pCert, 0, dummy,
|
|
out structSize))
|
|
{
|
|
if (structSize > 0)
|
|
{
|
|
IntPtr ekuBuffer = Marshal.AllocHGlobal(structSize);
|
|
|
|
try
|
|
{
|
|
if (Security.NativeMethods.CertGetEnhancedKeyUsage(pCert, 0,
|
|
ekuBuffer,
|
|
out structSize))
|
|
{
|
|
Security.NativeMethods.CERT_ENHKEY_USAGE ekuStruct =
|
|
(Security.NativeMethods.CERT_ENHKEY_USAGE)
|
|
ClrFacade.PtrToStructure<Security.NativeMethods.CERT_ENHKEY_USAGE>(ekuBuffer);
|
|
IntPtr ep = ekuStruct.rgpszUsageIdentifier;
|
|
IntPtr ekuptr;
|
|
|
|
for (int i = 0; i < ekuStruct.cUsageIdentifier; i++)
|
|
{
|
|
ekuptr = Marshal.ReadIntPtr(ep, i * Marshal.SizeOf(ep));
|
|
string eku = Marshal.PtrToStringAnsi(ekuptr);
|
|
ekus.Add(eku);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(ekuBuffer);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
|
|
return ekus;
|
|
}
|
|
|
|
/// <summary>
|
|
/// convert an int to a DWORD
|
|
/// </summary>
|
|
///
|
|
/// <param name="n"> signed int number </param>
|
|
///
|
|
/// <returns> DWORD </returns>
|
|
///
|
|
/// <remarks> </remarks>
|
|
///
|
|
internal static DWORD GetDWORDFromInt(int n)
|
|
{
|
|
UInt32 result = BitConverter.ToUInt32(BitConverter.GetBytes(n), 0);
|
|
return (DWORD)result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// convert a DWORD to int
|
|
/// </summary>
|
|
///
|
|
/// <param name="n"> number </param>
|
|
///
|
|
/// <returns> int </returns>
|
|
///
|
|
/// <remarks> </remarks>
|
|
///
|
|
internal static int GetIntFromDWORD(DWORD n)
|
|
{
|
|
Int64 n64 = n - 0x100000000L;
|
|
return (int)n64;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// information used for filtering a set of certs
|
|
/// </summary>
|
|
internal sealed class CertificateFilterInfo
|
|
{
|
|
internal CertificateFilterInfo()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// purpose of a certificate
|
|
/// </summary>
|
|
internal CertificatePurpose Purpose
|
|
{
|
|
get { return _purpose; }
|
|
set { _purpose = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// SSL Server Authentication
|
|
/// </summary>
|
|
internal bool SSLServerAuthentication
|
|
{
|
|
get { return _sslServerAuthentication; }
|
|
set { _sslServerAuthentication = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DNS name of a certificate
|
|
/// </summary>
|
|
internal string DnsName
|
|
{
|
|
set { _dnsName = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// EKU OID list of a certificate
|
|
/// </summary>
|
|
internal string[] Eku
|
|
{
|
|
set { _eku = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// remaining validity period in days for a certificate
|
|
/// </summary>
|
|
internal int ExpiringInDays
|
|
{
|
|
set { _expiringInDays = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// combine properties into a filter string
|
|
/// </summary>
|
|
internal string FilterString
|
|
{
|
|
get
|
|
{
|
|
string filterString = "";
|
|
|
|
if (_dnsName != null)
|
|
{
|
|
filterString = AppendFilter(filterString, "dns", _dnsName);
|
|
}
|
|
|
|
string ekuT = "";
|
|
if (_eku != null)
|
|
{
|
|
for (int i = 0; i < _eku.Length; i++)
|
|
{
|
|
if (ekuT.Length != 0)
|
|
{
|
|
ekuT = ekuT + ",";
|
|
}
|
|
ekuT = ekuT + _eku[i];
|
|
}
|
|
}
|
|
if (_purpose == CertificatePurpose.CodeSigning)
|
|
{
|
|
if (ekuT.Length != 0)
|
|
{
|
|
ekuT = ekuT + ",";
|
|
}
|
|
ekuT = ekuT + CodeSigningOid;
|
|
}
|
|
if (_purpose == CertificatePurpose.DocumentEncryption)
|
|
{
|
|
if (ekuT.Length != 0)
|
|
{
|
|
ekuT = ekuT + ",";
|
|
}
|
|
ekuT = ekuT + DocumentEncryptionOid;
|
|
}
|
|
if (_sslServerAuthentication)
|
|
{
|
|
if (ekuT.Length != 0)
|
|
{
|
|
ekuT = ekuT + ",";
|
|
}
|
|
ekuT = ekuT + szOID_PKIX_KP_SERVER_AUTH;
|
|
}
|
|
if (ekuT.Length != 0)
|
|
{
|
|
filterString = AppendFilter(filterString, "eku", ekuT);
|
|
if (_purpose == CertificatePurpose.CodeSigning ||
|
|
_sslServerAuthentication)
|
|
{
|
|
filterString = AppendFilter(filterString, "key", "*");
|
|
}
|
|
}
|
|
if (_expiringInDays >= 0)
|
|
{
|
|
filterString = AppendFilter(
|
|
filterString,
|
|
"ExpiringInDays",
|
|
_expiringInDays.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
if (filterString.Length == 0)
|
|
{
|
|
filterString = null;
|
|
}
|
|
return filterString;
|
|
}
|
|
}
|
|
|
|
private string AppendFilter(
|
|
string filterString,
|
|
string name,
|
|
string value)
|
|
{
|
|
string newfilter = value;
|
|
|
|
// append a "name=value" filter to the existing filter string.
|
|
// insert a separating "&" if existing filter string is not empty.
|
|
|
|
// if the value is empty, do nothing.
|
|
|
|
if (newfilter.Length != 0)
|
|
{
|
|
// if the value contains an equal sign or an ampersand, throw
|
|
// an exception to avoid compromising the native code parser.
|
|
|
|
if (newfilter.Contains("=") || newfilter.Contains("&"))
|
|
{
|
|
throw Marshal.GetExceptionForHR(
|
|
Security.NativeMethods.E_INVALID_DATA);
|
|
}
|
|
newfilter = name + "=" + newfilter;
|
|
if (filterString.Length != 0)
|
|
{
|
|
newfilter = "&" + newfilter;
|
|
}
|
|
}
|
|
return filterString + newfilter;
|
|
}
|
|
|
|
private CertificatePurpose _purpose = 0;
|
|
private bool _sslServerAuthentication = false;
|
|
private string _dnsName = null;
|
|
private string[] _eku = null;
|
|
private int _expiringInDays = -1;
|
|
|
|
internal const string CodeSigningOid = "1.3.6.1.5.5.7.3.3";
|
|
internal const string szOID_PKIX_KP_SERVER_AUTH = "1.3.6.1.5.5.7.3.1";
|
|
|
|
// The OID arc 1.3.6.1.4.1.311.80 is assigned to PowerShell. If we need
|
|
// new OIDs, we can assign them under this branch.
|
|
internal const string DocumentEncryptionOid = "1.3.6.1.4.1.311.80.1";
|
|
}
|
|
}
|
|
|
|
namespace Microsoft.PowerShell.Commands
|
|
{
|
|
/// <summary>
|
|
/// Defines the valid purposes by which
|
|
/// we can filter certificates.
|
|
/// </summary>
|
|
internal enum CertificatePurpose
|
|
{
|
|
/// <summary>
|
|
/// Certificates where a purpose has not been specified.
|
|
/// </summary>
|
|
NotSpecified = 0,
|
|
|
|
/// <summary>
|
|
/// Certificates that can be used to sign
|
|
/// code and scripts.
|
|
/// </summary>
|
|
CodeSigning = 0x1,
|
|
|
|
/// <summary>
|
|
/// Certificates that can be used to encrypt
|
|
/// data.
|
|
/// </summary>
|
|
DocumentEncryption = 0x2,
|
|
|
|
/// <summary>
|
|
/// Certificates that can be used for any
|
|
/// purpose.
|
|
/// </summary>
|
|
All = 0xffff
|
|
}
|
|
}
|
|
|
|
|
|
namespace System.Management.Automation
|
|
{
|
|
using System.Security.Cryptography.Pkcs;
|
|
|
|
/// <summary>
|
|
/// Utility class for CMS (Cryptographic Message Syntax) related operations
|
|
/// </summary>
|
|
internal static class CmsUtils
|
|
{
|
|
internal static string Encrypt(byte[] contentBytes, CmsMessageRecipient[] recipients, SessionState sessionState, out ErrorRecord error)
|
|
{
|
|
error = null;
|
|
|
|
if ((contentBytes == null) || (contentBytes.Length == 0))
|
|
{
|
|
return String.Empty;
|
|
}
|
|
|
|
// After review with the crypto board, NIST_AES256_CBC is more appropriate
|
|
// than .NET's default 3DES. Also, when specified, uses szOID_RSAES_OAEP for key
|
|
// encryption to prevent padding attacks.
|
|
const string szOID_NIST_AES256_CBC = "2.16.840.1.101.3.4.1.42";
|
|
|
|
ContentInfo content = new ContentInfo(contentBytes);
|
|
EnvelopedCms cms = new EnvelopedCms(content,
|
|
new AlgorithmIdentifier(
|
|
Oid.FromOidValue(szOID_NIST_AES256_CBC, OidGroup.EncryptionAlgorithm)));
|
|
|
|
CmsRecipientCollection recipientCollection = new CmsRecipientCollection();
|
|
foreach (CmsMessageRecipient recipient in recipients)
|
|
{
|
|
// Resolve the recipient, if it hasn't been done yet.
|
|
if ((recipient.Certificates != null) && (recipient.Certificates.Count == 0))
|
|
{
|
|
recipient.Resolve(sessionState, ResolutionPurpose.Encryption, out error);
|
|
}
|
|
|
|
if (error != null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (X509Certificate2 certificate in recipient.Certificates)
|
|
{
|
|
recipientCollection.Add(new CmsRecipient(certificate));
|
|
}
|
|
}
|
|
|
|
cms.Encrypt(recipientCollection);
|
|
|
|
byte[] encodedBytes = cms.Encode();
|
|
string encodedContent = CmsUtils.GetAsciiArmor(encodedBytes);
|
|
return encodedContent;
|
|
}
|
|
|
|
internal static string BEGIN_CMS_SIGIL = "-----BEGIN CMS-----";
|
|
internal static string END_CMS_SIGIL = "-----END CMS-----";
|
|
|
|
internal static string BEGIN_CERTIFICATE_SIGIL = "-----BEGIN CERTIFICATE-----";
|
|
internal static string END_CERTIFICATE_SIGIL = "-----END CERTIFICATE-----";
|
|
|
|
/// <summary>
|
|
/// Adds Ascii armour to a byte stream in Base64 format
|
|
/// </summary>
|
|
/// <param name="bytes">The bytes to encode</param>
|
|
internal static string GetAsciiArmor(byte[] bytes)
|
|
{
|
|
StringBuilder output = new StringBuilder();
|
|
output.AppendLine(BEGIN_CMS_SIGIL);
|
|
|
|
string encodedString = Convert.ToBase64String(bytes, Base64FormattingOptions.InsertLineBreaks);
|
|
output.AppendLine(encodedString);
|
|
output.Append(END_CMS_SIGIL);
|
|
|
|
return output.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes Ascii armour from a byte stream
|
|
/// </summary>
|
|
/// <param name="actualContent">The Ascii armored content</param>
|
|
/// <param name="beginMarker">The marker of the start of the Base64 content</param>
|
|
/// <param name="endMarker">The marker of the end of the Base64 content</param>
|
|
/// <param name="startIndex">The beginning of where the Ascii armor was detected</param>
|
|
/// <param name="endIndex">The end of where the Ascii armor was detected</param>
|
|
internal static byte[] RemoveAsciiArmor(string actualContent, string beginMarker, string endMarker, out int startIndex, out int endIndex)
|
|
{
|
|
byte[] messageBytes = null;
|
|
startIndex = -1;
|
|
endIndex = -1;
|
|
|
|
startIndex = actualContent.IndexOf(beginMarker, StringComparison.OrdinalIgnoreCase);
|
|
if (startIndex < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
endIndex = actualContent.IndexOf(endMarker, startIndex, StringComparison.OrdinalIgnoreCase) +
|
|
endMarker.Length;
|
|
if (endIndex < endMarker.Length)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
int startContent = startIndex + beginMarker.Length;
|
|
int endContent = endIndex - endMarker.Length;
|
|
string encodedContent = actualContent.Substring(startContent, endContent - startContent);
|
|
encodedContent = System.Text.RegularExpressions.Regex.Replace(encodedContent, "\\s", "");
|
|
messageBytes = Convert.FromBase64String(encodedContent);
|
|
|
|
return messageBytes;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a message recipient for the Cms cmdlets.
|
|
/// </summary>
|
|
public class CmsMessageRecipient
|
|
{
|
|
/// <summary>
|
|
/// Creates an instance of the CmsMessageRecipient class
|
|
/// </summary>
|
|
internal CmsMessageRecipient() { }
|
|
|
|
/// <summary>
|
|
/// Creates an instance of the CmsMessageRecipient class
|
|
/// </summary>
|
|
/// <param name="identifier">
|
|
/// The identifier of the CmsMessageRecipient.
|
|
/// Can be either:
|
|
/// - The path to a file containing the certificate
|
|
/// - The path to a directory containing the certificate
|
|
/// - The thumbprint of the certificate, used to find the certificate in the certificate store
|
|
/// - The Subject name of the recipient, used to find the certificate in the certificate store
|
|
/// </param>
|
|
public CmsMessageRecipient(string identifier)
|
|
{
|
|
_identifier = identifier;
|
|
this.Certificates = new X509Certificate2Collection();
|
|
}
|
|
private string _identifier = null;
|
|
|
|
/// <summary>
|
|
/// Creates an instance of the CmsMessageRecipient class
|
|
/// </summary>
|
|
/// <param name="certificate">The certificate to use.</param>
|
|
public CmsMessageRecipient(X509Certificate2 certificate)
|
|
{
|
|
_pendingCertificate = certificate;
|
|
this.Certificates = new X509Certificate2Collection();
|
|
}
|
|
private X509Certificate2 _pendingCertificate = null;
|
|
|
|
/// <summary>
|
|
/// Gets the certificate associated with this recipient
|
|
/// </summary>
|
|
public X509Certificate2Collection Certificates
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the provided identifier into a collection of certificates.
|
|
/// </summary>
|
|
/// <param name="sessionState">A reference to an instance of Powershell's SessionState class.</param>
|
|
/// <param name="purpose">The purpose for which this identifier is being resolved (Encryption / Decryption.</param>
|
|
/// <param name="error">The error generated (if any) for this resolution.</param>
|
|
public void Resolve(SessionState sessionState, ResolutionPurpose purpose, out ErrorRecord error)
|
|
{
|
|
error = null;
|
|
|
|
// Process the certificate if that was supplied exactly
|
|
if (_pendingCertificate != null)
|
|
{
|
|
ProcessResolvedCertificates(purpose,
|
|
new List<X509Certificate2> { _pendingCertificate }, out error);
|
|
if ((error != null) || (Certificates.Count != 0))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (_identifier != null)
|
|
{
|
|
// First try to resolve assuming that the cert was Base64 encoded.
|
|
ResolveFromBase64Encoding(purpose, out error);
|
|
if ((error != null) || (Certificates.Count != 0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Then try to resolve by path.
|
|
ResolveFromPath(sessionState, purpose, out error);
|
|
if ((error != null) || (Certificates.Count != 0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Then by thumbprint
|
|
ResolveFromThumbprint(sessionState, purpose, out error);
|
|
if ((error != null) || (Certificates.Count != 0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Then by Subject Name
|
|
ResolveFromSubjectName(sessionState, purpose, out error);
|
|
if ((error != null) || (Certificates.Count != 0))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Generate an error if no cert was found (and this is an encryption attempt).
|
|
// If it is only decryption, then the system will always look in the 'My' store anyways, so
|
|
// don't generate an error if they used wildcards. If they did not use wildcards,
|
|
// then generate an error because they were expecting something specific.
|
|
if ((purpose == ResolutionPurpose.Encryption) ||
|
|
(!WildcardPattern.ContainsWildcardCharacters(_identifier)))
|
|
{
|
|
error = new ErrorRecord(
|
|
new ArgumentException(
|
|
String.Format(CultureInfo.InvariantCulture,
|
|
SecuritySupportStrings.NoCertificateFound, _identifier)),
|
|
"NoCertificateFound", ErrorCategory.ObjectNotFound, _identifier);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
private void ResolveFromBase64Encoding(ResolutionPurpose purpose, out ErrorRecord error)
|
|
{
|
|
error = null;
|
|
int startIndex, endIndex;
|
|
byte[] messageBytes = null;
|
|
try
|
|
{
|
|
messageBytes = CmsUtils.RemoveAsciiArmor(_identifier, CmsUtils.BEGIN_CERTIFICATE_SIGIL, CmsUtils.END_CERTIFICATE_SIGIL, out startIndex, out endIndex);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
// Not Base-64 encoded
|
|
return;
|
|
}
|
|
|
|
// Didn't have the sigil
|
|
if (messageBytes == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
List<X509Certificate2> certificatesToProcess = new List<X509Certificate2>();
|
|
try
|
|
{
|
|
X509Certificate2 newCertificate = new X509Certificate2(messageBytes);
|
|
certificatesToProcess.Add(newCertificate);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// User call-out, catch-all OK
|
|
|
|
// Wasn't certificate data
|
|
return;
|
|
}
|
|
|
|
// Now validate the certificate
|
|
ProcessResolvedCertificates(purpose, certificatesToProcess, out error);
|
|
}
|
|
|
|
private void ResolveFromPath(SessionState sessionState, ResolutionPurpose purpose, out ErrorRecord error)
|
|
{
|
|
error = null;
|
|
ProviderInfo pathProvider = null;
|
|
Collection<string> resolvedPaths = null;
|
|
|
|
try
|
|
{
|
|
resolvedPaths = sessionState.Path.GetResolvedProviderPathFromPSPath(_identifier, out pathProvider);
|
|
}
|
|
catch (SessionStateException)
|
|
{
|
|
// If we got an ItemNotFound / etc., then this didn't represent a valid path.
|
|
}
|
|
|
|
// If we got a resolved path, try to load certs from that path.
|
|
if ((resolvedPaths != null) && (resolvedPaths.Count != 0))
|
|
{
|
|
// Ensure the path is from the file system provider
|
|
if (!String.Equals(pathProvider.Name, "FileSystem", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
error = new ErrorRecord(
|
|
new ArgumentException(
|
|
String.Format(CultureInfo.InvariantCulture,
|
|
SecuritySupportStrings.CertificatePathMustBeFileSystemPath, _identifier)),
|
|
"CertificatePathMustBeFileSystemPath", ErrorCategory.ObjectNotFound, pathProvider);
|
|
return;
|
|
}
|
|
|
|
// If this is a directory, add all certificates in it. This will be the primary
|
|
// scenario for decryption via Group Protected PFX files
|
|
// (http://social.technet.microsoft.com/wiki/contents/articles/13922.certificate-pfx-export-and-import-using-ad-ds-account-protection.aspx)
|
|
List<string> pathsToAdd = new List<string>();
|
|
List<string> pathsToRemove = new List<string>();
|
|
foreach (string resolvedPath in resolvedPaths)
|
|
{
|
|
if (System.IO.Directory.Exists(resolvedPath))
|
|
{
|
|
// It would be nice to limit this to *.pfx, *.cer, etc., but
|
|
// the crypto APIs support extracting certificates from arbitrary file types.
|
|
pathsToAdd.AddRange(System.IO.Directory.GetFiles(resolvedPath));
|
|
pathsToRemove.Add(resolvedPath);
|
|
}
|
|
}
|
|
|
|
// Update resolved paths
|
|
foreach (string path in pathsToAdd)
|
|
{
|
|
resolvedPaths.Add(path);
|
|
}
|
|
|
|
foreach (string path in pathsToRemove)
|
|
{
|
|
resolvedPaths.Remove(path);
|
|
}
|
|
|
|
List<X509Certificate2> certificatesToProcess = new List<X509Certificate2>();
|
|
foreach (string path in resolvedPaths)
|
|
{
|
|
X509Certificate2 certificate = null;
|
|
|
|
try
|
|
{
|
|
certificate = new X509Certificate2(path);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// User call-out, catch-all OK
|
|
continue;
|
|
}
|
|
|
|
certificatesToProcess.Add(certificate);
|
|
}
|
|
|
|
ProcessResolvedCertificates(purpose, certificatesToProcess, out error);
|
|
}
|
|
}
|
|
|
|
private void ResolveFromThumbprint(SessionState sessionState, ResolutionPurpose purpose, out ErrorRecord error)
|
|
{
|
|
// Quickly check that this is a thumbprint-like pattern (just hex)
|
|
if (!System.Text.RegularExpressions.Regex.IsMatch(_identifier, "^[a-f0-9]+$", Text.RegularExpressions.RegexOptions.IgnoreCase))
|
|
{
|
|
error = null;
|
|
return;
|
|
}
|
|
|
|
Collection<PSObject> certificates = new Collection<PSObject>();
|
|
|
|
try
|
|
{
|
|
// Get first from 'My' store
|
|
string certificatePath = sessionState.Path.Combine("Microsoft.PowerShell.Security\\Certificate::CurrentUser\\My", _identifier);
|
|
if (sessionState.InvokeProvider.Item.Exists(certificatePath))
|
|
{
|
|
foreach (PSObject certificateObject in sessionState.InvokeProvider.Item.Get(certificatePath))
|
|
{
|
|
certificates.Add(certificateObject);
|
|
}
|
|
}
|
|
|
|
// Second from 'LocalMachine' store
|
|
certificatePath = sessionState.Path.Combine("Microsoft.PowerShell.Security\\Certificate::LocalMachine\\My", _identifier);
|
|
if (sessionState.InvokeProvider.Item.Exists(certificatePath))
|
|
{
|
|
foreach (PSObject certificateObject in sessionState.InvokeProvider.Item.Get(certificatePath))
|
|
{
|
|
certificates.Add(certificateObject);
|
|
}
|
|
}
|
|
}
|
|
catch (SessionStateException)
|
|
{
|
|
// If we got an ItemNotFound / etc., then this didn't represent a valid path.
|
|
}
|
|
|
|
List<X509Certificate2> certificatesToProcess = new List<X509Certificate2>();
|
|
foreach (PSObject certificateObject in certificates)
|
|
{
|
|
X509Certificate2 certificate = certificateObject.BaseObject as X509Certificate2;
|
|
if (certificate != null)
|
|
{
|
|
certificatesToProcess.Add(certificate);
|
|
}
|
|
}
|
|
|
|
ProcessResolvedCertificates(purpose, certificatesToProcess, out error);
|
|
}
|
|
|
|
private void ResolveFromSubjectName(SessionState sessionState, ResolutionPurpose purpose, out ErrorRecord error)
|
|
{
|
|
Collection<PSObject> certificates = new Collection<PSObject>();
|
|
WildcardPattern subjectNamePattern = WildcardPattern.Get(_identifier, WildcardOptions.IgnoreCase);
|
|
|
|
try
|
|
{
|
|
// Get first from 'My' store, then 'LocalMachine'
|
|
string[] certificatePaths = new string[] {
|
|
"Microsoft.PowerShell.Security\\Certificate::CurrentUser\\My",
|
|
"Microsoft.PowerShell.Security\\Certificate::LocalMachine\\My" };
|
|
|
|
foreach (string certificatePath in certificatePaths)
|
|
{
|
|
foreach (PSObject certificateObject in sessionState.InvokeProvider.ChildItem.Get(certificatePath, false))
|
|
{
|
|
if (subjectNamePattern.IsMatch(certificateObject.Properties["Subject"].Value.ToString()))
|
|
{
|
|
certificates.Add(certificateObject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (SessionStateException)
|
|
{
|
|
// If we got an ItemNotFound / etc., then this didn't represent a valid path.
|
|
}
|
|
|
|
List<X509Certificate2> certificatesToProcess = new List<X509Certificate2>();
|
|
foreach (PSObject certificateObject in certificates)
|
|
{
|
|
X509Certificate2 certificate = certificateObject.BaseObject as X509Certificate2;
|
|
if (certificate != null)
|
|
{
|
|
certificatesToProcess.Add(certificate);
|
|
}
|
|
}
|
|
|
|
ProcessResolvedCertificates(purpose, certificatesToProcess, out error);
|
|
}
|
|
|
|
private void ProcessResolvedCertificates(ResolutionPurpose purpose, List<X509Certificate2> certificatesToProcess, out ErrorRecord error)
|
|
{
|
|
error = null;
|
|
HashSet<String> processedThumbprints = new HashSet<string>();
|
|
|
|
foreach (X509Certificate2 certificate in certificatesToProcess)
|
|
{
|
|
if (!SecuritySupport.CertIsGoodForEncryption(certificate))
|
|
{
|
|
// If they specified a specific cert, generate an error if it isn't good
|
|
// for encryption.
|
|
if (!WildcardPattern.ContainsWildcardCharacters(_identifier))
|
|
{
|
|
error = new ErrorRecord(
|
|
new ArgumentException(
|
|
String.Format(CultureInfo.InvariantCulture,
|
|
SecuritySupportStrings.CertificateCannotBeUsedForEncryption, certificate.Thumbprint, CertificateFilterInfo.DocumentEncryptionOid)),
|
|
"CertificateCannotBeUsedForEncryption", ErrorCategory.InvalidData, certificate);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// When decrypting, only look for certs that have the private key
|
|
if (purpose == ResolutionPurpose.Decryption)
|
|
{
|
|
if (!certificate.HasPrivateKey)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (processedThumbprints.Contains(certificate.Thumbprint))
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
processedThumbprints.Add(certificate.Thumbprint);
|
|
}
|
|
|
|
|
|
if (purpose == ResolutionPurpose.Encryption)
|
|
{
|
|
// Only let wildcards expand to one recipient. Otherwise, data
|
|
// may be encrypted to the wrong person on accident.
|
|
if (Certificates.Count > 0)
|
|
{
|
|
error = new ErrorRecord(
|
|
new ArgumentException(
|
|
String.Format(CultureInfo.InvariantCulture,
|
|
SecuritySupportStrings.IdentifierMustReferenceSingleCertificate, _identifier, "To")),
|
|
"IdentifierMustReferenceSingleCertificate", ErrorCategory.LimitsExceeded, certificatesToProcess);
|
|
Certificates.Clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
Certificates.Add(certificate);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the purpose for resolution of a CmsMessageRecipient
|
|
/// </summary>
|
|
public enum ResolutionPurpose
|
|
{
|
|
/// <summary>
|
|
/// This message recipient is intended to be used for message encryption
|
|
/// </summary>
|
|
Encryption,
|
|
|
|
/// <summary>
|
|
/// This message recipient is intended to be used for message decryption
|
|
/// </summary>
|
|
Decryption
|
|
}
|
|
|
|
internal class AmsiUtils
|
|
{
|
|
internal static int Init()
|
|
{
|
|
Diagnostics.Assert(s_amsiContext == IntPtr.Zero, "Init should be called just once");
|
|
|
|
lock (s_amsiLockObject)
|
|
{
|
|
Process currentProcess = Process.GetCurrentProcess();
|
|
string hostname;
|
|
try
|
|
{
|
|
var processModule = PsUtils.GetMainModule(currentProcess);
|
|
hostname = string.Concat("PowerShell_", processModule.FileName, "_",
|
|
processModule.FileVersionInfo.ProductVersion);
|
|
}
|
|
catch (ComponentModel.Win32Exception)
|
|
{
|
|
// This exception can be thrown during thread impersonation (Access Denied for process module access).
|
|
// Use command line arguments or process name.
|
|
string[] cmdLineArgs = Environment.GetCommandLineArgs();
|
|
string processPath = (cmdLineArgs.Length > 0) ? cmdLineArgs[0] : currentProcess.ProcessName;
|
|
hostname = string.Concat("PowerShell_", processPath, ".exe_0.0.0.0");
|
|
}
|
|
|
|
#if !CORECLR
|
|
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
|
|
#endif
|
|
|
|
var hr = AmsiNativeMethods.AmsiInitialize(hostname, ref s_amsiContext);
|
|
if (!Utils.Succeeded(hr))
|
|
{
|
|
s_amsiInitFailed = true;
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans a string buffer for malware using the Antimalware Scan Interface (AMSI).
|
|
/// Caller is responsible for calling AmsiCloseSession when a "session" (script)
|
|
/// is complete, and for calling AmsiUninitialize when the runspace is being torn down.
|
|
/// </summary>
|
|
/// <param name="content">The string to be scanned</param>
|
|
/// <param name="sourceMetadata">Information about the source (filename, etc.)</param>
|
|
/// <returns>AMSI_RESULT_DETECTED if malware was detected in the sample.</returns>
|
|
internal static AmsiNativeMethods.AMSI_RESULT ScanContent(string content, string sourceMetadata)
|
|
{
|
|
#if UNIX
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
|
|
#else
|
|
return WinScanContent(content, sourceMetadata);
|
|
#endif
|
|
}
|
|
|
|
internal static AmsiNativeMethods.AMSI_RESULT WinScanContent(string content, string sourceMetadata)
|
|
{
|
|
if (String.IsNullOrEmpty(sourceMetadata))
|
|
{
|
|
sourceMetadata = String.Empty;
|
|
}
|
|
|
|
const string EICAR_STRING = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
|
if (InternalTestHooks.UseDebugAmsiImplementation)
|
|
{
|
|
if (content.IndexOf(EICAR_STRING, StringComparison.Ordinal) >= 0)
|
|
{
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED;
|
|
}
|
|
}
|
|
|
|
// If we had a previous initialization failure, just return the neutral result.
|
|
if (s_amsiInitFailed)
|
|
{
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
|
|
}
|
|
|
|
lock (s_amsiLockObject)
|
|
{
|
|
if (s_amsiInitFailed)
|
|
{
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
|
|
}
|
|
|
|
try
|
|
{
|
|
int hr = 0;
|
|
|
|
// Initialize AntiMalware Scan Interface, if not already initialized.
|
|
// If we failed to initialize previously, just return the neutral result ("AMSI_RESULT_NOT_DETECTED")
|
|
if (s_amsiContext == IntPtr.Zero)
|
|
{
|
|
hr = Init();
|
|
|
|
if (!Utils.Succeeded(hr))
|
|
{
|
|
s_amsiInitFailed = true;
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
|
|
}
|
|
}
|
|
|
|
// Initialize the session, if one isn't already started.
|
|
// If we failed to initialize previously, just return the neutral result ("AMSI_RESULT_NOT_DETECTED")
|
|
if (s_amsiSession == IntPtr.Zero)
|
|
{
|
|
hr = AmsiNativeMethods.AmsiOpenSession(s_amsiContext, ref s_amsiSession);
|
|
AmsiInitialized = true;
|
|
|
|
if (!Utils.Succeeded(hr))
|
|
{
|
|
s_amsiInitFailed = true;
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
|
|
}
|
|
}
|
|
|
|
AmsiNativeMethods.AMSI_RESULT result = AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_CLEAN;
|
|
|
|
hr = AmsiNativeMethods.AmsiScanString(
|
|
s_amsiContext,
|
|
content,
|
|
sourceMetadata,
|
|
s_amsiSession,
|
|
ref result);
|
|
|
|
if (!Utils.Succeeded(hr))
|
|
{
|
|
// If we got a failure, just return the neutral result ("AMSI_RESULT_NOT_DETECTED")
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (DllNotFoundException)
|
|
{
|
|
s_amsiInitFailed = true;
|
|
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static void CurrentDomain_ProcessExit(object sender, EventArgs e)
|
|
{
|
|
#if !UNIX
|
|
VerifyAmsiUninitializeCalled();
|
|
#endif
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")]
|
|
private static IntPtr s_amsiContext = IntPtr.Zero;
|
|
[SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")]
|
|
private static IntPtr s_amsiSession = IntPtr.Zero;
|
|
|
|
private static bool s_amsiInitFailed = false;
|
|
private static object s_amsiLockObject = new Object();
|
|
|
|
/// <summary>
|
|
/// Reset the AMSI session (used to track related script invocations)
|
|
/// </summary>
|
|
internal static void CloseSession()
|
|
{
|
|
#if !UNIX
|
|
WinCloseSession();
|
|
#endif
|
|
}
|
|
|
|
internal static void WinCloseSession()
|
|
{
|
|
if (!s_amsiInitFailed)
|
|
{
|
|
if ((s_amsiContext != IntPtr.Zero) && (s_amsiSession != IntPtr.Zero))
|
|
{
|
|
lock (s_amsiLockObject)
|
|
{
|
|
// Clean up the session if one was open.
|
|
if ((s_amsiContext != IntPtr.Zero) && (s_amsiSession != IntPtr.Zero))
|
|
{
|
|
AmsiNativeMethods.AmsiCloseSession(s_amsiContext, s_amsiSession);
|
|
s_amsiSession = IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uninitialize the AMSI interface
|
|
/// </summary>
|
|
internal static void Uninitialize()
|
|
{
|
|
#if !UNIX
|
|
WinUninitialize();
|
|
#endif
|
|
}
|
|
|
|
internal static void WinUninitialize()
|
|
{
|
|
AmsiUninitializeCalled = true;
|
|
if (!s_amsiInitFailed)
|
|
{
|
|
lock (s_amsiLockObject)
|
|
{
|
|
if (s_amsiContext != IntPtr.Zero)
|
|
{
|
|
CloseSession();
|
|
|
|
// Uninitialize the AMSI interface.
|
|
AmsiCleanedUp = true;
|
|
AmsiNativeMethods.AmsiUninitialize(s_amsiContext);
|
|
s_amsiContext = IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
public static bool AmsiUninitializeCalled = false;
|
|
public static bool AmsiInitialized = false;
|
|
public static bool AmsiCleanedUp = false;
|
|
|
|
private static void VerifyAmsiUninitializeCalled()
|
|
{
|
|
Debug.Assert((!AmsiInitialized) || AmsiUninitializeCalled, "AMSI should have been uninitialized.");
|
|
}
|
|
|
|
internal class AmsiNativeMethods
|
|
{
|
|
internal enum AMSI_RESULT
|
|
{
|
|
/// AMSI_RESULT_CLEAN -> 0
|
|
AMSI_RESULT_CLEAN = 0,
|
|
|
|
/// AMSI_RESULT_NOT_DETECTED -> 1
|
|
AMSI_RESULT_NOT_DETECTED = 1,
|
|
|
|
/// AMSI_RESULT_DETECTED -> 32768
|
|
AMSI_RESULT_DETECTED = 32768,
|
|
}
|
|
|
|
/// Return Type: HRESULT->LONG->int
|
|
///appName: LPCWSTR->WCHAR*
|
|
///amsiContext: HAMSICONTEXT*
|
|
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiInitialize", CallingConvention = CallingConvention.StdCall)]
|
|
internal static extern int AmsiInitialize(
|
|
[InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string appName, ref System.IntPtr amsiContext);
|
|
|
|
|
|
/// Return Type: void
|
|
///amsiContext: HAMSICONTEXT->HAMSICONTEXT__*
|
|
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiUninitialize", CallingConvention = CallingConvention.StdCall)]
|
|
internal static extern void AmsiUninitialize(System.IntPtr amsiContext);
|
|
|
|
|
|
/// Return Type: HRESULT->LONG->int
|
|
///amsiContext: HAMSICONTEXT->HAMSICONTEXT__*
|
|
///amsiSession: HAMSISESSION*
|
|
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiOpenSession", CallingConvention = CallingConvention.StdCall)]
|
|
internal static extern int AmsiOpenSession(System.IntPtr amsiContext, ref System.IntPtr amsiSession);
|
|
|
|
|
|
/// Return Type: void
|
|
///amsiContext: HAMSICONTEXT->HAMSICONTEXT__*
|
|
///amsiSession: HAMSISESSION->HAMSISESSION__*
|
|
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiCloseSession", CallingConvention = CallingConvention.StdCall)]
|
|
internal static extern void AmsiCloseSession(System.IntPtr amsiContext, System.IntPtr amsiSession);
|
|
|
|
|
|
/// Return Type: HRESULT->LONG->int
|
|
///amsiContext: HAMSICONTEXT->HAMSICONTEXT__*
|
|
///buffer: PVOID->void*
|
|
///length: ULONG->unsigned int
|
|
///contentName: LPCWSTR->WCHAR*
|
|
///amsiSession: HAMSISESSION->HAMSISESSION__*
|
|
///result: AMSI_RESULT*
|
|
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiScanBuffer", CallingConvention = CallingConvention.StdCall)]
|
|
internal static extern int AmsiScanBuffer(
|
|
System.IntPtr amsiContext, System.IntPtr buffer, uint length,
|
|
[InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string contentName, System.IntPtr amsiSession, ref AMSI_RESULT result);
|
|
|
|
|
|
/// Return Type: HRESULT->LONG->int
|
|
///amsiContext: HAMSICONTEXT->HAMSICONTEXT__*
|
|
///string: LPCWSTR->WCHAR*
|
|
///contentName: LPCWSTR->WCHAR*
|
|
///amsiSession: HAMSISESSION->HAMSISESSION__*
|
|
///result: AMSI_RESULT*
|
|
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiScanString", CallingConvention = CallingConvention.StdCall)]
|
|
internal static extern int AmsiScanString(
|
|
System.IntPtr amsiContext, [InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string @string,
|
|
[InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string contentName, System.IntPtr amsiSession, ref AMSI_RESULT result);
|
|
}
|
|
}
|
|
}
|
|
#pragma warning restore 56523
|