Programmatically Determining Access Rights on Certificate Private Keys

There are times when the private key associated with a certificate needs to be accessible by multiple identities, not just the identity which owns the key material. An example is the deployment of the Microsoft Network Device Enrollment Services (NDES) role service on a server which needs to connect to a remotely hosted CA. In such a scenario, the identity of the IIS application pool that facilitates NDES certificate enrollment needs to be able to read the private keys associated with the NDES signing and encryption certificates, which are owned by the local system account.

Since this is the case, it would be useful to be able to programmatically check the rights that exist on a given certificate private key.

As luck would have it, this is achievable easily, at least when dealing with legacy cryptographic service providers (CSPs). The below example assumes an RSA public key algorithm. The logic would be the same for a DSA public key simply by replacing RSACryptoServiceProvider with DSACryptoServiceProvider:

The output of this method will look similar to this:

Looks to be just what we need, right? Well, there’s a critical scalability flaw to this approach. Can you guess what it is?

We know that COMPANY\SCEP Users Active Directory group has the GenericRead right to the private key, however, who are the members of SCEP Users? Are there other security groups nested within that one? This obviously presents an inconvenient challenge. Resolving potentially nested Active Directory groups to determine the rights granted to a single identity adds significant churn to our task.

Luckily, there is a better way. P/Invoke to the rescue! Have a look at the GetEffectiveRightsFromAcl Win32 function. In simplified terms, this function takes, as input, an Access Control List (ACL), which we can still retrieve using the CryptoKeySecurity class and an identity reference (such as an Active Directory username). It returns a bitwise access mask, which expresses the effective rights for the identity based on the supplied ACL. The resolving of AD group membership is taken care of for us natively by Windows!

With P/Invoke comes a bit more complexity to our code. We need to make the .NET Framework aware of the native Win32 data structures and functions we wish to use. For this purpose the community-maintained website pinvoke.net is an invaluable resource. It provides the syntax required to expose the most commonly used Win32 C/C++ functions in .NET.  In the case of using GetEffectiveRightsFromAcl for our purposes, the P/Invoke declarations should be:

Now, we can pass in the access rules from the CryptoKeySecurity object and the identity we want to determine rights for into the GetEffectiveRightsFromAcl function:

As you can see from this output, the function appears to work!

However, the value for “Effective Rights” is incorrect. My service account (scep01), is a member of the COMPANY\SCEP_Users group.  Looking at the GUI, this group clearly has READ access to the private key:

So, why did the GetEffectiveRightsFromAcl function not detect this? As it turns out, the problem has to do with the flag values defined in the CryptoKeyRights enumeration:

Conversely, let’s have a look at a more common access rights enumeration (FileSystemRights):

Don’t get caught up with the values being different for similar flags between these two enumerations; instead, focus on the fact that CryptoKeyRights contains some members with very high values (namely GenericAll, GenericExecute, GenericWrite) and one member, GenericRead which is below zero. The members we are most concerned about are GenericRead and GenericAll as these are the permissions assigned when you select Read or Full Control respectively in the GUI.

The reason the values of these members is problematic is that when GetEffectiveRightsFromAcl looks at an access rights mask, it only looks at the lower 16 bits of the value! So, for instance, take the value of GenericAll from the CryptoKeyRights enumeration (268435456) and look at its binary representation:

8        7        6       5      4       3       2       1

0001 0000 0000 0000 0000 0000 0000 0000

The bit for this value is set at the 29th position.  Since GetEffectiveRightsFromAcl only looks at the 16 least significant bits (the bits to the right), the value for GenericAll is initerpretted by the function as zero (no rights)!

So, it seems that we’re out of luck. Is it true that such an efficient tool is beyond our grasp due to an inconveniently-defined enumeration? Luckily, this is not the case; we just need to transform the values from the CryptoKeyRights enumeration to an enumeration that is usable.

Let’s start by establishing a custom rights enumeration to which CryptoKeyRights values can be meaningfully converted:

What we’ve done here is to shift the bit positions for GenericAll, GenericExecute, GenericWrite and GenericRead 4 bytes (16 bits) to the right. This is illustrated in the table below:

The last order of business is to re-read the access rules contained within CryptoKeySecurity, and this time, use some bitwise logic to transform all CryptoKeyRights values to values that can be mapped to our custom PrivateKeyRights enumeration.  This involves shifting the 5 most significant bits 16 positions to the right and then resetting the 16 most significant bits.

When we pass the manipulated discretionary ACL into GetEffectiveRightsFromAcl this time, we’ll receive a meaningful answer, which we can cast to our custom PrivateKeySecurity enumeration.

Here’s what the revised method looks like:

The result now matches what is displayed in the GUI:

1 Comment

  1. Matthew Wells on November 16, 2022 at 1:32 pm

    Great post! It drives home how difficult it can be to just gather the information correctly, much less account for all of the configuration possibilities so that you can create visibility and alerting around it. The need for expertise like this is only growing; so happy to have somewhere to look for insight and information.

Leave a Comment





This site uses Akismet to reduce spam. Learn how your comment data is processed.