Introduction
SmartCard Logon for a One Click application
Background
I have looked for a solution for this project for about a week. All the reading I have done just ended up giving me bits and pieaces of code. I found some sites that said they where able to login a application using a smartcard yet they provided no explanation of how after about a week of research I finialy put all the pieaces togather and was able to succefully create a login script that would accept my cac cert and allow me to run the applcation as another user on a cac enforced system.
I know of lots of diffrent ways to start a application under diffrent credentials. The problem faced here was cac enforcement and the lack of a runas option on the rightclick menue of the app. This project was designed to allow a user to load the application and then change users using their cac. This gets around the OneClick problem of not having the runas option.
Using the code
First off most people break code down and explain each portion. I will break it down but first I will give you the intire code for the project and then explain some of the key components.
using System;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Security.Permissions;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Windows.Forms;
using System.Diagnostics;
using System.Security;
using System.ComponentModel;
namespace SmartCardApplication
{
public class SmartCard
{
internal static int CertCredential = 1;
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredMarshalCredential(
int credType,
IntPtr credential,
out IntPtr marshaledCredential
);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential)]
internal struct CERT_CREDENTIAL_INFO
{
public uint cbSize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] rgbHashOfCert;
}
public static string pin = null;
public static X509Certificate2 cert = null;
public struct UserLoginInfo
{
public string domain;
public string username;
public SecureString password;
}
public void SmartCardLogon()
{
cert = GetClientCertificate();
Form login = new Form();
login.Height = 75;
login.Width = 165;
login.MaximizeBox = false;
login.MinimizeBox = false;
login.ControlBox = false;
login.Name = "frmCaCLogin";
login.Text = "Enter Pin";
login.FormBorderStyle = FormBorderStyle.FixedSingle;
TextBox TextBox1 = new TextBox();
TextBox1.Name = "txtCaCLogin";
TextBox1.PasswordChar = '*';
TextBox1.Width = 152;
login.Controls.Add(TextBox1);
Button b = new Button();
b.FlatStyle = FlatStyle.Flat;
b.Text = "Login";
b.Name = "butCacLogin";
b.Click += new EventHandler(b_Click);
Button c = new Button();
c.Text = "Cancel";
c.FlatStyle = FlatStyle.Flat;
c.Name = "butCancel";
c.Click += new EventHandler(c_Click);
login.Controls.Add(b);
login.Controls.Add(c);
login.Controls["butCacLogin"].Top += 20;
login.Controls["butCancel"].Left += login.Controls["butCacLogin"].Width + 2;
login.Controls["butCancel"].Top += 20;
login.TopMost = true;
login.ShowDialog();
}
void c_Click(object sender, EventArgs e)
{
Application.OpenForms["frmHome"].Show();
Application.OpenForms["frmLogin"].Close();
Application.OpenForms["frmCaCLogin"].Close();
}
void b_Click(object sender, EventArgs e)
{
pin = Application.OpenForms["frmCaCLogin"].Controls["txtCaCLogin"].Text;
Application.OpenForms["frmCaCLogin"].Hide();
if (cert != null)
{
UserLoginInfo user = Login(cert);
if (user.username != string.Empty)
{
try
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = Application.ExecutablePath;
psi.UserName = user.username;
psi.Password = user.password;
psi.UseShellExecute = false;
Process.Start(psi);
Application.Exit();
}
catch (Exception ex)
{
if (ex.Message.Contains("Logon failure: unknown user name or bad password"))
{
MessageBox.Show("Ensure you SmartCard has been inserted and you have entered the correct pin!", ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Application.OpenForms["frmCaCLogin"].Show();
}
}
}
}
public UserLoginInfo Login(X509Certificate2 cert)
{
UserLoginInfo uli = new UserLoginInfo();
try
{
CERT_CREDENTIAL_INFO certInfo =
new CERT_CREDENTIAL_INFO();
certInfo.cbSize = (uint)Marshal.SizeOf(typeof(CERT_CREDENTIAL_INFO));
certInfo.rgbHashOfCert = cert.GetCertHash();
int size = Marshal.SizeOf(certInfo);
IntPtr pCertInfo = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(certInfo, pCertInfo, false);
IntPtr marshaledCredential = IntPtr.Zero;
bool result =
CredMarshalCredential(CertCredential,
pCertInfo,
out marshaledCredential);
string domainName = string.Empty;
string userName = string.Empty;
string password = string.Empty;
if (result)
{
domainName = String.Empty;
userName = Marshal.PtrToStringUni(marshaledCredential);
password = pin;
}
SecureString sc = new SecureString();
foreach (char c in pin)
{
sc.AppendChar(c);
}
uli.domain = Environment.UserDomainName;
uli.username = userName;
uli.password = sc;
return uli;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return uli;
}
}
public static X509Certificate2 GetClientCertificate()
{
IntPtr ptr = IntPtr.Zero;
X509Certificate2 certificate = null;
X509Certificate t = null;
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
if (store.Certificates != null && store.Certificates.Count > 0)
{
if (store.Certificates.Count == 1)
{
certificate = store.Certificates[0];
}
else
{
var certificates = X509Certificate2UI.SelectFromCollection(store.Certificates, "Digital Certificates", "Select a certificate from the following list:", X509SelectionFlag.SingleSelection, ptr);
if (certificates != null && certificates.Count > 0)
certificate = certificates[0];
}
}
}
finally
{
store.Close();
}
return certificate;
}
}
}
The file above dose all the work and is simply a class file. This can easly be converted to a API and used with any windows application. Now lets hit some of the key points in this class. In order to login using you CaC certificate the first then one needs to do is be able to select from a list of certificates so that you can get the cert you need. The code below connects to your certifcate store and produces the certifcate selector that you are normally see. Once you select the cert it will return it to the caller so it can be used in the next step.
public static X509Certificate2 GetClientCertificate()
{
IntPtr ptr = IntPtr.Zero;
X509Certificate2 certificate = null;
X509Certificate t = null;
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
if (store.Certificates != null && store.Certificates.Count > 0)
{
if (store.Certificates.Count == 1)
{
certificate = store.Certificates[0];
}
else
{
var certificates = X509Certificate2UI.SelectFromCollection(store.Certificates, "Digital Certificates", "Select a certificate from the following list:", X509SelectionFlag.SingleSelection, ptr);
if (certificates != null && certificates.Count > 0)
certificate = certificates[0];
}
}
}
finally
{
store.Close();
}
return certificate;
}
In order to pass a certifcate to a process as a username you will need to convert it to a username. This is the more complicated pieace of the code and requires you to import advapi32 into your project. So this pieace contains the most work. This simply allows the application to use some of the data in the advapi32.dll wich is needed for the method that converts the cert to a username.
internal static int CertCredential = 1;
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredMarshalCredential(
int credType,
IntPtr credential,
out IntPtr marshaledCredential
);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential)]
internal struct CERT_CREDENTIAL_INFO
{
public uint cbSize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] rgbHashOfCert;
}
This is the actuall class that will do the conversion. The UserLoginInfo is just a simple structure I created to store the credentials. At this point the application only takes the pin you you typed in and converts it to a secure string and adds it to the UserLoginInfo.password field. Not autentication is taking place at this point. We are just getting the information we need.
public UserLoginInfo Login(X509Certificate2 cert)
{
UserLoginInfo uli = new UserLoginInfo();
try
{
CERT_CREDENTIAL_INFO certInfo =
new CERT_CREDENTIAL_INFO();
certInfo.cbSize = (uint)Marshal.SizeOf(typeof(CERT_CREDENTIAL_INFO));
certInfo.rgbHashOfCert = cert.GetCertHash();
int size = Marshal.SizeOf(certInfo);
IntPtr pCertInfo = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(certInfo, pCertInfo, false);
IntPtr marshaledCredential = IntPtr.Zero;
bool result =
CredMarshalCredential(CertCredential,
pCertInfo,
out marshaledCredential);
string domainName = string.Empty;
string userName = string.Empty;
string password = string.Empty;
if (result)
{
domainName = String.Empty;
userName = Marshal.PtrToStringUni(marshaledCredential);
password = pin;
}
SecureString sc = new SecureString();
foreach (char c in pin)
{
sc.AppendChar(c);
}
uli.domain = Environment.UserDomainName;
uli.username = userName;
uli.password = sc;
return uli;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return uli;
}
}
Now you will notice that the uli.username has a strange username associated with it. This is what windows uses for the username when authenticating using a cac. Now that you have all the information needed you simply need to start the process up. Below I call the method above and return the UserLoginInfo structure with the username and password set. I added the domain name but its not needed in this case. I read about using LogonUser class and a bunch of other ways to do this nothing worked the user would logon using that but whil impersanating a user I was unable to start the process no matter what I did. then I decided to just pass the username and pin directly to the process instead. What do you know it worked just fine. So once you get the cert converted to a user name and the pin converted to a secure string you can start the process with that information directly no need to use any other method of starting a process.
UserLoginInfo user = Login(cert);
if (user.username != string.Empty)
{
try
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = Application.ExecutablePath;
psi.UserName = user.username;
psi.Password = user.password;
psi.UseShellExecute = false;
Process.Start(psi);
Application.Exit();
}
catch (Exception ex)
{
if (ex.Message.Contains("Logon failure: unknown user name or bad password"))
{
MessageBox.Show("Ensure you SmartCard has been inserted and you have entered the correct pin!", ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Application.OpenForms["frmCaCLogin"].Show();
}
}
}
Now your probally wondering what creates the ping. If you have done the cert selection before when you select the cert you dont see a pin logon it just selects the cert. You actually have to create the pin login form yourself and since this was built in a class I will create the Login form directly. This portion of the code is what connects everything togather. It is also the method you would call to fire it. Basicly what this dose is create the form, textbox, two buttons (Submit, Cancel) formats it slightly so nothing overlaps.
Step1 GetClientCert is called the cert is returned
Step2 create the form for the pin login assign the event handelers and show the form
Step 3 submit the pin that the user entered
Step 4 Converts the cert to username and pin to secure string
Step 5 Take that information and pass it to the username and password field for the process
Step 6 Start the proces and close the existing process (Simply reloading the app with the new credentials)
In this case I wanted to reload the current application with the new credentials so I simply started a new instence of the current application then closed to old instance. Applcation loads with elevated rights and certifcate auntentication. So if your in a cac enforced enviroment this code will allow you to exacute as a diffrent user using you cac.
public void SmartCardLogon()
{
cert = GetClientCertificate();
Form login = new Form();
login.Height = 75;
login.Width = 165;
login.MaximizeBox = false;
login.MinimizeBox = false;
login.ControlBox = false;
login.Name = "frmCaCLogin";
login.Text = "Enter Pin";
login.FormBorderStyle = FormBorderStyle.FixedSingle;
TextBox TextBox1 = new TextBox();
TextBox1.Name = "txtCaCLogin";
TextBox1.PasswordChar = '*';
TextBox1.Width = 152;
login.Controls.Add(TextBox1);
Button b = new Button();
b.FlatStyle = FlatStyle.Flat;
b.Text = "Login";
b.Name = "butCacLogin";
b.Click += new EventHandler(b_Click);
Button c = new Button();
c.Text = "Cancel";
c.FlatStyle = FlatStyle.Flat;
c.Name = "butCancel";
c.Click += new EventHandler(c_Click);
login.Controls.Add(b);
login.Controls.Add(c);
login.Controls["butCacLogin"].Top += 20;
login.Controls["butCancel"].Left += login.Controls["butCacLogin"].Width + 2;
login.Controls["butCancel"].Top += 20;
login.TopMost = true;
login.ShowDialog();
}
Below are the event handelers for the form I created above.
void c_Click(object sender, EventArgs e)
{
Application.OpenForms["frmHome"].Show();
Application.OpenForms["frmLogin"].Close();
Application.OpenForms["frmCaCLogin"].Close();
}
void b_Click(object sender, EventArgs e)
{
pin = Application.OpenForms["frmCaCLogin"].Controls["txtCaCLogin"].Text;
Application.OpenForms["frmCaCLogin"].Hide();
if (cert != null)
{
UserLoginInfo user = Login(cert);
if (user.username != string.Empty)
{
try
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = Application.ExecutablePath;
psi.UserName = user.username;
psi.Password = user.password;
psi.UseShellExecute = false;
Process.Start(psi);
Application.Exit();
}
catch (Exception ex)
{
if (ex.Message.Contains("Logon failure: unknown user name or bad password"))
{
MessageBox.Show("Ensure you SmartCard has been inserted and you have entered the correct pin!", ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Application.OpenForms["frmCaCLogin"].Show();
}
}
}
}
One last pieace of code that I have to share is the application form that actually calls this. Create a windows form add a button the the form and add this code to the button_click event
SmartCard sc = new SmartCard();
sc.SmartCardLogon();
And thats it in a nutshell hopefully everyone will find this intresting.
Points of Interest
One thing I have found out researching this is that almost everything out there on this subject is only partialy there. I could not find any working examples on on how to achive this anywhere only partial examples. So I hope this will help somone else out that may need to achive this. I did see alot of question while looking reguarding starting a app up with a smart card but no working answers. I built this using visual studio 2010 on Windows 7 so As fare as compatibility it may or may not work using other windows enviroments ore versions of visual stuido.
History