3. 与Slot管理
和Slot,是PKCS#11里最基础的两个概念。你想想看,你把USB Key插入电脑,那个USB口就是Slot,Key本身就是一个。没错,一个Slot可以是一个物理插槽,也可以是一个虚拟的逻辑槽位。则是插在上面的密码设备——HSM、智能卡、软件都算。
我个人习惯把这三兄弟——C_GetSlotList、C_GetTokenInfo、C_GetMechanismList——叫做“环境摸底三件套”。做审计时,90%的接口调用合规问题都出在这里:要么没查信息就直接用,要么查了但忽略关键字段。
下面我们一个一个讲,顺便带点我踩过的坑。
3.1 C_GetSlotList:先搞清楚有几个插槽
这个函数的签名很简单:
CK_RV C_GetSlotList(
CK_BBOOL tokenPresent, // 是否只返回有的Slot
CK_SLOT_ID_PTR pSlotList, // 输出缓冲区
CK_ULONG_PTR pulCount // 输入:缓冲区大小,输出:实际Slot数量
);
第一次调用时,pSlotList传NULL,pulCount传一个地址。函数会把可用Slot的数量写进pulCount。然后你根据这个数量分配内存,再调用一次。
参数tokenPresent是个坑。设为CK_TRUE时,只返回插着且可用的Slot。设为CK_FALSE则返回所有物理Slot。我记得有一次审计中发现,某个应用写死传CK_TRUE,结果在没插的测试环境里总是返回0个Slot,导致程序直接崩溃。嗯,这里要注意——生产环境可能没问题,但测试和灾备场景下容易翻车。
3.2 C_GetTokenInfo:看的“户口本”
拿到Slot ID后,下一步就是查的信息。这个函数返回一个CK__INFO结构体,里面包含制造商、型号、序列号、容量、各种标志位等。
| 字段 | 说明 | 审计关注点 |
|---|---|---|
label | 32字节的标签 | 是否为空?是否可读? |
manufacturerID | 厂商标识 | 与硬件厂商是否一致? |
model | 设备型号 | 是否有版本依赖? |
serialNumber | 序列号 | 能否用做审计追踪? |
flags | 状态标志 | 是否已登录?是否可写? |
ulMaxSessionCount | 最大会话数 | 是否超限?0表示无限制 |
ulTotalPublicMemory | 公有区总大小 | 能否存放所需证书? |
审计时我基本必看flags字段。比如CKF__INITIALIZED标志如果没置位,表示这个还没初始化,根本没法用。另一个是CKF_LOGIN_REQUIRED,有些即使初始化了,也要先登录才能操作。
上个月我审一个金融项目,代码里只取了label和model来做显示,完全没检查flags。结果生产上有个损坏了,CKF__INITIALIZED消失,应用照样老老实实去创建会话,然后返回各种诡异错误。那不是浪费排查时间嘛。
3.3 C_GetMechanismList:摸清的能力
这个函数返回支持的密码学机制列表。RSA、ECC、AES、SHA……全在这个列表里。用法和C_GetSlotList类似,也是两遍调用。
CK_ULONG ulCount;
C_GetMechanismList(slotID, NULL, &ulCount); // 第一遍:获取数量
CK_MECHANISM_TYPE_PTR pMechList = malloc(ulCount * sizeof(CK_MECHANISM_TYPE));
C_GetMechanismList(slotID, pMechList, &ulCount); // 第二遍:获取列表
这里有一个我亲身踩过的坑:返回的机制类型并不是固定的。同一个型号,固件版本不同,支持的机制可能有差异。我记得有一次在审计日志里发现,前置版本支持CKM_RSA_PKCS_OAEP,升级后就没了,而应用代码里硬编码了这个机制ID。结果加解密失败,业务中断两小时。
另外,机制类型是一个CK_ULONG,常见值如下表:
| 机制宏 | 值 | 用途 |
|---|---|---|
CKM_RSA_PKCS_KEY_PAIR_GEN | 0x00000000 | RSA密钥对生成 |
CKM_RSA_PKCS | 0x00000001 | RSA PKCS#1 v1.5签名/加密 |
CKM_AES_KEY_GEN | 0x00001080 | AES密钥生成 |
CKM_ECDSA | 0x00001041 | ECDSA签名 |
3.4 完整调用流程与关系
这三个函数通常放在一起用,顺序不能乱。下面这张图就是整个流程:
3.5 审计要点与常见问题
做合规审计时,我会重点检查下面几个方面:
- 内存管理:两遍调用模式是否正确?有没有先传NULL,分配后再传?有些人直接分配一个固定大小的数组(比如100个slot),万一数量超过100就崩了。
- 错误处理:每次调用后检查返回值了吗?
CKR__NOT_PRESENT、CKR_BUFFER_TOO_SMALL这些错误码是否被妥善处理? - 信息完整度:拿到
CK__INFO后,是否检查了flags关键位?我建议至少要检查CKF__INITIALIZED和CKF_LOGIN_REQUIRED。 - 机制列表缓存:不允许缓存机制列表超过一个会话生命周期。每次新会话都应该重新查询。
- 并发安全:多线程环境下,调用
C_GetSlotList时,其他线程可能正在插拔导致列表变化。虽然PKCS#11规范说这个函数是线程安全的,但返回的数据可能已经过时。
说到这,我想起一个真实的审计案例。某支付公司使用C_GetTokenInfo后,直接拿serialNumber当数据库主键用。结果厂商更换后序列号前几位变了,导致一大堆数据关联不上。嗯,这里提醒一下——序列号只是标识,别当业务键。最好把manufacturerID、model、serialNumber三者联合起来做唯一性判断。
另外,C_GetMechanismList返回的机制类型列表中,可能包含重复项吗?规范没说不能有重复,但现网设备我没遇到过。不过我还是在代码里加了个去重逻辑——反正不费事。
好了,关于“环境摸底三件套”就讲这么多。这几步看似简单,一旦出问题往往很难排查。你只要记住:先查数量、再取数据、检查标志、不要缓存,基本就能避开大多数坑。