3. HSM 密钥生成:密钥对生成与导出策略归纳总结

这一章我们聊聊 HSM 里最核心的操作——生成密钥对。很多人以为在 HSM 里生成密钥跟跑个 openssl genrsa 一样简单,但实际上 HSM 的密钥生成涉及策略、属性、CryptoAPI 调用等多个层面的约束。我自己最早接触时也踩过不少坑,尤其是那个“密钥导出策略”,一不留神就把自己锁死了。

3.1 HSM 支持哪些密钥算法?

不同厂商、不同型号的 HSM 支持的算法不太一样,但主流的基本都覆盖了。我把常见的整理成一个表,方便你核对。

算法族具体算法密钥长度(典型值)HSM 支持情况
RSARSA-PKCS1 / RSA-OAEP / RSA-PSS2048, 4096几乎通用
ECCECDSA / ECDH / EdDSAP-256, P-384, P-521, Curve25519主流 HSM 都支持
对称算法AES (CBC / GCM / 其他)128, 192, 256所有 HSM 必支持

不过要注意,有些 HSM 的固件版本不同,支持的算法也有差异。比如我之前对接某款国产 HSM,ECC 的 P-521 在旧固件上就报错,最后升了级才解决。所以做集成前,最好先看 HSM 的 CSP / FIPS 证书或者官方规格表。

3.2 生成密钥对:PKCS#11 的标准流程

绝大多数 HSM 都遵循 PKCS#11 标准,通过 C_GenerateKeyPair 生成非对称密钥。这个函数调用起来其实很简单,但参数里的密钥模板(CK_TEMPLATE)才是关键。

// 伪代码示例:生成 RSA 2048 密钥对
CK_OBJECT_CLASS publicClass = CKO_PUBLIC_KEY;
CK_KEY_TYPE keyType = CKK_RSA;
CK_ULONG modulusBits = 2048;
CK_BYTE publicExponent[] = {0x01, 0x00, 0x01};

CK_ATTRIBUTE publicTemplate[] = {
    {CKA_CLASS, &publicClass, sizeof(publicClass)},
    {CKA_KEY_TYPE, &keyType, sizeof(keyType)},
    {CKA_MODULUS_BITS, &modulusBits, sizeof(modulusBits)},
    {CKA_PUBLIC_EXPONENT, &publicExponent, sizeof(publicExponent)},
    {CKA_, &ckTrue, sizeof(ckTrue)},
    {CKA_ENCRYPT, &ckTrue, sizeof(ckTrue)},
    {CKA_VERIFY, &ckTrue, sizeof(ckTrue)},
};

// 私钥模板:关键属性 CKA_EXTRACTABLE
CK_ATTRIBUTE privateTemplate[] = {
    {CKA_CLASS, &privateClass, sizeof(privateClass)},
    {CKA_KEY_TYPE, &keyType, sizeof(keyType)},
    {CKA_, &ckTrue, sizeof(ckTrue)},
    {CKA_PRIVATE, &ckTrue, sizeof(ckTrue)},
    {CKA_SIGN, &ckTrue, sizeof(ckTrue)},
    {CKA_DECRYPT, &ckTrue, sizeof(ckTrue)},
    {CKA_EXTRACTABLE, &ckFalse, sizeof(ckFalse)},  // 这里决定私钥能否以明文导出
};

rv = pFunctionList->C_GenerateKeyPair(hSession,
    CKF_HW, CKF__LOGIN,
    publicTemplate, 5,
    privateTemplate, 7,
    &hPublicKey, &hPrivateKey);

你看,私钥模板里的 CKA_EXTRACTABLE 设为 CK_FALSE 时,私钥永远不能以明文形式被导出。这个属性是安全策略的核心,也是一把双刃剑。

⚠️ 我曾经踩过的坑: 早期为了调试方便,我把所有私钥都设成 CKA_EXTRACTABLE=TRUE,结果安全审计没通过。后来只能重新生成一批密钥,把不可导出的策略彻底固化到 HSM 的全局策略里。

