适用于 .NET 的智能卡框架
描述了一个用于 .NET 编程智能卡应用程序的 XML 框架。
引言
在 第一部分 中,我描述了如何使用 C# 编写一组类来封装 Windows 的 PC/SC API。PC/SC 是一组用于与智能卡通信的 API。即使这些类比 PC/SC 函数更容易使用,您仍然需要编写一些不总是易于理解的代码。与智能卡的通信使用一种名为 APDU 的协议来向卡发送命令,这些命令被称为 APDU 命令。我提出的框架旨在简化智能卡应用程序的开发。大多数情况下,您需要链接多个命令来执行诸如读取或写入文件之类的操作。通过我在此描述的框架,您可以轻松地链接命令,然后使用 XML 描述来描述应用程序的智能卡元素。
背景
本文假设您已阅读了本文的 第一部分,并且对 XML 有一定的了解。即使不是必需的,对智能卡编程的基本了解也会有所帮助。
APDU 命令协议
大多数人都有手机,因此每天都在使用智能卡。例如,当您在手机上输入 PIN 码或读取 SIM 卡目录中的电话号码时,您的手机会向卡发送一组命令来执行操作。APDU 命令是一组发送到卡的字节,卡将以一个代码并可能以您想要读取的数据进行响应。
APDU 命令描述
APDU 命令基本上由以下字节组成
APDU 字节 | 字节长度 | 描述 |
类 | 1 | 类字节 |
Ins | 1 | 指令字节 |
P1 | 1 | 参数 1 |
P2 | 1 | 参数 2 |
P3 | 1 | 此参数是发送数据的长度或期望数据的长度 |
Data | N | 发送到卡的数据 |
卡将以字节数组响应命令。
字节 | 长度 | 描述 |
Data | 0 到 255 | 响应数据 |
SW1, SW2 | 2 | 命令状态 |
APDU 命令总是返回至少两个字节,即状态字节。当命令返回一些数据时,状态字节是命令返回的最后两个字节。
有五种不同的配置用于向卡发送命令。它们分为两组:发送数据的命令和接收数据的命令。
发送数据的 APDU
无数据发送,无数据接收
CLASS
|
INS
|
P1
|
P2
|
P3
|
SW1
|
SW2
|
|
lgth = 0 |
90
|
00
|
将数据发送到卡
CLASS
|
INS
|
P1
|
P2
|
P3
|
具有长度 datalgth 的 DATA |
SW1
|
SW2
|
|
datalgth |
90
|
00
|
如果 datalgth
= 0,则发送 256 字节到卡。
在正常执行中,这些命令将以 9000 或错误代码响应。
接收数据的 APDU
接收已知长度的数据
CLASS
|
INS
|
P1
|
P2
|
P3
|
具有长度
datalgth 的 DATA |
SW1
|
SW2
|
|
datalgth |
90
|
00
|
接收未知长度的数据
当命令请求卡返回未知字节数的数据时,您必须首先发送长度为 0 的命令以获取卡将返回的字节数。然后,您可以使用小于或等于给定长度的请求长度调用 GET RESPONSE
命令。
1 - 以请求长度 0 发送命令。
CLASS
|
INS
|
P1
|
P2
|
P3
|
SW1
|
SW2
|
||
0
|
9F
|
lgth
|
2 - 发送 GET RESPONSE
,req_lgth
< lgth
。
CLASS
|
INS
|
P1
|
P2
|
P3
|
具有长度
req_lgth <= lgth 的 DATA |
SW1
|
SW2
|
|
C0
|
0
|
0
|
req_lgth |
90
|
00
|
发送数据和接收已知或未知长度的数据
这种情况与前一种非常相似。区别在于您首先发送一个将数据传输到卡的命令。该命令以 9FXX 代码进行响应,其中 XX 表示使用 GET RESPONSE
命令可以读取的最大数据长度。
1 - 将数据发送到卡
CLASS
|
INS
|
P1
|
P2
|
P3
|
具有长度 datalgth 的 DATA |
SW1
|
SW2
|
|
datalgth |
90
|
00
|
2 - 发送 GET RESPONSE
,req_lgth
< lgth
。
CLASS
|
INS
|
P1
|
P2
|
P3
|
具有长度
req_lgth <= lgth 的 DATA |
SW1
|
SW2
|
|
C0
|
0
|
0
|
req_lgth |
90
|
00
|
SIM 卡命令
可以发送到 SIM 卡的命令在名为 3GPP TS 11.11 (又名 GSM 11.11) 的规范中有描述。我不会在此描述所有命令和 SIM 卡文件。如果您对此感兴趣,可以在 此处 找到 GSM1111 的版本。
以下是 SIM 卡命令的简短列表
GSM 的类字节是 **A0**,S 表示数据发送到卡,R 表示数据从卡接收。所有字节值均以十六进制给出。
命令
|
INS
|
p1
|
P2
|
P3
|
S/R
|
SELECT |
A4
|
00
|
00
|
02
|
S/R
|
STATUS |
F2
|
00
|
00
|
lght |
R
|
READ BINARY |
B0
|
offset_high |
offset_low |
lgth |
R
|
UPDATE BINARY |
D6
|
offset_high |
offset_low |
lgth |
S
|
READ RECORD |
B2
|
rec_No |
模式 |
lgth |
R
|
UPDATE RECORD |
DC
|
rec_No |
模式 |
lgth |
S
|
VERIFY CHV |
20
|
00
|
CHV_No |
08
|
S
|
CHANGE CHV |
24
|
00
|
CHV_No |
10
|
S
|
RUN GSM ALGORITHM |
88
|
00
|
00
|
10
|
S/R
|
GET RESPONSE |
C0
|
00
|
00
|
lgth |
R
|
现在,让我们看看如何编写一个 XML 框架来简化智能卡应用程序的编写。
APDU 命令的 XML 框架
基本思想是提供一个简单灵活的 XML 框架来描述 APDU 命令,并编写一个可以是一系列命令或序列的应用程序。命令是描述的原子单元,它可以单独使用,也可以从序列中使用,如果需要将参数传递给命令本身。
APDU 命令
原子 APDU 命令由 XML 元素表示。APDU 命令被组装在 ApduList
文档中。ApduList
和 Apdu
元素由以下模式定义
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="ApduList">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="Apdu">
<xs:complexType>
<xs:attribute name="Name" type="xs:string" use="required" />
<xs:attribute name="Class" type="xs:string" use="required" />
<xs:attribute name="Ins" type="xs:string" use="required" />
<xs:attribute name="P1" type="xs:unsignedByte" use="required" />
<xs:attribute name="P2" type="xs:unsignedByte" use="required" />
<xs:attribute name="P3" type="xs:string" use="required" />
<xs:attribute name="Data" type="xs:string" use="optional" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
命令文件包含一组预定义的 APDU 命令。在加载 APDU 命令列表后,可以通过名称播放命令。有不同类型的命令;有些命令可以单独播放,有些需要使用前一个命令的结果。
当 P3
参数表示命令的预期数据长度时,其值可以是前一个命令调用的结果,或者命令必须重播以获取此可变值。以下是参数接受的语法,用于处理此情况
P3 = "R,0:SW1?xx"
R 表示在第一次调用 P3
=0 后,必须在 SW1
值满足条件的情况下重播命令。如果 SW1
== xx
,则命令将以 P3
= SW2
重播。
<Apdu Name="Get OTP ID" Class="A0" Ins="1A"
P1="80" P2="2" Lc="0" Le="R,0:SW1?6C" Data="" />
P3 = "R,xx:DRyy"
R 表示必须重播命令,但这次没有条件。第一次调用时,P3
= xx
,然后命令重播为 P3 = xx + RespData[yy]
(响应数据的第 yy
个数据,第一个数据索引在规范中为 1)。
<Apdu Name="Get Status" Class="A0" Ins="F2" P1="0"
P2="0" Lc="0" Le="R,13:DR13" Data="" />
P3 = "SW2"
如果上一个命令的调用中的 SW1
== 0x9F,则使用上一个命令的 P3
= SW2
来播放此命令。
<Apdu Name="Get Response" Class="A0" Ins="C0" P1="0" P2="0" P3="SW2" />
P3 = "DRxx"
如果前一个命令有数据,则 xx
用作响应数据中的索引以获取 P3
的值。Le
= RespData[xx]
。
<Apdu Name="Read Binary" Class="A0" Ins="B0" P1="0" P2="0" P3="DR15" />
命令序列
命令序列用于链接原子 APDU 命令或序列。序列可以使用传递给其组成命令的参数。SequenceList
描述了一组可以在应用程序中调用的 Sequence
元素。以下模式描述了 Sequence
元素的 SequenceList
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="SequenceList">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="Sequence">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="Command">
<xs:complexType>
<xs:attribute name="Apdu"
type="xs:string" use="optional" />
<xs:attribute name="P2"
type="xs:unsignedByte" use="optional" />
<xs:attribute name="Data"
type="xs:string" use="optional" />
<xs:attribute
name="Sequence" type="xs:string" use="optional" />
<xs:attribute name="P1"
type="xs:string" use="optional" />
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="Name" type="xs:string" use="required" />
<xs:attribute name="PIN" type="xs:string" use="optional" />
<xs:attribute name="Record"
type="xs:unsignedByte" use="optional" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Sequence
元素允许您将参数传递给命令或其他 Sequence
。Sequence
使用一个子元素 Command
。Command
用于调用带有或不带参数的 APDU。当在 Command
中提供参数时,它将覆盖 APDU 元素的默认参数。
例如,如果您想验证 PIN 码,可以使用以下 Sequence
<Sequence Name="Verify CHV1" PIN="FFFFFFFFFFFFFFFF">
<Command Apdu="Verify CHV" P2="1" Data="PIN"/>
</Sequence>
用于调用此序列的类允许您为 PIN
参数提供一个值。当 Command
被执行时,参数 Data
将获得为参数 PIN 提供的值。
Sequence
参数可以取任何名称。在此版本中,参数的值是一个字符串,表示一组字节值。
调用此 Sequence
的代码如下
SequenceParameter seqParam = new SequenceParameter();
// Process Apdu: VerifyCHV
Console.WriteLine("Sequence: Verify CHV1");
seqParam.Add("PIN", "31323334FFFFFFFF");
apduResp = player.ProcessSequence("Verify CHV1", seqParam);
Console.WriteLine(apduResp.ToString());
现在我们已经看到了如何声明命令并使用 Sequence
元素将它们链接起来,我将描述利用这个简单的“XML 语言”的代码。
APDUPlayer:一个用于 XML 描述的 C# 类
为了使用前面描述的 XML 格式,我开发了一个简单的 C# 类,该类能够处理 APDU 命令或 APDU 序列。
APDUPlayer
类有三个构造函数,并公开了一些公共方法。两个最重要的方法是用于执行 APDU 命令或 APDU 序列的方法。
以下方法用于执行单个 APDU 命令。APDUParam
参数可用于修改从 XML 描述中读取的 APDU 参数。
/// <summary>
/// Process a simple APDU command, Parameters
/// can be provided in the APDUParam object
/// </summary>
/// <param name="command">APDU command name</param>
/// <param name="apduParam">Parameters for the command</param>
/// <returns>An APDUResponse object with the response of the card </returns>
public APDUResponse ProcessCommand(string apduName, APDUParam apduParam);
此其他方法用于执行 APDU 序列。SequenceParameter
类是参数/值对的列表,用作要播放的序列的输入参数。
/// <summary>
/// Process an APDU sequence and execute each
/// of its commands in the sequence order
/// </summary>
/// <param name="apduSequenceName">Name of the sequence to play</param>
/// <param name="seqParam">An array of SequenceParam
/// object used as parameters for the sequence</param>
/// <returns>APDUResponse object of the last command executed</returns>
public APDUResponse ProcessSequence(string apduSequenceName,
SequenceParameter seqParam);
使用此方法之前已显示过。
读取 SIM 卡电话簿的示例应用程序
在我之前的文章中,我们使用我描述的 PC/SC 包装器读取了 SIM 卡的电话簿,但该应用程序仅获取电话簿文件 (6F3A) 的原始内容。在此示例中,我们将解释每个记录的数据。电话记录的内容如下
此 EF 包含缩写拨号号码 (ADN)。此外,它还包含相关网络/运营商能力和扩展记录的标识符。它也可能包含相关的 alpha 标签。
标识符:6F3A |
记录长度:X + 14 字节 | ||
字节 | 描述 | 强制/可选 | 长度 |
1 到 X | Alpha 标识符 | O | X 字节 |
X + 1 | 电话号码字符串长度 | M | 1 字节 |
X + 2 | TON & NPI | M | 1 字节 |
X + 3 到 X + 12 | 电话号码字符串 | M | 10 字节 |
X + 13 | CCP 标识符 | M | 1 字节 |
X + 14 | 扩展 1 记录 |
此规范摘自 GSM11.11 文档,该文档规定了 SIM 卡的逻辑。我们将从该描述中提取我们需要获取电话号码及其相关名称的信息。
前几个字节是在手机上与电话号码一起显示的名称。默认情况下,它是一个接近 ASCII 编码的字符串。
接下来的十四个字节包含电话号码本身以及一些描述字节。第一个字节是电话号码的字节长度,包括 TON & NPI 字节,该字节指示号码是国家号 (Ax, 8x) 还是国际号 (9x)。
该组的最后十个字节包含电话号码本身。编码是 BCD,字节顺序相反。如果数字位数是奇数,则最后一个字节包含 F 和数字。例如,数字 **015648327** 的编码方式为:**10658423F7**。
记录的最后两个字节包含很少使用的数据。
PhoneNumber
类是一个辅助类,用于解释电话号码记录的字节。它的使用方式如下
PhoneNumber phone = new PhoneNumber(apduResp.Data);
Console.WriteLine("ADN n°" + nI.ToString());
Console.WriteLine(phone.ToString());
ToString
方法以以下格式获取电话记录内容的字符串:<name> : <number>。
ReadPhonebook 程序是一个控制台应用程序,默认情况下读取 ADN 文件的前十个电话号码。您可以通过将其作为参数传递给程序来读取更多记录。
命令行:ReadPhonebook P <pincode> <nbRecord>
参数是可选的。
- P <pincode>:提供给卡的 PIN 码。如果您不提供此参数,则不会向卡提供 PIN 码。
- <nbRecord>:要读取的记录数。默认情况下,程序读取 10 条记录。
示例
- ReadPhonebook P 1234 25,将提供 1234 作为 PIN 并读取 25 条记录。
- ReadPhonebook 50,读取 50 条记录,不提供 PIN。
- ReadPhonebook P 4567,提供 4567 作为 PIN 并读取 10 条记录。
APDExchange 应用程序
APDUExchange Windows Forms 应用程序是一个基本的 C# 应用程序,可用于向卡发送任何 APDU 命令。可以键入命令或使用 APDUList
文件预加载命令。
该应用程序会根据需要自动检测卡的插入或移除。它使用上一篇文章中描述的智能卡 API 的 NativeCard 实现。
Exchange APDU 窗体如下所示
如果您想发送的命令不在 APDU 列表中,您只需使用前面描述的格式将命令添加到文件中即可。
关注点
我在智能卡行业工作了相当长一段时间,在网上很少找到关于这个主题的文章。随着智能卡如今的广泛应用,甚至一些计算机都配备了嵌入式智能卡读卡器,我认为演示一个简单的 XML 框架如何简化智能卡应用程序的开发会很有趣。您可以根据需要使用此代码并扩展框架,我希望这些文章已经向您展示了使用智能卡可能非常简单。
历史
- 2007 年 3 月 5 日 - 更新了
ExchangeAPDU
应用程序以显示插入卡的 ATR 值。另一个改进是为选定的读卡器激活卡事件,如果您的 PC 上安装了多个读卡器。错误消息以十六进制显示,使用 errorlookup 更容易理解其含义。 - 2011 年 8 月 16 日 - 添加了 VS 2010 的项目更新
- 2011 年 8 月 26 日 - 为 Visual Studio 2008、2010 添加了 64 位项目