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":
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.
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.