天涯一飘絮

导航

 
本文讨论:

  Windows 智能卡编程基础

  示例智能卡应用程序的实现方法

  编写实现智能卡功能的托管打包程序

  智能卡事务管理

  这篇文章基于 Windows Vista 的预发布版而撰写。其中包含的信息可能会有所变动。

  本文使用了以下技术:

  Windows Vista, C++, C#

通过托管代码和 Windows Vista 智能卡 API 来保护您的数据目录

  Windows 智能卡编程

  智能卡的发展

  示例应用程序的实现方法

  WinSCard API 打包程序

  GetSmartCard 帮助器例程

  卡模块 API 打包程序

  处理 CardAcquireContext

  事务管理

  卡模块接口设计基本原理

  使用 CLR 加密

  依赖关系和测试

  智能卡(简单地说,就是嵌入了微型芯片的信用卡)的概念已经提出将近 30 年了。但现在安全工作的重点是让公司和政府等机构重新审视一些早已有之的理念。

  对于身份验证系统的脆弱连接(即密码)来说,智能卡是一个很吸引人的替代方法。整个行业非常需要能够替代密码的技术。借助嵌入式的加密处理器,智能卡提供了非常安全且易于使用的身份验证机制。

  然而,智能卡的部署也带来了其特有的挑战。整个行业需要更好的产品来部署和管理复杂的身份验证技术。比尔·盖茨在 RSA 2006 大会上做的主题发言中,演示了 Microsoft Certificate Lifecycle Manager,该产品充分利用了我们这篇文章中讨论的 API。

 

  Microsoft 已经认识到智能卡在其平台的安全战略中所扮演的重要角色。开发人员需要了解可识别智能卡的应用程序的工作原理,以及 Windows 操作系统使用了何种方法,使生活变得更加轻松。

  Windows 智能卡编程

  Windows 已支持个人计算机/智能卡 (PC/SC) 软件堆栈将近十年了(详细信息,请参见 www.pcscworkgroup.com)。PC/SC 功能通过 Windows 智能卡 (WinSCard) 客户端 API 提供给应用程序,该 API 在 winscard.dll 和 scarddlg.dll 中实现(在后者中实现程度较小)。客户端 API 允许主机应用程序通过智能卡资源管理器服务(也称作 SCardSvr)与智能卡间接进行通信。

  由于智能卡是一种共享资源,在任何给定时间都可能有多个应用程序试图与该卡进行通信,因此要使用资源管理器模型。与该卡的通信必须是串行的。因此,资源管理器强制采用简化的、类似于数据库的锁定模型。在 PC/SC 中,持有给定智能卡读卡器(也称为接口设备或 IFD)的锁,就等于掌握了事务。然而,本文中“事务”这个术语可能会引起误解。例如,没有回滚或取消提交的事情发生,PC/SC 不能自动撤销应用程序在事务中作出的更改。

  在 SCardEstablishContext、SCardBeginTransaction 以及其他 WinSCard 函数的平台 SDK 文档中可找到对 PC/SC 的其他说明。主机应用程序使用这些例程建立到资源管理器的管道,获得智能卡读卡器的独占锁,向卡发送一系列命令,然后释放锁。

 图 1 中汇总了 PC/SC 组件堆栈。请注意,PC/SC 客户端代码是在应用程序进程中加载的。资源管理器是一种系统服务,是独立于客户端的。读卡器设备驱动程序是唯一在内核模式下运行的 PC/SC 堆栈组件。读卡器驱动程序被资源管理器独占加载,以防止应用程序忽略事务模型。这是 PC/SC 安全模型很重要的一个方面。最后,本文中讨论的 API 只供用户模式调用程序使用。遗憾的是,没有内核模式的资源管理器。

