Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.11 Session管理函数:应用程序与Token的“握手协议“

Session管理函数概览

PKCS#11规范第5.6节定义了Session管理函数,控制应用程序与Token之间的连接和认证状态。

Session管理函数列表:

函数名           作用
─────────────────────────────────────
C_OpenSession    打开与应用程序的会话
C_CloseSession   关闭单个会话
C_CloseAllSessions 关闭所有会话
C_GetSessionInfo  获取会话状态信息
C_Login          登录Token(用户认证)
C_Logout         登出Token
C_InitPIN        初始化用户PIN
C_SetPIN         修改PIN

Session管理是PKCS#11应用程序的“入口门户“——任何密码操作都必须先打开Session,必要时还要登录认证。


C_OpenSession:打开会话

函数原型

CK_RV C_OpenSession(
    CK_SLOT_ID slotID,        // Slot ID
    CK_FLAGS flags,           // 会话类型标志
    CK_VOID_PTR pApplication, // 应用程序指针(回调用)
    CK_NOTIFY Notify,         // 回调函数
    CK_SESSION_HANDLE_PTR phSession // 返回会话句柄
);

flags参数详解

flags标志位组合:

CKF_SERIAL_SESSION  必须设置(历史遗留,必须为1)
CKF_RW_SESSION      可选(设置=读写会话,不设置=只读会话)

典型组合:
- CKF_SERIAL_SESSION           只读会话(R/O)
- CKF_SERIAL_SESSION | CKF_RW_SESSION 读写会话(R/W)

基本用法

CK_SLOT_ID slotID = 0;
CK_SESSION_HANDLE hSession;

/* 打开只读会话 */

CK_RV rv = C_OpenSession(slotID, CKF_SERIAL_SESSION,
                         NULL_PTR, NULL_PTR, &hSession);

/* 打开读写会话 */

CK_RV rv = C_OpenSession(slotID,
                         CKF_SERIAL_SESSION | CKF_RW_SESSION,
                         NULL_PTR, NULL_PTR, &hSession);

只读会话 vs 读写会话

两种会话类型决定了应用程序可以执行的操作范围:

会话类型对比:

                    只读会话(R/O)         读写会话(R/W)
─────────────────────────────────────────────────────────
创建Token对象       不可以                  可以
修改Token对象       不可以                  可以
删除Token对象       不可以                  可以
创建Session对象     可以                    可以
密码运算            可以                    可以
SO登录              不可以                  可以
─────────────────────────────────────────────────────────

设计逻辑

  • 只读会话:适合“消费型“应用程序,只做密码运算,不管理密钥
  • 读写会话:适合“管理型“应用程序,需要创建、导入、删除密钥

C_CloseSession:关闭会话

函数原型

CK_RV C_CloseSession(CK_SESSION_HANDLE hSession);

关闭会话的副作用

C_CloseSession执行后果:

1. Session对象自动销毁
   - CKA_TOKEN = FALSE的对象被删除
   - Token对象(CKA_TOKEN = TRUE)保留

2. 登录状态可能改变
   - 如果是最后一个会话,登录状态回到Public

3. 活动操作被取消
   - 正在进行的签名、加密等操作被中止

重要约束:关闭最后一个会话后,应用程序的登录状态回到Public,任何新打开的会话都是“未登录“状态。


C_CloseAllSessions:关闭所有会话

函数原型

CK_RV C_CloseAllSessions(CK_SLOT_ID slotID);

一次性关闭应用程序在指定Slot上的所有会话。通常用于应用程序退出前清理资源。

/* 应用程序退出清理 */

C_CloseAllSessions(slotID);
C_Finalize(NULL_PTR);

C_Login:登录认证

函数原型

CK_RV C_Login(
    CK_SESSION_HANDLE hSession,  // 会话句柄
    CK_USER_TYPE userType,       // 用户类型
    CK_UTF8CHAR_PTR pPin,        // PIN值
    CK_ULONG ulPinLen            // PIN长度
);

用户类型

CK_USER_TYPE定义:

CKU_SO            Security Officer(安全管理员)
                  - 可以初始化Token
                  - 可以设置PIN
                  - 可以创建/删除Token对象

CKU_USER          Normal User(普通用户)
                  - 可以使用密码功能
                  - 可以访问私有对象
                  - 不能初始化Token

CKU_CONTEXT_SPECIFIC 上下文特定认证
                  - 用于"每次操作前认证"
                  - 配合CKA_ALWAYS_AUTHENTICATE

登录状态转换

登录操作会改变Session的状态:

