3.7 密钥对象与安全属性:密码安全的"四大金刚"
📚 本文内容摘自本人的开源书《HSM技术书 - 从思想实验到安全基石》
🔗 在线阅读/下载:hsm-book
bash
git clone https://github.com/Lularible/hsm-book.git
⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。
密钥对象的分类
PKCS#11规范第4.7节定义了密钥对象的基本分类,第4.8-4.10节详细定义了三种密钥对象:
密钥对象分类:
CKO_PUBLIC_KEY(公钥对象)
├── RSA公钥
├── DSA公钥
├── EC公钥(ECDSA/ECDH)
├── DH公钥
└── 其他公钥
CKO_PRIVATE_KEY(私钥对象)
├── RSA私钥
├── DSA私钥
├── EC私钥
├── DH私钥
└── 其他私钥
CKO_SECRET_KEY(对称密钥对象)
├── AES密钥
├── DES/3DES密钥
├── HMAC密钥
├── 国密SM4密钥
└── 其他对称密钥
每种密钥对象都有专属属性,但所有密钥对象共享一组核心安全属性------我们称之为"四大金刚"。
四大安全属性:密钥的"安全边界"
PKCS#11定义了四个关键的安全属性,控制密钥的导出和读取:
| 属性 | 控制什么 | TRUE | FALSE |
|---|---|---|---|
| CKA_SENSITIVE | 密钥值是否可读取 | C_GetAttributeValue无法读取密钥值 | 可以读取密钥值 |
| CKA_EXTRACTABLE | 密钥是否可导出 | 可以通过C_WrapKey导出 | 不能导出 |
| CKA_ALWAYS_SENSITIVE | 是否"始终敏感" | 创建时敏感,永远保持敏感 | 可以修改CKA_SENSITIVE |
| CKA_NEVER_EXTRACTABLE | 是否"从不可导出" | 创建时不可导出,永远保持不可导出 | 可以修改CKA_EXTRACTABLE |
这四个属性的组合,定义了密钥的安全边界。
CKA_SENSITIVE:密钥值读取的闸门
定义:密钥值是否敏感(不可通过C_GetAttributeValue读取)。
适用范围:私钥对象、对称密钥对象(公钥对象通常CKA_SENSITIVE = FALSE)
行为:
c
/* CKA_SENSITIVE = TRUE 时的行为 */
CK_ATTRIBUTE getValueAttr = {CKA_VALUE, NULL_PTR, 0};
rv = C_GetAttributeValue(hSession, hPrivateKey, &getValueAttr, 1);
// 返回CKR_ATTRIBUTE_SENSITIVE,无法读取密钥值
/* CKA_SENSITIVE = FALSE 时的行为 */
CK_ATTRIBUTE getValueAttr = {CKA_VALUE, keyBuf, 0};
rv = C_GetAttributeValue(hSession, hPrivateKey, &getValueAttr, 1);
// 成功,可以读取密钥值(但这是不安全的!)
安全意义:
- TRUE:密钥值"锁在Token内",外部无法读取
- FALSE:密钥值可以读取(不安全,只适合开发测试)
典型配置:
- 私钥:CKA_SENSITIVE = TRUE(必须)
- 对称密钥:CKA_SENSITIVE = TRUE(推荐)
- 公钥:CKA_SENSITIVE = FALSE(公钥本意是公开)
CKA_EXTRACTABLE:密钥导出的开关
定义:密钥是否可以通过C_WrapKey导出(包装)。
行为:
c
/* CKA_EXTRACTABLE = TRUE 时的行为 */
CK_BYTE wrappedKey[256];
CK_ULONG wrappedKeyLen = 256;
rv = C_WrapKey(hSession, &wrapMech, hWrappingKey, hKeyToWrap, wrappedKey, &wrappedKeyLen);
// 成功,密钥被包装导出(但导出的是加密数据,不是明文密钥)
/* CKA_EXTRACTABLE = FALSE 时的行为 */
rv = C_WrapKey(hSession, &wrapMech, hWrappingKey, hKeyToWrap, wrappedKey, &wrappedKeyLen);
// 返回CKR_ACTION_PROHIBITED,密钥不能导出
安全意义:
- TRUE:密钥可以包装导出(用于备份、迁移)
- FALSE:密钥永远不能导出(最高安全)
典型配置:
- 最高安全密钥:CKA_EXTRACTABLE = FALSE
- 需备份密钥:CKA_EXTRACTABLE = TRUE
CKA_ALWAYS_SENSITIVE与CKA_NEVER_EXTRACTABLE:属性的"不可逆锁"
这两个属性是PKCS#11的安全精髓------一旦设置,永远不能改变。
CKA_ALWAYS_SENSITIVE:
如果创建密钥时设置CKA_ALWAYS_SENSITIVE = TRUE:
- 密钥创建时就CKA_SENSITIVE = TRUE
- CKA_SENSITIVE永远不能改为FALSE
- 密钥值永远不能读取
c
/* 创建"始终敏感"的密钥 */
CK_BBOOL bAlwaysSensitive = TRUE;
CK_BBOOL bSensitive = TRUE;
CK_ATTRIBUTE template[] = {
{CKA_CLASS, &privKeyClass, sizeof(privKeyClass)},
{CKA_SENSITIVE, &bSensitive, sizeof(bSensitive)},
{CKA_ALWAYS_SENSITIVE, &bAlwaysSensitive, sizeof(bAlwaysSensitive)},
};
rv = C_CreateObject(hSession, template, 3, &hKey);
/* 之后尝试修改CKA_SENSITIVE */
CK_BBOOL bNewSensitive = FALSE;
CK_ATTRIBUTE setTemplate[] = {{CKA_SENSITIVE, &bNewSensitive, sizeof(bNewSensitive)}};
rv = C_SetAttributeValue(hSession, hKey, setTemplate, 1);
// 返回CKR_ACTION_PROHIBITED,无法修改
CKA_NEVER_EXTRACTABLE:
如果创建密钥时设置CKA_NEVER_EXTRACTABLE = TRUE:
- 密钥创建时就CKA_EXTRACTABLE = FALSE
- CKA_EXTRACTABLE永远不能改为TRUE
- 密钥永远不能导出
c
/* 创建"从不可导出"的密钥 */
CK_BBOOL bNeverExtractable = TRUE;
CK_BBOOL bExtractable = FALSE;
CK_ATTRIBUTE template[] = {
{CKA_CLASS, &privKeyClass, sizeof(privKeyClass)},
{CKA_EXTRACTABLE, &bExtractable, sizeof(bExtractable)},
{CKA_NEVER_EXTRACTABLE, &bNeverExtractable, sizeof(bNeverExtractable)},
};
rv = C_CreateObject(hSession, template, 3, &hKey);
/* 之后尝试修改CKA_EXTRACTABLE */
CK_BBOOL bNewExtractable = TRUE;
CK_ATTRIBUTE setTemplate[] = {{CKA_EXTRACTABLE, &bNewExtractable, sizeof(bNewExtractable)}};
rv = C_SetAttributeValue(hSession, hKey, setTemplate, 1);
// 返回CKR_ACTION_PROHIBITED,无法修改
属性的自动联动
PKCS#11规范定义了属性的自动联动规则:
属性联动规则:
规则一:CKA_ALWAYS_SENSITIVE的联动
├── 如果创建时CKA_ALWAYS_SENSITIVE = TRUE
│ └── 必须同时设置CKA_SENSITIVE = TRUE
│ └── 之后CKA_SENSITIVE永远不能改为FALSE
│
└── 如果创建时CKA_ALWAYS_SENSITIVE = FALSE
└── CKA_SENSITIVE可以自由设置(但要注意其他规则)
规则二:CKA_NEVER_EXTRACTABLE的联动
├── 如果创建时CKA_NEVER_EXTRACTABLE = TRUE
│ └── 必须同时设置CKA_EXTRACTABLE = FALSE
│ └── 之后CKA_EXTRACTABLE永远不能改为TRUE
│
└── 如果创建时CKA_NEVER_EXTRACTABLE = FALSE
└── CKA_EXTRACTABLE可以自由设置(但要注意其他规则)
规则三:C_GenerateKeyPair的自动设置
├── 使用C_GenerateKeyPair生成密钥对时
│ ├── 公钥:CKA_SENSITIVE = FALSE, CKA_ALWAYS_SENSITIVE = FALSE
│ ├── 公钥:CKA_EXTRACTABLE = TRUE, CKA_NEVER_EXTRACTABLE = FALSE
│ ├── 私钥:CKA_SENSITIVE = TRUE, CKA_ALWAYS_SENSITIVE = TRUE(如果Token支持)
│ └── 私钥:CKA_EXTRACTABLE = FALSE, CKA_NEVER_EXTRACTABLE = TRUE(如果Token支持)
│
└── Token会根据安全策略自动设置私钥的安全属性
安全等级的属性组合
让我们看不同安全等级的属性组合:
等级一:最高安全(金融级别)
最高安全私钥属性组合:
CKA_CLASS = CKO_PRIVATE_KEY 私钥对象
CKA_SENSITIVE = TRUE 密钥值不可读取
CKA_EXTRACTABLE = FALSE 密钥不可导出
CKA_ALWAYS_SENSITIVE = TRUE 创建时敏感,永远保持
CKA_NEVER_EXTRACTABLE = TRUE 创建时不可导出,永远保持
CKA_PRIVATE = TRUE 需要登录
CKA_TOKEN = TRUE 持久存储
CKA_MODIFIABLE = FALSE 属性不可修改
CKA_DESTROYABLE = FALSE 不可销毁
效果:
- 密钥值永远无法读取
- 密钥永远无法导出
- 密钥永远无法销毁
- 属性永远无法修改
= 密钥"永久锁死"在Token中
等级二:高安全(企业级别)
高安全私钥属性组合:
CKA_CLASS = CKO_PRIVATE_KEY 私钥对象
CKA_SENSITIVE = TRUE 密钥值不可读取
CKA_EXTRACTABLE = FALSE 密钥不可导出
CKA_ALWAYS_SENSITIVE = TRUE 始终敏感
CKA_NEVER_EXTRACTABLE = TRUE 从不可导出
CKA_PRIVATE = TRUE 需要登录
CKA_TOKEN = TRUE 持久存储
CKA_MODIFIABLE = FALSE 属性不可修改
CKA_DESTROYABLE = TRUE 可以销毁
效果:
- 密钥值永远无法读取
- 密钥永远无法导出
- 密钥可以销毁(允许密钥轮换)
= 密钥"锁死"但可以更换
等级三:中等安全(备份级别)
中等安全私钥属性组合:
CKA_CLASS = CKO_PRIVATE_KEY 私钥对象
CKA_SENSITIVE = TRUE 密钥值不可读取
CKA_EXTRACTABLE = TRUE 密钥可以导出(用于备份)
CKA_ALWAYS_SENSITIVE = TRUE 始终敏感
CKA_NEVER_EXTRACTABLE = FALSE 不是"从不可导出"
CKA_PRIVATE = TRUE 需要登录
CKA_TOKEN = TRUE 持久存储
效果:
- 密钥值无法直接读取
- 密钥可以通过C_WrapKey导出(加密导出)
- 导出的密钥是加密数据,需要另一个密钥才能解密
= 密钥安全但可备份
等级四:低安全(开发级别)
低安全私钥属性组合:
CKA_CLASS = CKO_PRIVATE_KEY 私钥对象
CKA_SENSITIVE = FALSE 密钥值可以读取
CKA_EXTRACTABLE = TRUE 密钥可以导出
CKA_ALWAYS_SENSITIVE = FALSE 不是始终敏感
CKA_NEVER_EXTRACTABLE = FALSE 不是从不可导出
CKA_PRIVATE = FALSE 不需要登录
效果:
- 密钥值可以直接读取(不安全!)
- 密钥可以直接导出(不安全!)
= 密钥完全暴露,仅用于开发测试
一个类比:保险箱钥匙的四个锁
让我用一个类比来理解四大安全属性:
保险箱钥匙的四个锁:
保险箱钥匙(密钥)
│
├── 锁一:透明锁(CKA_SENSITIVE)
│ ├── 上锁(TRUE):钥匙被遮挡,看不到形状
│ └── 开锁(FALSE):钥匙可见,可以复制
│
├── 锁二:出口锁(CKA_EXTRACTABLE)
│ ├── 上锁(TRUE):钥匙可以取出(但要加密包裹)
│ └── 开锁(FALSE):钥匙永远锁在保险箱内
│
├── 锁三:透明永久锁(CKA_ALWAYS_SENSITIVE)
│ ├── 上锁(TRUE):一旦透明锁上锁,永远不能开锁
│ └── 开锁(FALSE):透明锁可以自由开关
│
└── 锁四:出口永久锁(CKA_NEVER_EXTRACTABLE)
├── 上锁(TRUE):一旦出口锁上锁,永远不能开锁
└── 开锁(FALSE):出口锁可以自由开关
最高安全钥匙:
├── 透明锁:上锁(看不到)
├── 出口锁:上锁(锁在箱内)
├── 透明永久锁:上锁(永远看不到)
└── 出口永久锁:上锁(永远锁在箱内)
= 钥匙永远锁在箱内,只能使用,不能取出,不能查看
实际代码示例:生成最高安全密钥对
c
/* 生成最高安全RSA密钥对 */
CK_MECHANISM mechanism = {CKM_RSA_PKCS_KEY_PAIR_GEN, NULL_PTR, 0};
CK_OBJECT_HANDLE hPublicKey, hPrivateKey;
CK_BBOOL bTrue = TRUE;
CK_BBOOL bFalse = FALSE;
CK_ULONG modulusBits = 2048;
CK_BYTE publicExponent[] = {0x01, 0x00, 0x01}; /* 65537 */
/* 公钥模板 */
CK_ATTRIBUTE publicKeyTemplate[] = {
{CKA_CLASS, &pubKeyClass, sizeof(pubKeyClass)},
{CKA_KEY_TYPE, &rsaKeyType, sizeof(rsaKeyType)},
{CKA_TOKEN, &bTrue, sizeof(bTrue)},
{CKA_PRIVATE, &bFalse, sizeof(bFalse)}, /* 公开 */
{CKA_SENSITIVE, &bFalse, sizeof(bFalse)}, /* 公钥不敏感 */
{CKA_EXTRACTABLE, &bTrue, sizeof(bTrue)}, /* 公钥可导出 */
{CKA_MODULUS_BITS, &modulusBits, sizeof(modulusBits)},
{CKA_PUBLIC_EXPONENT, publicExponent, sizeof(publicExponent)},
};
/* 私钥模板(最高安全) */
CK_ATTRIBUTE privateKeyTemplate[] = {
{CKA_CLASS, &privKeyClass, sizeof(privKeyClass)},
{CKA_KEY_TYPE, &rsaKeyType, sizeof(rsaKeyType)},
{CKA_TOKEN, &bTrue, sizeof(bTrue)},
{CKA_PRIVATE, &bTrue, sizeof(bTrue)}, /* 私有 */
{CKA_SENSITIVE, &bTrue, sizeof(bTrue)}, /* 敏感 */
{CKA_EXTRACTABLE, &bFalse, sizeof(bFalse)}, /* 不可导出 */
{CKA_ALWAYS_SENSITIVE, &bTrue, sizeof(bTrue)}, /* 始终敏感 */
{CKA_NEVER_EXTRACTABLE, &bTrue, sizeof(bTrue)},/* 从不可导出 */
{CKA_MODIFIABLE, &bFalse, sizeof(bFalse)}, /* 属性不可修改 */
{CKA_DESTROYABLE, &bFalse, sizeof(bFalse)}, /* 不可销毁 */
};
rv = C_GenerateKeyPair(hSession, &mechanism,
publicKeyTemplate, 8,
privateKeyTemplate, 10,
&hPublicKey, &hPrivateKey);
if (rv == CKR_OK) {
printf("生成最高安全RSA密钥对成功\n");
printf("公钥handle: %lu, 私钥handle: %lu\n", hPublicKey, hPrivateKey);
}
本篇小结
密钥对象有四大安全属性,构成了密钥的"安全边界":
四大金刚:
- CKA_SENSITIVE:密钥值是否可读取(TRUE=锁住)
- CKA_EXTRACTABLE:密钥是否可导出(FALSE=锁住)
- CKA_ALWAYS_SENSITIVE:始终敏感,一旦TRUE永远TRUE
- CKA_NEVER_EXTRACTABLE:从不可导出,一旦TRUE则CKA_EXTRACTABLE永远保持FALSE
不可逆设计:
- CKA_ALWAYS_SENSITIVE和CKA_NEVER_EXTRACTABLE一旦设置,永远不能改变
- 这是PKCS#11防止密钥安全等级"降级"的关键机制
安全等级:
- 最高安全:四个锁全部锁死,密钥永久锁在Token内
- 高安全:敏感和不可导出锁死,但可以销毁(密钥轮换)
- 中等安全:敏感锁死,但可以加密导出(备份)
- 低安全:所有锁打开,仅用于开发测试
下一节,我们将看看证书对象------X.509公钥证书如何存储在Token中。
【下集预告】
密钥存储在Token中,证书也需要存储。
X.509公钥证书对象:存储证书数据和公钥引用。
WTLS证书对象:用于无线通信的轻量证书。
X.509属性证书:存储属性声明而非公钥。
下一节,证书对象详解。