第四章:Windows CNG 架构深度解析
CNG 是 Windows 的下一代加密框架。说实话,我刚开始接触时也被它那堆 Provider、KSP、算法 Provider 绕得晕头转向。但当你把它的分层模型搞明白以后,就会发现 —— 其实结构挺清爽的。
咱们这一章就专门拆解 CNG 的 Provider 模型、密钥存储提供程序 (KSP)、加密算法提供程序,以及各个 API 层怎么协作。我个人习惯先把概念图画出来,再逐层往下讲。
4.1 CNG 的 Provider 模型
CNG 最核心的设计思想,就是分离。它把加密算法和密钥存储拆成了两个完全独立的 Provider 家族。你想想看,如果没有这个分层,每次换一种加密实现或换一台 HSM,整个应用都得重写 —— 这谁受得了?
CNG 定义了两种 Provider 类型:
- 加密算法提供程序 (Cryptographic Algorithm Provider, CAP) —— 负责具体的加密、解密、哈希、签名验证等运算。注册到 BCrypt 函数族。
- 密钥存储提供程序 (Key Storage Provider, KSP) —— 负责密钥的生成、存储、导入导出、生命周期管理。注册到 NCrypt 函数族。
我这里有个小经验:在对接 HSM 时,通常 KSP 是定制工作量最大的部分。因为 HSM 的密钥未必存在 Windows 文件系统里,可能是在硬件安全域内。KSP 就像一座桥,把 Windows 的密钥抽象和 HSM 的实际情况接起来。
4.2 加密算法提供程序(CAP)
CAP 说白了就是拿来就用的“运算器”。你给它数据和密钥,它给你出结果。它不知道自己存了啥秘钥 —— 那是 KSP 的事。
CAP 通过 BCrypt 系列 API 暴露,常用的算法包括:
| 算法类型 | BCrypt 标识符 | 常见算法 |
|---|---|---|
| 对称加密 | BCRYPT_AES_ALGORITHM |
AES-128, AES-256 |
| 非对称加密 | BCRYPT_RSA_ALGORITHM |
RSA, ECDSA |
| 哈希 | BCRYPT_SHA256_ALGORITHM |
SHA-1, SHA-256, SHA-512 |
| 签名 | BCRYPT_RSA_SIGN_ALGORITHM |
RSA-PSS, ECDSA |
注册 CAP 时要实现一个 BCRYPT_CRYPTO_OPERATION 回调表。我在一个项目中需要实现 SM2(国密算法)的 CAP,当时为了把 SM2 签名映射到 Windows 的标准签名接口,折腾了整整两天。关键是一个细节:CNG 的签名原语要求输出纯 r||s 的 ISO 格式,而 HSM 那边给的是 DER 编码。这个解码/重新编码写错了,签名校验死活过不了。
注意: 自己实现 CAP 时,必须严格按照 BCryptEncrypt / BCryptDecrypt / BCryptSignHash / BCryptVerifySignature 的参数语义来映射。特别留意 padding 模式,PKCS#7 vs OAEP 在 HSM 中处理方式完全不同。
4.3 密钥存储提供程序(KSP)
KSP 是 CNG 中最复杂、也最容易被忽视的部分。它管理密钥的整个生命周期:
- 密钥生成:调用
NCryptGenerateKey,由 KSP 决定是让 HSM 生成还是在软件中生成。 - 密钥打开:通过
NCryptOpenKey和密钥名称或句柄绑定。 - 密钥使用:配合 CAP 完成签名、解密等操作。注意,密钥本身不暴露给应用,只传句柄。
- 密钥销毁:
NCryptDeleteKey负责安全擦除。HSM 上通常有防拔出的策略,这需要 KSP 适配。 - 密钥导入导出:
NCryptExportKey/NCryptImportKey。很多 HSM 不允许私钥以明文形式导出,那你的 KSP 就得实现“受保护的导出”机制。
我印象最深的是有一次对接某品牌国产 HSM,其密钥始终存储在加密卡内部,Windows 根本拿不到原始私钥。KSP 必须把 NCryptSignHash 调用转换成向 HSM 发送签名请求并等待返回。这里面坑很多:超时控制、会话管理、甚至 HSM 的重连逻辑都要做好。我曾遇到过 HSM 在弱网环境下返回 NTE_BAD_KEYSET,检查后发现是会话令牌过期了。KSP 里必须实现会话心跳和自动续期。
4.4 CNG API 层:BCrypt vs NCrypt
这两个就是你在应用层直接调用的接口。我习惯叫 BCrypt 是“无状态计算 API”,NCrypt 是“有状态密钥管理 API”。
它们的核心区别如下表:
| 特性 | BCrypt | NCrypt |
|---|---|---|
| 主要函数 | BCryptEncrypt, BCryptDecrypt, BCryptHash, BCryptSignHash |
NCryptOpenKey, NCryptSignHash, NCryptEncrypt, NCryptDecrypt |
| 密钥来源 | 临时密钥(如内存中的 AES 密钥) | 持久密钥(由 KSP 管理) |
| 典型场景 | 对称加密、哈希计算 | 非对称签名、HSM 密钥操作 |
| Provider 种类 | 算法提供程序 (CAP) | 密钥存储提供程序 (KSP) |
| 示例 | BCryptEncrypt(hKey, ...) |
NCryptEncrypt(hKey, ...) |
实际编码中,很多人会犯一个错误:把 BCrypt 的句柄和 NCrypt 的句柄混用。BNCrypt 的密钥句柄是 BCRYPT_KEY_HANDLE,NCrypt 的是 NCRYPT_KEY_HANDLE,底层指针类型不同。我曾经调试过一个诡异的崩溃,最后发现是某个 SDK 里直接把 BCrypt 句柄传给了 NCrypt 函数 —— 看上去编译没报错,但运行时直接访问违例。
建议: 开发时开启 STRICT 类型检查,并且在代码中明确注释每个句柄归属了哪个 Provider 族。这能减少不少低级错误。
4.5 Provider 注册与发现机制
CNG 怎么知道系统上装了哪些 Provider?答案是注册表。每个 CAP 和 KSP 都需要写入注册表的特定路径:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Cryptography\Providers
└─ {Provider GUID}
├─ Type = DWORD (0=CAP, 1=KSP)
├─ ImagePath = REG_SZ (DLL 路径)
├─ FunctionList = REG_BINARY (对应 BCRYPT/NCrypt 接口 vtable)
└─ Flags = DWORD (如 NCRYPT_IS_CRITICAL)
CNG 在初始化时,遍历此路径加载所有 DLL,然后调用其 DllMain 和 Provider 初始化函数。如果一个 Provider 初始化失败,默认不会阻塞系统 —— 但如果你设了 NCRYPT_IS_CRITICAL 标志,那系统会直接蓝屏。嗯,这个标志我建议只在调试阶段用,千万别在生产环境开,我曾经不小心设过,结果改注册表改得满头大汗。
4.6 与 HSM 对接时的架构要点
把 HSM 接入 CNG,本质上就是实现一个 KSP(有时也需要 CAP)。你的 KSP DLL 会:
- 通过
NCryptOpenStorageProvider暴露给上层 - 内部封装与 HSM 的通信协议(PKCS#11、CAPI、HSM 私有 SDK 等)
- 管理会话池、错误码映射、超时与重试
核心原则: KSP 是 Windows 安全子系统的扩展,必须通过微软的签名验证(WHQL 或 Authenticode)。未签名的 KSP 在部分场景(如 TPM、BitLocker)下会被拒绝加载。
我个人习惯在 KSP 里做一个“健康检查”接口,比如暴露一个 NCryptIsAlgSupported 或者自定义属性来模拟 HSM 心跳。这样应用层能实时知道 HSM 是否在线。