Session状态转换图:

                    ┌──────────────────┐
                    │  Public Session  │
                    │  (未登录状态)     │
                    └──────────────────┘
                            │
            ┌───────────────┼───────────────┐
            │               │               │
    C_Login(CKU_USER)  C_Login(CKU_SO)  (无)
    (R/O或R/W)        (仅R/W会话)       │
            │               │               │
            ▼               ▼               ▼
    ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
    │R/O User Func │ │R/W SO Func   │ │  保持Public   │
    │(用户只读)    │ │(管理员读写) │ │              │
    └───────────────┘ └───────────────┘ └───────────────┘
            │               │
            │               │
            └───────────────┼───────────────┐
                            │               │
                    C_Logout(hSession)      │
                            │               │
                            ▼               │
                    ┌──────────────────┐    │
                    │  Public Session  │────┘
                    │  (回到未登录)    │
                    └──────────────────┘

关键约束

  1. SO登录只能在读写会话中进行(只读会话会返回CKR_SESSION_READ_ONLY)
  2. 同一应用程序的所有会话共享登录状态(登录一次影响所有会话)
  3. 关闭最后一个会话后,登录状态回到Public

登录示例代码

CK_SESSION_HANDLE hSession;
CK_UTF8CHAR userPin[] = "12345678";
CK_UTF8CHAR soPin[] = "87654321";

/* 普通用户登录 */

rv = C_Login(hSession, CKU_USER, userPin, sizeof(userPin)-1);
if (rv == CKR_OK) {
    printf("用户登录成功\n");
    // 现在可以访问私有对象(CKA_PRIVATE = TRUE)
}

/* 安全管理员登录(需要读写会话) */

rv = C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION,
                   NULL_PTR, NULL_PTR, &hRwSession);
rv = C_Login(hRwSession, CKU_SO, soPin, sizeof(soPin)-1);
if (rv == CKR_OK) {
    printf("SO登录成功\n");
    // 可以初始化Token、设置PIN等
}

CKU_CONTEXT_SPECIFIC:每次操作前认证

某些私钥设置了CKA_ALWAYS_AUTHENTICATE = TRUE,要求每次使用该密钥前都要重新认证。

CKA_ALWAYS_AUTHENTICATE场景:

普通私钥:
┌─────────────────────────────┐
│ C_Login一次                  │
│                             │
│ C_Sign ─────────→ OK        │
│ C_Sign ─────────→ OK        │
│ C_Sign ─────────→ OK        │
└─────────────────────────────┘

带CKA_ALWAYS_AUTHENTICATE的私钥:
┌─────────────────────────────┐
│ C_Login(CKU_USER)           │
│                             │
│ C_Sign ─────────→ OK        │
│                             │
│ C_Login(CKU_CONTEXT_SPECIFIC)│ ← 每次签名前必须重新认证
│ C_Sign ─────────→ OK        │
│                             │
│ C_Login(CKU_CONTEXT_SPECIFIC)│
│ C_Sign ─────────→ OK        │
└─────────────────────────────┘

代码示例

/* 使用需要每次认证的私钥 */

rv = C_Login(hSession, CKU_USER, userPin, pinLen);

/* 第一次签名 */

rv = C_SignInit(hSession, &mechanism, hPrivateKey);
rv = C_Sign(hSession, data, dataLen, signature, &sigLen);

/* 第二次签名前必须重新认证 */

rv = C_Login(hSession, CKU_CONTEXT_SPECIFIC, userPin, pinLen);
rv = C_SignInit(hSession, &mechanism, hPrivateKey);
rv = C_Sign(hSession, data, dataLen, signature, &sigLen);

C_Logout:登出

函数原型

CK_RV C_Logout(CK_SESSION_HANDLE hSession);

登出后果

C_Logout执行后果:

1. 私有对象句柄失效
   - CKA_PRIVATE = TRUE的对象不可访问
   - 但Token对象仍在Token中存在

2. Session状态回到Public
   - 所有会话变成Public状态

3. 密码操作受限
   - 只能使用公开对象
   - 不能使用需要认证的密钥

PIN管理:C_InitPIN与C_SetPIN

C_InitPIN:初始化用户PIN(SO权限)

CK_RV C_InitPIN(
    CK_SESSION_HANDLE hSession,
    CK_UTF8CHAR_PTR pPin,
    CK_ULONG ulPinLen
);

C_SetPIN:修改PIN(用户自己修改)

CK_RV C_SetPIN(
    CK_SESSION_HANDLE hSession,
    CK_UTF8CHAR_PTR pOldPin,   // 旧PIN
    CK_ULONG ulOldLen,
    CK_UTF8CHAR_PTR pNewPin,   // 新PIN
    CK_ULONG ulNewLen
);

使用场景

PIN管理流程:

Token初始化(SO执行):
┌───────────────────────────────────────────┐
│ C_InitToken(slotID, soPin, label)         │
│     ↓                                     │
│ C_Login(CKU_SO)                           │
│     ↓                                     │
│ C_InitPIN(hSession, userPin)  ← 设置用户PIN│
└───────────────────────────────────────────┘

