第四章:Windows CNG 架构深度解析

CNG 是 Windows 的下一代加密框架。说实话,我刚开始接触时也被它那堆 Provider、KSP、算法 Provider 绕得晕头转向。但当你把它的分层模型搞明白以后,就会发现 —— 其实结构挺清爽的。

咱们这一章就专门拆解 CNG 的 Provider 模型、密钥存储提供程序 (KSP)、加密算法提供程序,以及各个 API 层怎么协作。我个人习惯先把概念图画出来,再逐层往下讲。

CNG 架构分层模型 应用层(应用程序) CNG API 层(BCrypt / NCrypt) CNG 路由层(Provider 发现与调度) 加密算法提供程序 (BCrypt 算法族) 密钥存储提供程序 (NCrypt KSP) 底层:硬件 / 软件密码机 💡 应用层通过 BCrypt/NCrypt API 调用,经路由层分发到对应的 Provider,最终由底层密码机执行。 KSP 管理密钥生命周期,算法提供程序执行加密/解密/签名/哈希等操作。

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 中最复杂、也最容易被忽视的部分。它管理密钥的整个生命周期:

  1. 密钥生成:调用 NCryptGenerateKey,由 KSP 决定是让 HSM 生成还是在软件中生成。
  2. 密钥打开:通过 NCryptOpenKey 和密钥名称或句柄绑定。
  3. 密钥使用:配合 CAP 完成签名、解密等操作。注意,密钥本身不暴露给应用,只传句柄。
  4. 密钥销毁:NCryptDeleteKey 负责安全擦除。HSM 上通常有防拔出的策略,这需要 KSP 适配。
  5. 密钥导入导出: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 是否在线。