3.3 密钥导出策略:三种典型模型

HSM 怎么控制密钥能不能被拿走?我总结了三种常见的策略模型:

  1. 完全不导出(Non-Extractable) —— 私钥永久留在 HSM 内部,即使用户是管理员也无法获取原始密钥字节。安全性最高,但备份不方便。
  2. 可包裹导出(Wrappable) —— 私钥可以被另一个密钥(KEK)加密后导出到外部文件。这种模型下,KEK 本身不可导出,但可以被多个 HSM 共享(比如同一批设备的 KEK 相同)。
  3. 可明文导出(Sensitive + Extractable) —— 允许直接 GET 私钥值(通常需要特别高的权限审批)。极少用于生产,多用于灾备或初始注入。

大多数合规场景(PCI DSS、FIPS 140-2/3)要求非对称私钥必须属于“完全不导出”或“可包裹导出但不能明文”。我个人习惯在生产环境中一律设 CKA_EXTRACTABLE=FALSE,然后把备份方案设计成 HSM 到 HSM 的克隆(clone)或密钥同步。

3.4 结合实践:导出策略的配置步骤

实际集成时,你需要和 HSM 管理员一起明确几个策略点。下面这张图描述了密钥生成到策略决策的全流程:

HSM 密钥生成与导出策略决策流程 用户生成密钥请求 选择算法 & 密钥长度 构建密钥模板(CKA_*) 策略判断:可导出? C_GenerateKeyPair 返回密钥句柄 不可导出 可导出(需额外 key) CKA_EXTRACTABLE=FALSE 私钥永不离开 HSM CKA_EXTRACTABLE=TRUE 需配合 Wrap 机制导出

从图上可以看到,策略判断分支直接影响后续操作。如果你的业务需要密钥备份,建议在生成时就规划好:要么用 HSM 的克隆功能(同一品牌),要么在生成密钥后立即用对称 KEK 把私钥包裹出来保存。

💡 一个小技巧: 我习惯在私钥模板里同时设置 CKA_SENSITIVE=TRUE 和 CKA_EXTRACTABLE=FALSE,这样即使有管理员权限也读取不到私钥内容。如果需要导出,可以单独创建用于包装的 KEK,只允许用该 KEK 包裹目标私钥。

3.5 导出策略的“陷阱”清单

最后我把一些常见问题整理成清单,你可以对照检查自己的集成代码:

  • 陷阱一: 模板里忘记设置 CKA_=TRUE —— 密钥变成 session 对象,会话一关就丢失。
  • 陷阱二: CKA_EXTRACTABLE 对公钥无效 —— 公钥默认就可以导出,所以不要试图用它控制公钥。
  • 陷阱三: 某些 HSM 在 CKA_EXTRACTABLE=FALSE 时 C_GetAttributeValue(CKA_VALUE) 会返回 CKR_ATTRIBUTE_SENSITIVE,但不会报错 —— 很多新手以为取到了空值,其实是 API 返回了错误码被忽略。
  • 陷阱四: 包裹导出时 KEK 的存储策略不匹配 —— 如果 KEK 本身也不能导出,那整个备份方案就失效了。

📌 核心原则: 密钥的生成策略一旦固化,后续很难修改。如果你不确定当前业务是否需要导出,建议先让 HSM 管理员确认全局策略,再在 SDK 里做对应约束。宁可一开始保守(全部不可导出),后续需求明确再开接口,切勿反过来。

嗯,这一章讲到这里。记住:HSM 的密钥生成不是简单的 API 调用,而是一次安全策略的落地。模板里的每一个属性字段,都可能影响整个系统的合规性。建议你在做集成测试时,先把一个密钥生成出来,然后用 C_GetAttributeValue 打印它的所有属性,亲眼看一遍再上线。