PIN修改(用户执行):
┌───────────────────────────────────────────┐
│ C_Login(CKU_USER)                         │
│     ↓                                     │
│ C_SetPIN(hSession, oldPin, newPin)        │
└───────────────────────────────────────────┘

Protected Authentication Path

某些Token支持“保护认证路径“——用户无需通过应用程序输入PIN,而是直接在Token设备上输入。

保护认证路径类型:

PIN Pad Token:
┌─────────────────────────────────────┐
│         ┌───────────────────┐       │
│ Token   │  █ █ █ █ █ █      │ ← PIN │
│         │  小键盘           │       │
│         └───────────────────┘       │
│                                     │
│ Host调用:C_Login(hSession,         │
│         CKU_USER, NULL_PTR, 0)      │
│                                     │
│ Token提示用户在键盘输入PIN           │
└─────────────────────────────────────┘

指纹识别Token:
┌─────────────────────────────────────┐
│ Token                               │
│         ┌───────────────────┐       │
│         │  指纹扫描器       │       │
│         └───────────────────┘       │
│                                     │
│ Host调用:C_Login(hSession,         │
│         CKU_USER, NULL_PTR, 0)      │
│                                     │
│ Token扫描用户指纹完成认证            │
└─────────────────────────────────────┘

代码处理

CK_TOKEN_INFO tokenInfo;
rv = C_GetTokenInfo(slotID, &tokenInfo);

if (tokenInfo.flags & CKF_PROTECTED_AUTHENTICATION_PATH) {
    /* Token有保护认证路径,不传PIN */

    rv = C_Login(hSession, CKU_USER, NULL_PTR, 0);
    // Token会通过自己的设备(键盘、指纹)完成认证
} else {
    /* 正常PIN认证 */

    rv = C_Login(hSession, CKU_USER, userPin, pinLen);
}

Session并发限制

Token可能限制并发Session数量:

CK_TOKEN_INFO tokenInfo;
rv = C_GetTokenInfo(slotID, &tokenInfo);

/* 检查Session数量限制 */

if (tokenInfo.ulMaxSessionCount == CK_EFFECTIVELY_INFINITE) {
    // 无限制
} else {
    printf("最大Session数: %lu\n", tokenInfo.ulMaxSessionCount);
    printf("当前Session数: %lu\n", tokenInfo.ulSessionCount);
}

/* 打开过多Session会失败 */

rv = C_OpenSession(...);  // 可能返回CKR_SESSION_COUNT

完整Session生命周期示例

/* 完整的Session生命周期 */

CK_SLOT_ID slotID = 0;
CK_SESSION_HANDLE hSession;
CK_RV rv;

/* 1. 打开读写会话 */

rv = C_OpenSession(slotID,
                   CKF_SERIAL_SESSION | CKF_RW_SESSION,
                   NULL_PTR, NULL_PTR, &hSession);
if (rv != CKR_OK) {
    printf("打开会话失败: %d\n", rv);
    return;
}

/* 2. 用户登录 */

CK_UTF8CHAR userPin[] = "12345678";
rv = C_Login(hSession, CKU_USER, userPin, sizeof(userPin)-1);
if (rv != CKR_OK) {
    printf("登录失败: %d\n", rv);
    C_CloseSession(hSession);
    return;
}

/* 3. 执行密码操作 */

// ... C_FindObjects, C_Sign, C_Decrypt ...

/* 4. 登出 */

rv = C_Logout(hSession);

/* 5. 关闭会话 */

rv = C_CloseSession(hSession);

/* 6. 清理(可选) */

C_CloseAllSessions(slotID);
C_Finalize(NULL_PTR);

小结:Session管理函数的设计智慧

Session管理要点回顾:

1. 会话是应用与Token的"桥梁"
   - 任何密码操作都需要Session
   - Session管理应用程序的生命周期

2. 只读vs读写决定权限
   - 只读:只能消费密码服务
   - 读写:可以管理密钥对象

3. 登录状态全局共享
   - 一个应用程序的所有会话共享登录状态
   - 登录一次,所有会话都生效

4. SO与User角色分离
   - SO:管理Token、初始化PIN
   - User:使用密码功能、访问私钥

5. 保护认证路径增强安全
   - PIN不经过应用程序
   - 防止Host端截获PIN

Session管理函数为PKCS#11建立了“安全门户“——通过会话和认证,控制应用程序对Token的访问权限。 下一节,我们将学习Object管理函数——看密钥和证书的管理。

【下集预告】

C_CreateObject怎么创建对象?

C_DestroyObject怎么销毁?

C_FindObjects怎么查找?

下一节,Object管理函数。