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 │────┘
│ (回到未登录) │
└──────────────────┘
关键约束:
- SO登录只能在读写会话中进行(只读会话会返回CKR_SESSION_READ_ONLY)
- 同一应用程序的所有会话共享登录状态(登录一次影响所有会话)
- 关闭最后一个会话后,登录状态回到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管理函数。