3.4 Session状态与并发管理:PKCS#11的"会话状态机"
📚 本文内容摘自本人的开源书《HSM技术书 - 从思想实验到安全基石》
🔗 在线阅读/下载:hsm-book
bash
git clone https://github.com/Lularible/hsm-book.git
⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。
为什么需要理解Session状态?
Session不仅仅是"连接"------它是有状态的。
理解Session状态,对于以下场景至关重要:
- 调试问题:为什么C_CreateObject失败?可能是Session是只读状态
- 权限控制:为什么C_Login(CKU_SO)失败?可能是Session不是读写状态
- 并发设计:多个Session如何协同工作?
CK_SESSION_INFO:Session的"身份证"
PKCS#11规范定义了CK_SESSION_INFO结构,记录Session的完整状态:
c
typedef struct CK_SESSION_INFO {
CK_SLOT_ID slotID; // 所属Slot ID
CK_STATE state; // Session当前状态
CK_FLAGS flags; // Session标志
CK_ULONG ulDeviceError; // 设备错误码
} CK_SESSION_INFO;
字段详解:
| 字段 | 类型 | 含义 |
|---|---|---|
| slotID | CK_SLOT_ID | Session绑定的Slot ID |
| state | CK_STATE | Session的当前状态(5种状态) |
| flags | CK_FLAGS | Session的标志位 |
| ulDeviceError | CK_ULONG | 设备错误码(硬件故障时使用) |
flags字段:
Session flags定义:
CKF_SERIAL_SESSION (0x00000004) 必须设置(历史遗留)
CKF_RW_SESSION (0x00000002) 读写会话标志
CK_STATE:Session的五种状态
PKCS#11定义了五种Session状态:
c
#define CKS_RO_PUBLIC_SESSION 0UL // 只读,未登录
#define CKS_RO_USER_FUNCTIONS 1UL // 只读,User登录
#define CKS_RW_PUBLIC_SESSION 2UL // 读写,未登录
#define CKS_RW_USER_FUNCTIONS 3UL // 读写,User登录
#define CKS_RW_SO_FUNCTIONS 4UL // 读写,SO登录
状态命名规则:
状态命名规则:
CKS_[R/O或R/W]_[角色]_[Functions]
R/O = Read-Only(只读)
R/W = Read-Write(读写)
PUBLIC = 未登录(公共状态)
USER_FUNCTIONS = User登录后可执行功能
SO_FUNCTIONS = SO登录后可执行功能
状态矩阵:
| 状态 | 读/写 | 登录角色 | 可执行操作 |
|---|---|---|---|
| CKS_RO_PUBLIC_SESSION | 只读 | 未登录 | 只读公开对象、密码运算 |
| CKS_RO_USER_FUNCTIONS | 只读 | User | 访问私有对象、密码运算 |
| CKS_RW_PUBLIC_SESSION | 读写 | 未登录 | 创建Session对象、密码运算 |
| CKS_RW_USER_FUNCTIONS | 读写 | User | 创建Token对象、访问私有对象 |
| CKS_RW_SO_FUNCTIONS | 读写 | SO | 初始化PIN、管理Token |
Session状态机:完整的状态转换图
Session状态不是静态的------它随着操作动态变化。
完整的状态转换图:
Session状态机:
C_OpenSession(CKF_SERIAL_SESSION)
│
│ 只读会话
▼
┌───────────────────────────────┐
│ CKS_RO_PUBLIC_SESSION │
│ (只读,未登录) │
└───────────────────────────────┘
│
│ C_Login(CKU_USER, pin)
│ ✓ 成功
▼
┌───────────────────────────────┐
│ CKS_RO_USER_FUNCTIONS │
│ (只读,User登录) │
└───────────────────────────────┘
│
│ C_Logout()
▼
┌───────────────────────────────┐
│ CKS_RO_PUBLIC_SESSION │
│ (回到未登录) │
└───────────────────────────────┘
C_OpenSession(CKF_SERIAL_SESSION | CKF_RW_SESSION)
│
│ 读写会话
▼
┌───────────────────────────────┐
│ CKS_RW_PUBLIC_SESSION │
│ (读写,未登录) │
└───────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
│ │ │
C_Login(CKU_USER) C_Login(CKU_SO) 无操作
│ │ │
▼ ▼ │
┌─────────────────────┐ ┌─────────────────────┐
│CKS_RW_USER_FUNCTIONS│ │CKS_RW_SO_FUNCTIONS │
│(读写,User登录) │ │(读写,SO登录) │
└─────────────────────┘ └─────────────────────┘
│ │
│ │
└───────┬───────┘
│
│ C_Logout()
▼
┌───────────────────────────────┐
│ CKS_RW_PUBLIC_SESSION │
│ (回到未登录) │
└───────────────────────────────┘
关键约束:
-
SO登录只允许在读写会话
- 在只读会话调用C_Login(CKU_SO)会返回CKR_SESSION_READ_ONLY
-
登录状态全局共享
- 同一应用程序的所有Session共享登录状态
- 在一个Session登录,其他Session状态也会改变
-
关闭最后一个Session回到Public
- 关闭应用程序在Token上的最后一个Session后
- 登录状态回到Public
状态转换的代码验证
让我们用代码验证Session状态:
c
CK_SESSION_HANDLE hSession;
CK_SESSION_INFO info;
CK_RV rv;
/* 打开读写会话 */
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION,
NULL_PTR, NULL_PTR, &hSession);
/* 获取Session信息 */
rv = C_GetSessionInfo(hSession, &info);
printf("Slot ID: %lu\n", info.slotID);
printf("State: %lu\n", info.state); // CKS_RW_PUBLIC_SESSION (2)
printf("Flags: 0x%08lx\n", info.flags); // CKF_SERIAL_SESSION | CKF_RW_SESSION
/* User登录 */
rv = C_Login(hSession, CKU_USER, userPin, pinLen);
/* 再次获取Session信息 */
rv = C_GetSessionInfo(hSession, &info);
printf("State: %lu\n", info.state); // CKS_RW_USER_FUNCTIONS (3)
/* SO登录(需要先Logout) */
rv = C_Logout(hSession);
rv = C_Login(hSession, CKU_SO, soPin, soPinLen);
rv = C_GetSessionInfo(hSession, &info);
printf("State: %lu\n", info.state); // CKS_RW_SO_FUNCTIONS (4)
不同状态下的操作权限
每种状态允许不同的操作:
状态权限矩阵:
状态权限矩阵:
操作类型 R/O Public R/O User R/W Public R/W User R/W SO
───────────────────────────────────────────────────────────────────────────────
读取公开对象 ✓ ✓ ✓ ✓ ✓
读取私有对象 ✗ ✓ ✗ ✓ ✗
创建Session对象 ✓ ✓ ✓ ✓ ✓
创建Token对象 ✗ ✗ ✗ ✓ ✓
修改Token对象 ✗ ✗ ✗ ✓ ✓
删除Token对象 ✗ ✗ ✗ ✓ ✓
密码运算(公开密钥) ✓ ✓ ✓ ✓ ✓
密码运算(私有密钥) ✗ ✓ ✗ ✓ ✗
初始化PIN ✗ ✗ ✗ ✗ ✓
修改PIN ✗ ✗ ✗ ✓ ✗
───────────────────────────────────────────────────────────────────────────────
注意:SO登录状态下,SO不能访问私有对象(User的密钥)。SO的职责是管理Token,不是使用密钥。
并发Session:一个Token可以有多个会话
PKCS#11允许应用程序在一个Token上打开多个Session。
并发Session的用途:
- 分工协作:一个Session做管理(读写),另一个Session做运算(只读)
- 性能优化:多线程环境下,每个线程一个Session
- 角色分离:一个Session登录User,另一个Session保持Public
Session数量限制:
c
CK_TOKEN_INFO tokenInfo;
rv = C_GetTokenInfo(slotID, &tokenInfo);
printf("最大Session数: %lu\n", tokenInfo.ulMaxSessionCount);
printf("当前Session数: %lu\n", tokenInfo.ulSessionCount);
printf("最大读写Session数: %lu\n", tokenInfo.ulMaxRwSessionCount);
printf("当前读写Session数: %lu\n", tokenInfo.ulRwSessionCount);
/* 特殊值 */
if (tokenInfo.ulMaxSessionCount == CK_EFFECTIVELY_INFINITE) {
printf("Session数无限制\n");
}
并发Session的登录状态共享
这是一个容易混淆的概念:同一应用程序的所有Session共享登录状态。
登录状态共享示例:
应用程序 A
│
├── Session 1(读写)
│ state = CKS_RW_PUBLIC_SESSION
│
├── Session 2(只读)
│ state = CKS_RO_PUBLIC_SESSION
│
│ C_Login(Session 1, CKU_USER, pin)
│
├── Session 1
│ state = CKS_RW_USER_FUNCTIONS ← 改变
│
└── Session 2
state = CKS_RO_USER_FUNCTIONS ← 也改变!
同一个应用程序的所有Session共享登录状态。
设计意图:
这个设计是为了安全:
- 防止攻击者打开多个Session,只登录一个就获得访问权限
- 确保整个应用程序的认证状态一致
代码验证:
c
CK_SESSION_HANDLE hSession1, hSession2;
CK_SESSION_INFO info1, info2;
/* 打开两个Session */
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION,
NULL_PTR, NULL_PTR, &hSession1);
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION,
NULL_PTR, NULL_PTR, &hSession2);
/* 查看初始状态 */
rv = C_GetSessionInfo(hSession1, &info1);
rv = C_GetSessionInfo(hSession2, &info2);
printf("Session1 state: %lu\n", info1.state); // 2 (R/W Public)
printf("Session2 state: %lu\n", info2.state); // 0 (R/O Public)
/* 在Session1登录 */
rv = C_Login(hSession1, CKU_USER, userPin, pinLen);
/* 再次查看状态 */
rv = C_GetSessionInfo(hSession1, &info1);
rv = C_GetSessionInfo(hSession2, &info2);
printf("Session1 state: %lu\n", info1.state); // 3 (R/W User)
printf("Session2 state: %lu\n", info2.state); // 1 (R/O User) ← 也变了!
多应用程序的Session隔离
不同应用程序之间的Session是隔离的:
多应用程序Session隔离:
应用程序 A
│
├── Session A1 ────── 登录User
│ state = CKS_RW_USER_FUNCTIONS
│
├── Session A2
│ state = CKS_RO_USER_FUNCTIONS ← 共享A的登录状态
│
应用程序 B
│
├── Session B1 ────── 未登录
│ state = CKS_RW_PUBLIC_SESSION ← 不受A影响
│
├── Session B2
│ state = CKS_RW_PUBLIC_SESSION ← 共享B的登录状态
│
A和B的Session是隔离的,登录状态不共享。
"应用程序"的定义:
PKCS#11中,"应用程序"的定义取决于平台:
- Linux/Unix:同一进程及其子进程
- Windows:同一进程
Session关闭的影响
关闭Session有以下影响:
C_CloseSession的影响:
1. Session Object被销毁
- CKA_TOKEN = FALSE的对象被删除
- Token对象(CKA_TOKEN = TRUE)保留
2. 当前操作被中止
- 正在进行的签名、加密等操作被取消
3. 如果是最后一个Session
- 登录状态回到Public
- 应用程序与Token的连接断开
代码示例:
c
/* 创建Session Object */
CK_ATTRIBUTE template[] = {
{CKA_CLASS, &secretKeyClass, sizeof(secretKeyClass)},
{CKA_KEY_TYPE, &aesKeyType, sizeof(aesKeyType)},
{CKA_TOKEN, &bFalse, sizeof(bFalse)}, /* Session Object */
{CKA_VALUE, keyData, 32},
};
rv = C_CreateObject(hSession, template, 4, &hKey);
/* 关闭Session */
rv = C_CloseSession(hSession);
/* hKey失效!Session Object被销毁 */
/* 如果再次打开Session,hKey不可用 */
C_CloseAllSessions:一次性清理
C_CloseAllSessions关闭应用程序在指定Token上的所有Session:
c
CK_RV C_CloseAllSessions(CK_SLOT_ID slotID);
使用场景:
- 应用程序退出前清理
- 错误恢复:重置所有连接
- 测试环境:清理测试Session
c
/* 应用程序退出清理 */
void cleanup(void) {
C_CloseAllSessions(slotID);
C_Finalize(NULL_PTR);
}
/* 错误恢复 */
void error_recovery(void) {
/* 关闭所有Session,重置状态 */
C_CloseAllSessions(slotID);
/* 重新打开Session */
C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION,
NULL_PTR, NULL_PTR, &hSession);
C_Login(hSession, CKU_USER, userPin, pinLen);
}
Session并发设计模式
模式一:管理Session + 运算Session
c
CK_SESSION_HANDLE hManageSession, hComputeSession;
/* 管理Session:读写,用于创建密钥 */
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION,
NULL_PTR, NULL_PTR, &hManageSession);
rv = C_Login(hManageSession, CKU_USER, userPin, pinLen);
/* 运算Session:只读,用于密码运算 */
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION,
NULL_PTR, NULL_PTR, &hComputeSession);
/* hComputeSession已经是CKS_RO_USER_FUNCTIONS(共享登录状态) */
/* 在管理Session创建密钥 */
rv = C_CreateObject(hManageSession, template, count, &hKey);
/* 在运算Session使用密钥 */
rv = C_SignInit(hComputeSession, &mechanism, hKey);
rv = C_Sign(hComputeSession, data, dataLen, sig, &sigLen);
模式二:多线程Session
c
/* 每个线程一个Session */
void* worker_thread(void* arg) {
CK_SESSION_HANDLE hSession;
CK_RV rv;
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION,
NULL_PTR, NULL_PTR, &hSession);
/* 执行密码运算 */
rv = C_SignInit(hSession, &mechanism, hKey);
rv = C_Sign(hSession, data, dataLen, sig, &sigLen);
rv = C_CloseSession(hSession);
return NULL;
}
/* 主线程先登录 */
rv = C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION,
NULL_PTR, NULL_PTR, &hMainSession);
rv = C_Login(hMainSession, CKU_USER, userPin, pinLen);
/* 工作线程打开的Session自动共享登录状态 */
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, worker_thread, NULL);
}
一个类比:银行的多窗口系统
让我们用银行类比理解并发Session:
银行多窗口系统类比:
银行大厅(Token)
│
├── 窗口1(Session 1,读写)
│ 客户已出示身份证(User登录)
│ 可以存取物品(创建/删除Token对象)
│ 可以办理业务(密码运算)
│
├── 窗口2(Session 2,只读)
│ 共享客户身份(共享登录状态)
│ 只能查看物品(只读)
│ 可以办理业务(密码运算)
│
├── 窗口3(Session 3,读写)
│ 管理员已出示工作证(SO登录)
│ 可以初始化保险箱(初始化PIN)
│ 不能查看客户物品(SO不能访问私有对象)
│
└── 窗口4(Session 4,只读)
客户未出示身份证(未登录)
只能查看公开物品(公开对象)
不能办理需要身份的业务
同一个客户的所有窗口共享身份状态。
本篇小结
今天我们深入分析了Session的状态与并发管理。
CK_SESSION_INFO结构:
- slotID:所属Slot
- state:当前状态(5种)
- flags:Session标志
- ulDeviceError:设备错误码
五种Session状态:
- CKS_RO_PUBLIC_SESSION:只读,未登录
- CKS_RO_USER_FUNCTIONS:只读,User登录
- CKS_RW_PUBLIC_SESSION:读写,未登录
- CKS_RW_USER_FUNCTIONS:读写,User登录
- CKS_RW_SO_FUNCTIONS:读写,SO登录
状态转换规则:
- C_OpenSession决定读/写类型
- C_Login改变登录状态
- C_Logout回到未登录
- SO登录只允许在读写会话
并发Session:
- 一个Token可以有多个Session
- 同一应用程序的Session共享登录状态
- 不同应用程序的Session隔离
- 关闭最后一个Session回到Public状态
下一节,我们将详细解析Object与属性------密码资源的"数据容器"。
【下集预告】
Object是什么?
密钥、证书、数据如何被存储?
CK_ATTRIBUTE结构如何工作?
属性模板如何定义Object特征?
下一节,Object与属性。