PKCS#11 对象模型:数据对象、密钥对象、证书对象

说实话,搞 PKCS#11 开发,头一件事就是要弄明白它的对象模型。你想想看,HSM 里存的无非就是各种“东西”——密钥、证书、敏感数据。PKCS#11 把这堆东西抽象成了统一的对象体系。我刚开始学的时候也觉得这模型有点绕,但用久了就会发现,这套设计确实很巧妙。

所有对象都被分为两大类:存储对象 和 密钥对象。存储对象是那些可以被读出来的东西,密钥对象则是专门干密码活儿的。每个对象都有一堆属性,就像人的身份证信息一样,用属性来描述这个对象是什么、能干什么。

PKCS#11 对象模型分类 PKCS#11 对象 存储对象 密钥对象 数据对象 证书对象 公钥 私钥 秘密密钥 PKCS#11 对象模型层次结构 每个对象通过属性 (CKA_*) 描述身份。密钥对象又有不同分类: 公钥、私钥、秘密密钥

对象的通用属性

每个 PKCS#11 对象,不管是什么类型,都有一组公共属性。就像每个人都有姓名和身份证号一样。我列几个你一定会打交道的:

属性名称 作用 值的类型
CKA_CLASS 对象的大类 CK_OBJECT_CLASS 枚举
CKA_ 是令牌对象还是会话对象 CK_BBOOL (TRUE/FALSE)
CKA_PRIVATE 是否需要登录才能访问 CK_BBOOL
CKA_MODIFIABLE 创建后能不能改属性 CK_BBOOL
CKA_LABEL 给对象取个名字 UTF-8 字符串
CKA_ID 关联密钥和证书的标识 字节数组

重点提醒:CKA_ 决定对象的生命周期。TRUE 表示永久保存在 HSM 里,会话结束也不丢。FALSE 表示会话级临时对象,一关会话就没了。我见过有人把临时密钥当成永久的来用,结果重启后全丢了——教训啊。

数据对象——最底层的存储单元

数据对象,说白了就是个“盒子”,里面装的就是一段纯字节流。它没有密码学属性,不能用来签名、加密。它唯一能干的事就是存和取。

它的核心属性就两个:

  • CKA_VALUE:真正存的数据内容
  • CKA_APPLICATION:给数据打个标签,说明是哪个应用存的

我见过不少实现,把私钥也当成普通数据对象存储——这是很危险的做法。数据对象没有密码学保护,直接能被读出来。私钥必须用密钥对象来存,HSM 硬件会保证私钥永远不暴露在芯片外部。

避坑: 不要用 CKA_CLASS 为 CKO_DATA 的对象来存密钥材料。虽然技术上能存,但安全级别完全不同。我曾经在客户现场看到一套遗留系统,用数据对象存 RSA 私钥,然后调 C_SignInit 的时候发现不行——数据对象没有密钥操作的接口。

密钥对象——密码操作的核心

这才是 PKCS#11 的灵魂。密钥对象分成三种:公钥、私钥、秘密密钥。每种都有自己的专属属性和操作。

公钥对象 (CKO_PUBLIC_KEY)

公钥是可以公开的,所以它的 CKA_PRIVATE 属性通常是 FALSE。公钥对象包含算法类型、密钥材料(比如 RSA 的模数 N 和公钥指数 E),还有一组 CKA_VERIFY、CKA_ENCRYPT、CKA_WRAP 这样的属性——这些表示这把公钥能用来干什么。

私钥对象 (CKO_PRIVATE_KEY)

私钥是机密。它支持的操作是签名、解密、拆封。私钥对象有一个特殊属性 CKA_SENSITIVE,如果设置为 TRUE,那么 C_GetAttributeValue 是拿不到 CKA_VALUE 的。硬件压根就不会把私钥材料吐出来。

秘密密钥对象 (CKO_SECRET_KEY)

秘密密钥就是对称密钥——AES、DES、HMAC 这类。它的核心属性是 CKA_KEY_TYPE、CKA_VALUE_LEN、CKA_VALUE(如果 CKA_SENSITIVE=TRUE 就不可读)。秘密密钥能干的事很多:加密、解密、签名(HMAC 模式)、验证、派生其他密钥。

个人习惯: 每次创建密钥对象时,我必设三个属性:CKA_=TRUE、CKA_PRIVATE=TRUE、CKA_SENSITIVE=TRUE。这样才能保证密钥持久化、登录保护、材料不透传。三件套齐了才靠谱。

证书对象——绑定身份

证书对象用来存 X.509 数字证书。它本质上是只读的——你创建完证书对象后,基本不会去改它。

重要的属性:

  • CKA_VALUE:完整的 DER 编码证书
  • CKA_CERTIFICATE_TYPE:一般是 CKC_X_509
  • CKA_TRUSTED:是否为受信任的根证书
  • CKA_CERTIFICATE_CATEGORY:分类(用户/CA/其他)

很多 HSM 厂商会把证书对象跟密钥对象做关联——通过 CKA_ID 属性,把同属一个实体的证书和私钥配成一对。你想想看,签名验证时,用私钥签名,用证书里的公钥验证,CKA_ID 一匹配,非常方便。

对象句柄的生命周期

PKCS#11 里你操作对象不是直接拿属性访问,而是通过 对象句柄(CK_OBJECT_HANDLE)。这个句柄是一个数字 ID,你在会话里用它来操作对象。

我建议注意几点:

  1. C_FindObjects 才能拿到句柄——你不能自己猜一个数字当成句柄。
  2. 会话对象只有当前会话能用——换一个会话,句柄就失效了。
  3. 令牌对象句柄全局有效——只要令牌不拔,任何会话都能用这个句柄。
  4. 句柄本身没有安全含义——它就是个引用,真正的安全由 HSM 内核保障。

我曾经翻过一个大跟头: 在一款国产 HSM 上创建了一个令牌级 AES 密钥,然后没关闭会话就直接拔 了。重新插回去,老句柄还能用?结果 C_EncryptInit 返回 CKR_DEVICE_ERROR。后来才明白,某些厂商的句柄缓存在会话里,拔 后要重新查找。所以我的经验是——每次建立新会话,都老老实实重新查找对象,别偷懒用老的句柄。

好了,PKCS#11 的对象模型就这些核心东西。记住三个要点:第一,所有对象都是属性驱动的;第二,密钥对象分公钥、私钥、秘密密钥三种,各有各的用途;第三,证书对象本质上是只读容器。搞清楚了这三层,后面的签名、加密操作才有根基。