3.3 PKCS#11的安全哲学:"提供机制而非策略"的深意
📚 本文内容摘自本人的开源书《HSM技术书 - 从思想实验到安全基石》
🔗 在线阅读/下载:hsm-book
bash
git clone https://github.com/Lularible/hsm-book.git
⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。
一个关键的设计决策
PKCS#11的设计哲学中,最关键的一句话是:
"提供机制而非策略"(Provide mechanisms, not policies)
这句话是什么意思?
机制:技术性的功能实现。PKCS#11定义了密钥生成、加密、签名等功能的具体实现方式。
策略:管理性的安全规则。PKCS#11不定义"什么密钥可以导出"、"密钥应该多久更换"等管理规则。
PKCS#11把这个决策留给了部署者(使用HSM的组织)。部署者通过密钥属性来定义自己的安全策略。
为什么这样设计?
让我们思考这个问题。
假设PKCS#11定义了一个策略:"私钥永远不能导出"。
这个策略看起来很安全。但它有问题:
问题一:限制了合法需求
有些场景需要导出私钥:
- 密钥备份:为了防止HSM故障导致密钥丢失,需要备份私钥
- 密钥迁移:更换HSM时,需要迁移私钥
- 密钥托管:法律要求某些密钥由第三方托管
如果PKCS#11强制禁止导出,这些合法需求无法满足。
问题二:没有考虑场景差异
不同场景的安全要求不同:
- 金融场景:密钥绝对不能导出,最高安全
- 企业场景:密钥可以备份,中等安全
- 个人场景:密钥可以导出,方便使用
如果PKCS#11定义统一的策略,无法适应不同场景。
问题三:策略可能过时
安全策略需要根据威胁形势调整。如果PKCS#11定义了固定策略,可能:
- 一开始被认为是安全的
- 后来发现不安全,但标准难以修改
- 或者反过来:一开始被认为不安全,后来被认为是必要的
所以,PKCS#11的设计选择是:
提供机制(技术功能),让部署者自己定义策略(安全规则)。
密钥属性:策略的载体
部署者如何定义策略?
通过密钥属性。
PKCS#11定义了一系列密钥属性,部署者通过设置这些属性来定义安全策略。
核心安全属性:
| 属性 | 含义 | TRUE | FALSE |
|---|---|---|---|
| CKA_SENSITIVE | 密钥是否敏感 | 密钥值不可通过C_GetAttributeValue读取 | 密钥值可读取 |
| CKA_EXTRACTABLE | 密钥是否可导出 | 密钥可通过C_WrapKey导出 | 密钥不可导出 |
| CKA_ALWAYS_SENSITIVE | 密钥是否始终敏感 | 自创建以来CKA_SENSITIVE始终为TRUE | 可以修改CKA_SENSITIVE |
| CKA_NEVER_EXTRACTABLE | 密钥是否从不可导出 | 自创建以来CKA_EXTRACTABLE始终为FALSE | 可以修改CKA_EXTRACTABLE |
| CKA_PRIVATE | 密钥是否私有 | 密钥需要登录才能访问 | 密钥公开,不需要登录 |
| CKA_LOCAL | 密钥是否在Token内生成 | 密钥由Token内部生成 | 密钥由外部导入 |
| CKA_MODIFIABLE | 密钥属性是否可修改 | 属性可以修改 | 属性不可修改 |
| CKA_DESTROYABLE | 密钥是否可销毁 | 密钥可以销毁 | 密钥不可销毁 |
| CKA_COPYABLE | 密钥是否可复制 | 密钥可以复制 | 密钥不可复制 |
| CKA_TRUSTED | 密钥是否可信 | 密钥是可信的(需要SO权限设置) | 密钥不可信 |
这些属性的组合,定义了密钥的安全边界。
属性组合的安全等级
让我们看看不同属性组合的安全等级。
等级一:最高安全(金融级别)
最高安全密钥属性组合:
CKA_SENSITIVE = TRUE // 密钥值不可读取
CKA_EXTRACTABLE = FALSE // 密钥不可导出
CKA_ALWAYS_SENSITIVE = TRUE // 创建时就是敏感的
CKA_NEVER_EXTRACTABLE = TRUE // 创建时就是不可导出的
CKA_PRIVATE = TRUE // 需要登录才能访问
CKA_LOCAL = TRUE // 在Token内部生成
CKA_MODIFIABLE = FALSE // 属性不可修改
CKA_DESTROYABLE = FALSE // 密钥不可销毁
CKA_COPYABLE = FALSE // 密钥不可复制
这个组合的密钥:
- 只能在Token内部生成
- 永远不能导出
- 永远不能读取值
- 永远不能修改属性
- 永远不能销毁
- 永远不能复制
这意味着密钥"锁死"在Token内,只能通过Token执行密码运算。
适合金融、军事等最高安全场景。
等级二:高安全(企业级别)
高安全密钥属性组合:
CKA_SENSITIVE = TRUE // 密钥值不可读取
CKA_EXTRACTABLE = FALSE // 密钥不可导出
CKA_ALWAYS_SENSITIVE = TRUE // 创建时就是敏感的
CKA_NEVER_EXTRACTABLE = TRUE // 创建时就是不可导出的
CKA_PRIVATE = TRUE // 需要登录才能访问
CKA_LOCAL = TRUE // 在Token内部生成
CKA_MODIFIABLE = FALSE // 属性不可修改
CKA_DESTROYABLE = TRUE // 密钥可以销毁
CKA_COPYABLE = FALSE // 密钥不可复制
这个组合的密钥:
- 只能在Token内部生成
- 永远不能导出
- 永远不能读取值
- 永远不能修改属性
- 可以销毁(允许密钥轮换)
- 永远不能复制
适合企业、政府等高安全场景。
等级三:中等安全(备份级别)
中等安全密钥属性组合:
CKA_SENSITIVE = TRUE // 密钥值不可读取
CKA_EXTRACTABLE = TRUE // 密钥可以导出(用于备份)
CKA_ALWAYS_SENSITIVE = TRUE // 创建时就是敏感的
CKA_PRIVATE = TRUE // 需要登录才能访问
CKA_LOCAL = TRUE // 在Token内部生成
CKA_MODIFIABLE = FALSE // 属性不可修改
CKA_DESTROYABLE = TRUE // 密钥可以销毁
CKA_COPYABLE = TRUE // 密钥可以复制(用于备份)
这个组合的密钥:
- 只能在Token内部生成
- 可以导出(用于备份)
- 密钥值不可直接读取,但可以通过C_WrapKey导出
- 可以销毁
- 可以复制
适合需要备份、迁移的场景。
等级四:低安全(开发级别)
低安全密钥属性组合:
CKA_SENSITIVE = FALSE // 密钥值可读取(调试方便)
CKA_EXTRACTABLE = TRUE // 密钥可以导出
CKA_PRIVATE = FALSE // 不需要登录(开发方便)
CKA_MODIFIABLE = TRUE // 属性可修改
CKA_DESTROYABLE = TRUE // 密钥可以销毁
这个组合的密钥:
- 密钥值可以直接读取
- 可以导出
- 不需要登录
- 属性可修改
适合开发、测试场景。不适合生产环境。
属性的不可逆设计
PKCS#11设计了一个关键的安全机制:属性的不可逆性。
某些属性一旦设置为TRUE或FALSE,就不能再修改。
不可逆属性规则:
不可逆属性规则:
规则一:CKA_ALWAYS_SENSITIVE
- 如果创建时设置为TRUE,密钥永远保持CKA_SENSITIVE = TRUE
- 不能修改为CKA_SENSITIVE = FALSE
规则二:CKA_NEVER_EXTRACTABLE
- 如果创建时设置为TRUE,密钥永远保持CKA_EXTRACTABLE = FALSE
- 不能修改为CKA_EXTRACTABLE = TRUE
规则三:CKA_SENSITIVE与CKA_EXTRACTABLE的联动
- 如果CKA_SENSITIVE = TRUE,那么:
- C_GetAttributeValue不能读取密钥值(CKA_VALUE)
- C_WrapKey仍然可以导出密钥(如果CKA_EXTRACTABLE = TRUE)
这个设计确保:
密钥的安全等级在创建时决定,之后不能"降级"。
比如,你创建了一个最高安全密钥(CKA_NEVER_EXTRACTABLE = TRUE)。之后,你无法把CKA_EXTRACTABLE修改为TRUE。密钥永远不能导出。
这个设计防止了攻击者通过修改属性来降低密钥安全等级。
一个类比:保险箱的安全等级
让我用保险箱的类比来理解属性组合。
保险箱的安全等级设计:
保险箱安全等级:
等级一:最高安全(银行金库)
├── 钥匙在金库内生成(不来自外部)
├── 钥匙永不取出(不可导出)
├── 钥匙永不复制(不可复制)
├── 钥匙永不可销毁(不可销毁)
├── 需要双人授权才能打开(需要登录)
├── 所有操作记录审计(审计)
└── 钥匙在金库内使用,只返回操作结果
等级二:高安全(银行保险箱)
├── 钥匙在保险箱内生成
├── 钥匙永不取出
├── 需要客户授权才能打开
├── 客户可以销毁钥匙(更换保险箱)
└── 所有操作记录审计
等级三:中等安全(办公室保险箱)
├── 钥匙在保险箱内生成
├── 钥匙可以取出(备份)
├── 需要员工授权才能打开
├── 员工可以销毁钥匙
└── 操作记录可选
等级四:低安全(个人保险箱)
├── 钥匙可以外部导入
├── 钥匙可以取出
├── 不需要授权(方便)
└── 无操作记录
这些安全等级对应PKCS#11的属性组合:
- 最高安全 → CKA_NEVER_EXTRACTABLE = TRUE, CKA_DESTROYABLE = FALSE
- 高安全 → CKA_NEVER_EXTRACTABLE = TRUE, CKA_DESTROYABLE = TRUE
- 中等安全 → CKA_EXTRACTABLE = TRUE, CKA_SENSITIVE = TRUE
- 低安全 → CKA_SENSITIVE = FALSE, CKA_PRIVATE = FALSE
C_WrapKey与C_UnwrapKey:安全的密钥导出
密钥导出是一个敏感操作。PKCS#11如何安全地导出密钥?
答案是通过密钥包装(Key Wrapping)。
密钥包装的原理:
密钥包装不是直接导出密钥值,而是用另一个密钥加密要导出的密钥。
密钥包装流程:
要导出的密钥(被包装密钥)
│
│ C_WrapKey(hSession, hWrappingKey, hKeyToBeWrapped, ...)
▼
用包装密钥加密被包装密钥
│
▼
包装后的密钥(加密的数据)
│
│ 导出
▼
外部存储/传输
│
│ C_UnwrapKey(hSession, hUnwrappingKey, pWrappedKey, ...)
▼
用解包密钥解密
│
▼
在Token内创建新密钥(恢复被包装密钥)
密钥包装的特点:
- 被包装密钥被加密:导出的数据是加密的,不是明文密钥
- 包装密钥需要授权:只有拥有包装密钥访问权限的人才能执行包装
- 解包后密钥在Token内:解包操作在Token内执行,密钥值不暴露
- 可以设置属性:解包时可以设置新密钥的属性
密钥包装的代码示例:
c
CK_OBJECT_HANDLE hWrappingKey; // 包装密钥(AES密钥)
CK_OBJECT_HANDLE hKeyToWrap; // 要包装的密钥
CK_BYTE wrappedKey[256]; // 包装后的密钥数据
CK_ULONG wrappedKeyLen = 256; // 包装数据长度
// 包装密钥
CK_MECHANISM wrapMechanism = {CKM_AES_KEY_WRAP, NULL_PTR, 0};
rv = C_WrapKey(hSession, &wrapMechanism, hWrappingKey, hKeyToWrap, wrappedKey, &wrappedKeyLen);
if (rv != CKR_OK) {
printf("密钥包装失败\n");
return rv;
}
// 包装后的密钥数据可以存储到文件或传输到另一个Token
// 在另一个Token上解包
CK_OBJECT_HANDLE hUnwrappedKey;
CK_ATTRIBUTE unwrapTemplate[] = {
{CKA_CLASS, &keyClass, sizeof(keyClass)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
{CKA_TOKEN, &bTrue, sizeof(bTrue)},
{CKA_SENSITIVE, &bTrue, sizeof(bTrue)},
{CKA_EXTRACTABLE, &bFalse, sizeof(bFalse)},
};
rv = C_UnwrapKey(hSession, &wrapMechanism, hUnwrappingKey, wrappedKey, wrappedKeyLen, unwrapTemplate, 5, &hUnwrappedKey);
if (rv != CKR_OK) {
printf("密钥解包失败\n");
return rv;
}
密钥包装的安全意义:
密钥包装解决了"密钥导出"的安全问题:
- 密钥导出时不是明文,而是加密数据
- 加密数据需要另一个密钥才能解密
- 解密操作在Token内执行,密钥值不暴露
- 可以控制新密钥的属性
所以,CKA_EXTRACTABLE = TRUE的密钥,可以通过C_WrapKey导出。但导出的数据是加密的,需要另一个密钥才能恢复。这比直接导出密钥值更安全。
本篇小结
今天我们分析了PKCS#11的安全哲学。
核心原则:"提供机制而非策略"------PKCS#11提供技术功能,部署者定义安全策略。
部署者通过密钥属性定义策略:
- CKA_SENSITIVE:密钥值是否可读取
- CKA_EXTRACTABLE:密钥是否可导出
- CKA_ALWAYS_SENSITIVE:密钥是否始终敏感
- CKA_NEVER_EXTRACTABLE:密钥是否从不可导出
- CKA_PRIVATE:密钥是否需要登录
- CKA_MODIFIABLE:属性是否可修改
- CKA_DESTROYABLE:密钥是否可销毁
不同的属性组合对应不同安全等级:
- 最高安全:金融级别,密钥锁死在Token内
- 高安全:企业级别,密钥可以销毁但不能导出
- 中等安全:备份级别,密钥可以包装导出
- 低安全:开发级别,密钥值可读取
属性的不可逆设计:一旦设置为TRUE或FALSE,不能再修改。这防止密钥安全等级降级。
密钥包装(C_WrapKey/C_UnwrapKey):安全的密钥导出方式。密钥导出时被加密,需要另一个密钥才能解密恢复。
下一节,我们将深入Session状态管理------看Session如何成为有状态的"会话状态机"。
【下集预告】
Session不仅仅是"连接"------它是有状态的。
CK_SESSION_INFO结构是什么?
Session有哪些状态?读/写、登录/未登录如何转换?
多个Session如何并发工作?
下一节,Session状态与并发管理。