The problem appears when a WCF Service hosted in an IIS tries to load a certificate from the Windows Certificates Store with the account of the Application Pool where the service runs, and the account’s profile is not previously loaded. When a user logs on interactively, the system automatically loads the user's profile. If a service or an application impersonates a user, the system does not load the user's profile. Therefore, the service or application should load the user's profile with LoadUserProfile.
When this happens the operation throws the following exception:
System.Security.Cryptography.CryptographicException: The system cannot find the file specified.
See http://support.microsoft.com/kb/939761 Microsoft Knowledge Base Article for detailed information.
A workaround to this problem is to load the Application Pool Identity Account´s profile before the service call is executed. Placing the code in the Application_Start() method on the Global.asax of the IIS host will solve the problem (see http://msdn2.microsoft.com/en-us/library/aa374341.aspx for detailed information).
Here is the code:
private ProfileManager.PROFILEINFO profile;
protected void Application_Start(object sender, EventArgs e)
{
bool retVal = false;
// Need to duplicate the token. LoadUserProfile needs a token with
// TOKEN_IMPERSONATE and TOKEN_DUPLICATE.
const int SecurityImpersonation = 2;
dupeTokenHandle = DupeToken(WindowsIdentity.GetCurrent().Token, SecurityImpersonation);
if (IntPtr.Zero == dupeTokenHandle)
{
throw new Exception("Unable to duplicate token.");
}
// Load the profile.
profile = new ProfileManager.PROFILEINFO();
profile.dwSize = 32;
//Domain\User
profile.lpUserName = @"MyDomain\UserName";
retVal = ProfileManager.LoadUserProfile(dupeTokenHandle, ref profile);
if (!retVal)
{
throw new Exception("Error loading user profile. " + Marshal.GetLastWin32Error());
}
}
protected void Application_End(object sender, EventArgs e)
{
ProfileManager.UnloadUserProfile(WindowsIdentity.GetCurrent().Token, profile.hProfile);
CloseHandle(dupeTokenHandle);
}
private IntPtr DupeToken(IntPtr token, int Level)
{
IntPtr dupeTokenHandle = new IntPtr(0);
bool retVal = DuplicateToken(token, Level, ref dupeTokenHandle);
if (false == retVal)
{
return IntPtr.Zero;
}
return dupeTokenHandle;
}
}
internal class ProfileManager
{
[DllImport("Userenv.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
internal static extern bool LoadUserProfile(IntPtr hToken, ref PROFILEINFO lpProfileInfo);
[DllImport("Userenv.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
internal static extern bool UnloadUserProfile(IntPtr hToken, IntPtr hProfile);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct PROFILEINFO
{
public int dwSize;
public int dwFlags;
public String lpUserName;
public String lpProfilePath;
public String lpDefaultPath;
public String lpServerName;
public String lpPolicyPath;
public IntPtr hProfile;
}
}
}
An alternative workaround consists in creating a Windows Service account that loads at system start-up using the Application Pool Service Identity.
Thanks to JavierA for helping me to find the solution.