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.2 核心对象模型:Slot、Token、Session、Object的“四重奏“

银行保险箱系统的“四重奏“

让我们继续用银行保险箱的比喻。

一个完整的银行保险箱系统,有四个关键概念:

  1. 服务窗口:客户与保险箱系统交互的入口。窗口可以有多个,每个窗口可以服务不同的保险箱库。
  2. 保险箱库:存储客户物品的物理空间。一个窗口对应一个库。
  3. 访问记录:客户每次访问库时,建立一条记录。记录包含访问时间、访问权限、操作类型。
  4. 保险箱内容:库里存储的具体物品(文件、珠宝、证书等)。

这四个概念,对应了PKCS#11的核心对象:

银行概念PKCS#11概念含义
服务窗口SlotHSM的物理插槽/逻辑接口
保险箱库TokenHSM的逻辑实例/密钥存储区
访问记录Session与Token的交互会话
保险箱内容Object密钥、证书等密码对象

Slot:密码世界的“服务窗口“

Slot是PKCS#11的第一个概念。

Slot的定义

Slot(插槽)是一个逻辑概念,代表一个密码令牌可以插入的位置。

Slot可以是:

  • 物理Slot:HSM硬件的实际插槽(如智能卡读卡器的插槽)
  • 逻辑Slot:HSM软件模拟的逻辑接口(如SoftHSM的虚拟Slot)

Slot的特点

  1. Slot有编号:Slot ID,通常是0、1、2…的整数
  2. Slot可以包含Token:一个Slot可以插入一个Token(也可以没有Token)
  3. Slot有状态:Token存在或Token不存在
  4. 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的特点

  1. Token存储密码对象:密钥、证书、数据对象都存储在Token中
  2. Token有访问控制:需要PIN码登录才能访问私有对象
  3. Token有状态:初始化状态、正常状态、锁定状态等
  4. 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交互时,需要:

  1. 打开一个Session
  2. 在Session中执行操作
  3. 关闭Session

Session的特点

  1. Session绑定Token:Session与特定的Token绑定
  2. Session有状态:读/写、登录/未登录
  3. Session有权限:基于登录状态决定可执行的操作
  4. 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_CLASSObject类型公钥/私钥/对称密钥等
CKA_KEY_TYPE密钥类型RSA/ECC/AES等
CKA_LABEL标签用户定义的标签
CKA_IDID用户定义的ID
CKA_TOKEN是否存储在Token中TRUE=持久存储
CKA_PRIVATE是否私有TRUE=需要登录才能访问
CKA_SENSITIVE是否敏感TRUE=不可导出
CKA_EXTRACTABLE是否可导出FALSE=不能导出
CKA_MODIFIABLE是否可修改TRUE=属性可修改
CKA_DESTROYABLE是否可销毁TRUE=可以销毁
CKA_VALUE密钥值密钥的实际值
CKA_MODULUSRSA模数RSA密钥的模数
CKA_PUBLIC_EXPONENTRSA公钥指数RSA公钥的指数
CKA_PRIVATE_EXPONENTRSA私钥指数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(不存在)

关系的要点

  1. Slot与Token:Slot是位置,Token是内容。一个Slot可以有一个Token,也可以没有。
  2. Token与Session:Session是Token的交互。一个Token可以有多个Session。
  3. Session与Object:Object通过Session访问。Session决定了Object的访问权限。
  4. 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这些属性如何工作?

下一节,安全哲学。