4、APDU通信详解
APDU,全称是Application Protocol Data Unit。说白了,就是卡片和读卡器之间对话的语言。我刚开始接触JCOP时,觉得这东西挺唬人,一堆十六进制数。后来发现,搞懂它其实就两件事:命令怎么写,响应怎么读。
这一章,咱们就把APDU的底裤扒干净。从命令结构到响应结构,从T=0到T=1,再到我这些年攒下的调试技巧,一次性讲透。
4.1 命令APDU结构
命令APDU,就是终端发给卡片的消息。它由两部分组成:头部(必选)和体部(可选)。
头部固定4个字节:
- CLA (Class):指令类别。比如00是标准ISO指令,80是私有指令。我见过有人把CLA写成0x80,结果卡片死活不认——因为卡片只支持00类。
- INS (Instruction):指令码。比如0xA4是SELECT,0xB0是READ BINARY。
- P1, P2 (Parameter 1 & 2):参数。具体含义看指令定义。比如SELECT时,P1=0x04表示按文件名选择。
体部最多3个字段:
- Lc (Length of command data):命令数据的长度。1个字节(短格式)或3个字节(扩展格式)。
- Data:实际要发送的数据。长度由Lc决定。
- Le (Length of expected response data):期望响应的最大长度。0x00表示期望最多256字节。
一个典型的SELECT命令:
00 A4 04 00 07 A0 00 00 00 03 00 00 00
分解一下:CLA=00, INS=A4, P1=04, P2=00, Lc=07, Data=7字节AID, Le=00(期望最多256字节响应)。
嗯,这里要注意:Lc和Le不会同时出现。有数据发就带Lc,想收数据就带Le。如果既发又收,那就Lc和Le都带。我见过新手把Lc和Le写反,卡片直接返回6A86(参数错误)。
4.2 响应APDU结构
响应APDU,就是卡片回给终端的话。结构更简单:数据体(可选)+ 状态字(必选)。
状态字(SW1 SW2):
- 两个字节,表示命令执行结果。
- 90 00:成功。看到这个就放心了。
- 6A 82:文件未找到。我调SELECT时经常遇到。
- 69 85:安全状态不满足。权限不够,先认证吧。
- 67 00:Lc长度错误。数据长度不对。
数据体:
- 可选。如果命令有Le,且卡片有数据返回,就放在状态字前面。
- 长度由命令中的Le决定,但实际返回可能更短。
我的调试习惯:每次收到响应,先看SW1 SW2。如果不是90 00,就别急着看数据。先把状态字查明白,问题就解决了一半。
4.3 T=0与T=1协议区别
T=0和T=1,是ISO 7816-3定义的两种传输协议。说白了,就是数据怎么在线上跑。
| 特性 | T=0(字符传输) | T=1(块传输) |
|---|---|---|
| 传输单位 | 单个字节 | 数据块(含头、体、尾) |
| 错误检测 | 奇偶校验(每个字节) | CRC/LRC(整个块) |
| 重传机制 | 简单重发 | 块序号+重传请求 |
| 典型应用 | 老式SIM卡、存储卡 | Java Card、高端安全芯片 |
| 传输效率 | 低(每个字节都要握手) | 高(批量传输) |
T=0协议的特点:
- 每个字节传输后,接收方要回一个ACK(0x90)或NAK(0x60)。
- 如果收到NAK,发送方重传该字节。
- 命令APDU的Le字段在T=0下有点特殊:如果Le=0x00,表示期望256字节;如果Le=0x01~0xFF,就是实际期望长度。
T=1协议的特点:
- 数据被封装成块(Block)。每个块有块头(1字节NAD + 1字节PCB + 1字节LEN)、数据体、和1~2字节CRC。
- 块序号(0或1)用于检测重复或丢失。
- 支持链式传输(大块数据分多次发)。
我个人习惯用T=1。为什么?因为T=0的奇偶校验在噪声环境下太脆弱了。我在项目中遇到过,读卡器离卡片稍微远一点,T=0就疯狂重传,效率惨不忍睹。换成T=1后,CRC校验加上块重传,稳得很。
避坑指南:我曾经在T=0协议下,Le字段写0x00想收256字节,结果卡片返回了0字节数据。后来查规范才发现:T=0下Le=0x00表示「我不收数据」,而不是「收256字节」。这个坑,我记了好几年。
4.4 APDU调试技巧
调试APDU,说白了就是跟十六进制打交道。我总结了几个实用技巧,希望能帮你少走弯路。
技巧1:用日志记录每一次交互
我习惯在代码里加一个APDU日志模块。每次发送命令和接收响应,都原样记录下来。格式很简单:
[发送] 00 A4 04 00 07 A0 00 00 00 03 00 00 00
[接收] 6F 1A 84 07 A0 00 00 00 03 00 00 A5 0F 9F 38 0C 9F 66 04 9F 02 06 9F 03 06 90 00
这样一旦出问题,翻日志就能定位。我靠这招解决过不少疑难杂症。
技巧2:善用APDU调试工具
别只用代码调。用工具先验证命令对不对。我推荐几个:
- GPJ (GlobalPlatform Java Tool):开源,支持脚本,适合批量测试。
- JCOP Tools:NXP官方工具,调试JCOP卡首选。
- PC/SC 调试器:Windows下用,能抓取底层APDU。
技巧3:从状态字反推问题
状态字是卡片给你的第一线索。我整理了一个速查表:
| SW1 SW2 | 含义 | 常见原因 |
|---|---|---|
| 90 00 | 成功 | 一切正常 |
| 6A 82 | 文件未找到 | AID或文件路径写错了 |
| 69 85 | 安全状态不满足 | 没认证就操作安全对象 |
| 67 00 | Lc长度错误 | 数据长度跟指令不匹配 |
| 6D 00 | INS不支持 | 指令码写错了 |
| 6E 00 | CLA不支持 | 类别字节不对 |
技巧4:用脚本自动化测试
手动敲APDU太慢了。我写过一个Python脚本,批量发送命令并验证响应:
import smartcard.System
readers = smartcard.System.readers()
reader = readers[0]
connection = reader.createConnection()
connection.connect()
# 发送SELECT命令
cmd = [0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00]
data, sw1, sw2 = connection.transmit(cmd)
print(f"SW: {sw1:02X} {sw2:02X}")
print(f"Data: {bytes(data).hex()}")
这个脚本我用了好几年,改改就能适配不同场景。
技巧5:注意时序和重试
卡片不是电脑,处理需要时间。我建议:
- 命令之间加至少10ms延迟。
- 如果收到6A 82(文件未找到),别急着放弃。可能是卡片还没准备好,重试一次。
- 如果收到69 85(安全状态不满足),先检查认证流程。我遇到过认证顺序搞反的情况。
核心总结:APDU调试,说白了就是「发命令、看状态、查数据」三步走。状态字是卡片给你的第一线索,别忽略它。工具用起来,日志记起来,问题就好解决了。