6.3 通信协议实战:从代码到波形的“变身”
一个场景:你的代码终于调通了,能够向HSM发送命令并获得响应。但当你用逻辑分析仪观察波形时,发现发送的数据和代码里构造的数据不完全一样——多了一些字节,少了另一些字节。为什么?
一个问题浮出水面:数据从代码到HSM芯片,经历了怎样的“变身”?什么是帧格式?什么是APDU?它们之间是什么关系?
一个隐喻:这像寄快递——你写的信(APDU命令)需要装进信封(帧),贴上地址标签(PIB),交给快递员(I2C/SPI司机),才能送达目的地(HSM)。每一层都有自己的格式要求。
通信协议层次
Host与HSM之间的通信采用三层协议结构:
通信协议层次
┌─────────────────────────────────────┐
│ 应用层:APDU(应用协议数据单元) │ ← 业务命令
│ 格式:CLA | INS | P1 | P2 | Lc | Data | Le
├─────────────────────────────────────┤
│ 数据链路层:帧(Frame) │ ← 传输容器
│ 格式:PIB | LEN | DATA | CRC
├─────────────────────────────────────┤
│ 物理层:I2C/SPI │ ← 硬件传输
│ I2C:地址 + 读/写 + 数据
│ SPI:片选 + 全双工传输
└─────────────────────────────────────┘
每一层协议都有自己的职责:
- 应用层APDU:定义业务命令的内容,如“生成密钥”、“验证PIN”
- 数据链路层帧:封装APDU,添加长度、校验等传输必要信息
- 物理层I2C/SPI:执行实际的电信号传输
APDU协议详解
APDU(Application Protocol Data Unit)是智能卡通信的标准格式,源自ISO 7816-4规范。HSM沿用了这一格式。
命令APDU结构
命令APDU结构
┌────┬────┬────┬────┬────┬─────────┬────┐
│CLA │INS │ P1 │ P2 │ Lc │ Data │ Le │
├────┼────┼────┼────┼────┼─────────┼────┤
│ 1 │ 1 │ 1 │ 1 │变长│ 变长 │变长│
│字节│字节│字节│字节│ │ 字节 │ │
└────┴────┴────┴────┴────┴─────────┴────┘
各字段含义:
| 字段 | 长度 | 说明 |
|---|---|---|
| CLA | 1字节 | 指令类别。0x00表示标准命令,0x80表示厂商自定义命令 |
| INS | 1字节 | 指令代码。如0xA4表示SELECT,0x84表示GET CHALLENGE |
| P1 | 1字节 | 参数1,为INS提供更详细参数 |
| P2 | 1字节 | 参数2,为INS提供更详细参数 |
| Lc | 变长 | 命令数据长度,表示Data字段的字节数 |
| Data | 变长 | 命令数据,由Lc指定长度 |
| Le | 变长 | 期望响应数据的最大长度 |
响应APDU结构
响应APDU结构
┌─────────┬────┬────┐
│ Data │SW1 │SW2 │
├─────────┼────┼────┤
│ 变长 │ 1 │ 1 │
│ 字节 │字节│字节│
└─────────┴────┴────┘
各字段含义:
| 字段 | 长度 | 说明 |
|---|---|---|
| Data | 变长 | 响应数据,由命令APDU中的Le指定最大长度 |
| SW1 | 1字节 | 状态字1,与SW2组合表示执行结果 |
| SW2 | 1字节 | 状态字2 |
常见状态字
| SW1-SW2 | 含义 | 说明 |
|---|---|---|
| 0x9000 | 成功 | 命令执行成功 |
| 0x6982 | 安全条件不满足 | 需要先验证PIN才能执行 |
| 0x6A82 | 文件未找到 | 指定的文件不存在 |
| 0x6A86 | 参数不正确 | P1或P2参数无效 |
| 0x6D00 | 指令不支持 | INS代码不被HSM支持 |
APDU命令示例
获取随机数:
发送:00 84 00 00 08
解析:
CLA = 0x00 // 标准命令
INS = 0x84 // GET CHALLENGE(获取随机数)
P1 = 0x00 // 参数1
P2 = 0x00 // 参数2
Le = 0x08 // 期望8字节随机数
响应:61 65 5A DC E1 61 C5 73 90 00
解析:
Data = 61 65 5A DC E1 61 C5 73 // 8字节随机数
SW1 = 0x90 // 成功
SW2 = 0x00
选择文件:
发送:00 A4 00 00 02 00 06
解析:
CLA = 0x00 // 标准命令
INS = 0xA4 // SELECT FILE(选择文件)
P1 = 0x00 // 参数1
P2 = 0x00 // 参数2
Lc = 0x02 // 数据长度为2字节
Data = 00 06 // 文件ID = 0x0006
响应:90 00
解析:
Data = 无 // 选择文件不需要返回数据
SW1 = 0x90 // 成功
SW2 = 0x00
帧格式详解
APDU不能直接通过I2C/SPI发送,需要封装成帧。帧格式定义了数据链路层的传输单元。
帧结构
帧结构
┌────┬─────────┬─────────┬────┐
│PIB │ LEN │ DATA │CRC │
├────┼─────────┼─────────┼────┤
│ 1 │ 2 │ 变长 │ 2 │
│字节│ 字节 │ 字节 │字节│
└────┴─────────┴─────────┴────┘
各字段含义:
| 字段 | 长度 | 说明 |
|---|---|---|
| PIB | 1字节 | 协议控制字节,定义帧类型 |
| LEN | 2字节 | 数据长度,DATA字段的有效字节数 |
| DATA | 变长 | 有效数据,即APDU内容 |
| CRC | 2字节 | 校验码,用于检测传输错误 |
PIB帧类型
PIB(Protocol Control Byte)最高两位定义帧类型:
| PIB高两位 | 类型 | 说明 |
|---|---|---|
| 00xxxxxx | I帧 | 信息帧,承载APDU数据 |
| 01xxxxxx | R帧 | 响应帧,确认接收或请求重发 |
| 10xxxxxx | S帧 | 状态帧,传输控制信息 |
帧示例
发送获取随机数命令,完整帧为:
发送帧:20 00 05 00 84 00 00 08 D3 42
解析:
PIB = 0x20 // I帧,序号0
LEN = 0x0005 // 数据长度5字节
DATA = 00 84 00 00 08 // APDU命令
CRC = 0xD342 // 校验码
响应帧:20 00 0A 61 65 5A DC E1 61 C5 73 90 00 XX XX
解析:
PIB = 0x20 // I帧
LEN = 0x000A // 数据长度10字节(8字节随机数 + 2字节状态)
DATA = 61 65 5A DC E1 61 C5 73 90 00 // APDU响应
CRC = 校验码
帧组包过程
SDK内部的组包流程:
组包流程
用户调用 hsm_get_random()
↓
构造APDU:00 84 00 00 08
↓
调用 tpdu_pack() 组帧
↓
添加 PIB = 0x20
添加 LEN = 0x0005
DATA = APDU内容
计算 CRC
↓
完整帧:20 00 05 00 84 00 00 08 D3 42
↓
调用 driver_spi_transceive() 发送
↓
SPI硬件传输
CRC计算
CRC使用CRC-16算法,确保数据传输的可靠性:
uint16_t calculate_crc(uint8_t *data, uint32_t len) {
uint16_t crc = 0xFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
I2C vs SPI对比
HSM支持两种物理层协议:I2C和SPI。它们各有特点。
I2C特点
I2C通信模型
Host(主设备) HSM(从设备)
│ │
│──── SCL(时钟线)───────│
│ │
│──── SDA(数据线)───────│
│ │
│ START → 地址 → 数据 → STOP
优点:
- 只需要2根线(SCL、SDA)
- 硬件简单,成本低
- 支持多从设备共享总线
缺点:
- 速率较低(标准模式100kHz,快速模式400kHz)
- 主设备控制时序,从设备被动响应
- 某些主设备硬件有限制,无法一次接收长帧
SPI特点
SPI通信模型
Host(主设备) HSM(从设备)
│ │
│──── SCK(时钟线)───────│
│ │
│──── MOSI(主出从入)────│
│ │
│──── MISO(主入从出)────│
│ │
│──── CS(片选线)────────│
│ │
│ 全双工同步传输
优点:
- 全双工传输,效率高
- 速率可达数MHz
- 主设备完全控制传输过程
- 无硬件接收长度限制
缺点:
- 需要4根线(SCK、MOSI、MISO、CS)
- 每个从设备需要独立的片选线
实际案例:I2C硬件限制
在一个车载平台上,我们遇到了I2C接收长帧的问题:
问题描述
发送帧:请求获取密钥信息(帧长度519字节)
↓
I2C主设备开始接收
↓
接收约250字节后,主设备硬件自动STOP
↓
主设备再次START
↓
从设备(HSM)从头开始重新发送
↓
主设备收到的数据:
20 00 08 01 20 00 08 01 20 00 08 01 ...
(数据重复,CRC错误)
原因分析:某些I2C控制器在处理长数据读取时,会将一次事务拆分成多个小事务。每次拆分后释放总线(STOP),再重新获取总线(START)。HSM从设备收到新的START后,会从头开始发送数据,导致接收端数据错乱。
解决方案:切换到SPI协议。SPI没有这种硬件限制,可以一次传输完整帧。
SPI配置示例
设备文件:/dev/spidev1.1
模式:Mode 1(CPOL=0, CPHA=1)
位宽:8位
速率:1MHz
SPI验证代码
#include <linux/spi/spidev.h>
int spi_transfer(int fd, uint8_t *tx_buf, uint8_t *rx_buf, uint32_t len) {
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx_buf,
.rx_buf = (unsigned long)rx_buf,
.len = len,
.delay_usecs = 0,
.bits_per_word = 8,
.speed_hz = 1000000,
};
return ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
}
自定义APDU发送
SDK提供了hsm_transceive函数,允许用户发送自定义APDU:
uint8_t in_buf[5] = {0x00, 0x84, 0x00, 0x00, 0x08}; // 获取8字节随机数
uint8_t out_buf[32];
uint32_t out_buf_len;
ret = hsm_transceive(in_buf, 5, out_buf, &out_buf_len);
if (ret == HSM_OK) {
// out_buf包含响应数据 + SW1/SW2
// out_buf_len为响应总长度
}
这给了用户最大的灵活性,可以发送SDK API未直接支持的命令。
小结
通信协议像信件投递系统:
- APDU是信件内容,写着你要HSM做的事
- 帧是信封,保护信件并添加邮递必要信息(长度、校验)
- I2C/SPI是邮递员,负责把信封送到目的地
理解每一层协议的职责,是调试通信问题的关键。当你看到波形分析仪上的数据时,就能一层层解析:这是什么帧?承载什么APDU?命令是什么?响应是什么?
下一节,我们将分享真实踩坑案例——看调试实战经验。
【下集预告】
SDK也会有bug?
段错误怎么定位?类型转换问题怎么发现?
I2C硬件限制怎么解决?
下一节,踩坑与修复。