4. PKCS#11对象模型:数据对象、密钥对象、证书对象、模板与属性
这一章咱们聊聊PKCS#11的对象模型。嗯,这算是整个标准里最核心的概念之一。你想想看,HSM也好、也罢,里面存的无非就是几类东西——密钥、证书、还有你自定义的数据。PKCS#11把这些东西统称为"对象"。
我刚接触PKCS#11那会儿,最怕的就是搞不清对象类型和属性之间的关系。一个RSA公钥,到底该用CKO_PUBLIC_KEY还是CKO_CERTIFICATE?后来磕了几次才明白,它们根本就是两回事。这节课咱们就把这些概念彻底理清。
4.1 对象模型的总体结构
PKCS#11里的对象,说白了就是一个"属性袋子"。每个对象都有一个类型(CKO_*)和一堆属性(CKA_*)。你创建对象时,就是通过一个模板(CK_ATTRIBUTE数组)把这些属性塞进去。
我个人习惯把对象分成三大类:
- 数据对象(CKO_DATA)—— 存任意字节,比如日志、配置文件
- 密钥对象 —— 又分公钥、私钥、密钥类型三种
- 证书对象(CKO_CERTIFICATE)—— 存X.509证书本体
除此之外还有Domain Parameters、Mechanism等冷门类型,但实战中咱们主要跟这三类打交道。
核心记忆点:PKCS#11对象 = CKO_类型 + CKA_属性集合。你操作一个对象,实际上就是在读写它的属性。
下面这张图展示了各类对象之间的关系:
4.2 数据对象(CKO_DATA)
数据对象很简单,就是存一段二进制。它的核心属性只有一个:CKA_VALUE,长度不限。
我用它存过审计日志的HMAC密钥种子。当时需要把种子和证书分开保管,就用数据对象放在另一个上。你想想看,私钥不能导出,但数据对象的CKA_VALUE可以自由读取,前提是你有相应的操作权限。
| 属性 | 类型 | 说明 |
|---|---|---|
CKA_CLASS | CK_OBJECT_CLASS | 固定为 CKO_DATA |
CKA_ | CK_BBOOL | TRUE=存在HSM上;FALSE=临时会话对象 |
CKA_PRIVATE | CK_BBOOL | TRUE=需要登录才能访问 |
CKA_MODIFIABLE | CK_BBOOL | TRUE=创建后可修改属性 |
CKA_LABEL | CK_UTF8CHAR[] | 给对象取个名字,方便识别 |
CKA_VALUE | CK_BYTE[] | 实际存储的字节数据 |
实战技巧:用数据对象存日志序列号或审计计数器时,记得设置CKA_MODIFIABLE = CK_FALSE。这样一旦写入就不能改了,防篡改。我之前一个项目就因为忘了设这个,被客户审计时发现计数器被重写,搞得重做了全套方案。
4.3 密钥对象(公钥、私钥、密钥)
密钥对象分三大类,每一类又有自己的子类型属性。咱们先看公共部分:
- CKO_PUBLIC_KEY:公钥,可以导出,通常不加密。
- CKO_PRIVATE_KEY:私钥,永远不能导出(除非支持,但一般禁掉)。
- CKO_SECRET_KEY:对称密钥,两端共享。
记得我刚开始做审计日志加密时,想用RSA公钥直接加密每条日志。结果发现PKCS#11的公钥对象只存储模数n和指数e,真正做加密是通过C_EncryptInit传入CK_OBJECT_HANDLE,而不是自己操作公钥字节。这是个常见误区。
4.3.1 公钥对象属性
以RSA为例,创建公钥对象至少要指定:
CK_OBJECT_CLASS class = CKO_PUBLIC_KEY;
CK_KEY_TYPE keyType = CKK_RSA;
CK_BYTE modulus[] = { 0xAB, 0xCD, ... }; // 模数
CK_BYTE exponent[] = { 0x01, 0x00, 0x01 }; // 公钥指数
CK_ATTRIBUTE template[] = {
{CKA_CLASS, &class, sizeof(class)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
{CKA_, &ckTrue, sizeof(CK_BBOOL)},
{CKA_MODULUS, modulus, sizeof(modulus)},
{CKA_PUBLIC_EXPONENT, exponent, sizeof(exponent)},
{CKA_VERIFY, &ckTrue, sizeof(CK_BBOOL)} // 允许用此公钥验签
};
我习惯在创建前先用C_FindObjectsInit搜索是否已有同指纹的公钥,避免重复存储。嗯,后面讲模板搜索时会细说。
4.3.2 私钥对象属性
私钥比公钥多几个关键属性:
CKA_SIGN/CKA_UNWRAP:控制私钥用途CKA_PRIVATE:必须为TRUE,确保需登录才能访问CKA_SENSITIVE:TRUE时私钥值永远不可导出CKA_EXTRACTABLE:FALSE则完全禁止导出
避坑指南:我曾经在生产上创建了一对RSA密钥,忘记设置CKA_SENSITIVE = CK_TRUE。结果运维人员用C_GetAttributeValue把私钥的模数和私有指数全部读出来了——虽然没明文导出整个私钥,但指数泄露等于私钥暴露。从那以后,我规定所有生产私钥必须设CKA_SENSITIVE = TRUE且CKA_EXTRACTABLE = FALSE。
4.3.3 对称密钥属性
对称密钥(如AES)的属性相对简单:
| 属性 | 典型值 | 说明 |
|---|---|---|
CKA_VALUE_LEN | 16/24/32 | 密钥字节长度(AES-128/192/256) |
CKA_VALUE | (密钥字节) | 生成后才能读取?不,通常设为敏感不可读 |
CKA_ENCRYPT | CK_TRUE | 允许加密 |
CKA_DECRYPT | CK_TRUE | 允许解密 |
CKA_WRAP | CK_TRUE | 允许用此密钥封装其他密钥 |
CKA_UNWRAP | CK_TRUE | 允许解封 |
注意:对称密钥的CKA_VALUE创建时由HSM内部生成,你没法通过C_GetAttributeValue读出来(如果设了敏感)。这也是保护机制。
4.4 证书对象(CKO_CERTIFICATE)
证书对象用来存储X.509证书的DER编码。它的核心属性:
CKA_VALUE:证书本体(DER字节)CKA_CERTIFICATE_TYPE:通常为CKC_X_509CKA_SUBJECT:主题DNCKA_ISSUER:签发者DNCKA_SERIAL_NUMBER:序列号CKA_ID:标识符,用来和私钥对象关联
这里CKA_ID特别重要。你回想一下,我登录后怎么把证书和对应的私钥匹配起来?就是靠这个ID。通常做法是:生成密钥对时给私钥和公钥都设同一个CKA_ID,然后导入证书时也设同样的ID。
// 假设已经生成了RSA密钥对,且获得了 handlePrivate, handlePublic
CK_BYTE keyId[] = {0x01, 0x02, 0x03, 0x04};
// 为密钥设置CKA_ID
CK_ATTRIBUTE setKeyId[] = {
{CKA_ID, keyId, sizeof(keyId)}
};
C_SetAttributeValue(session, handlePrivate, setKeyId, 1);
C_SetAttributeValue(session, handlePublic, setKeyId, 1);
// 导入证书时也带同样的CKA_ID
CK_ATTRIBUTE certTemplate[] = {
{CKA_CLASS, &certClass, sizeof(certClass)},
{CKA_CERTIFICATE_TYPE, &certType, sizeof(certType)},
{CKA_VALUE, certDer, certDerLen},
{CKA_SUBJECT, subjectDer, subjectDerLen},
{CKA_ID, keyId, sizeof(keyId)} // 关联密钥
};
C_CreateObject(session, certTemplate, 4, &hCert);
我遇到过的一个麻烦:证书对象的CKA_SUBJECT是DER编码(不是字符串),很多人误传了字符串进去,结果C_CreateObject返回CKR_ATTRIBUTE_VALUE_INVALID。你要通过ASN.1库把DN编码成DER再传。
4.5 模板(Template)与属性(Attribute)
模板就是CK_ATTRIBUTE数组,它是操作对象的核心。不管是创建、搜索、还是修改对象,都得通过模板传递属性。
几个关键操作:
- 创建对象:
C_CreateObject(session, template, ulCount, &hObject)。模板里必须包含CKA_CLASS和类型相关的必填属性。 - 搜索对象:
C_FindObjectsInit(session, template, ulCount)。模板里只放你想匹配的属性,比如只搜CKA_CLASS = CKO_PRIVATE_KEY。 - 读取属性:
C_GetAttributeValue(session, hObject, template, ulCount)。先把属性模板传进去(属性类型填好,pValue=NULL),第一次调用获取长度;分配内存后再调一次获取值。 - 修改属性:
C_SetAttributeValue(session, hObject, template, ulCount)。注意有些属性不可修改(如CKA_KEY_TYPE)。
我的经验:搜索对象时,模板属性越多,匹配越精确,但性能会下降。实际项目中,我一般只用CKA_CLASS + CKA_ID两个属性搜索,又快又准。如果ID用随机数生成(比如UUID),几乎不会碰撞。
属性有五种取值来源:
- 默认值:PKCS#11标准为每个属性定义了默认值,比如
CKA_默认为CK_FALSE(会话对象)。 - 创建时指定:你在模板里显式设置的属性值。
- 派生/计算值:比如RSA私钥的
CKA_MODULUS是从公钥部分复制过来的。 - 不可修改的固定值:如
CKA_CLASS、CKA_KEY_TYPE。 - 敏感/不可读值:如私钥的
CKA_PRIVATE_EXPONENT,你读不到。
最后说个细节:C_GetAttributeValue如果某个属性不可读,会返回CKR_ATTRIBUTE_SENSITIVE或CKR_ATTRIBUTE_TYPE_INVALID。你不能假设所有属性都能读出来。我记得调试一次时,程序直接崩溃了——因为没检查返回值,结果把一个敏感属性的长度当成0,分配了0字节缓冲区,后面memcpy就崩了。所以一定要逐个检查返回值。
本章核心:PKCS#11的一切都是对象。对象由类型决定结构,由模板驱动操作。理解这点,你就掌握了80%的PKCS#11编程。剩下的20%是各种机制的细节,但对象模型是底座。