Search code examples
c#asp.net-corecertificatex509impersonation

Accessing certificates of another user via impersonation


I need to digitally sign PDF documents on a web server running ASP.NET Core 2.2 in IIS. The web app is running with a service user and the impersonation shall be done in the code. The problem is, I can't access the users certificates via the X509Store class. I tried to create a minimal example which only impersonates a different user and outputs the certificates in the console. I ran it as an administrator but no certifiactes were found.

Permissions I granted in the "Local Group Policy Editor" (gpedit.msc) under "Computer Configuration > Windows Settings > Security Settings > Local Policies > User Rights Management":

  • Act as part of the operating system: Added ServiceUser
  • Allow log on locally: Added UserToBeImpersonated
  • Log on as batch job: Added UserToBeImpersonated
  • Create a token object: Added ServiceUser
  • Replace a process level token: Added ServiceUser
  • Adjust Memory Quotas for a process: Added ServiceUser

For the impersonation I used the SimpleImpersonation from https://github.com/mj1856/SimpleImpersonation and extended it to also load the user profile like this:

public static void RunAsUser(UserCredentials credentials, LogonType logonType, Action action)
{
    // this method tells Windows to dynamically determine where to look for the HKEY_CURRENT_USER registry hive,
    // rather than using the cached location from when the process was initially invoked
    RegDisablePredefinedCache();

    using (var tokenHandle = credentials.Impersonate(logonType))
    using (var profileToken = credentials.ImpersonateUserProfile(tokenHandle.DangerousGetHandle()))
    {
        RunImpersonated(tokenHandle, _ => action());
    }
}       

internal UserProfileToken ImpersonateUserProfile(IntPtr tokenHandle)
{
    var tokenDuplicate = IntPtr.Zero;

    // Not sure if the token needs to be duplicated or not
    if (DuplicateToken(tokenHandle, 2, ref tokenDuplicate) == 0)
        HandleError(tokenHandle);

    // Load User profile
    var profileInfo = new ProfileInfo();
    profileInfo.dwSize = Marshal.SizeOf(profileInfo);
    profileInfo.lpUserName = _username;
    profileInfo.dwFlags = 1;

    // LoadUserProfile() failed
    if (!LoadUserProfile(tokenDuplicate, ref profileInfo))
        HandleError(tokenDuplicate);

    // LoadUserProfile() failed - HKCU handle was not loaded
    if (profileInfo.hProfile == IntPtr.Zero)
        HandleError(tokenDuplicate);

    return new UserProfileToken() { ProfileInfo = profileInfo, Token = tokenDuplicate };
}

public class UserProfileToken : IDisposable
{
    public ProfileInfo ProfileInfo { get; set; }

    public IntPtr Token { get; set; }

    ~UserProfileToken()
    {
        Dispose();
    }

    private bool isDisposed = false;

    public void Dispose()
    {
        if (isDisposed) return;
        isDisposed = true;

        UnloadUserProfile(Token, ProfileInfo.hProfile);
    }
}
       
[DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool LoadUserProfile(IntPtr hToken, ref ProfileInfo lpProfileInfo);

[DllImport("Userenv.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool UnloadUserProfile(IntPtr hToken, IntPtr lpProfileInfo);

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);

[DllImport("advapi32.dll", CharSet = CharSet.Auto)]
public static extern int RegDisablePredefinedCache();

The main code for the testing looks like this:

var credentials = new Impersonator.UserCredentials(domain, username, password);
Impersonator.RunAsUser(credentials, Impersonator.LogonType.Interactive, () =>
{
    Console.WriteLine($"Current user: {WindowsIdentity.GetCurrent().Name}");
    
    var store = new X509Store(StoreLocation.CurrentUser);
    store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

    if (store.Certificates.Count == 0)
    {
        Console.WriteLine("No Certificates found.");
    }
    else
    {
        var certCount = 0;
        foreach (var cert in store.Certificates)
        {
            Console.WriteLine($"[{certCount}] Subject: {cert.Subject}");
            Console.WriteLine($"[{certCount}] Issuer: {cert.Issuer}");

            certCount++;
        }
    }
});

Unfortunately no certificats are found when the same code (without impersonation) shows two certificates on the target users machine.

Another interesting thing I found on the server is that the users profile directory under C:\Users<username> has not the users name but some Chinese? characters. When checking the user profiles in the system settings the username is correct though.


Solution

  • Seems like the impersonation (with loading the profile) does not trigger the Auto-Enrollment for receiving the certificate from the certificate authority. This must be done seperately with something like this:

    using CERTENROLLLib;
    
    public static void EnrollUserCertificateByTemplate(string templateName, string subjectName, string friendlyName = null)
        {
            var enrollment = new CX509Enrollment();
    
            // Set target store
            enrollment.InitializeFromTemplateName(X509CertificateEnrollmentContext.ContextUser, templateName);
    
            var request = enrollment.Request;
            var innerRequest = request.GetInnerRequest(InnerRequestLevel.LevelInnermost);
            var innerRequestPkcs10 = innerRequest as IX509CertificateRequestPkcs10;
    
            // Set the subject name
            var distinguishedName = new CX500DistinguishedName();
            distinguishedName.Encode(subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);            
            innerRequestPkcs10.Subject = distinguishedName;
    
            // Set the friendly name
            if (friendlyName != null) enrollment.CertificateFriendlyName = friendlyName;
    
            // Enroll for the certificate into MY store if it is successfully issued by CA
            enrollment.Enroll();
        }
    

    Additionaly you need to check if there is already a valid certificate available in the users certificate store. Otherwise you add a certificate everytime you impersonate (and execute the code above).

    Anothr thing I needed to do was giving Administrative rights to the executing user. All the other permissions posted in my question could be remove.