通过托管代码和 Windows Vista 智能卡 API 来保护您的数据

  图 1PC/SC 组件堆栈

  文章的剩余部分将重点讨论栈顶:主机应用程序和 PC/SC 客户端 API。接着,我将讨论此编程模型的局限性及其发展过程。

  智能卡的发展

  迄今为止,智能卡在 Windows 中的典型用途通常与身份验证相关。例如,它们可以代替密码进行登录。这些智能卡身份验证方案是基于加密技术的。这就导致一直以来,在 Windows 中添加对新型智能卡的支持始终要求供应商部署一个名为 CSP(加密服务提供商)的插件,该插件部署了名为 Crypto API 1.0 的接口。不幸的是,人人都知道 CSP 的编写工作非常复杂。而且,由于 CSP 接口是专用于低级别加密操作的,因此不能提供现代智能卡的各种丰富的新功能。这些功能包括增加的数据存储容量、高带宽 I/O、卡载小程序以及新的加密算法。由于缺少可实现这些功能的应用程序级别的编程模型,阻碍了硬件供应商生产出有别于人的商品,也阻碍着编程人员为各种硬件编写程序。

  值得高兴的是,由于采用了被称作“卡模块 API”的新插件模型,Windows Vista™ 中的情况已大大改善。新 API 最大的优点是允许可识别智能卡的应用程序不知道卡的具体类型。让我们看一个示例。

我的示例是一个 Windows 数字权限管理 (DRM) 应用程序,该程序使用智能卡来存储数字媒体许可证。数字许可证包括数字签名的、经过压缩的 XML Blob。通过在许可证生命周期中与智能卡的交互,主机应用程序在卡上执行一些简单的命令:创建存储许可证的文件、写入文件、读取文件和删除文件。每个命令都对应于一个不同的字节序列。例如,如果应用程序确定被智能卡识别为“02”的文件(很多智能卡的文件系统非常简单)包含了许可证 Blob,那么可以对相关的命令/字节序列进行编码:“读取 02 文件的前 128 个字节。”每个命令都是通过 WinSCard 函数 SCardTransmit 发送到卡上的。稍后,我将转回到这个话题。

  要普及可识别智能卡的应用程序,需要注意的一个问题是各个供应商使用的命令编码是不同的。就算已经有了一些标准化编码(如 ISO 7816),并且针对基本操作(如读取文件)的 ISO 编码可能的确适用于多种卡,但通常开发人员不能指望这些编码。因此,必须对本示例中的 DRM 应用程序进行修改,以支持各种新的智能卡类型。根据我的经验,构建这些智能卡命令字节序列的应用程序代码非常杂乱,很难维护。

  新的卡模块 API 通过提供与常见文件系统类似的接口,处理卡不兼容的问题,并使用其他一些例程满足前面提到过的与加密有关的身份验证要求。例如,如果应用程序采用的是针对各种卡进行“读取 02 文件的前 128 个字节”命令编码的旧模式,则伪代码可能与图 2 类似。

