2. PKCS#11 体系结构:令牌、会话、对象
进入实战之前,先把 PKCS#11 最核心的三个概念理清楚:令牌、会话、对象。
说白了,这三样东西就是你跟密码设备打交道的全部入口。我刚开始接触 PKCS#11 时,总觉得文档绕来绕去,后来自己画了一张关系图,才一下通透。今天就带你从这张图入手。
2.1 令牌()—— 逻辑上的密码设备
令牌是什么?你可以把它理解成逻辑上的密码设备。它不一定是真实的 USB Key 或 HSM,也可能是一个软件模拟的虚拟密码容器。
令牌的核心属性包括:
| 属性 | 含义 | 典型值 |
|---|---|---|
CK__INFO.label | 令牌标签,标识设备 | "MyHSM" |
CK__INFO.flags | 能力标志位,比如登录、加密、RNG | 0x00000401 |
CK__INFO.ulSessionCount | 当前打开的会话数 | 0 ~ maxSessionCount |
CK__INFO.ulMaxSessionCount | 最大并发会话数 | 65535 或 CK_EFFECTIVELY_INFINITE |
CK__INFO.hardwareVersion | 硬件版本号 | 2.00 |
我记得有个项目,用 PKCS#11 和 HSM,结果令牌老是断开。一开始我以为是代码问题,查了一圈才发现是令牌的会话计数超了。驱动默认只支持 15 个并发会话,多开一个就挂。所以,令牌的资源上限一定要先确认。
C_GetTokenInfo() 查看 ulMaxSessionCount 和 ulMaxRwSessionCount,心里有个数,避免运行时翻车。
2.2 会话(Session)—— 操作的上下文
会话,就是你跟令牌之间的一次逻辑连接。所有的加密、解密、签名、验证、查找对象,都必须在一个会话里完成。没有会话,你连密钥都读不出来。
会话分为两种:
- 只读会话(R/O Session):只能读对象、执行密码操作,不能修改令牌上的持久对象。适合查询和验证场景。
- 读写会话(R/W Session):可以创建、修改、删除令牌上的对象,适合初始化令牌、导入密钥、生成密钥等写操作。
调用 C_OpenSession() 时指定 CKF_SERIAL_SESSION 是必须的,几乎所有实现都要求串行会话。你想想看,如果并发串行混用,令牌状态机可能直接崩掉。
但有一点容易踩坑:我见过不少工程师把会话当作线程安全变量来用。一个会话同时被多个线程调用,结果就是 CKR_SESSION_HANDLE_INVALID 满天飞。每个线程最好自己开独立会话,或者做好锁保护。
2.3 对象(Object)—— 密码材料的数据结构
对象,就是令牌上存储的密码材料。包括密钥、证书、数据文件等。PKCS#11 把对象分类得很清楚,方便统一管理和查找。
对象分两大类:
- 令牌对象( Object):存到令牌上的持久对象,拔出重新插入后还在。适合根密钥、CA 证书这类长期材料。
- 会话对象(Session Object):只在当前会话存在,会话结束后自动销毁。适合临时密钥、一次性的签名参数等。
我习惯把 PKCS#11 的对象看作一个通用容器,它不关心你放的是什么密钥类型,只关心你怎么查找它。查找靠的是属性模板,比如:
CK_OBJECT_CLASS classAttr = CKO_SECRET_KEY;
CK_KEY_TYPE keyType = CKK_AES;
CK_ATTRIBUTE template[] = {
{CKA_CLASS, &classAttr, sizeof(classAttr)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
};
// 用 C_FindObjectsInit / C_FindObjects / C_FindObjectsFinal 遍历查找
对象的属性不仅用于分类,还用于访问控制。比如 CKA_PRIVATE 为真时,该对象只有登录后才能查找到;CKA_MODIFIABLE 为假时,对象不可修改。这些属性的配合,能让你的密钥安全策略更加精细。
2.4 三者关系速查表
| 概念 | 作用范围 | 生命周期 | 典型创建方式 |
|---|---|---|---|
| 插槽(Slot) | 管理所有令牌 | 全局(设备插入时存在) | 系统自动发现 |
| 令牌() | 一个插槽一个 | 持久(直到设备拔出/复位) | 厂商初始化(C_InitToken) |
| 会话(Session) | 一个令牌多个会话 | 打开到关闭之间 | C_OpenSession |
| 对象(Object) | 会话内操作 | 令牌对象持久 / 会话对象临时的 | C_CreateObject/C_GenerateKey |
你只要在实战中记住这三层的调用顺序:先列插槽 → 打开令牌 → 打开会话 → 操作对象。我每次写新代码都按这个顺序画一遍草稿,思路就清晰了。
嗯,这一章就到这。把令牌、会话、对象的关系刻在脑子里,后面每一章都会用到这三个家伙。