第三章:APDU协议深度解析
APDU协议,说白了就是智能卡和终端设备之间对话的“语法规则”。我刚开始接触JCOP时,觉得这东西不就是几个字节来回传吗?后来踩了不少坑才明白——细节全在协议里。
这一章我们彻底吃透APDU。从命令格式到响应格式,从Case分类到T=0/T=1的底层差异,我会把我在项目中遇到的那些“诡异问题”一并讲清楚。
3.1 APDU命令格式:你发给卡片的每一句话
APDU命令由两部分组成:头部(Header)和主体(Body)。头部固定4个字节,主体可选。
| 字段 | 长度 | 说明 |
|---|---|---|
| CLA | 1字节 | 指令类别,比如00是标准ISO指令,80是私有指令 |
| INS | 1字节 | 指令码,比如A4是SELECT,B0是READ BINARY |
| P1 | 1字节 | 参数1,具体含义由指令定义 |
| P2 | 1字节 | 参数2,常与P1配合使用 |
| Lc | 0或1或3字节 | 命令体数据长度(可选) |
| Data | Lc字节 | 命令体数据(可选) |
| Le | 0或1或2字节 | 期望响应数据长度(可选) |
关键点:CLA和INS的组合决定了卡片要执行什么操作。我见过有人把CLA写成0x80去发标准SELECT指令,结果卡片一直返回6E00(不支持)——其实CLA错了。
举个例子,一个典型的SELECT命令:
00 A4 04 00 08 A0 00 00 00 03 00 00 00 00
这里CLA=00,INS=A4,P1=04(按名称选择),P2=00,Lc=08,后面8字节是AID。Le没写,表示不期望返回数据。
3.2 APDU响应格式:卡片给你的回答
响应格式更简单:数据体(可选)+ 状态字(SW1 SW2)。状态字固定2字节,是判断命令是否成功的唯一标准。
| 字段 | 长度 | 说明 |
|---|---|---|
| Data | 可变 | 响应数据(可选) |
| SW1 | 1字节 | 状态高位 |
| SW2 | 1字节 | 状态低位 |
常见的状态字:
- 90 00:成功。看到这个就放心了。
- 6A 82:文件未找到。我调JCOP时经常遇到,多半是AID写错了。
- 69 85:安全条件不满足。嗯,权限不够。
- 6E 00:CLA不支持。指令类别不对。
- 6D 00:INS不支持。指令码不对。
个人经验:调试时我习惯先打印SW1 SW2,再去看数据。状态字能告诉你80%的问题。我曾经花了一下午查一个数据解析bug,最后发现是卡片返回了69 85——我忘了先验证PIN。
3.3 Case 1-4指令分类:四种对话模式
APDU命令根据是否有数据发送和接收,分为4种Case。理解这个分类,你才能正确构造命令。
| Case | 命令格式 | 说明 | 示例 |
|---|---|---|---|
| Case 1 | CLA INS P1 P2 | 无数据发送,无数据接收 | GET CHALLENGE(部分实现) |
| Case 2 | CLA INS P1 P2 Le | 无数据发送,期望接收数据 | READ BINARY |
| Case 3 | CLA INS P1 P2 Lc Data | 发送数据,不期望接收 | WRITE BINARY(不带返回) |
| Case 4 | CLA INS P1 P2 Lc Data Le | 发送数据,期望接收 | VERIFY PIN + 返回尝试次数 |
为什么会这样分?说白了就是通信效率。Case 1最轻量,Case 4最重。我见过有人把Case 2的Le写成0x00,结果卡片返回了256字节——其实0x00在T=1协议里表示期望最多256字节,但在T=0里含义不同。
避坑指南:我曾经在Case 4命令里忘了写Le,结果卡片一直不返回数据。查了半天才发现——JCOP的Java Card实现里,如果Le缺失,卡片会认为你不需要响应数据,直接返回SW1 SW2。
3.4 T=0与T=1协议区别:底层传输的两种哲学
T=0和T=1是ISO 7816-3定义的两种传输协议。它们决定了APDU在物理层怎么传。嗯,这里要注意——它们不是APDU本身,而是APDU的“运输方式”。
| 特性 | T=0(字符协议) | T=1(块协议) |
|---|---|---|
| 传输单位 | 单个字符(字节) | 数据块(可变长度) |
| 错误检测 | 奇偶校验(每个字节) | CRC/LRC(整个块) |
| 重传机制 | 简单重发字符 | 块重传 + 序号管理 |
| APDU适配 | 需要过程字节(如INS=0xC0) | 直接映射APDU |
| 典型应用 | 老式SIM卡、存储卡 | Java Card、高端安全芯片 |
T=0协议:每个字节独立传输,卡片收到一个字节就处理一个。如果卡片需要额外时间,会发一个过程字节(比如0x60表示“我还在忙”)。我调过一款老式SIM卡,用的就是T=0,每次发命令都要手动处理过程字节,很麻烦。
T=1协议:把整个APDU打包成一个块,一次性发送。块有头(NAD、PCB、LEN)、数据体、尾(CRC)。卡片收到完整块后才解析。JCOP默认用T=1,因为Java Card的APDU处理天然适合块传输。
核心差异:T=0是“字节流”,T=1是“报文”。T=0的每个字节都可能改变通信状态,T=1的块边界清晰。你想想看,如果APDU有256字节数据,T=0要来回握手几百次,T=1一次搞定。
我举个例子。在T=0下发一个Case 4命令(有发送有接收),流程是这样的:
- 终端发CLA INS P1 P2
- 卡片回过程字节(比如0x60)
- 终端发Lc
- 卡片回过程字节
- 终端发Data
- 卡片回过程字节(比如0x61 xx,表示有xx字节要返回)
- 终端发INS=0xC0(GET RESPONSE)
- 卡片返回数据 + SW1 SW2
而在T=1下,一步到位:
终端 -> 卡片: [块头] CLA INS P1 P2 Lc Data Le [CRC]
卡片 -> 终端: [块头] Data SW1 SW2 [CRC]
我的建议:做JCOP开发,优先用T=1。T=0的握手逻辑太容易出错了。我曾经在T=0下忘记处理0x61过程字节,结果卡片一直不返回数据——其实它在等我发GET RESPONSE。
3.5 知识体系总览
下面这张图把APDU协议的核心逻辑串起来了。从命令构造到响应解析,从Case分类到协议选择,每一步都有坑,但也都有规律可循。
这张图把APDU协议拆成了三层:命令/响应格式是基础,Case分类决定了通信模式,T=0/T=1决定了底层传输方式。我个人习惯把这三层分开理解,调试时逐层排查问题。
最后提醒:别把Case分类和T=0/T=1搞混。Case分类是APDU层面的,T=0/T=1是传输层面的。我见过有人问“Case 4是不是只能用T=1?”——不是的,Case 4在T=0下也能跑,只是要多几个握手步骤。
好了,APDU协议的核心就这些。下一章我们进入实战——用Python写一个APDU调试工具,把今天讲的理论全用上。
公众号:蓝海资料掘金营,微信deep3321