第二章 APDU命令解析:APDU结构详解、命令APDU与响应APDU、常见APDU指令分析
好,咱们进入第二章。APDU,全称是Application Protocol Data Unit,应用协议数据单元。说白了,它就是JavaCard世界里,终端(比如读卡器)和卡片之间对话的“语言”。你发一句,卡片回一句,一来一回,完成一次交互。
我个人习惯把APDU比作“信封”。命令APDU是终端写给卡片的信,响应APDU是卡片回给终端的信。信的内容有固定格式,不能乱写,否则卡片就不理你。
2.1 APDU结构详解
APDU分为两种:命令APDU和响应APDU。咱们先看命令APDU。
2.1.1 命令APDU结构
一个完整的命令APDU,由两部分组成:头部(Header)和体部(Body)。头部是必须的,体部可选。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| CLA | 1 | 指令类别,比如00表示ISO标准指令,80表示私有指令 |
| INS | 1 | 指令码,比如A4表示SELECT,B0表示READ BINARY |
| P1 | 1 | 参数1,具体含义由INS决定 |
| P2 | 1 | 参数2,与P1配合使用 |
| Lc | 0或1或3 | 命令数据长度(Data字段的字节数) |
| Data | 0~65535 | 命令携带的数据 |
| Le | 0或1或2或3 | 期望响应的最大数据长度 |
嗯,这里要注意Lc和Le的编码方式。在短APDU(Short APDU)中,Lc和Le各占1个字节,所以数据长度最多255字节。在扩展APDU(Extended APDU)中,Lc和Le各占3个字节,数据长度可达65535字节。不过,我接触的大多数JavaCard应用,用的都是短APDU。毕竟卡片资源有限,一次传太多数据也不现实。
核心要点:命令APDU的头部(CLA+INS+P1+P2)是必须的,体部(Lc+Data+Le)可选。如果Lc=0,表示没有Data字段;如果Le=0,表示不期望返回数据。
2.1.2 响应APDU结构
响应APDU就简单多了,只有两个字段:体部(Body)和尾部(Trailer)。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| Data | 0~65535 | 响应数据,可选 |
| SW1 | 1 | 状态字高位 |
| SW2 | 1 | 状态字低位 |
SW1和SW2合起来叫状态字(Status Word),表示命令执行的结果。比如,90 00表示成功,6A 82表示文件未找到,69 85表示安全状态不满足(没通过认证)。
我的经验:调试卡片时,我最先看的就是SW1 SW2。如果返回的不是9000,那肯定有问题。我曾经花了一整天,就为了查一个“6A 82”的错误,最后发现是SELECT指令的AID写错了。嗯,这种低级错误,犯过一次就记住了。
2.2 命令APDU与响应APDU的交互流程
咱们用一张图来展示这个交互过程。这张图我画了很多次,每次给新人讲APDU时都会用到。
你看,流程其实很简单:终端发命令APDU给卡片,卡片处理,然后返回响应APDU。但这里有个坑:卡片处理命令是需要时间的。如果终端发完命令后,卡片还没处理完,终端就等着。等多久?这取决于卡片的具体实现。我遇到过一些卡片,处理复杂命令需要几百毫秒,如果终端超时设置得太短,就会误判为通信失败。
2.3 常见APDU指令分析
咱们来看几个实际项目中常用的APDU指令。这些指令,你以后做JavaCard开发时,几乎天天都会用到。
2.3.1 SELECT指令(INS = 0xA4)
SELECT指令用于选择应用或文件。这是卡片上电后,终端发送的第一条指令。说白了,就是告诉卡片:“我要跟哪个应用对话?”
// 选择AID为"A000000003000000"的应用
CLA = 0x00
INS = 0xA4
P1 = 0x04 // 按名称选择
P2 = 0x00 // 第一个或唯一一个
Lc = 0x08 // AID长度8字节
Data = A0 00 00 00 03 00 00 00 // AID
Le = 0x00 // 不期望返回数据
P1参数的值很关键:
0x04:按名称选择(最常用)0x00:按文件ID选择0x08:按路径选择
注意:SELECT指令执行成功后,卡片内部会切换当前上下文。后续的所有指令,都是针对当前选中的应用或文件。如果你忘了SELECT,直接发其他指令,卡片会返回6A 82(文件未找到)或69 85(安全状态不满足)。
2.3.2 READ BINARY指令(INS = 0xB0)
READ BINARY用于读取二进制文件中的数据。我经常用它来读取卡片上的证书或公钥。
// 从偏移量0x0000开始,读取16字节数据
CLA = 0x00
INS = 0xB0
P1 = 0x00 // 偏移量高位
P2 = 0x00 // 偏移量低位
Le = 0x10 // 期望读取16字节
这里P1和P2组合成16位的偏移量。比如,要读取偏移量0x0100处的数据,P1=0x01,P2=0x00。
2.3.3 UPDATE BINARY指令(INS = 0xD6)
UPDATE BINARY用于写入二进制文件。注意,这个指令通常需要安全认证,否则卡片会拒绝执行。
// 从偏移量0x0000开始,写入"Hello"(5字节)
CLA = 0x00
INS = 0xD6
P1 = 0x00
P2 = 0x00
Lc = 0x05
Data = 48 65 6C 6C 6F // "Hello"的ASCII码
Le = 0x00
避坑指南:我曾经在项目中遇到一个问题:UPDATE BINARY执行成功了,但读出来的数据却是错的。排查了半天,发现是偏移量算错了。写入时用的是绝对偏移量,但卡片内部的文件结构是分段的。所以,写数据前一定要搞清楚文件的结构,特别是那些有多个EF(基本文件)的应用。
2.3.4 VERIFY指令(INS = 0x20)
VERIFY用于验证PIN码或口令。这是JavaCard安全机制的核心之一。
// 验证PIN码"1234"
CLA = 0x00
INS = 0x20
P1 = 0x00 // 参考控制参数
P2 = 0x81 // PIN ID(0x81表示第一个PIN)
Lc = 0x04
Data = 31 32 33 34 // "1234"的ASCII码
Le = 0x00
P2参数指定要验证的PIN ID。常见的PIN ID有:
0x81:全局PIN(通常用于用户认证)0x82:应用PIN(用于特定应用)0x83:管理员PIN(用于卡片管理)
如果PIN验证失败,卡片会返回63 Cx,其中x表示剩余的重试次数。比如63 C2表示还剩2次机会。如果重试次数用完,PIN会被锁定,卡片返回69 83(认证方法被锁定)。
2.3.5 GET CHALLENGE指令(INS = 0x84)
GET CHALLENGE用于获取卡片生成的随机数。这个随机数通常用于后续的安全认证,比如内部认证(INTERNAL AUTHENTICATE)或外部认证(EXTERNAL AUTHENTICATE)。
// 获取8字节随机数
CLA = 0x00
INS = 0x84
P1 = 0x00
P2 = 0x00
Le = 0x08 // 期望获取8字节
卡片返回的随机数,每次都不一样。这就是“挑战-响应”认证的基础。终端拿到随机数后,用密钥加密,再发回给卡片验证。卡片验证通过,才允许执行后续的敏感操作。
2.4 状态字速查表
最后,我整理了一份常用的状态字速查表。你调试卡片时,对照这个表,能快速定位问题。
| SW1 SW2 | 含义 | 常见原因 |
|---|---|---|
| 90 00 | 执行成功 | 一切正常 |
| 61 xx | 有数据可读,xx为剩余字节数 | 需要再用GET RESPONSE指令读取 |
| 62 00 | 警告:无信息 | 操作成功,但可能有副作用 |
| 63 Cx | PIN验证失败,x为剩余次数 | PIN码错误 |
| 65 81 | 内存写入失败 | EEPROM写入错误 |
| 67 00 | Lc或Le长度错误 | APDU格式不对 |
| 69 82 | 安全状态不满足 | 未通过认证 |
| 69 83 | 认证方法被锁定 | PIN重试次数用完 |
| 69 85 | 使用条件不满足 | 未选择应用或文件 |
| 6A 82 | 文件未找到 | AID或文件ID错误 |
| 6A 86 | P1 P2参数错误 | 参数值不在有效范围内 |
| 6D 00 | INS指令不支持 | 卡片不支持该指令 |
| 6E 00 | CLA类别不支持 | CLA值错误 |
嗯,这张表你最好保存下来。我刚开始做JavaCard开发时,就是靠这张表活下来的。每次看到6A 82,我就知道该检查AID了;看到69 85,就知道忘了SELECT。时间长了,这些状态码就刻在脑子里了。
好了,APDU的基础知识就讲到这里。记住,APDU是JavaCard通信的基石。你写的每一个Applet,本质上都是在处理APDU。把APDU搞懂了,后面的路就好走了。
公众号:蓝海资料掘金营,微信deep3321