基础概念:Slot、Session、Object、Attribute 术语解析
做 PKCS 11 适配,第一关就是把这四个词搞透。我当年刚接触时,看着文档里一堆 Slot、Session 头都大了。后来在项目里踩了两次坑,才真正明白它们之间的关系。说白了,这套概念就是一套抽象模型——把密码硬件(或软件模拟器)包装成统一的接口,让你写应用时不用管底层是 USB Key 还是 HSM。
整体关系鸟瞰
先看一张图,帮你把四个术语的位置理清楚。后面我逐条拆解。
从上往下看:一个 对应一个物理或逻辑密码设备; 里有一个或多个 Slot;每个 Slot 可以打开多个 Session;Session 里操作 Object;每个 Object 通过一组 Attribute 来描述。嗯,这里要注意——Slot 和 的关系容易搞混,我待会细说。
Slot —— 你插拔的那个“口”
Slot 直译就是“插槽”。最早 PKCS 11 是为智能卡设计的,你想想看,读卡器上有一个插卡的口,那就是一个 Slot。后来扩展到 HSM、软件 ,Slot 变成一个逻辑概念:代表一个可用的密码资源位置。
- Slot 有状态: 令牌是否插入(
CK__PRESENT)、是否可移除等。你通过C_GetSlotInfo拿到描述信息。 - Slot 和 是 1:N 的关系? 其实一个 可能对应多个 Slot(比如多分区 HSM),但典型场景是一个 Slot 对应一个 。我见过有的 SDK 把 Slot 0 和 Slot 1 映射到同一个硬件设备的不同密钥分区,这算是个坑——你写代码时不能假设 Slot 编号固定。
C_GetSlotList 枚举全部 Slot,再根据 标签或厂商信息选择正确的那个,别硬编码索引。
Session —— 你操作时的“会话”
Session 就是一个打开的门。你调用 C_OpenSession 时,传入 Slot ID、标志(只读/读写)和一个回调函数(用于通知),返回一个 Session Handle。后续所有密码操作(签名、加密、查找对象)都在这个 Session 里完成。
我个人习惯在应用启动时打开一个 Session,反复使用,直到退出时才关闭。但要注意:
- 有些 限制了最大 Session 数(比如硬件 只允许 16 个)。如果你忘记关闭 Session,后面再开就会失败。我曾经调试一整天,最后发现是 Session 泄漏——每个请求开一个新 Session 但不关,直到 Pool 耗尽。
- Session 分为 R/W 和 R/O 两种。如果你想创建或修改对象,必须用 R/W 模式。只读 Session 只能做查询和密码运算。
// 典型的 Session 打开方式
CK_SESSION_HANDLE hSession;
CK_RV rv = C_OpenSession(slotID,
CKF_SERIAL_SESSION | CKF_RW_SESSION,
NULL, // 应用定义的数据指针
NULL, // 通知回调函数
&hSession);
if (rv != CKR_OK) {
// 处理打开失败——可能是 未插入或 Session 已满
}
C_CloseSession。
Object —— 你存的“东西”
Object 包括密钥、证书、数据等。PKCS 11 用 CK_OBJECT_HANDLE 来标识每个对象。创建对象时,你提供一组 Attribute;查找对象时,你也用 Attribute 模板。
常见对象类型有:
| 类型常量 | 说明 |
|---|---|
CKO_PUBLIC_KEY | 公钥(RSA、EC 等) |
CKO_PRIVATE_KEY | 私钥 |
CKO_SECRET_KEY | 对称密钥(AES、3DES) |
CKO_CERTIFICATE | X.509 证书 |
CKO_DATA | 用户自定义数据块 |
每个对象有一个句柄(Handle),但这个句柄只在当前 Session 内有效?不一定。其实句柄在整个 生命周期内有效——只要你没有重新初始化 。我遇到过一种情况:在 Session A 里查找对象拿到句柄,然后关闭 Session A,再开 Session B,那个句柄竟然还能用?规范上说不行(句柄是 Session 相关的),但有些实现允许跨 Session。为了可移植,别这么干。
Attribute —— 对象的“属性字典”
每个 Object 都由一组 Attribute 描述。比如 RSA 公钥必须有 CKA_MODULUS 和 CKA_PUBLIC_EXPONENT;私钥必须有 CKA_PRIVATE、CKA_SENSITIVE 等。
Attribute 的结构是 CK_ATTRIBUTE:
typedef struct CK_ATTRIBUTE {
CK_ATTRIBUTE_TYPE type; // 例如 CKA_CLASS, CKA_LABEL
CK_VOID_PTR pValue; // 指向数据缓冲区
CK_ULONG ulValueLen; // 数据长度
} CK_ATTRIBUTE;
查询属性时,你要先设好 type,然后把 pValue 设为 NULL,调用 C_GetAttributeValue 获取长度,再分配缓冲区调用第二次获取实际数据。这是个经典的两步模式。
CKA_ID 属性时,我忘了检查返回值 CKR_ATTRIBUTE_SENSITIVE。有些私钥对敏感属性(比如 CKA_VALUE)会直接拒绝读取,不是报错而是返回一个特殊状态。你得逐一检查每个属性的返回值,不能只信任 ulValueLen。
四个术语放在一起看
回过头看这张图:Slot 是总线上的一个位置,Session 是打开后的一次“通话”,Object 是通话中操作的文件,Attribute 是文件的元数据字段。你想想看,没有这套抽象,每个硬件厂商都定义自己的 API,那适配工作就没完没了了。