Windows 智能卡编程基础
示例智能卡应用程序的实现方法
编写实现智能卡功能的托管打包程序
智能卡事务管理
这篇文章基于 Windows Vista 的预发布版而撰写。其中包含的信息可能会有所变动。
本文使用了以下技术:
Windows Vista, C++, C#
目录
Windows 智能卡编程
智能卡的发展
示例应用程序的实现方法
WinSCard API 打包程序
GetSmartCard 帮助器例程
卡模块 API 打包程序
处理 CardAcquireContext
事务管理
卡模块接口设计基本原理
使用 CLR 加密
依赖关系和测试
智能卡(简单地说,就是嵌入了微型芯片的信用卡)的概念已经提出将近 30 年了。但现在安全工作的重点是让公司和政府等机构重新审视一些早已有之的理念。
对于身份验证系统的脆弱连接(即密码)来说,智能卡是一个很吸引人的替代方法。整个行业非常需要能够替代密码的技术。借助嵌入式的加密处理器,智能卡提供了非常安全且易于使用的身份验证机制。
然而,智能卡的部署也带来了其特有的挑战。整个行业需要更好的产品来部署和管理复杂的身份验证技术。比尔·盖茨在 RSA 2006 大会上做的主题发言中,演示了 Microsoft Certificate Lifecycle Manager,该产品充分利用了我们这篇文章中讨论的 API。
Windows 智能卡编程
图 1 中汇总了 PC/SC 组件堆栈。请注意,PC/SC 客户端代码是在应用程序进程中加载的。资源管理器是一种系统
图 1PC/SC 组件堆栈
智能卡的发展
我的示例是一个 Windows 数字权限管理 (DRM)
要普及可识别智能卡的应用程序,需要注意的一个问题是各个供应商使用的命令编码是不同的。就算已经有了一些标准化编码(如 ISO 7816),并且针对基本操作(如读取文件)的 ISO 编码可能的确适用于多种卡,但通常开发人员不能指望这些编码。因此,必须对本示例中的 DRM 应用程序进行修改,以支持各种新的智能卡类型。根据我的经验,构建这些智能卡命令字节序列的应用程序代码非常杂乱,很难维护。
新的卡模块 API 通过提供与常见文件系统类似的接口,处理卡不兼容的问题,并使用其他一些例程满足前面提到过的与加密有关的身份验证要求。例如,如果应用程序采用的是针对各种卡进行“读取 02 文件的前 128 个字节”命令编码的旧模式,则伪代码可能与图 2 类似。
Figure2Pseudocode for Per-Card-Type Command Handling
if (cardType == A) {
// Build read-file transmit buffer for card type A
...
// Send the command to the card
SCardTransmit(
cardHandle, // PC/SC context handle
pciInputPointerA, // Protocol Control Information (in)
transmitBufferA, // Our read-file command
transmitBufferLength,// Read-file command length
pciOutputPointerA, // Protocol Control Information (out)
receiveBuffer, // Data response from card
receiveBufferLength);// Length of data response
// Interpret the response from this card type
...
}
else if (cardType == B) {
// Build read-file transmit buffer for card type A
...
// Send the command to the card
SCardTransmit(
cardHandle, // PC/SC context handle
pciInputPointerB, // Protocol Control Information (in)
transmitBufferB, // Our read-file command
transmitBufferLength,// Read-file command length
pciOutputPointerB, // Protocol Control Information (out)
receiveBuffer, // Data response from card
receiveBufferLength);// Length of data response
// Interpret the response from this card type
...
}
相反,如果使用新的 API,就可以通过调用单个函数来代替整个块:
CardReadFile(
如果您觉得新的 API 比旧方法简单很多,我完全同意。但别着急,还有一些工作要做!
contextPointer, // PC/SC context handles
NULL, // Directory in which requested file is located
"license", // File name to read
&dataOut, // Contents of the requested file
&dataLengthOut); // Size of the file contents
到现在为止,我已经描述了 PC/SC 软件堆栈和卡模块 API 的一些基本情况。我已经做好解决 DRM 应用程序示例代码的准备。由于我描述的智能卡相关接口都不向 Microsoft® .NET Framework 代码开放,如果我能将卡模块 API 的功能与 .NET 的方便之处结合起来,是不是很酷?
我发现可采用最常规的解决方案来提供代码打包程序,该程序可通过 P/Invoke(一种机制,用于调用来自托管代码的本机 DLL 输出)实现所需的 WinSCard 和卡模块例程。然后,我可以用托管代码演示整个应用程序。我发现如果编写其他打包程序代码来简化任务,可以使事情简单一点。以前,如果不熟悉本机 API 的知识,实现托管接口是非常困难的。
首先,我将概述示例应用程序需要做什么。然后详细讨论打包程序和 P/Invoke 接口。
示例应用程序的实现方法
首先,应用程序要找到并绑定到一个插入的智能卡。如果未插入智能卡,程序将提示用户插入一个智能卡。实现这些操作需要用到 WinSCard API,因此这一部分的应用程序对 P/Invoke 和本机打包程序非常依赖。
然后,我将打开卡上的加密密钥对的句柄。如何没有合适的密钥对,我将新建一个。幸运的是,所需的与加密相关的功能已经通过 CLR 提供了,因此一切都很简单。对于应用程序的加密部分,有一个潜在的障碍:必须安装新版本的基本智能卡加密服务提供程序 (Base Smart Card Crypto Service Provider),这是与基于卡模块的卡交换数据最可靠的方法。
我将创建一些代表数字媒体许可证的示例 XML 数据。然后,我将使用加密密钥对对数据进行加密。
WinSCard API 打包程序
任何 Windows 智能卡应用程序的基础都是 SCardEstablishContext。从 winscard.dll 输出的 PC/SC 例程建立了句柄,该句柄允许应用程序与智能卡资源管理器交互。其功能非常简单,对于这些简单的功能,无需实现额外的本机打包程序。P/Invoke 接口足以:
[DllImport("winscard.dll")]
public static extern Int32 SCardEstablishContext(
UInt32 scope,
[MarshalAs(UnmanagedType.AsAny)] Object reserved1,
[MarshalAs(UnmanagedType.AsAny)] Object reserved2,
ref IntPtr pcscContext);
第二种方法,您可以使用一个或多个 CLR 编排原始字节(如 Marshal.SizeOf)来确定被编排的结构在运行时的大小。不幸的是,在我的测试中,该特殊例程得到了令人意外的结果,表明特定数据成员经编排后的大小是无法确定的。而且,除非此方法得到的大小与本机大小是完全一样的,否则,这不是一个好主意。
在示例代码中,MgSc.dll(托管智能卡的缩写)输出本机打包程序例程。打包程序例程的代码(包括 MgScSCardUIDlgSelectCardW)存储在 MgSc.cpp 中。该 DLL 中用于所有本机打包程序的 P/Invoke 类也称作 MgSc;它包含与以下内容类似的 P/Invoke 占位程序:
[DllImport("mgsc.dll", CharSet=CharSet.Unicode)]
public static extern Int32 MgScSCardUIDlgSelectCardW(
[MarshalAs(UnmanagedType.LPStruct)] [In, Out]
PCSCOpenCardNameWEx ocnwex);
GetSmartCard 帮助器例程
图 3 显示了摘录的示例代码中的 GetSmartCard 帮助器例程。请注意,如果 MgScSCardUIDlgSelectCardW 是成功的,则意味着 PCSCOpenCardNameWEx 参数的 cardHandle 成员被初始化并对应于一个已插入的智能卡。换言之,我们已经可以使用这张卡了。
Figure3GetSmartCard Helper Routine
static Int32 GetSmartCard(
[In, Out] ref MgScContext mgscContext,
[In, Out] ref IntPtr pcscContext,
[In, Out] ref IntPtr cardHandle)
{
Int32 result = 0;
PCSCOpenCardNameWEx ocnwex = new PCSCOpenCardNameWEx();
bool cardIsLocked = false;
string readerName;
UInt32 readerNameLength = 0, cardState = 0, cardProtocol = 0;
byte [] atr;
UInt32 atrLength = 0;
try
{
// Get a context handle from the smart card resource manager
if (0 != (result = PCSC.SCardEstablishContext(
PCSCScope.SCARD_SCOPE_USER, null, null, ref pcscContext)))
throw new Exception("SCardEstablishContext");
// Get a handle to a card, prompting the user if necessary
ocnwex.flags = PCSCDialogUI.SC_DLG_MINIMAL_UI;
ocnwex.pcscContext = pcscContext;
ocnwex.shareMode = PCSCShareMode.SCARD_SHARE_SHARED;
ocnwex.preferredProtocols = PCSCProtocol.SCARD_PROTOCOL_Tx;
ocnwex.reader = new string(
(char) 0, (int) readerAndCardBufferLength);
ocnwex.maxReaderLength = readerAndCardBufferLength;
ocnwex.card = new string(
(char) 0, (int) readerAndCardBufferLength);
ocnwex.maxCardLength = readerAndCardBufferLength;
if (0 != (result = MgSc.MgScSCardUIDlgSelectCardW(ocnwex)))
throw new Exception("SCardUIDlgSelectCardW");
// Lock the card
if (0 != (result = PCSC.SCardBeginTransaction(ocnwex.cardHandle)))
throw new Exception("SCardBeginTransaction");
cardIsLocked = true;
// Get the ATR for the selected card
if (0 != (result = PCSC.SCardStatusW(ocnwex.cardHandle, null,
ref readerNameLength, ref cardState, ref cardProtocol,
null, ref atrLength)))
throw new Exception("SCardStatusW");
readerName = new string((char) 0, (int) readerNameLength);
atr = new byte [atrLength];
if (0 != (result = PCSC.SCardStatusW(ocnwex.cardHandle,
readerName, ref readerNameLength, ref cardState,
ref cardProtocol, atr, ref atrLength)))
throw new Exception("SCardStatusW");
// Get a card module handle for this card
mgscContext = new MgScContext();
if (0 != (result = (int) MgSc.MgScCardAcquireContext(
mgscContext, pcscContext, ocnwex.cardHandle,
ocnwex.card, atr, atrLength, 0)))
throw new Exception("MgScCardAcquireContext");
cardHandle = ocnwex.cardHandle;
}
finally
{
if (0 != result)
{
// Something failed, so we need to cleanup.
if (cardIsLocked)
PCSC.SCardEndTransaction(ocnwex.cardHandle,
PCSCDisposition.SCARD_LEAVE_CARD);
if ((IntPtr)0 != ocnwex.cardHandle)
PCSC.SCardDisconnect(ocnwex.cardHandle,
PCSCDisposition.SCARD_LEAVE_CARD);
if ((IntPtr)0 != ocnwex.pcscContext)
{
PCSC.SCardReleaseContext(ocnwex.pcscContext);
pcscContext = (IntPtr)0;
}
}
}
return result;
}
一旦获取了 ATR,我就会将选定的卡映射到卡模块。我决定将操作提取到打包程序函数(我所实现的将卡模块提供给托管代码的那些函数)之一中。这是下一节的主题。
卡模块 API 打包程序
本机卡模块接口是在 cardmod.h 中定义的,Windows Vista 平台 SDK 的最新版本中提供了这一文件。如果您看一下在头文件尾部定义的 CARD_DATA 结构,就会注意到,在 .NET 代码中采用卡模块例程会有一些复杂化。与某些 PC/SC 例程中由 SCARD_AUTOALLOCATE 带来的有关内存管理的
typedef DWORD (WINAPI *PFN_CARD_READ_FILE)(
__in PCARD_DATA pCardData,
__in LPSTR pszDirectoryName,
__in LPSTR pszFileName,
__in DWORD dwFlags,
__out_bcount(*pcbData) PBYTE *ppbData,
__out PDWORD pcbData);
现在看一下 CARD_DATA 结构的两个成员。第一个成员是创建输出缓冲区时,卡模块使用的堆分配器回调。第二个成员是在 CardAcquireContext 执行期间由卡模块初始化的 PFN_CARD_READ_FILE 函数指针,供后面的调用应用程序使用。
// These members must be initialized by the CSP/KSP before
// calling CardAcquireContext.
...
PFN_CSP_ALLOC pfnCspAlloc;
PFN_CSP_FREE pfnCspFree;
...
// These members are initialized by the card module
...
PFN_CARD_READ_FILE pfnCardReadFile;
总之,在 CardReadFile 调用过程中,卡模块或多或少会执行以下一些操作:通过 pfnCspAlloc 分配足够大的缓冲区、通过 SCardTransmit 从卡中读取请求的文件、设置 pcbData 并返回。一旦调用程序结束并返回数据缓冲区,则调用 pfnCspFree。
我知道,与手工编写一组 P/Invokes 相比,为每个卡模块例程使用一个本机打包程序会缩短时间。例如,在调用 CardReadFile 时,.NET 应用程序调用一次本地打包程序,确定输出缓冲区的大小,然后用足够大小的缓冲区再次调用。这很明显是一个性能上的折衷,因为应用程序从智能卡上读取两次文件。这种情况很有可能通过为堆回调实现委托得以改善。
处理 CardAcquireContext
让我们回到示例数字许可证应用程序的思路中。回想一下,我通过 PC/SC 例程,获取了卡以及该卡的 ATR。基于该 ATR,我在称作 Calais 的数据库中查找对应卡模块 DLL 的名称,该名称就是存储在系统注册表 HKLMSoftwareMicrosoftCryptographyCalais 中的数据。如果 PC/SC 堆栈最初是在 Microsoft 实现的,则将“Calais”选为项目代码名称。根据某些流传的说法,我想智能卡是由法国人发明并不完全是巧合。
Calais 数据库查找是通过 SCardGetCardTypeProviderName 传送 cardmod.h 中定义的 SCARD_PROVIDER_CARD_MODULE 值完成的。我决定将其滚入到 MgScCardAcquireContext,即 CardAcquireContext 的本机代码打包程序,而不是通过 P/Invoke 接口,将特定的 PC/SC 例程提供给托管环境。为什么呢?让我们向下看几步(本机代码)操作。下一步是获得由 Calais 数据库返回的 DLL 字符串名称并将其传递给 LoadLibrary。必须在整个上下文的生命周期中,用卡模块维护得到的 HMODULE(我当然不希望卸载该卡模块)。然后调用 GetProcAddress 找到 CardAcquireContext 输出。最后,我用卡模块期待的本机回调(包括堆分配例程)初始化 CARD_DATA 结构。
总之,如果 .NET 代码仅在准备调用 CardAcquireContext 时执行,则通过 .NET 代码让所有与本机代码相关的材料运行起来,不会有什么好的结果。将其滚动到该例程的本机打包程序会好一些。
请考虑卡模块实际执行了什么操作。给定的卡模块处理 CardAcquireContext 时的步骤取决于卡的类型。如果该卡基于 ISO 命令集,则此时卡模块无需向卡发送任何命令。相反,卡模块将确认是否的确支持指定的 ATR(通过 CARD_DATA 由调用程序输入)。然后可能设置一些内部上下文状态并将其附加到调用程序的 CARD_DATA 的 pvVendorSpecific 成员。另一方面,较新的卡可能是基于代理或虚拟机的。Sun 的 Java 卡是一个很明显的示例。在这种情况下,卡模块将很可能通过 SCardTransmit 向卡发送命令,将卡端卡模块 API 处理程序 (card-side card module API handler) 小程序实例化。
事务管理
通过 SCardBeginTransaction 锁定卡。
对卡进行身份验证。我使用 CardAuthenticatePin 完成此操作。
执行
取消对卡的身份验证。
通过 SCardBeginTransaction 解锁卡。
if (0 != (result = (Int32) MgSc.MgScCardDeauthenticate(
mgscContext, MgScUserId.User, 0)))
cardDispositionOnRelease = PCSCDisposition.SCARD_RESET_CARD;
卡模块接口设计基本原理
但与 Windows CreateFile 函数
使用 CLR 加密
请注意,我在示例代码中做的第一件事是通过 .NET 加密 API 绑定到基于智能卡的 RSA 密钥对。(参见 System.Security.Cryptography 命名空间中 RSACryptoServiceProvider 类的文档。)
CspParameters cspParameters = new CspParameters(
1, "Microsoft Base Smart Card Crypto Provider");
cspParameters.Flags = CspProviderFlags.UseExistingKey;
cspParameters.KeyContainerName = "";
rsa = new RSACryptoServiceProvider(cspParameters);
我可以假定此代码至少应调用 CryptAcquireContext 和 CryptGetUserKey。如果这样不行,则将 cspParameters.Flags 设置为零并再次尝试。这将导致在调用 CryptAcquireContext 后,调用 CryptGenKey。
依赖关系和测试
图 4程序样品的依赖关系
基于卡模块的智能卡。即已由供应商提供了卡模块的智能卡。
智能卡读卡器。在某些情况下,诸如特定的 USB 密钥、智能卡和读卡器等是一体的。
基本智能卡加密提供程序 (Base Smart Card Crypto Provider)。可通过 Windows Update 下载。单击“自定义”按钮,并选择左侧的“软件”、“可选”。