From 2f2ecc12e77da307e02d82b7a5c27f6ee1aaf1f7 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Mon, 18 Mar 2024 16:30:45 +0000 Subject: [PATCH] Add ACL support for Certificates Add the ability to get and set ACLs for certificate keys under the Certificate PSProvider. This can be used to manage the permissions of the keys associated with a certificate and not the certificates themselves which are governed by the store they are located in. --- .../resources/CertificateProviderStrings.resx | 21 + .../security/CertificateProvider.cs | 5 +- .../security/CertificateSecurity.cs | 760 ++++++++++++++++++ .../CryptAcquireCertificatePrivateKey.cs | 107 +++ .../Interop/Windows/CryptGetProvParam.cs | 27 + .../Interop/Windows/CryptReleaseContext.cs | 21 + .../Interop/Windows/CryptSetProvParam.cs | 24 + .../engine/Interop/Windows/Errors.cs | 5 + .../Interop/Windows/NCryptFreeObject.cs | 19 + .../Interop/Windows/NCryptGetProperty.cs | 27 + .../Interop/Windows/NCryptSetProperty.cs | 24 + .../CertificateProviderAcl.Tests.ps1 | 293 +++++++ 12 files changed, 1332 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.PowerShell.Security/security/CertificateSecurity.cs create mode 100644 src/System.Management.Automation/engine/Interop/Windows/CryptAcquireCertificatePrivateKey.cs create mode 100644 src/System.Management.Automation/engine/Interop/Windows/CryptGetProvParam.cs create mode 100644 src/System.Management.Automation/engine/Interop/Windows/CryptReleaseContext.cs create mode 100644 src/System.Management.Automation/engine/Interop/Windows/CryptSetProvParam.cs create mode 100644 src/System.Management.Automation/engine/Interop/Windows/NCryptFreeObject.cs create mode 100644 src/System.Management.Automation/engine/Interop/Windows/NCryptGetProperty.cs create mode 100644 src/System.Management.Automation/engine/Interop/Windows/NCryptSetProperty.cs create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Security/CertificateProviderAcl.Tests.ps1 diff --git a/src/Microsoft.PowerShell.Security/resources/CertificateProviderStrings.resx b/src/Microsoft.PowerShell.Security/resources/CertificateProviderStrings.resx index c45c640bbb0d0..756df72208633 100644 --- a/src/Microsoft.PowerShell.Security/resources/CertificateProviderStrings.resx +++ b/src/Microsoft.PowerShell.Security/resources/CertificateProviderStrings.resx @@ -186,4 +186,25 @@ . The following error may be a result of user credentials required on the remote machine. See Enable-WSManCredSSP Cmdlet help on how to enable and use CredSSP for delegation with PowerShell remoting. + + You cannot get an ACL for the certificate provider path '{0}', only certificate items are supported. + + + You cannot set an ACL for the certificate provider path '{0}', only certificate items are supported. + + + You cannot get Certificate SecurityDescriptor for the type '{0}', only the 'Key' type is supported. + + + Failed to retrieve certificate key handle due to access permissions: {0} + + + Failed to retrieve certificate key handle as certificate has no associated key: {0} + + + Failed to retrieve certificate key security descriptor: {0} + + + Failed to set certificate key security descriptor: {0} + diff --git a/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs b/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs index 527bf6be67538..9422164121307 100644 --- a/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs +++ b/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs @@ -17,6 +17,7 @@ using System.Management.Automation.Provider; using System.Runtime.InteropServices; using System.Security; +using System.Security.AccessControl; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; @@ -559,7 +560,9 @@ internal enum CertificateProviderItem [OutputType(typeof(PathInfo), ProviderCmdlet = ProviderCmdlet.PopLocation)] [OutputType(typeof(Microsoft.PowerShell.Commands.X509StoreLocation), typeof(X509Certificate2), ProviderCmdlet = ProviderCmdlet.GetItem)] [OutputType(typeof(X509Store), typeof(X509Certificate2), ProviderCmdlet = ProviderCmdlet.GetChildItem)] - public sealed class CertificateProvider : NavigationCmdletProvider, ICmdletProviderSupportsHelp + [OutputType(typeof(CertificateKeySecurity), ProviderCmdlet = ProviderCmdlet.GetAcl)] + [OutputType(typeof(CertificateKeySecurity), ProviderCmdlet = ProviderCmdlet.SetAcl)] + public sealed partial class CertificateProvider : NavigationCmdletProvider, ICmdletProviderSupportsHelp, ISecurityDescriptorCmdletProvider { #region tracer diff --git a/src/Microsoft.PowerShell.Security/security/CertificateSecurity.cs b/src/Microsoft.PowerShell.Security/security/CertificateSecurity.cs new file mode 100644 index 0000000000000..4f3bc051194ad --- /dev/null +++ b/src/Microsoft.PowerShell.Security/security/CertificateSecurity.cs @@ -0,0 +1,760 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable +#if !UNIX + +using System; +using System.Buffers; +using System.ComponentModel; +using System.Management.Automation; +using System.Management.Automation.Provider; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; + +namespace Microsoft.PowerShell.Commands; + +/// +/// The CertificateProvider ISecurityDescriptorCmdletProvider +/// implementation. +/// +public sealed partial class CertificateProvider : ISecurityDescriptorCmdletProvider +{ + private const int OWNER_SECURITY_INFORMATION = 0x00000001; + private const int GROUP_SECURITY_INFORMATION = 0x00000002; + private const int DACL_SECURITY_INFORMATION = 0x00000004; + private const int SACL_SECURITY_INFORMATION = 0x00000008; + + #region ISecurityDescriptorCmdletProvider members + + /// + /// Gets the SecurityDescriptor at the specified path, including only the specified + /// AccessControlSections. + /// + /// + /// The path of the item to retrieve. It may be a drive or provider-qualified path and may include. + /// glob characters. + /// + /// + /// The sections of the security descriptor to include. + /// + public void GetSecurityDescriptor( + string? path, + AccessControlSections includeSections) + { + if (string.IsNullOrEmpty(path)) + { + throw PSTraceSource.NewArgumentNullException(nameof(path)); + } + + if ((includeSections & ~AccessControlSections.All) != 0) + { + throw PSTraceSource.NewArgumentException(nameof(includeSections)); + } + + path = NormalizePath(path); + object item = GetItemAtPath(path, false, out var _); + if (!(item is X509Certificate2 cert)) + { + throw PSTraceSource.NewArgumentException( + nameof(path), + CertificateProviderStrings.CannotGetAclWrongPathType, + path); + } + + using var keyHandle = GetCertificateKeyHandle(cert); + try + { + CertificateKeySecurity sd = GetKeySecurity(keyHandle, includeSections); + WriteSecurityDescriptorObject(sd, path); + } + catch (Win32Exception e) + { + string msg = string.Format(CertificateProviderStrings.GetKeySDFailure, e.Message); + ErrorRecord err = new ErrorRecord( + e, + "GetCertKeyDescriptorWin32Failure", + ErrorCategory.NotSpecified, + path) + { + ErrorDetails = new ErrorDetails(msg), + }; + throw new CertificateProviderWrappedErrorRecord(err); + } + } + + /// + /// Creates a new empty security descriptor of the same type as + /// the item specified by the path. + /// + /// + /// Path of the item to use to determine the type of resulting + /// SecurityDescriptor. + /// + /// + /// The sections of the security descriptor to create. + /// + /// + /// A new ObjectSecurity object of the same type as + /// the item specified by the path. + /// + public ObjectSecurity NewSecurityDescriptorFromPath( + string? path, + AccessControlSections includeSections) + { + if (string.IsNullOrEmpty(path)) + { + throw PSTraceSource.NewArgumentNullException(nameof(path)); + } + + if ((includeSections & ~AccessControlSections.All) != 0) + { + throw PSTraceSource.NewArgumentException(nameof(includeSections)); + } + + return new CertificateKeySecurity(); + } + + /// + /// Creates a new empty security descriptor of the specified type. + /// + /// + /// The type of Security Descriptor to create. The only valid type is + /// "key". + /// + /// + /// The sections of the security descriptor to create. + /// + /// + /// A new ObjectSecurity object of the specified type. + /// + public ObjectSecurity NewSecurityDescriptorOfType( + string type, + AccessControlSections includeSections) + { + if (type.ToLowerInvariant() != "key") + { + throw PSTraceSource.NewArgumentException( + nameof(type), + CertificateProviderStrings.CannotGetAclWrongItemType, + type); + } + + if ((includeSections & ~AccessControlSections.All) != 0) + { + throw PSTraceSource.NewArgumentException(nameof(includeSections)); + } + + return new CertificateKeySecurity(); + } + + /// + /// Sets the SecurityDescriptor at the specified path. + /// + /// + /// The path of the item to set the security descriptor on. + /// It may be a drive or provider-qualified path and may include. + /// glob characters. + /// + /// + /// The new security descriptor for the item. + /// + public void SetSecurityDescriptor( + string? path, + ObjectSecurity? securityDescriptor) + { + if (string.IsNullOrEmpty(path)) + { + throw PSTraceSource.NewArgumentNullException(nameof(path)); + } + + if (securityDescriptor is null) + { + throw PSTraceSource.NewArgumentNullException(nameof(securityDescriptor)); + } + + path = NormalizePath(path); + object item = GetItemAtPath(path, false, out var _); + if (!(item is X509Certificate2 cert)) + { + throw PSTraceSource.NewArgumentException( + nameof(path), + CertificateProviderStrings.CannotSetAclWrongPathType, + path); + } + + using var keyHandle = GetCertificateKeyHandle(cert); + try + { + try + { + // First try to set with all as we don't know what the caller + // has changed on the SD. + SetKeySecurity(keyHandle, securityDescriptor, AccessControlSections.All); + } + catch (Win32Exception e) when ( + e.NativeErrorCode == Interop.Windows.ERROR_PRIVILEGE_NOT_HELD || + e.NativeErrorCode == Interop.Windows.ERROR_INVALID_SECURITY_DESCR || + e.NativeErrorCode == Interop.Windows.ERROR_INVALID_PARAMETER) + { + /* + Failed to set all sections of the SD, this fallback tries to + determine what sections can be removed in the set operation. + We expect the following error codes: + + ERROR_PRIVILEGE_NOT_HELD: Setting the SACL requires + SeSecurityPrivilege to be enabled. We can avoid that + if the SD has no SACL entries. + + ERROR_INVALID_SECURITY_DESCR: Tries to set an SD with a + null owner or group (CNG based keys). The filter will + handle this. + + ERROR_INVALID_PARAMETER: Same as the above but for + CryptoAPI based keys. + */ + CertificateKeySecurity currentSD = GetKeySecurity( + keyHandle, + AccessControlSections.Owner | AccessControlSections.Group | AccessControlSections.Access); + AccessControlSections sections = GetLimitedSections(securityDescriptor, currentSD); + if (sections == AccessControlSections.All) + { + throw; + } + + SetKeySecurity(keyHandle, securityDescriptor, sections); + } + } + catch (Win32Exception e) + { + string msg = string.Format(CertificateProviderStrings.SetKeySDFailure, e.Message); + + ErrorRecord err = new ErrorRecord( + e, + "SetCertKeyDescriptorWin32Failure", + ErrorCategory.NotSpecified, + null) + { + ErrorDetails = new ErrorDetails(msg), + }; + throw new CertificateProviderWrappedErrorRecord(err); + } + } + + #endregion ISecurityDescriptorCmdletProvider members + + /// + /// Gets the certificate key handle. + /// + /// The certificate to get the handle for. + /// The certificate key handle. + private static Interop.Windows.SafeCryptoPrivateKeyHandle GetCertificateKeyHandle(X509Certificate2 cert) + { + // Try to enable SeBackupPrivilege which can allow the caller to get + // the key handle even if the SD doesn't give it explicit rights. + var currentPrivilegeState = new PlatformInvokes.TOKEN_PRIVILEGE(); + bool backupEnabled = PlatformInvokes.EnableTokenPrivilege("SeBackupPrivilege", ref currentPrivilegeState); + try + { + if (Interop.Windows.CryptAcquireCertificatePrivateKey( + cert.Handle, + Interop.Windows.CRYPT_ACQUIRE_SILENT_FLAG | Interop.Windows.CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG, + nint.Zero, + out var keyHandle, + out int _)) + { + return keyHandle; + } + + Win32Exception exp = new Win32Exception(); + + // NTE_BAD_KEYSET is returned when the user does not have + // permissions to access the key SD. + ErrorRecord err; + if (exp.NativeErrorCode == Interop.Windows.ERROR_ACCESS_DENIED || exp.NativeErrorCode == Interop.Windows.NTE_BAD_KEYSET) + { + string errMsg = string.Format(CertificateProviderStrings.GetKeyHandleAuthFailure, exp.Message); + err = new ErrorRecord( + exp, + "GetCertificateKeyHandleAccessDenied", + ErrorCategory.PermissionDenied, + cert) + { + ErrorDetails = new ErrorDetails(errMsg), + }; + } + else if (exp.NativeErrorCode == Interop.Windows.CRYPT_E_NO_KEY_PROPERTY) + { + string errMsg = string.Format(CertificateProviderStrings.GetKeyHandleMissingFailure, exp.Message); + err = new ErrorRecord( + exp, + "GetCertificateKeyHandleNoKey", + ErrorCategory.ObjectNotFound, + cert) + { + ErrorDetails = new ErrorDetails(errMsg), + }; + } + else + { + err = new ErrorRecord( + exp, + "GetCertificateKeyHandleNativeError", + ErrorCategory.NotSpecified, + cert); + } + + throw new CertificateProviderWrappedErrorRecord(err); + } + finally + { + if (backupEnabled) + { + PlatformInvokes.RestoreTokenPrivilege("SeBackupPrivilege", ref currentPrivilegeState); + } + } + } + + /// + /// Gets the SD for the provided key. + /// + /// The certificate key handle to get the SD for. + /// SD sections to retrieve. + /// The CertificateKeySecurity object. + /// /// The Win32 native error on failure. + private static CertificateKeySecurity GetKeySecurity( + Interop.Windows.SafeCryptoPrivateKeyHandle key, + AccessControlSections sections) + { + int sectionsFlag = GetAccessControlSectionsValue(sections); + + GetKeySecurityDescriptorDelegate getFunc = key.IsNCryptKey + ? GetNCryptKeySecurityDescriptor + : GetCryptoAPIKeySecurityDescriptor; + + CertificateKeySecurity sd = new(); + int errCode = getFunc(key, sectionsFlag, null, out int sdSize); + if (errCode == Interop.Windows.ERROR_INSUFFICIENT_BUFFER) + { + using var pool = MemoryPool.Shared.Rent(sdSize); + Span descriptorBuffer = pool.Memory.Span; + errCode = getFunc(key, sectionsFlag, descriptorBuffer, out sdSize); + sd.SetSecurityDescriptorBinaryForm(descriptorBuffer[..sdSize].ToArray()); + } + + if (errCode != Interop.Windows.ERROR_SUCCESS) + { + throw new Win32Exception(errCode); + } + + return sd; + } + + /// + /// Sets the SD for the provided key. + /// + /// The certificate key handle to set the SD for. + /// The ObjectSecurity descriptor to set. + /// The sections of the SD to set. + /// The Win32 native error on failure. + private static void SetKeySecurity( + Interop.Windows.SafeCryptoPrivateKeyHandle key, + ObjectSecurity securityDescriptor, + AccessControlSections sections) + { + int sectionsFlag = GetAccessControlSectionsValue(sections); + byte[] descriptorBytes = securityDescriptor.GetSecurityDescriptorBinaryForm(); + + /* + None of the APIs enable these privileges, as the API in PowerShell + currently cannot adjust multiple privileges we need to do it one at a + time. We require these privileges for: + + SeRestorePrivilege - Needed to set if we have no WritePermissions + rights or changing the owner to a SID that is not part of the + user's groups with SE_GROUP_ENABLED (whoami /groups). + SeTakeOwnershipPrivilege - Needed to overwrite the owner if we + do not have WritePermissions. + */ + var previousRestoreState = new PlatformInvokes.TOKEN_PRIVILEGE(); + bool revertRestore = false; + var previousOwnershipState = new PlatformInvokes.TOKEN_PRIVILEGE(); + bool revertOwnership = false; + try + { + revertRestore = PlatformInvokes.EnableTokenPrivilege("SeRestorePrivilege", ref previousRestoreState); + revertOwnership = PlatformInvokes.EnableTokenPrivilege("SeTakeOwnershipPrivilege", ref previousOwnershipState); + + int errCode = key.IsNCryptKey + ? SetNCryptKeySecurityDescriptor(key, sectionsFlag, descriptorBytes) + : SetCryptoAPIKeySecurityDescriptor(key, sectionsFlag, descriptorBytes); + if (errCode != Interop.Windows.ERROR_SUCCESS) + { + throw new Win32Exception(errCode); + } + } + finally + { + if (revertRestore) + { + PlatformInvokes.RestoreTokenPrivilege("SeRestorePrivilege", ref previousRestoreState); + } + + if (revertOwnership) + { + PlatformInvokes.RestoreTokenPrivilege("SeTakeOwnershipPrivilege", ref previousOwnershipState); + } + } + } + + private static AccessControlSections GetLimitedSections( + ObjectSecurity newSD, + ObjectSecurity existingSD) + { + AccessControlSections sections = AccessControlSections.All; + Type accountType = typeof(SecurityIdentifier); + + SecurityIdentifier? newOwner = (SecurityIdentifier?)newSD.GetOwner(accountType); + SecurityIdentifier? existingOwner = (SecurityIdentifier?)existingSD.GetOwner(accountType); + if (newOwner == null || newOwner == existingOwner) + { + sections &= ~AccessControlSections.Owner; + } + + SecurityIdentifier? newGroup = (SecurityIdentifier?)newSD.GetGroup(accountType); + SecurityIdentifier? existingGroup = (SecurityIdentifier?)existingSD.GetGroup(accountType); + if (newGroup == null || newGroup == existingGroup) + { + sections &= ~AccessControlSections.Group; + } + + // We cannot distinguish between clearing the audit rules and wanting + // to skip changing them. To replicate the FileSystem behaviour we + // unset the Audit section if the rules are empty. + if (newSD is CertificateKeySecurity certSD && certSD.GetAuditRules(true, true, accountType).Count == 0) + { + sections &= ~AccessControlSections.Audit; + } + + return sections; + } + + private static int GetAccessControlSectionsValue(AccessControlSections sections) + { + int sectionsFlag = 0; + if (sections.HasFlag(AccessControlSections.Owner)) + { + sectionsFlag |= OWNER_SECURITY_INFORMATION; + } + + if (sections.HasFlag(AccessControlSections.Group)) + { + sectionsFlag |= GROUP_SECURITY_INFORMATION; + } + + if (sections.HasFlag(AccessControlSections.Access)) + { + sectionsFlag |= DACL_SECURITY_INFORMATION; + } + + if (sections.HasFlag(AccessControlSections.Audit)) + { + sectionsFlag |= SACL_SECURITY_INFORMATION; + } + + return sectionsFlag; + } + + private delegate int GetKeySecurityDescriptorDelegate( + SafeHandle key, + int sections, + Span data, + out int dataSize); + + private delegate int SetKeySecurityDescriptorDelegate( + SafeHandle key, + int sections, + ReadOnlySpan data); + + private static int GetNCryptKeySecurityDescriptor( + SafeHandle key, + int sections, + Span data, + out int dataSize) + { + bool checkingSize = data.Length == 0; + + int errCode = Interop.Windows.NCryptGetProperty( + key, + Interop.Windows.NCRYPT_SECURITY_DESCR_PROPERTY, + data, + data.Length, + out dataSize, + sections | Interop.Windows.CRYPT_ACQUIRE_SILENT_FLAG); + + // Treat both Get functions the same as other normal Win32 APIs. I + // have seen this method set the error to ERROR_SUCCESS as well as + // ERROR_INSUFFICIENT_BUFFER so this ensures consistent behaviour. + if (checkingSize && errCode == 0) + { + errCode = Interop.Windows.ERROR_INSUFFICIENT_BUFFER; + } + + return errCode; + } + + private static int GetCryptoAPIKeySecurityDescriptor( + SafeHandle key, + int sections, + Span data, + out int dataSize) + { + int dataCount = data.Length; + bool checkingSize = dataCount == 0; + + // CryptGetProvParam does not automatically enable SeSecurityPrivilege + // required to get the SACL so we do so ourselves if possible. + var currentPrivilegeState = new PlatformInvokes.TOKEN_PRIVILEGE(); + bool privEnabled = false; + if ((sections & SACL_SECURITY_INFORMATION) == SACL_SECURITY_INFORMATION) + { + privEnabled = PlatformInvokes.EnableTokenPrivilege("SeSecurityPrivilege", ref currentPrivilegeState); + } + + bool res; + int errCode; + try + { + res = Interop.Windows.CryptGetProvParam( + key, + Interop.Windows.PP_KEYSET_SEC_DESCR, + data, + ref dataCount, + sections); + errCode = Marshal.GetLastPInvokeError(); + } + finally + { + if (privEnabled) + { + PlatformInvokes.RestoreTokenPrivilege("SeSecurityPrivilege", ref currentPrivilegeState); + } + } + + /* + CryptGetProvParam returns true with ERROR_SUCCESS when dataSize was set + to 0. To replicate the behaviour of NCryptGetProperty and other Win32 + APIs we return ERROR_INSUFFICIENT_BUFFER in that example. The function + is also buggy in that it calls GetNamedSecurityInfoW but doesn't pass + back any error codes from that function if it failed. The best we can + do is when dataSize stays at 0 we change the error code to a failure + manually. We don't know the reason why but it still fails. + + Examples of when it could happen is if trying to retrieve the SACL but + the SeSecurityPrivilege isn't held. + */ + if (checkingSize) + { + res = false; + + if (dataCount != 0) + { + errCode = Interop.Windows.ERROR_INSUFFICIENT_BUFFER; + } + else if (dataCount == 0 && errCode == 0) + { + // Shown as 'Unknown error (0xffffffff)' in Win32Exception. + errCode = -1; + } + } + + dataSize = dataCount; + return res ? Interop.Windows.ERROR_SUCCESS : errCode; + } + + private static int SetNCryptKeySecurityDescriptor( + SafeHandle key, + int sections, + ReadOnlySpan data) + { + return Interop.Windows.NCryptSetProperty( + key, + Interop.Windows.NCRYPT_SECURITY_DESCR_PROPERTY, + data, + data.Length, + sections | Interop.Windows.CRYPT_ACQUIRE_SILENT_FLAG); + } + + private static int SetCryptoAPIKeySecurityDescriptor( + SafeHandle key, + int sections, + ReadOnlySpan data) + { + // CryptSetProvParam does not automatically enable SeSecurityPrivilege + // required to set the SACL so we do so ourselves if possible. + var currentPrivilegeState = new PlatformInvokes.TOKEN_PRIVILEGE(); + bool privEnabled = false; + if ((sections & SACL_SECURITY_INFORMATION) == SACL_SECURITY_INFORMATION) + { + privEnabled = PlatformInvokes.EnableTokenPrivilege("SeSecurityPrivilege", ref currentPrivilegeState); + } + + bool res; + int errCode; + try + { + res = Interop.Windows.CryptSetProvParam( + key, + Interop.Windows.PP_KEYSET_SEC_DESCR, + data, + sections); + errCode = Marshal.GetLastPInvokeError(); + } + finally + { + if (privEnabled) + { + PlatformInvokes.RestoreTokenPrivilege("SeSecurityPrivilege", ref currentPrivilegeState); + } + } + + return res ? Interop.Windows.ERROR_SUCCESS : errCode; + } +} + +/// +/// Used to wrap a provider exception ErrorRecord. +/// +internal class CertificateProviderWrappedErrorRecord : Exception, IContainsErrorRecord +{ + /// + /// Gets the ErrorRecord the exception wraps. + /// + public ErrorRecord ErrorRecord { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The ErrorRecord to wrap. + public CertificateProviderWrappedErrorRecord(ErrorRecord err) : base(string.Empty) + { + ErrorRecord = err; + } +} + +/// +/// Specifies the certificate key access and auditing rights. +/// +[Flags] +public enum CertificateKeyRights +{ + /// + /// Read the key data. + /// + ReadData = 0x00000001, + + /// + /// Specifies the right to append data. + /// + AppendData = 0x00000004, + + /// + /// Read extended attributes of the key. + /// + ReadExtendedAttributes = 0x00000008, + + /// + /// Write extended attributes of the key. + /// + WriteExtendedAttributes = 0x00000010, + + /// + /// Read attributes of the key. + /// + ReadAttributes = 0x00000080, + + /// + /// Write attributes of the key. + /// + WriteAttributes = 0x00000100, + + /// + /// Delete the key. + /// + Delete = 0x00010000, + + /// + /// Read permissions for the key. + /// + ReadPermissions = 0x00020000, + + /// + /// Change permissions for the key. + /// + ChangePermissions = 0x00040000, + + /// + /// Take ownership of the key. + /// + TakeOwnership = 0x00080000, + + /// + /// Use the key for synchronization. + /// + Synchronize = 0x00100000, + + /// + /// Read access to the key. This represents the Read right as shown in the + /// security dialogue GUI. + /// + Read = Synchronize | ReadPermissions | ReadAttributes | + ReadExtendedAttributes | ReadData, + + /// + /// Full control of the key. This represents the Full Control right as + /// shown in the security dialogue GUI. + /// + FullControl = Synchronize | TakeOwnership | ChangePermissions | + ReadPermissions | Delete | WriteAttributes | ReadAttributes | + WriteExtendedAttributes | ReadExtendedAttributes | AppendData | + ReadData | 0x62, // extra mask fills in missing rights for FC. + + /// + /// A combination of GenericRead and GenericWrite. + /// + GenericAll = 0x10000000, + + /// + /// Not used. + /// + GenericExecute = 0x20000000, + + /// + /// Write the key data, extended attributes of the key, attributes + /// of the key, and permissions for the key. + /// + GenericWrite = 0x40000000, + + /// + /// Read the key data, extended attributes of the key, attributes of + /// the key, and permissions for the key. + /// + GenericRead = unchecked((int)0x80000000), +} + +/// +/// The CertificateKeySecurity ObjectSecurity implementation. +/// +public sealed class CertificateKeySecurity : ObjectSecurity +{ + /// + /// Initializes a new instance of the class. + /// + public CertificateKeySecurity() + : base(false, ResourceType.Unknown) + { } +} + +#endif diff --git a/src/System.Management.Automation/engine/Interop/Windows/CryptAcquireCertificatePrivateKey.cs b/src/System.Management.Automation/engine/Interop/Windows/CryptAcquireCertificatePrivateKey.cs new file mode 100644 index 0000000000000..685d1d96b6a09 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/CryptAcquireCertificatePrivateKey.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Keep native method argument names.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Keep native method argument names.")] + internal static unsafe partial class Windows + { + internal const int CRYPT_ACQUIRE_CACHE_FLAG = 0x00000001; + internal const int CRYPT_ACQUIRE_USE_PROV_INFO_FLAG = 0x00000002; + internal const int CRYPT_ACQUIRE_COMPARE_KEY_FLAG = 0x00000004; + internal const int CRYPT_ACQUIRE_NO_HEALING = 0x00000008; + internal const int CRYPT_ACQUIRE_SILENT_FLAG = 0x00000040; + internal const int CRYPT_ACQUIRE_WINDOW_HANDLE_FLAG = 0x00000080; + internal const int CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG = 0x00010000; + internal const int CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG = 0x00020000; + internal const int CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG = 0x00040000; + + internal const int CERT_NCRYPT_KEY_SPEC = unchecked((int)0xFFFFFFFF); + + internal const int NTE_BAD_KEYSET = unchecked((int)0x80090016); + internal const int CRYPT_E_NO_KEY_PROPERTY = unchecked((int)0x8009200B); + + internal sealed class SafeCryptoPrivateKeyHandle : SafeHandle + { + private readonly bool _shouldFree; + + public bool IsNCryptKey { get; } + + internal SafeCryptoPrivateKeyHandle( + nint handle, + bool isNCryptKey, + bool shouldFree, + bool ownsHandle) : base(handle, ownsHandle) + { + IsNCryptKey = isNCryptKey; + _shouldFree = shouldFree; + } + + public override bool IsInvalid => handle == nint.Zero; + + protected override bool ReleaseHandle() + { + if (!_shouldFree) + { + return true; + } + + if (IsNCryptKey) + { + return NCryptFreeObject(handle) == 0; + } + else + { + return CryptReleaseContext(handle, 0); + } + } + } + + [LibraryImport("Crypt32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool CryptAcquireCertificatePrivateKey( + nint pCert, + int dwFlags, + nint pvParameters, + out nint phCryptProvOrNCryptKey, + out int pdwKeySpec, + [MarshalAs(UnmanagedType.Bool)] out bool pfCallerFreeProvOrNCryptKey); + + internal static bool CryptAcquireCertificatePrivateKey( + nint cert, + int flags, + nint parameters, + out SafeCryptoPrivateKeyHandle keyHandle, + out int keySpec) + { + bool res = CryptAcquireCertificatePrivateKey( + cert, + flags, + parameters, + out nint key, + out keySpec, + out bool shouldFree); + + if (res) + { + keyHandle = new SafeCryptoPrivateKeyHandle( + key, + (keySpec & CERT_NCRYPT_KEY_SPEC) == CERT_NCRYPT_KEY_SPEC, + shouldFree, + true); + } + else + { + keyHandle = new SafeCryptoPrivateKeyHandle(nint.Zero, false, false, false); + } + + return res; + } + } +} diff --git a/src/System.Management.Automation/engine/Interop/Windows/CryptGetProvParam.cs b/src/System.Management.Automation/engine/Interop/Windows/CryptGetProvParam.cs new file mode 100644 index 0000000000000..9018284f77573 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/CryptGetProvParam.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Keep native method argument names.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Keep native method argument names.")] + internal static unsafe partial class Windows + { + internal const int PP_KEYSET_SEC_DESCR = 8; + + [LibraryImport("Advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool CryptGetProvParam( + SafeHandle hProv, + int dwParam, + Span pbData, + ref int pdwDataLen, + int dwFlags); + } +} diff --git a/src/System.Management.Automation/engine/Interop/Windows/CryptReleaseContext.cs b/src/System.Management.Automation/engine/Interop/Windows/CryptReleaseContext.cs new file mode 100644 index 0000000000000..122093bc6ddac --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/CryptReleaseContext.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Keep native method argument names.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Keep native method argument names.")] + internal static unsafe partial class Windows + { + [LibraryImport("Advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool CryptReleaseContext( + nint hProc, + int dwFlags); + } +} diff --git a/src/System.Management.Automation/engine/Interop/Windows/CryptSetProvParam.cs b/src/System.Management.Automation/engine/Interop/Windows/CryptSetProvParam.cs new file mode 100644 index 0000000000000..e8f9633e595a3 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/CryptSetProvParam.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Keep native method argument names.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Keep native method argument names.")] + internal static unsafe partial class Windows + { + [LibraryImport("Advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool CryptSetProvParam( + SafeHandle hProv, + int dwParam, + ReadOnlySpan pbData, + int dwFlags); + } +} diff --git a/src/System.Management.Automation/engine/Interop/Windows/Errors.cs b/src/System.Management.Automation/engine/Interop/Windows/Errors.cs index bef9e17219331..bc63f19548a3a 100644 --- a/src/System.Management.Automation/engine/Interop/Windows/Errors.cs +++ b/src/System.Management.Automation/engine/Interop/Windows/Errors.cs @@ -10,10 +10,15 @@ internal static partial class Windows // List of error constants https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes internal const int ERROR_SUCCESS = 0; internal const int ERROR_FILE_NOT_FOUND = 2; + internal const int ERROR_ACCESS_DENIED = 5; internal const int ERROR_GEN_FAILURE = 31; internal const int ERROR_NOT_SUPPORTED = 50; + internal const int ERROR_INVALID_PARAMETER = 87; + internal const int ERROR_INSUFFICIENT_BUFFER = 122; internal const int ERROR_NO_NETWORK = 1222; internal const int ERROR_MORE_DATA = 234; internal const int ERROR_CONNECTION_UNAVAIL = 1201; + internal const int ERROR_PRIVILEGE_NOT_HELD = 1314; + internal const int ERROR_INVALID_SECURITY_DESCR = 1338; } } diff --git a/src/System.Management.Automation/engine/Interop/Windows/NCryptFreeObject.cs b/src/System.Management.Automation/engine/Interop/Windows/NCryptFreeObject.cs new file mode 100644 index 0000000000000..757748eef6a78 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/NCryptFreeObject.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Keep native method argument names.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Keep native method argument names.")] + internal static unsafe partial class Windows + { + [LibraryImport("Ncrypt.dll")] + internal static partial int NCryptFreeObject( + nint hObject); + } +} diff --git a/src/System.Management.Automation/engine/Interop/Windows/NCryptGetProperty.cs b/src/System.Management.Automation/engine/Interop/Windows/NCryptGetProperty.cs new file mode 100644 index 0000000000000..3a7febb6310f0 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/NCryptGetProperty.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Keep native method argument names.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Keep native method argument names.")] + internal static unsafe partial class Windows + { + internal const string NCRYPT_SECURITY_DESCR_PROPERTY = "Security Descr"; + + [LibraryImport("Ncrypt.dll", StringMarshalling = StringMarshalling.Utf16)] + internal static partial int NCryptGetProperty( + SafeHandle hObject, + string pszProperty, + Span pbOutput, + int cbOutput, + out int pcbResult, + int dwFlags); + } +} diff --git a/src/System.Management.Automation/engine/Interop/Windows/NCryptSetProperty.cs b/src/System.Management.Automation/engine/Interop/Windows/NCryptSetProperty.cs new file mode 100644 index 0000000000000..e4c64e4bfbb68 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/NCryptSetProperty.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Keep native method argument names.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Keep native method argument names.")] + internal static unsafe partial class Windows + { + [LibraryImport("Ncrypt.dll", StringMarshalling = StringMarshalling.Utf16)] + internal static partial int NCryptSetProperty( + SafeHandle hObject, + string pszProperty, + ReadOnlySpan pbInput, + int cbInput, + int dwFlags); + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/CertificateProviderAcl.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/CertificateProviderAcl.Tests.ps1 new file mode 100644 index 0000000000000..7010aa12c923e --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/CertificateProviderAcl.Tests.ps1 @@ -0,0 +1,293 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +using namespace System.Security.AccessControl +using namespace System.Security.Cryptography +using namespace System.Security.Cryptography.X509Certificates +using namespace System.Security.Principal + +Describe "Certificate Provider ACL tests" -Tags "CI" { + $IsAdmin = ([WindowsPrincipal][WindowsIdentity]::GetCurrent()).IsInRole([WindowsBuiltInRole]::Administrator) + + BeforeAll { + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + if (-not $IsWindows) { + $PSDefaultParameterValues["It:Skip"] = -not $IsWindows + return + } + + $currentSid = [WindowsIdentity]::GetCurrent().User + $currentAccount = $currentSid.Translate([NTAccount]) + + $everyoneSid = [SecurityIdentifier]::new('S-1-1-0') + $everyoneAccount = $everyoneSid.Translate([NTAccount]) + + $rsaParams = @{ + KeyAlgorithm = 'RSA' + KeyExportPolicy = 'Exportable' + KeyLength = 2048 + CertStoreLocation = 'Cert:\CurrentUser\My' + } + $rsaCrypto = New-SelfSignedCertificate @rsaParams -Subject "ACL Test RSA - CryptoAPI" -Provider "Microsoft Base Cryptographic Provider v1.0" -KeySpec Signature + $rsaCNG = New-SelfSignedCertificate @rsaParams -Subject "ACL Test RSA - CNG" -Provider "Microsoft Software Key Storage Provider" + + $ecdsaParams = @{ + CertStoreLocation = 'Cert:\CurrentUser\My' + KeyUsage = 'DigitalSignature' + KeyAlgorithm = 'ECDSA_nistP256' + CurveExport = 'CurveName' + Type = 'Custom' + } + $ecdsaCNG = New-SelfSignedCertificate @ecdsaParams -Subject "ACL Test ECDSA - CNG" + + $request = [CertificateRequest]::new( + "CN=ACL Test - NoKey", + [System.Security.Cryptography.RSA]::Create(2048), + "SHA256", + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) + $notBefore = [DateTimeOffset]::UtcNow.AddDays(-1) + $notAfter = $notBefore.AddDays(30) + $testCert = $request.CreateSelfSigned($notBefore, $notAfter) + $userStore = Get-Item Cert:\CurrentUser\My + $userStore.Open('ReadWrite') + $userStore.Add([X509Certificate2]::new($testCert.Export('Cert'))) + $userStore.Dispose() + $certWithoutKey = Get-Item "Cert:\CurrentUser\My\$($testCert.Thumbprint)" + $testCert.Dispose() + + $testCerts = @{ + 'RSA-CryptoAPI' = $rsaCrypto + 'RSA-CNG' = $rsaCNG + 'ECDSA-CNG' = $ecdsaCNG + 'No-Key' = $certWithoutKey + } + } + + AfterAll { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + + if ($testCerts.Count) { + $testCerts.Values | ForEach-Object { + # We cannot pipe as -DeleteKey won't work. + Remove-Item -LiteralPath $_.PSPath -DeleteKey + } + } + } + + Context "*-Acl cmdlets" { + It "Fails to get ACL when getting location" { + $expected = "You cannot get an ACL for the certificate provider path 'CurrentUser', only certificate items are supported." + { Get-Acl -Path Cert:\CurrentUser } | Should -Throw $expected + } + + It "Fails to get ACL when getting store" { + $expected = "You cannot get an ACL for the certificate provider path 'CurrentUser\My', only certificate items are supported." + { Get-Acl -Path Cert:\CurrentUser\My } | Should -Throw $expected + } + + It "Fails to get ACL on cert without a key" { + $expected = "Failed to retrieve certificate key handle as certificate has no associated key: *" + { Get-Acl -Path $certWithoutKey.PSPath } | Should -Throw + [string]$Error[0] | Should -BeLike $expected + } + + It "Gets ACL for <_> key" -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + $actual = $cert.PSPath | Get-Acl + + $actual | Should -BeOfType ([Microsoft.PowerShell.Commands.CertificateKeySecurity]) + $actual.Owner | Should -Not -BeNullOrEmpty # Changes based on elevation + $actual.Group | Should -BeOfType ([string]) + $actual.Access | Should -BeOfType ([AccessRule[Microsoft.PowerShell.Commands.CertificateKeyRights]]) + + $userAccess = $actual.Access | Where-Object IdentityReference -EQ $currentAccount + $userAccess | Should -Not -BeNullOrEmpty + $userAccess.Rights | Should -BeOfType ([Microsoft.PowerShell.Commands.CertificateKeyRights]) + $userAccess.AccessControlType | Should -Be Allow + $userAccess.InheritanceFlags | Should -Be None + $userAccess.PropagationFlags | Should -Be None + } + + It "Adds and Removes DACL for <_> key" -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + $sd = $cert.PSPath | Get-Acl + + $newRule = $sd.AccessRuleType::new( + $everyoneSid, + 'Read', + 'Allow') + $sd.AddAccessRule($newRule) + + $sd | Set-Acl -LiteralPath $cert.PSPath + $addActual = $cert.PSPath | Get-Acl + $everyoneRule = $addActual.Access | Where-Object IdentityReference -EQ $everyoneAccount + $everyoneRule | Should -Not -BeNullOrEmpty + + $sd.RemoveAccessRuleSpecific($newRule) + $sd | Set-Acl -LiteralPath $cert.PSPath + $removeActual = $cert.PSPath | Get-Acl + $everyoneRule = $removeActual.Access | Where-Object IdentityReference -EQ $everyoneAccount + $everyoneRule | Should -BeNullOrEmpty + } + + It "Failed to get SACL for <_> key - Non-Admin" -Skip:($IsAdmin) -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + { $cert.PSPath | Get-Acl -Audit } | Should -Throw + [string]$Error[0] | Should -BeLike 'Failed to retrieve certificate key security descriptor: *' + } + + It "Failed to add SACL for <_> key - Non-Admin" -Skip:($IsAdmin) -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + $sd = $cert.PSPath | Get-Acl + + $newRule = $sd.AuditRuleType::new( + $everyoneSid, + 'Read', + 'Success') + $sd.AddAuditRule($newRule) + + { $sd | Set-Acl -LiteralPath $cert.PSPath } | Should -Throw + [string]$Error[0] | Should -Be 'Failed to set certificate key security descriptor: A required privilege is not held by the client.' + } + } + + Context "*-Acl cmdlets - Admin" -Skip:(-not $IsAdmin) -Tags 'RequireAdminOnWindows' { + It "Changes key owner for <_> key" -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + $sd = $cert.PSPath | Get-Acl + + $oldOwner = $sd.GetOwner([SecurityIdentifier]) + $sd.SetOwner($everyoneSid) + try { + $sd | Set-Acl -LiteralPath $cert.PSPath + + $actual = $cert.PSPath | Get-Acl + $actual.Owner | Should -Be $everyoneAccount.Value + } + finally { + $sd.SetOwner($oldOwner) + $sd | Set-Acl -LiteralPath $cert.PSPath + } + } + + It "Changes key group for <_> key" -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + $sd = $cert.PSPath | Get-Acl + + $oldOwner = $sd.GetGroup([SecurityIdentifier]) + $sd.SetGroup($everyoneSid) + try { + $sd | Set-Acl -LiteralPath $cert.PSPath + + $actual = $cert.PSPath | Get-Acl + $actual.Group | Should -Be $everyoneAccount.Value + } + finally { + $sd.SetGroup($oldOwner) + $sd | Set-Acl -LiteralPath $cert.PSPath + } + } + + It "Adds and Removes SACL for <_> key" -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + # We should start off without any audit rules. + $sd = $cert.PSPath | Get-Acl -Audit + $sd.Audit | Should -BeNullOrEmpty + + $newRule = $sd.AuditRuleType::new( + $everyoneSid, + 'Read', + 'Success') + $sd.AddAuditRule($newRule) + + $sd | Set-Acl -LiteralPath $cert.PSPath + $addActual = $cert.PSPath | Get-Acl -Audit + $everyoneRule = $addActual.Audit | Where-Object IdentityReference -EQ $everyoneAccount + $everyoneRule | Should -Not -BeNullOrEmpty + + $sd.RemoveAuditRuleSpecific($newRule) + $sd | Set-Acl -LiteralPath $cert.PSPath + $removeActual = $cert.PSPath | Get-Acl -Audit + $everyoneRule = $removeActual.Audit | Where-Object IdentityReference -EQ $everyoneAccount + $everyoneRule | Should -BeNullOrEmpty + } + } + + Context "Provider API" { + It "Gets empty SD from path" { + $actual = $ExecutionContext.InvokeProvider.SecurityDescriptor.NewFromPath($rsaCrypto.PSPath, "All") + $actual | Should -BeOfType ([Microsoft.PowerShell.Commands.CertificateKeySecurity]) + } + + It "Fails to get SD from path - null path" { + $expected = "*Cannot process argument because the value of argument ""path"" is null*" + { + $ExecutionContext.InvokeProvider.SecurityDescriptor.NewFromPath([NullString]::Value, "All") + } | Should -Throw $expected + } + + It "Fails to get SD from path - invalid sections" { + $sections = [Enum]::ToObject([System.Security.AccessControl.AccessControlSections], 20) + $expected = "*Cannot process argument because the value of argument ""includeSections"" is not valid*" + { + $ExecutionContext.InvokeProvider.SecurityDescriptor.NewFromPath($rsaCrypto.PSPath, $sections) + } | Should -Throw $expected + } + + It "Gets empty SD from type" { + $actual = $ExecutionContext.InvokeProvider.SecurityDescriptor.NewOfType("certificate", "key", "All") + $actual | Should -BeOfType ([Microsoft.PowerShell.Commands.CertificateKeySecurity]) + } + + It "Fails to get SD from type - invalid type" { + $expected = "*. You cannot get Certificate SecurityDescriptor for the type 'invalid', only the 'Key' type is supported*" + { + $ExecutionContext.InvokeProvider.SecurityDescriptor.NewOfType("certificate", "invalid", "All") + } | Should -Throw $expected + } + + It "Fails to get SD from type - invalid sections" { + $sections = [Enum]::ToObject([System.Security.AccessControl.AccessControlSections], 20) + $expected = "*Cannot process argument because the value of argument ""includeSections"" is not valid*" + { + $ExecutionContext.InvokeProvider.SecurityDescriptor.NewOfType("certificate", "Key", $sections) + } | Should -Throw $expected + } + + It "Adds and Removes DACL for <_> key" -TestCases @('RSA-CryptoAPI', 'RSA-CNG', 'ECDSA-CNG') { + $cert = $testCerts[$_] + + $sd = $ExecutionContext.InvokeProvider.SecurityDescriptor.Get($cert.PSPath, 'Access')[0] + + $newRule = $sd.AccessRuleType::new( + $everyoneSid, + 'Read', + 'Allow') + $sd.AddAccessRule($newRule) + + try { + $null = $ExecutionContext.InvokeProvider.SecurityDescriptor.Set($cert.PSPath, $sd) + } + catch { + Get-Error | Out-Host + throw + } + $addActual = $ExecutionContext.InvokeProvider.SecurityDescriptor.Get($cert.PSPath, 'Access') + $everyoneRule = $addActual.Access | Where-Object IdentityReference -EQ $everyoneAccount + $everyoneRule | Should -Not -BeNullOrEmpty + + $sd.RemoveAccessRuleSpecific($newRule) + $null = $ExecutionContext.InvokeProvider.SecurityDescriptor.Set($cert.PSPath, $sd) + $removeActual = $ExecutionContext.InvokeProvider.SecurityDescriptor.Get($cert.PSPath, 'Access') + $everyoneRule = $removeActual.Access | Where-Object IdentityReference -EQ $everyoneAccount + $everyoneRule | Should -BeNullOrEmpty + } + } +}