通过托管代码和 Windows Vista 智能卡 API 来保护您的数据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(
  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
如果您觉得新的 API 比旧方法简单很多,我完全同意。但别着急,还有一些工作要做!

  到现在为止,我已经描述了 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 数据。然后,我将使用加密密钥对对数据进行加密。同样,必需的 XML 和与加密相关的功能已通过 CLR 提供。我将通过卡模块保存数据和签名。这需要新的本机打包程序和用于本机例程的 P/Invoke 接口。下一步,既然该程序是用于测试的,我将反向执行前面的步骤。这就是说,我将把刚刚写入卡的文件读取出来。最后,我将通过 CLR 加密例程对数据进行解密。

  我已经概要介绍了要在应用程序中看到的行为,现在让我们将各种功能要求分类。以下三个组成部分是所有我想要的东西:用于 WinSCard API 的打包程序、用于卡模块 API 的打包程序以及现有的 CLR 加密例程。一旦实现了这些操作并理解了其中的道理,编写应用程序这件事本身是很容易的!让我们仔细了解各个组成部分。我建议您下载示例 C# 代码并认真学习,这些代码中加入了一些关于智能卡编程基础的注释,增加了趣味性。

  WinSCard API 打包程序

  该应用程序做的第一件事情是调用 .NET 加密 API,找到合适的密钥对。但是,我将在介绍完该应用程序进行的底层智能卡 API 调用之后再讨论这部分问题,以阐明我对整体体系架构的看法。

  为了通过卡模块 API 与智能卡直接进行交互,我要做的第一件事就是为适当的卡获取 PC/SC 句柄。(请注意,此处引述的所有本机例程在 Platform SDK 中都有记录,因此我不会在此为用到的各个公共例程提供详细的使用方法。)为了绑定到来自托管应用程序的智能卡,我需要介绍几个本机 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);

  示例应用程序运用的第二个 PC/SC 例程 SCardUIDlgSelectCardW 是目前为止最难在 .NET 代码中使用的例程。这有两个原因。第一个原因,此函数使用了单个 C 样式结构作为其参数。(请参见平台 SDK 中 winscard.h 定义的 OPENCARDNAME_EXW。)该结构除了包括若干个其他字段外,还包括两个充当输入和输出的 Unicode 字符数组。要正确地排定这个结构是要费些周折的。需要说明的是,这个结构包含一个可选的嵌入式结构:OPENCARD_SEARCH_CRITERIAW。该结构也是在 winscard.h 中定义的。这个嵌套结构不比第一个结构简单。对于这个示例而言,我选择不采用依赖于第二个结构的 SCardUIDlgSelectCardW 的高级功能

  采用 SCardUIDlgSelectCardW 所面临的第二个挑战是,必须将 OPENCARDNAME_EXW 第一个数据成员初始化,使之与结构大小相同(以字节为单位)。我尝试了三种方法,试图将该字段初始化为正确的值。第一种方法,我试图对其进行硬编码 — 这就是说,确定该结构的大小(例如,通过调试程序或采用本机代码的简单测试应用程序),始终在托管代码中将该字段初始化为该值。使用易于移植的代码是很不错的,但是,在 64 位的环境下,该结构的大小会有所不同。

  第二种方法,您可以使用一个或多个 CLR 编排原始字节(如 Marshal.SizeOf)来确定被编排的结构在运行时的大小。不幸的是,在我的测试中,该特殊例程得到了令人意外的结果,表明特定数据成员经编排后的大小是无法确定的。而且,除非此方法得到的大小与本机大小是完全一样的,否则,这不是一个好主意。

  第三种方法,我实现了一个可接受部分初始化结构的本机打包程序,设置 dwStructSize 字段,然后调用 SCardUIDlgSelectCardW。然后,我通过 P/Invoke 向托管代码提供了本机打包程序。您可能已经猜出来了,我选择了这种做法。我知道,出于灵活性的考虑,将本机打包程序封装为独立的 DLL 以及添加额外 DLL 的管理开销以支持单个 API 的做法令人很难接受,但我已经获知卡模块例程也需要类似的解决方案。换言之,我发现提供一个独立的本机 DLL 是不可避免的。我将在下一节介绍卡模块例程。

  在示例代码中,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);
让该 API 正常工作后,其他工作就显得非常简单了!

  GetSmartCard 帮助器例程

  图 3 显示了摘录的示例代码中的 GetSmartCard 帮助器例程。请注意,如果 MgScSCardUIDlgSelectCardW 是成功的,则意味着 PCSCOpenCardNameWEx 参数的 cardHandle 成员被初始化并对应于一个已插入的智能卡。换言之,我们已经可以使用这张卡了。

通过托管代码和 Windows Vista 智能卡 API 来保护您的数据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;
}

  我使用返回的卡句柄在 GetSmartCard 中做的第一件事情是调用 SCardBeginTransaction。这一操作授予了对智能卡的独占访问权限,防止其他应用程序(甚至该进程中的其他卡句柄)与该卡交互。如果 GetSmartCard 例程成功返回,事务仍然是保留的,我将使卡保持在锁定状态,直到调用托管的加密 API 例程为止。在那个时间点释放锁的原因很深奥,我将稍后作出解释。

  既然我已经有了卡句柄,就可以通过 P/Invoke 调用 SCardStatus API 以获取 ATR 了。您可以在 GetSmartCard 中看到我调用了两次 SCardStatus:一次是查询两个变量长度输出参数的大小,第二次是用足够大小的缓冲区接受输出信息。但如果您看一下针对 SCardStatus 的平台 SDK 文档,会发现 API 还可以以调用程序的名义分配输出缓冲区(可选)。

  以 ATR 输出缓冲区为例。在本机代码中,调用程序可以将缓冲区长度的输入/输出参数 pcbAtrLen 设置为指向 SCARD_AUTOALLOCATE 值,该值在 winscard.h 中定义为 –1。然后,pbAtr 参数被作为 LPBYTE * 传送并转换为 LPBYTE。返回的缓冲区通过 SCcardFreeMemory 释放。很多 PC/SC 函数也使用同样的语义。实际上,每当我编写与 PC/SC 相关的本机代码时,都会出于方便的目的,频繁使用 SCARD_AUTOALLOCATE。然而,由于定义如此毫无规律的行为和正确编排这些行为的复杂性,我没有选择在打包程序中采用这一功能

  一旦获取了 ATR,我就会将选定的卡映射到卡模块。我决定将操作提取到打包程序函数(我所实现的将卡模块提供给托管代码的那些函数)之一中。这是下一节的主题。

  卡模块 API 打包程序

