2. PKCS#11 体系结构:令牌、会话、对象

进入实战之前,先把 PKCS#11 最核心的三个概念理清楚:令牌、会话、对象。

说白了,这三样东西就是你跟密码设备打交道的全部入口。我刚开始接触 PKCS#11 时,总觉得文档绕来绕去,后来自己画了一张关系图,才一下通透。今天就带你从这张图入手。

PKCS#11 体系结构:令牌 · 会话 · 对象 PKCS#11 核心体系结构 插槽 (Slot) 令牌 () 会话 (Session) 会话 (Session) 对象 (Object) 数据对象 密钥对象 证书对象 一个插槽只有一个令牌,一个令牌可打开多个会话,每个会话独立操作一组可见对象

2.1 令牌()—— 逻辑上的密码设备

令牌是什么?你可以把它理解成逻辑上的密码设备。它不一定是真实的 USB Key 或 HSM,也可能是一个软件模拟的虚拟密码容器。

令牌的核心属性包括:

属性含义典型值
CK__INFO.label令牌标签,标识设备"MyHSM"
CK__INFO.flags能力标志位,比如登录、加密、RNG0x00000401
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 满天飞。每个线程最好自己开独立会话,或者做好锁保护。

避坑:我曾经在一个项目里,为了省资源,一个会话到处传。结果 HSM 驱动频繁报错。后来改成每个请求新建会话 + 用后即关,稳定多了。记住,会话是廉价的,别舍不得开。

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 为假时,对象不可修改。这些属性的配合,能让你的密钥安全策略更加精细。

总结: 令牌 = 密码设备的抽象;会话 = 与令牌的对话通道;对象 = 通道里传递的密码材料。三者构成了 PKCS#11 的骨架。

2.4 三者关系速查表

概念作用范围生命周期典型创建方式
插槽(Slot)管理所有令牌全局(设备插入时存在)系统自动发现
令牌()一个插槽一个持久(直到设备拔出/复位)厂商初始化(C_InitToken)
会话(Session)一个令牌多个会话打开到关闭之间C_OpenSession
对象(Object)会话内操作令牌对象持久 / 会话对象临时的C_CreateObject/C_GenerateKey

你只要在实战中记住这三层的调用顺序:先列插槽 → 打开令牌 → 打开会话 → 操作对象。我每次写新代码都按这个顺序画一遍草稿,思路就清晰了。

嗯,这一章就到这。把令牌、会话、对象的关系刻在脑子里,后面每一章都会用到这三个家伙。