使用智能卡在 C# 中登录一次点击式 Windows 应用程序






4.75/5 (3投票s)
引言
一键式应用程序的智能卡登录
背景
我花了大约一周时间寻找这个项目的解决方案。我所做的所有阅读最终都只给了我一些零散的代码。我发现一些网站声称他们能够使用智能卡登录一个应用程序,但他们没有提供任何解释,在经过一周的研究后,我终于将所有零散的代码拼凑起来,成功创建了一个登录脚本,该脚本可以接受我的 CAC 证书,并允许我在一个强制使用 CAC 的系统上以另一个用户的身份运行该应用程序。
我知道有很多不同的方法可以在不同的凭据下启动应用程序。这里面临的问题是 CAC 强制和应用程序右键菜单中缺少 runas 选项。该项目旨在允许用户加载应用程序,然后使用他们的 CAC 更改用户。这可以绕过一键式应用程序缺乏 runas 选项的问题。
使用代码
首先,大多数人都会分解代码并解释每个部分。我也会分解,但首先我会提供整个项目的代码,然后解释一些关键组件。
//SmartCard.cs file
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()
{
//First Get the Certificate
cert = GetClientCertificate();
//Create New login form for user to enter pin
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;
//Create new text box for the pin
TextBox TextBox1 = new TextBox();
TextBox1.Name = "txtCaCLogin";
TextBox1.PasswordChar = '*';
TextBox1.Width = 152;
//Add textbox to form
login.Controls.Add(TextBox1);
//Create new button for user to submit the pin
Button b = new Button();
b.FlatStyle = FlatStyle.Flat;
b.Text = "Login";
b.Name = "butCacLogin";
b.Click += new EventHandler(b_Click);
//Create new button for user to cancel the login
Button c = new Button();
c.Text = "Cancel";
c.FlatStyle = FlatStyle.Flat;
c.Name = "butCancel";
c.Click += new EventHandler(c_Click);
//Add buttons to form
login.Controls.Add(b);
login.Controls.Add(c);
//position buttons under the textbox
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)
{
//if user cances the form show the main form and close the two login forms.
Application.OpenForms["frmHome"].Show();
Application.OpenForms["frmLogin"].Close();
Application.OpenForms["frmCaCLogin"].Close();
}
void b_Click(object sender, EventArgs e)
{
//Retrive the pin from the txt filed you added to the created form
pin = Application.OpenForms["frmCaCLogin"].Controls["txtCaCLogin"].Text;
//just hides the form
Application.OpenForms["frmCaCLogin"].Hide();
//
if (cert != null)
{
//This method is used to create a username from the selected certificate
UserLoginInfo user = Login(cert);
//using the returned username from the method above and the pin in securestring format from the user entry start the process over with new permissions
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)
{
// we need to do this here, before we free marshaledCredential
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);
// Nothing to do if no cert found.
if (store.Certificates != null && store.Certificates.Count > 0)
{
if (store.Certificates.Count == 1)
{
// Return the certificate present.
certificate = store.Certificates[0];
}
else
{
// Request the user to select a certificate
var certificates = X509Certificate2UI.SelectFromCollection(store.Certificates, "Digital Certificates", "Select a certificate from the following list:", X509SelectionFlag.SingleSelection, ptr);
// Check if one has been returned
if (certificates != null && certificates.Count > 0)
certificate = certificates[0];
}
}
}
finally
{
store.Close();
}
return certificate;
}
}
}
上面的文件完成了所有工作,它只是一个类文件。这可以轻松地转换为 API,并与任何 Windows 应用程序一起使用。现在让我们来看一下这个类中的一些关键点。为了使用您的 CAC 证书登录,首先需要做的是能够从证书列表中进行选择,以便获得所需的证书。下面的代码连接到您的证书存储,并显示您通常看到的证书选择器。一旦您选择了证书,它将返回给调用者,以便在下一步中使用。
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);
// Nothing to do if no cert found.
if (store.Certificates != null && store.Certificates.Count > 0)
{
if (store.Certificates.Count == 1)
{
// Return the certificate present.
certificate = store.Certificates[0];
}
else
{
// Request the user to select a certificate
var certificates = X509Certificate2UI.SelectFromCollection(store.Certificates, "Digital Certificates", "Select a certificate from the following list:", X509SelectionFlag.SingleSelection, ptr);
// Check if one has been returned
if (certificates != null && certificates.Count > 0)
certificate = certificates[0];
}
}
}
finally
{
store.Close();
}
return certificate;
}
为了将证书作为用户名传递给进程,您需要将其转换为用户名。这是代码中比较复杂的部分,需要您将 advapi32 导入您的项目。所以这部分包含的工作最多。这只是允许应用程序使用 advapi32.dll 中的一些数据,这是将证书转换为用户名的该方法所必需的。
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;
}
这是实际进行转换的类。UserLoginInfo 只是我创建的一个简单结构,用于存储凭据。此时,应用程序只接受您输入的 PIN,并将其转换为安全字符串并添加到 UserLoginInfo.password 字段。此时没有进行身份验证。我们只是获取所需的信息。
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)
{
// we need to do this here, before we free marshaledCredential
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;
}
}
现在您会注意到 uli.username 关联了一个奇怪的用户名。这是 Windows 在使用 CAC 进行身份验证时使用的用户名。现在您已经拥有了所有需要的信息,只需启动进程即可。下面我调用上面的方法,并返回设置了用户名和密码的 UserLoginInfo 结构。我添加了域名,但在这种情况下是不需要的。我阅读了有关使用 LogonUser 类以及许多其他方法的信息,但都没有成功。用户会使用该方法登录,但在冒充用户时,我无法启动进程,无论我做什么。然后我决定直接将用户名和 PIN 传递给进程。您猜怎么着,它运行得很好。所以一旦您将证书转换为用户名,并将 PIN 转换为安全字符串,您就可以直接使用这些信息启动进程,而无需使用任何其他启动进程的方法。
UserLoginInfo user = Login(cert);
//using the returned username from the method above and the pin in securestring format from the user entry start the process over with new permissions
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();
}
}
}
现在您可能想知道是什么创建了 PIN。如果您之前做过证书选择,当您选择证书时,您不会看到 PIN 登录,它只会选择证书。您实际上必须自己创建 PIN 登录表单,由于这是在一个类中构建的,我将直接创建登录表单。这段代码是将所有内容连接在一起的部分。它也是您要调用的启动方法。基本上,这个方法会创建表单、文本框、两个按钮(提交、取消),并对其进行少量格式化,以便没有任何重叠。
步骤 1 调用 GetClientCert,然后返回证书
步骤 2 创建 PIN 登录表单,分配事件处理程序并显示表单
步骤 3 提交用户输入的 PIN
步骤 4 将证书转换为用户名,将 PIN 转换为安全字符串
步骤 5 将该信息传递给进程的用户名和密码字段
步骤 6 启动进程并关闭现有进程(简单地使用新凭据重新加载应用程序)
在这种情况下,我希望使用新凭据重新加载当前应用程序,所以我只是启动了当前应用程序的新实例,然后关闭了旧实例。应用程序使用提升的权限和证书进行身份验证加载。所以,如果您处于强制使用 CAC 的环境中,这段代码将允许您使用您的 CAC 以另一个用户的身份执行。
public void SmartCardLogon()
{
//First Get the Certificate
cert = GetClientCertificate();
//Create New login form for user to enter pin
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;
//Create new text box for the pin
TextBox TextBox1 = new TextBox();
TextBox1.Name = "txtCaCLogin";
TextBox1.PasswordChar = '*';
TextBox1.Width = 152;
//Add textbox to form
login.Controls.Add(TextBox1);
//Create new button for user to submit the pin
Button b = new Button();
b.FlatStyle = FlatStyle.Flat;
b.Text = "Login";
b.Name = "butCacLogin";
b.Click += new EventHandler(b_Click);
//Create new button for user to cancel the login
Button c = new Button();
c.Text = "Cancel";
c.FlatStyle = FlatStyle.Flat;
c.Name = "butCancel";
c.Click += new EventHandler(c_Click);
//Add buttons to form
login.Controls.Add(b);
login.Controls.Add(c);
//position buttons under the textbox
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)
{
//if user cances the form show the main form and close the two login forms.
Application.OpenForms["frmHome"].Show();
Application.OpenForms["frmLogin"].Close();
Application.OpenForms["frmCaCLogin"].Close();
}
void b_Click(object sender, EventArgs e)
{
//Retrive the pin from the txt filed you added to the created form
pin = Application.OpenForms["frmCaCLogin"].Controls["txtCaCLogin"].Text;
//just hides the form
Application.OpenForms["frmCaCLogin"].Hide();
//
if (cert != null)
{
//This method is used to create a username from the selected certificate
UserLoginInfo user = Login(cert);
//using the returned username from the method above and the pin in securestring format from the user entry start the process over with new permissions
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();
}
}
}
}
我需要分享的最后一段代码是实际调用它的应用程序表单。创建一个 Windows 窗体,向表单添加一个按钮,并将此代码添加到 button_click 事件中。
SmartCard sc = new SmartCard();
sc.SmartCardLogon();
简而言之就是这样,希望大家觉得这个很有趣。
关注点
我通过研究发现的一件事是,关于这个主题几乎所有的内容都是不完整的。我找不到任何关于如何实现这一目标的有效示例,只有部分示例。所以我希望这能帮助到其他可能需要实现这一目标的人。我在查找过程中确实看到了很多关于使用智能卡启动应用程序的问题,但没有有效的答案。我是使用 Visual Studio 2010 在 Windows 7 上构建的,所以关于兼容性,它在其他 Windows 环境或 Visual Studio 版本上可能有效,也可能无效。