HSM技术精讲(3.4):Session状态与并发管理——PKCS#11的“会话状态机“

3.4 Session状态与并发管理:PKCS#11的"会话状态机"

📚 本文内容摘自本人的开源书《HSM技术书 - 从思想实验到安全基石》

🔗 在线阅读/下载:hsm-book

bash 复制代码
git clone https://github.com/Lularible/hsm-book.git

⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。

为什么需要理解Session状态?

Session不仅仅是"连接"------它是有状态的。

理解Session状态,对于以下场景至关重要:

  1. 调试问题:为什么C_CreateObject失败?可能是Session是只读状态
  2. 权限控制:为什么C_Login(CKU_SO)失败?可能是Session不是读写状态
  3. 并发设计:多个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        │
               │  (回到未登录)                 │
               └───────────────────────────────┘

关键约束

  1. SO登录只允许在读写会话

    • 在只读会话调用C_Login(CKU_SO)会返回CKR_SESSION_READ_ONLY
  2. 登录状态全局共享

    • 同一应用程序的所有Session共享登录状态
    • 在一个Session登录,其他Session状态也会改变
  3. 关闭最后一个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的用途

  1. 分工协作:一个Session做管理(读写),另一个Session做运算(只读)
  2. 性能优化:多线程环境下,每个线程一个Session
  3. 角色分离:一个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);

使用场景

  1. 应用程序退出前清理
  2. 错误恢复:重置所有连接
  3. 测试环境:清理测试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与属性。