本机卡模块接口是在 cardmod.h 中定义的,Windows Vista 平台 SDK 的最新版本中提供了这一文件。如果您看一下在头文件尾部定义的 CARD_DATA 结构,就会注意到,在 .NET 代码中采用卡模块例程会有一些复杂化。与某些 PC/SC 例程中由 SCARD_AUTOALLOCATE 带来的有关内存管理的挑战相似,一些卡模块例程以调用程序的名义分配动态大小的输出缓冲区。但是,与 SCARD_AUTOALLOCATE 不同的是,卡模块内存分配行为不是可选的。以 CardReadFile 为例(当我从智能卡读取数字许可证文件时,需要在示例应用程序中使用该文件)。我将通过函数指针终止调用 CardReadFile,指针类型按如下方式定义。

 

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) 小程序实例化。

 事务管理

  现在,让我们回来讨论示例代码。如果 GetSmartCard 帮助器例程成功返回,则卡上的事务仍处于被控制的状态。换言之,GetSmartCard 输出的卡句柄仍然对卡拥有独占的访问权限。这么做不仅仅是为了将一切呈现在您面前。请注意,示例应用程序的总体流首先要通过托管的加密 API 对数据加密,然后执行所有必要的卡模块工作,包括写回和读取文件,最后通过加密 API 将数据解密。

  操作的顺序突出了我称之为事务管理的几个需要注意的重要问题。第一个需要注意的问题是,在调用 PC/SC 和卡模块例程时,我希望在将独立事务的数量降到最低的同时根据需要执行尽可能多的操作。换言之,我要尽可能减少释放和重新获取与智能卡之间的独占通信链接的次数。通过在单个事务完成所有调用,消除了另外一个应用程序介入并使卡处于未知状态的可能性。

  然而,这个策略也只能有这么大作用;第二个要注意的问题是加密 API 自己进行事务管理。例如,我从卡读出加密文件之后,在调用托管的加密 API 对文件进行解密之前,必须调用 SCardEndTransaction。否则,对 RSACryptoServiceProvider.Decrypt 的调用将永远停止在深处 CSP 内部的 SCardBeginTransaction 上。

  第三个需要注意的事务管理问题(也是一个重要的安全问题)是,卡在进行身份验证的过程中,必须保持锁定状态。利用卡模块接口,当正确的 PIN 通过 CardAuthenticatePin 传送后,卡便通过了身份验证。我已经通过 MgScCardAuthenticatePin 的 P/Invoke 接口实现这一操作。PC/SC 安全模型是使用以下顺序对卡执行权限操作的应用程序。

  通过 SCardBeginTransaction 锁定卡。

  对卡进行身份验证。我使用 CardAuthenticatePin 完成此操作。

执行权限操作。在此示例中,我首先尝试从卡中删除一个文件,然后尝试写入一个文件。两个操作都要求进行身份验证。

  取消对卡的身份验证。

  通过 SCardBeginTransaction 解锁卡。

  这样,未经授权的应用程序不能向由已授权应用程序验证过的卡发送权限命令。

  某些卡不支持直接取消身份验证。应用程序还必须通过 PC/SC 重置卡。重置使卡回到最初的、未经身份验证的状态。虽然重置有效地“重新启动”了智能卡,但是这会对下一个尝试与卡进行通信的主机应用程序的性能带来负面影响。因此,卡模块接口在设计时采用了可选的例程,避免了这一问题以及由此带来的性能上的缺陷。

  在本机打包程序函数 MgScCardDeauthenticate 中,如果目标卡模块没有实现 CardDeauthenticate 函数,打包程序将返回 ERROR_CALL_NOT_IMPLEMENTED。如果取消身份验证的调用由于某种原因失败了,托管应用程序将通知自己在释放当前事务前,必须重置该卡。

if (0 != (result = (Int32) MgSc.MgScCardDeauthenticate(
  mgscContext, MgScUserId.User, 0)))
    cardDispositionOnRelease = PCSCDisposition.SCARD_RESET_CARD;
