HSM技术精讲(3.7):密钥对象与安全属性——密码安全的“四大金刚“

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属性证书:存储属性声明而非公钥。

下一节,证书对象详解。