3.2 核心对象模型:Slot、Token、Session、Object的“四重奏“
银行保险箱系统的“四重奏“
让我们继续用银行保险箱的比喻。
一个完整的银行保险箱系统,有四个关键概念:
- 服务窗口:客户与保险箱系统交互的入口。窗口可以有多个,每个窗口可以服务不同的保险箱库。
- 保险箱库:存储客户物品的物理空间。一个窗口对应一个库。
- 访问记录:客户每次访问库时,建立一条记录。记录包含访问时间、访问权限、操作类型。
- 保险箱内容:库里存储的具体物品(文件、珠宝、证书等)。
这四个概念,对应了PKCS#11的核心对象:
| 银行概念 | PKCS#11概念 | 含义 |
|---|---|---|
| 服务窗口 | Slot | HSM的物理插槽/逻辑接口 |
| 保险箱库 | Token | HSM的逻辑实例/密钥存储区 |
| 访问记录 | Session | 与Token的交互会话 |
| 保险箱内容 | Object | 密钥、证书等密码对象 |
Slot:密码世界的“服务窗口“
Slot是PKCS#11的第一个概念。
Slot的定义:
Slot(插槽)是一个逻辑概念,代表一个密码令牌可以插入的位置。
Slot可以是:
- 物理Slot:HSM硬件的实际插槽(如智能卡读卡器的插槽)
- 逻辑Slot:HSM软件模拟的逻辑接口(如SoftHSM的虚拟Slot)
Slot的特点:
- Slot有编号:Slot ID,通常是0、1、2…的整数
- Slot可以包含Token:一个Slot可以插入一个Token(也可以没有Token)
- Slot有状态:Token存在或Token不存在
- Slot有信息:厂商信息、Slot描述、硬件版本等
Slot的结构:
Slot信息结构(CK_SLOT_INFO):
typedef struct CK_SLOT_INFO {
CK_UTF8CHAR manufacturerID[32]; // 厂商ID
CK_UTF8CHAR slotDescription[64]; // Slot描述
CK_FLAGS flags; // Slot标志
CK_VERSION hardwareVersion; // 硬件版本
CK_VERSION firmwareVersion; // 固件版本
} CK_SLOT_INFO;
flags可能的值:
- CKF_TOKEN_PRESENT:Token存在
- CKF_REMOVABLE_DEVICE:可移除设备
- CKF_HW_SLOT:硬件Slot
Slot的典型场景:
Slot的典型场景:
场景一:独立HSM(车规级安全芯片)
Slot 0 → Token存在(独立HSM)
Slot 1 → Token不存在
Slot 2 → Token不存在
...
场景二:智能卡读卡器
Slot 0 → Token存在(插入的智能卡)
Slot 1 → Token不存在(空插槽)
场景三:软件HSM(SoftHSM2)
Slot 0 → Token存在(虚拟Token 0)
Slot 1 → Token存在(虚拟Token 1)
Slot 2 → Token不存在
...
获取Slot列表的代码:
CK_RV rv;
CK_SLOT_ID_PTR pSlotList;
CK_ULONG ulCount;
// 获取Slot数量
rv = C_GetSlotList(FALSE, NULL_PTR, &ulCount);
if (rv != CKR_OK) {
printf("获取Slot数量失败:%08x\n", rv);
return;
}
// 分配内存
pSlotList = (CK_SLOT_ID_PTR)malloc(ulCount * sizeof(CK_SLOT_ID));
// 获取Slot列表
rv = C_GetSlotList(FALSE, pSlotList, &ulCount);
if (rv != CKR_OK) {
printf("获取Slot列表失败:%08x\n", rv);
free(pSlotList);
return;
}
// 打印Slot信息
for (CK_ULONG i = 0; i < ulCount; i++) {
CK_SLOT_INFO slotInfo;
rv = C_GetSlotInfo(pSlotList[i], &slotInfo);
printf("Slot %lu: %s\n", pSlotList[i], slotInfo.slotDescription);
}
free(pSlotList);
Token:密码世界的“保险箱库“
Token是PKCS#11的第二个概念。
Token的定义:
Token(令牌)是存储密码对象的逻辑实体。
Token代表:
- 一个HSM实例(如车规级独立安全芯片)
- 一个智能卡(如插入读卡器的卡片)
- 一个虚拟Token(如SoftHSM2的虚拟Token)
Token的特点:
- Token存储密码对象:密钥、证书、数据对象都存储在Token中
- Token有访问控制:需要PIN码登录才能访问私有对象
- Token有状态:初始化状态、正常状态、锁定状态等
- Token有信息:厂商信息、Token描述、序列号等
Token的结构:
Token信息结构(CK_TOKEN_INFO):
typedef struct CK_TOKEN_INFO {
CK_UTF8CHAR label[32]; // Token标签
CK_UTF8CHAR manufacturerID[32]; // 厂商ID
CK_UTF8CHAR model[16]; // 型号
CK_UTF8CHAR serialNumber[16]; // 序列号
CK_FLAGS flags; // Token标志
CK_ULONG ulMaxSessionCount; // 最大会话数
CK_ULONG ulSessionCount; // 当前会话数
CK_ULONG ulMaxRwSessionCount; // 最大读写会话数
CK_ULONG ulRwSessionCount; // 当前读写会话数
CK_ULONG ulMaxPinLen; // 最大PIN长度
CK_ULONG ulMinPinLen; // 最小PIN长度
CK_ULONG ulTotalPublicMemory; // 公共内存总量
CK_ULONG ulFreePublicMemory; // 公共内存空闲量
CK_ULONG ulTotalPrivateMemory; // 私有内存总量
CK_ULONG ulFreePrivateMemory; // 私有内存空闲量
CK_VERSION hardwareVersion; // 硬件版本
CK_VERSION firmwareVersion; // 固件版本
CK_CHAR utcTime[16]; // UTC时间(可选)
} CK_TOKEN_INFO;
flags可能的值:
- CKF_RNG:有随机数生成器
- CKF_WRITE_PROTECTED:写保护
- CKF_LOGIN_REQUIRED:需要登录
- CKF_USER_PIN_INITIALIZED:用户PIN已初始化
- CKF_TOKEN_INITIALIZED:Token已初始化
Token的用户角色:
Token有两个用户角色:
| 角色 | 描述 | 权限 |
|---|---|---|
| Security Officer (SO) | 安全管理员 | 初始化Token、设置用户PIN、解锁Token |
| User | 普通用户 | 使用密钥、执行密码操作 |
Token的初始化流程:
Token初始化流程:
1. 获取Slot列表
C_GetSlotList()
2. 选择一个Slot
slotID = pSlotList[0]
3. 初始化Token(需要SO PIN)
C_InitToken(slotID, soPin, soPinLen, label)
4. 打开会话
C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION, ...)
5. 登录为SO
C_Login(hSession, CKU_SO, soPin, soPinLen)
6. 初始化用户PIN
C_InitPIN(hSession, userPin, userPinLen)
7. 注销
C_Logout(hSession)
8. 关闭会话
C_CloseSession(hSession)
Session:密码世界的“访问记录“
Session是PKCS#11的第三个概念。
Session的定义:
Session(会话)是应用程序与Token的一次交互过程。
每次应用程序与Token交互时,需要:
- 打开一个Session
- 在Session中执行操作
- 关闭Session
Session的特点:
- Session绑定Token:Session与特定的Token绑定
- Session有状态:读/写、登录/未登录
- Session有权限:基于登录状态决定可执行的操作
- Session可以并发:一个Token可以有多个Session
Session的状态:
Session状态:
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │读写会话 │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │R/W Public │ │R/W User │ │ │
│ │ │Session │ │Functions │ │ │
│ │ │(未登录) │ │(登录为User) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │R/W SO │ │R/W SO │ │ │
│ │ │Functions │ │Functions │ │ │
│ │ │(登录为SO) │ │(登录为SO) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │只读会话 │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │R/O Public │ │R/O User │ │ │
│ │ │Session │ │Functions │ │ │
│ │ │(未登录) │ │(登录为User) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Session的打开方式:
CK_SESSION_HANDLE hSession;
CK_FLAGS flags = CKF_SERIAL_SESSION | CKF_RW_SESSION;
// 打开读写会话
rv = C_OpenSession(slotID, flags, NULL_PTR, NULL_PTR, &hSession);
// 打开只读会话
flags = CKF_SERIAL_SESSION;
rv = C_OpenSession(slotID, flags, NULL_PTR, NULL_PTR, &hSession);
Session状态转换:
Session状态转换:
未登录
│
│ C_Login(CKU_USER, pin, pinLen)
▼
已登录User
│
│ C_Login(CKU_SO, soPin, soPinLen) ← 只有R/W会话可以
▼
已登录SO
│
│ C_Logout()
▼
未登录
Object:密码世界的“保险箱内容“
Object是PKCS#11的第四个概念。
Object的定义:
Object(对象)是存储在Token中的密码资源。
Object可以是:
- 密钥:公钥、私钥、对称密钥
- 证书:X.509证书
- 数据:用户数据对象
Object的类型:
| Object类型 | 常量 | 描述 |
|---|---|---|
| CKO_PUBLIC_KEY | 公钥对象 | RSA/ECC公钥 |
| CKO_PRIVATE_KEY | 私钥对象 | RSA/ECC私钥 |
| CKO_SECRET_KEY | 对称密钥对象 | AES/DES密钥 |
| CKO_CERTIFICATE | 证书对象 | X.509证书 |
| CKO_DATA | 数据对象 | 用户数据 |
| CKO_MECHANISM | 机制对象 | 密码机制描述 |
Object的属性:
每个Object有一组属性。属性定义了Object的特征和行为。
常见的属性:
| 属性 | 常量 | 描述 |
|---|---|---|
| CKA_CLASS | Object类型 | 公钥/私钥/对称密钥等 |
| CKA_KEY_TYPE | 密钥类型 | RSA/ECC/AES等 |
| CKA_LABEL | 标签 | 用户定义的标签 |
| CKA_ID | ID | 用户定义的ID |
| CKA_TOKEN | 是否存储在Token中 | TRUE=持久存储 |
| CKA_PRIVATE | 是否私有 | TRUE=需要登录才能访问 |
| CKA_SENSITIVE | 是否敏感 | TRUE=不可导出 |
| CKA_EXTRACTABLE | 是否可导出 | FALSE=不能导出 |
| CKA_MODIFIABLE | 是否可修改 | TRUE=属性可修改 |
| CKA_DESTROYABLE | 是否可销毁 | TRUE=可以销毁 |
| CKA_VALUE | 密钥值 | 密钥的实际值 |
| CKA_MODULUS | RSA模数 | RSA密钥的模数 |
| CKA_PUBLIC_EXPONENT | RSA公钥指数 | RSA公钥的指数 |
| CKA_PRIVATE_EXPONENT | RSA私钥指数 | RSA私钥的指数 |
Object的安全属性组合:
这些属性的组合决定了Object的安全行为:
Object安全属性组合示例:
组合一:不可导出的私钥(最安全)
- CKA_SENSITIVE = TRUE(敏感,不可导出)
- CKA_EXTRACTABLE = FALSE(不可导出)
- CKA_PRIVATE = TRUE(私有,需要登录)
组合二:可导出的对称密钥(可备份)
- CKA_SENSITIVE = FALSE(不敏感)
- CKA_EXTRACTABLE = TRUE(可导出)
- CKA_PRIVATE = TRUE(私有)
组合三:公开的公钥(可自由访问)
- CKA_SENSITIVE = FALSE(不敏感)
- CKA_EXTRACTABLE = TRUE(可导出)
- CKA_PRIVATE = FALSE(公开)
创建Object的代码:
CK_OBJECT_HANDLE hKey;
CK_ATTRIBUTE template[] = {
{CKA_CLASS, &keyClass, sizeof(keyClass)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
{CKA_TOKEN, &bTrue, sizeof(bTrue)},
{CKA_PRIVATE, &bTrue, sizeof(bTrue)},
{CKA_SENSITIVE, &bTrue, sizeof(bTrue)},
{CKA_EXTRACTABLE, &bFalse, sizeof(bFalse)},
{CKA_LABEL, "MyKey", 5},
};
rv = C_CreateObject(hSession, template, 6, &hKey);
四重奏的完整关系
现在让我们把这四个概念组合起来。
完整的层次结构:
PKCS#11对象模型层次结构:
Library(PKCS#11库)
│
├── Slot 0
│ ├── Token(存在)
│ │ ├── Session 0
│ │ │ ├── Object 0(公钥)
│ │ │ ├── Object 1(私钥)
│ │ │ ├── Object 2(对称密钥)
│ │ │ └── Object 3(证书)
│ │ └── Session 1
│ │ ├── Object 4(数据)
│ │ └── Object 5(证书)
│ └── Token(不存在)
│
├── Slot 1
│ └── Token(存在)
│ └── Session 2
│ ├── Object 0
│ └── Object 1
│
└── Slot 2
└── Token(不存在)
关系的要点:
- Slot与Token:Slot是位置,Token是内容。一个Slot可以有一个Token,也可以没有。
- Token与Session:Session是Token的交互。一个Token可以有多个Session。
- Session与Object:Object通过Session访问。Session决定了Object的访问权限。
- Object与Token:Object存储在Token中。Token是Object的容器。
访问流程:
访问Object的完整流程:
应用程序
│
│ C_Initialize()
▼
Library初始化
│
│ C_GetSlotList()
▼
获取Slot列表 → 选择Slot 0
│
│ C_OpenSession(slotID, ...)
▼
打开Session → Session绑定到Slot 0的Token
│
│ C_Login(hSession, CKU_USER, ...)
▼
登录 → Session变为已登录状态
│
│ C_FindObjectsInit(hSession, ...)
│ C_FindObjects(hSession, ...)
│ C_FindObjectsFinal(hSession)
▼
查找Object → 找到Object(私钥)
│
│ C_SignInit(hSession, mechanism, hKey)
│ C_Sign(hSession, ...)
▼
使用Object → 执行签名操作
│
│ C_Logout(hSession)
│ C_CloseSession(hSession)
│ C_Finalize()
▼
清理 → 结束会话
一个类比:完整的银行流程
让我用一个完整的银行类比来理解这个四重奏。
银行保险箱系统流程:
银行保险箱系统流程:
1. 客户进入银行大厅
= 应用程序初始化 C_Initialize()
2. 客户查看窗口列表
= 获取Slot列表 C_GetSlotList()
3. 客户选择一个窗口
= 选择Slot
4. 客户通过窗口请求访问保险箱库
= 打开Session C_OpenSession()
5. 客户出示身份证明,工作人员核验
= 登录 C_Login()
6. 工作人员带客户进入库房
= Session绑定到Token
7. 客户查找自己的保险箱
= 查找Object C_FindObjects()
8. 客户打开保险箱,取出/存入物品
= 使用Object C_Sign/C_Encrypt等
9. 客户离开库房,工作人员注销访问记录
= 注销 C_Logout()
10. 客户离开窗口
= 关闭Session C_CloseSession()
11. 客户离开银行大厅
= 清理 C_Finalize()
这个类比清晰地对应了PKCS#11的每个概念和流程。
实际代码示例:完整的签名流程
现在,让我们看一个完整的PKCS#11签名流程代码:
CK_RV rv;
CK_C_INITIALIZE_ARGS initArgs = {NULL_PTR, NULL_PTR, NULL_PTR, NULL_PTR, CKF_OS_LOCKING_OK, NULL_PTR};
CK_SLOT_ID slotID;
CK_SLOT_ID_PTR pSlotList;
CK_ULONG ulSlotCount;
CK_SESSION_HANDLE hSession;
CK_OBJECT_HANDLE hKey;
CK_BYTE data[] = "Hello, PKCS#11!";
CK_ULONG ulDataLen = sizeof(data);
CK_BYTE signature[256];
CK_ULONG ulSigLen = 256;
CK_CHAR userPin[] = "12345678";
CK_ULONG ulUserPinLen = sizeof(userPin);
// 1. 初始化
rv = C_Initialize(&initArgs);
if (rv != CKR_OK) {
printf("初始化失败:0x%08x\n", rv);
return rv;
}
// 2. 获取Slot列表
rv = C_GetSlotList(CK_TRUE, NULL_PTR, &ulSlotCount);
if (rv != CKR_OK) {
C_Finalize(NULL_PTR);
return rv;
}
pSlotList = malloc(ulSlotCount * sizeof(CK_SLOT_ID));
rv = C_GetSlotList(CK_TRUE, pSlotList, &ulSlotCount);
if (rv != CKR_OK) {
free(pSlotList);
C_Finalize(NULL_PTR);
return rv;
}
slotID = pSlotList[0]; // 选择第一个Slot
free(pSlotList);
// 3. 打开Session
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL_PTR, NULL_PTR, &hSession);
if (rv != CKR_OK) {
C_Finalize(NULL_PTR);
return rv;
}
// 4. 登录
rv = C_Login(hSession, CKU_USER, userPin, ulUserPinLen);
if (rv != CKR_OK) {
C_CloseSession(hSession);
C_Finalize(NULL_PTR);
return rv;
}
// 5. 查找私钥
CK_OBJECT_CLASS keyClass = CKO_PRIVATE_KEY;
CK_KEY_TYPE keyType = CKK_RSA;
CK_ATTRIBUTE findTemplate[] = {
{CKA_CLASS, &keyClass, sizeof(keyClass)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
};
rv = C_FindObjectsInit(hSession, findTemplate, 2);
if (rv != CKR_OK) {
C_Logout(hSession);
C_CloseSession(hSession);
C_Finalize(NULL_PTR);
return rv;
}
CK_ULONG ulCount;
rv = C_FindObjects(hSession, &hKey, 1, &ulCount);
if (rv != CKR_OK || ulCount == 0) {
C_FindObjectsFinal(hSession);
C_Logout(hSession);
C_CloseSession(hSession);
C_Finalize(NULL_PTR);
return rv;
}
rv = C_FindObjectsFinal(hSession);
if (rv != CKR_OK) {
C_Logout(hSession);
C_CloseSession(hSession);
C_Finalize(NULL_PTR);
return rv;
}
// 6. 签名
CK_MECHANISM mechanism = {CKM_RSA_PKCS, NULL_PTR, 0};
rv = C_SignInit(hSession, &mechanism, hKey);
if (rv != CKR_OK) {
C_Logout(hSession);
C_CloseSession(hSession);
C_Finalize(NULL_PTR);
return rv;
}
rv = C_Sign(hSession, data, ulDataLen, signature, &ulSigLen);
if (rv != CKR_OK) {
C_Logout(hSession);
C_CloseSession(hSession);
C_Finalize(NULL_PTR);
return rv;
}
printf("签名成功,长度:%lu字节\n", ulSigLen);
// 7. 注销
rv = C_Logout(hSession);
if (rv != CKR_OK) {
C_CloseSession(hSession);
C_Finalize(NULL_PTR);
return rv;
}
// 8. 关闭Session
rv = C_CloseSession(hSession);
if (rv != CKR_OK) {
C_Finalize(NULL_PTR);
return rv;
}
// 9. 清理
rv = C_Finalize(NULL_PTR);
return rv;
这个代码完整展示了PKCS#11的使用流程:
- 初始化 → 获取Slot → 打开Session → 登录 → 查找密钥 → 签名 → 注销 → 关闭Session → 清理
本篇小结
今天我们分析了PKCS#11的核心对象模型——四重奏。
Slot(服务窗口):
- 密码令牌的插入位置
- 可以包含Token,也可以不包含
- 有编号、状态、信息
Token(保险箱库):
- 存储密码对象的逻辑实体
- 有访问控制(SO和User)
- 有状态(初始化、正常、锁定)
Session(访问记录):
- 与Token的交互会话
- 有状态(读/写、登录/未登录)
- 有权限(基于登录状态)
Object(保险箱内容):
- 存储在Token中的密码资源
- 有类型(公钥、私钥、对称密钥、证书、数据)
- 有属性(安全属性定义行为)
这四个概念形成完整的层次结构:Library → Slot → Token → Session → Object。
下一节,我们将深入PKCS#11的安全哲学——属性如何定义安全边界,密钥如何被保护。
【下集预告】
PKCS#11的安全哲学是什么?
“提供机制而非策略”——这是PKCS#11的核心设计原则。
但机制如何转化为安全?属性如何定义边界?
CKA_SENSITIVE、CKA_EXTRACTABLE、CKA_PRIVATE这些属性如何工作?
下一节,安全哲学。