这与我之前描述的事务管理安全模型是一致的。

  卡模块接口设计基本原理

  应用程序与智能卡之间交互的最终细节是最简单的,上文已概要介绍。那就是,要实现将数字许可证文件放置在卡上的与存储相关的逻辑,首先通过 P/Invoke 调用本机 MgScCardDeleteFile。出于简便的考虑,我没有检查返回的值,这是因为,如果这是第一次对文件不存在的卡执行该应用程序,API 将返回一个错误。

  下一步,调用 CreateFile 和 WriteFile 的打包程序。我们一起来学习卡模块 API 的设计基本原理,这非常有趣。独立的 CardCreateFile 和 CardWriteFile 例程提供了复制 CREATE_NEW 和 OPEN_EXISTING 语义的简单方法,语义包括相应的错误条件,由人们熟悉的 Windows CreateFile API 提供。这一点非常重要,由于编程人员可以使用在文件 I/O 中大体掌握的模式与卡进行交互,因此这样做降低了编程人员工作的难度。

但与 Windows CreateFile 函数不同,CardCreateFile 将字节计数参数作为输入。有些卡模块会忽略该参数,但应用程序传入文件的大小最好不超过允许创建的文件的最大大小的底限。这么做的原因是因为有些卡必须在创建时预先分配整个文件。另一方面,您也不想浪费空间。这一设计中一个可能令人困惑的问题是,CardReadFile 可能返回文件的整体分配空间的大小(传递给 CardCreateFile 的可能较大的值),而不仅仅是通过 CardWriteFile 写入的可能较小的值。

  将加密的许可证写入卡之后,我通过 MgScCardReadFile 读取它。正式的应用程序不执行这一步骤,我这么做是出于说明问题和测试接口的目的。接下来的步骤是解密文件,将其与原始值比较,进行完整性检查。

  使用 CLR 加密

  请注意,我在示例代码中做的第一件事是通过 .NET 加密 API 绑定到基于智能卡的 RSA 密钥对。(参见 System.Security.Cryptography 命名空间中 RSACryptoServiceProvider 类的文档。)

  尽管 .NET 提供的抽象技术和强大的功能很不错,但我总是想要看看具体发生了什么。由于 .NET 加密 API 是基于本机加密 API 的,我可以对此代码做一些假定(代码是从 GetKey 帮助器例程改写而来的):

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。

依赖关系和测试

  我可以进一步做出 CSP 实际在执行什么操作的断言。这将易于创建示例应用程序的组件依赖关系图。我调用的 CSP 实际是在卡模块接口上构建的;这是首选的写入卡的主要基本原理。而它是智能卡 CSP 这一事实暗示,它可能在直接调用卡模块例程之前,使用与示例代码类似的方式,建立到智能卡的连接。

  换言之,在对上文讨论过的 SCardUIDlgSelectCardW、SCardBeginTransaction、SCardStatus 和 CardAcquireContext 进行显式调用前,CSP 或多或少也在内部进行同样的调用。特别是考虑到仅为调用 CardAcquireContext 而需要进行设置的工作量,这种重复工作显然不受欢迎。如果对卡模块进行直接调用的上下文信息可以从加密 API 检索或至少通过某些共享代码公布这些信息,那就更好了。

  图 4 描述了这些依赖关系。图的底层代表来自 winscard.dll 客户端 API 智能卡堆栈中的所有内容,上至资源管理器服务,下至驱动程序,以及智能卡。

通过托管代码和 Windows Vista 智能卡 API 来保护您的数据

  图 4程序样品的依赖关系

  我已经详细讨论了一些智能卡的基础知识和测试应用程序,我希望您下载示例代码并体验一下。要构建和运行示例代码需要几项特定的内容:

  最新版本的 Windows Vista 平台 SDK 中的 cardmod.h 头文件。

  基于卡模块的智能卡。即已由供应商提供了卡模块的智能卡。

  智能卡读卡器。在某些情况下,诸如特定的 USB 密钥、智能卡和读卡器等是一体的。

  基本智能卡加密提供程序 (Base Smart Card Crypto Provider)。可通过 Windows Update 下载。单击“自定义”按钮,并选择左侧的“软件”、“可选”。

posted on 2009-06-30 10:37  冰云  阅读(820)  评论(0编辑  收藏  举报