第二章 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时都会用到。

终端(读卡器) JavaCard 命令APDU CLA INS P1 P2 Lc Data Le 响应APDU Data SW1 SW2 卡片处理命令 图2-1: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