会话对象与令牌对象:CKA_ 决定一切
我们先看一个最直接的场景。你调用 C_GenerateKeyPair 生成一对 RSA 密钥,然后程序就正常退出了。等你下次启动应用,调用 C_FindObjects 却死活找不到那把私钥。不设置 CKA_ 属性再多次,它都“蒸发”了。
为什么会这样?因为它的 CKA_ 默认为 CK_FALSE。这意味着它只是一个会话对象(Session Object),随会话关闭而销毁。
我在项目中就栽过这个跟头。一次帮客户调试 HSM 密钥持久化问题,他们死活不理解为啥重启后密钥就没了。我过去一看,C_GenerateKey 里根本没设置 CKA_ = CK_TRUE。嗯,就是这个小属性,决定了密钥是“住酒店”还是“安家落户”。
CKA_ 是什么
PKCS#11 里,CKA_ 是一个布尔属性,取值 CK_TRUE 或 CK_FALSE。它决定了对象在 (令牌)上的存活范围。
CK_TRUE:令牌对象( Object)。存在于 的非易失性存储中。即使会话注销、 重新插拔,对象依然存在。CK_FALSE:会话对象(Session Object)。只存在于当前会话的内存中。会话关闭后,对象就被彻底释放。
说白了,会话对象就像你在记事本里临时记的电话号码,关掉电脑就没了。令牌对象则像写进冰箱贴上的备忘,下次打开冰箱还能看见。
生命周期对比
| 维度 | 会话对象 (CKA_=FALSE) | 令牌对象 (CKA_=TRUE) |
|---|---|---|
| 存储位置 | 内存 (RAM) | 持久存储 (NVRAM/Flash) |
| 持久性 | 仅当前会话有效 | 跨会话、跨应用重启 |
| 创建方式 | C_CreateObject/C_GenerateKey 默认值 | 必须显式设置属性为 CK_TRUE |
| 销毁方式 | 会话关闭 / 调用 C_DestroyObject | 需显式 C_DestroyObject 或 格式化 |
| 可见性 | 仅创建该对象的会话可见 | 所有会话(登录后)都能搜索到 |
| 性能 | 创建/访问速度极快 | 每次访问涉及慢速 I/O |
核心教训:如果你需要密钥跨会话使用,CKA_ 必须设置为 CK_TRUE。否则,即使你调用 C_FindObjects 也只会得到一个空列表。
什么时候用会话对象
- 临时对称密钥:比如仅在一次安全通道建立过程中使用的会话密钥。用完即弃,根本不需要持久化。
- 测试或开发阶段:快速创建、快速销毁,避免污染生产环境。
- 匿名会话中的临时对象:某些 HSM 限制令牌对象数量,临时场景用会话对象更节省空间。
我记得有个项目,应用中需要临时派生一个 AES 密钥用于加密一个数据块,之后就扔掉。如果用令牌对象,每次还得记得清理遗留密钥,麻烦还容易泄露。会话对象天然拥有“用完即焚”的属性,正合适。
什么时候用令牌对象
- 长期存储的密钥对:签名私钥、TLS 证书私钥、CA 根密钥。这些必须持久保存。
- 加密密钥需要被多个应用共享:比如数据加密密钥被业务系统和归档系统共用。
- 密钥生命周期需要集中管理:通过 PKCS#11 库统一存储,而不是散落在各应用的临时内存中。
警告:令牌对象写入速度慢,尤其是一些软件 或硬件 HSM,写入次数有限。我曾经遇到一个客户,每秒生成 1000 个临时密钥并保存为令牌对象,结果 HSM 的持久存储飞快写满。解决方案:改用会话对象,仅在最后需要持久化的那一步才转为令牌对象。
代码示例:显式设置 CKA_
我们看一个典型调用,生成一个永久 AES 密钥:
CK_OBJECT_HANDLE hKey;
CK_ULONG keySize = 32; // 256 bits
CK_OBJECT_CLASS class = CKO_SECRET_KEY;
CK_KEY_TYPE keyType = CKK_AES;
CK_BBOOL ckTrue = CK_TRUE;
CK_BBOOL ckFalse = CK_FALSE;
CK_ATTRIBUTE template[] = {
{CKA_CLASS, &class, sizeof(class)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
{CKA_VALUE_LEN, &keySize, sizeof(keySize)},
{CKA_, &ckTrue, sizeof(ckTrue)}, // 关键:令牌对象
{CKA_PRIVATE, &ckTrue, sizeof(ckTrue)}, // 需要登录才能访问
{CKA_ENCRYPT, &ckTrue, sizeof(ckTrue)},
{CKA_DECRYPT, &ckTrue, sizeof(ckTrue)},
{CKA_EXTRACTABLE, &ckFalse, sizeof(ckFalse)} // 不可导出
};
CK_RV rv = C_GenerateKey(hSession, CKF_GENERATE_KEY_PAIR? CKF_GENERATE_KEY_PAIR? 等等…
注意 CKA_ = CK_TRUE 这一行。如果你漏掉它,密钥就会变成会话对象,会话一关就丢失。我个人习惯在创建任何重要密钥时,第一件事就把 CKA_ 值设为 CK_TRUE,然后再加上其他属性。
与 CKA_PRIVATE 的混淆点
很多人搞混 CKA_PRIVATE 和 CKA_。记住:
CKA_控制“是否持久化”。CKA_PRIVATE控制“是否需要登录才能访问”。
它们互不依赖。一个令牌对象可以是公开的(CKA_PRIVATE = FALSE),比如 RSA 公钥;一个会话对象也可以要求登录后访问(CKA_PRIVATE = TRUE),虽然很少这么用。
技巧:当我需要调试时,我会先把 CKA_ 设成 CK_FALSE,方便快速测试。等逻辑验证通过,再改成 CK_TRUE。但记得在正式上线前做全面回归,防止某个对象忘记修改。
对象可见性规则
- 会话对象:默认只能被创建它的会话看到。其他会话即使登录同一 Slot,也无法通过
C_FindObjects找到它。 - 令牌对象:只要 插在 Slot 上,所有打开该 的会话都能搜索到(受
CKA_PRIVATE和登录状态限制)。 - 跨应用可见性:仅令牌对象可被不同进程/应用访问(需要共享同一个 PKCS#11 库实例或底层 驱动支持)。
我碰到过一个经典坑:应用 A 创建了一个令牌对象,应用 B 在同一台机器上另起一个进程,通过同一个 PKCS#11 模块打开同一个 ,但就是找不到对象。检查半天,发现应用 B 用的 Slot ID 不同。 可以插在 Slot 0,也可以插在 Slot 1,虽然物理上是同一个 ,但逻辑上 Slot 不同,对象索引方式也不同。记住:C_FindObjects 是基于当前会话绑定的 Slot 来搜索的。
SVG:会话对象 vs 令牌对象生命周期
常见问题排查清单
如果创建对象后找不到,按照下面顺序检查:
- 确认
CKA_是否设为你期望的值。 - 确认
CKA_PRIVATE:如果是CK_TRUE,必须登录才能看到。 - 确认当前会话绑定的 Slot 正确(尤其是多 Slot 设备)。
- 确认 是否已被初始化(某些 HSM 需要先
C_InitToken)。 - 调用
C_GetSlotInfo和C_GetTokenInfo查看 状态。
我曾经接手一个事故:客户抱怨某台机器的令牌对象“神秘丢失”。追查发现,他们的应用在 C_GenerateKeyPair 之前调用了 C_CloseAllSessions 清空会话,然后重新打开一个只读会话(Read-Only Session)。只读会话不能创建令牌对象,但代码里也没检查返回值——C_GenerateKeyPair 返回 CKR_SESSION_READ_ONLY 被忽略,密钥根本没生成。唉,读返回值这种老生常谈的事,却是最容易翻车的地方。
简单总结一下:CKA_ 这个属性看似不起眼,但它决定了密钥的生死边界。我建议你每次创建对象时,都主动设置 CKA_,不要依赖默认值。默认值是 CK_FALSE,这个“临时”标签很容易造成数据丢失。