一、被忽视的问题:密钥的"越权使用"
在绝大多数开发者的直觉里,密钥就是一段用来加密或签名的材料。只要密钥本身不泄露,似乎就万事大吉。但真实业务系统里,密钥的"用途"往往被混用,这才是更隐蔽的风险点。
设想这样一种部署:为了省事,运维把同一个 SM2 密钥同时用于两类操作——对外签发业务凭证(签名),以及给内部接口做身份认证(验签/派生)。从密码学上看,这一把密钥确实"没泄露",但从治理上看,它已经失控了:任何一个能调用签名接口的人,理论上也能用它做本不该由他发起的认证动作。一旦某个下游微服务被攻陷,攻击者不需要盗走密钥,只需要"借用"这把密钥去干一件它本不应干的事。
这就是密钥用途治理要解决的问题:密钥不仅要保密,还要被约束在它被设计出来的那一个用途上。这也是现代密钥管理系统与早期"加密机 + 手工配置"模式最本质的区别之一。
在以密评合规为目标的体系建设里,密钥管理控制项并不只问"密钥是否加密存储",还会追问:密钥是否按用途分类管理、是否有使用审批与使用记录、是否遵循最小权限原则。如果一把密钥能在毫无校验的情况下被任意用途调用,那么即便它躺在 HSM 里,这套管理在审计视角下也是不成立的。
二、密钥用途标签(keyUsage)的设计
要把"这把密钥能干什么"固化下来,第一步是给每把密钥建立结构化的用途元数据。在密码学协议里,这个思想并不新鲜——X.509 证书的 KeyUsage 扩展、PKCS#11 的密钥属性,本质上都是在做同一件事:声明一个密钥对象允许的操作集合。
2.1 四类基础用途
在业务密钥管理系统中,经常需要区分以下四类基础用途:
- 签名(sign):用私钥对数据进行数字签名,证明数据来源与完整性。典型用法:电子票据签名、报文签名、代码签名。
- 验证(verify):用公钥验证签名。通常验证密钥(公钥)可以广泛分发,而签名私钥必须受控。
- 封装(wrap / encrypt):用于信封加密中的 KEK(密钥加密密钥),把数据加密密钥 DEK 封装起来。信封加密正是依赖这种用途隔离:DEK 负责加密业务数据,KEK 负责保护 DEK,两者用途不同、权限不同。
- 派生(derive):基于主密钥派生出会话密钥或子密钥,常见于密钥分层体系。派生密钥本身不应直接拿去做签名或封装。
把用途拆成这四个方向,治理的颗粒度就清晰了。下面这张表说明了不同用途之间的"危险组合":
| 密钥声明的用途 | 若被拿去"验证" | 若被拿去"封装" | 若被拿去"派生" | 若被拿去"签名" |
|---|---|---|---|---|
| 仅签名 | 风险低(公钥本可公开) | 高:可伪造加密信封 | 高:可生成未知子密钥 | 不允许(即自身用途,无越权) |
| 仅封装 | 无意义 | 不允许 | 中:权限边界模糊 | 高:可冒用身份签名 |
| 仅派生 | 无意义 | 中:可能泄露派生链 | 不允许 | 高:派生链被滥用签名的桥梁 |
| 仅验证 | 允许(本就公开) | 无意义 | 无意义 | 不允许(验证密钥无签名能力) |
这张表的核心结论是:跨用途调用之所以危险,是因为它把"能力"和"身份"混在了一起。一个用于派生的密钥一旦能签名,就等于把"生成密钥的权限"升级成了"代表系统身份说话的权限"。
2.2 用途标签如何写入密钥元数据
在工程实现上,用途标签应当作为密钥对象的一等属性,随密钥一起由密钥管理系统在生成阶段写定,并且不可被普通业务方自行修改。一个典型的密钥元数据模型如下:
KeyObject: keyId: "ksp-2026-0a3f-..." # 密钥唯一标识 alg: "SM2" / "SM4" / "AES" / "RSA" keyUsage: ["sign"] # 声明的允许用途集合 notBefore: 2026-01-01T00:00:00 notAfter: 2027-01-01T00:00:00 state: "ACTIVE" # 生成→存储→激活→更新→归档→注销→销毁 ownerApp: "billing-service" exportable: false # HSM 内密钥永不明文导出 tenantId: "tenant-A" # 多租户隔离注意其中的keyUsage是一个集合而不是单个值。它允许一把密钥在受控前提下拥有多个用途(例如签名 + 验证),但每一个用途都必须被显式声明、被审批、被记录。更重要的是,这个集合在密钥"激活"之后应当是不可扩张的——想增加用途,必须走注销旧密钥、生成新密钥的流程,而不能在运行时偷偷追加。
在以安当KSP为例的落地实践中,用途约束与密钥生命周期是绑定的:密钥在"生成"阶段就写入 keyUsage,"激活"阶段才允许被调用,"归档/注销"之后即便物理材料还在,用途校验也会直接拒绝任何调用。这样用途治理就不再依赖人的自觉,而是被系统强制执行。
三、API 层的用途校验与跨用途拒绝
只有标签还不够。标签如果只写在文档里、只展示在管理界面上,而 API 在收到调用时并不校验,那它毫无意义。真正的防护必须发生在每一次密钥调用的入口处。
3.1 请求必须显式携带"用途意图"
调用方在请求密钥服务时,不能只说"请用这把密钥处理这段数据",而必须明确声明"我要用这把密钥做什么"。一个设计良好的密钥服务 API 调用,大致长这样:
POST /v1/keys/{keyId}/operate Headers: X-App-Id: billing-service X-Request-Id: req-9f2c-... Body: { "intent": "sign", # 调用方声明的用途意图 "payloadB64": "eyJpc3MiOi4uLn0=", "encoding": "sm2-sm3" }这里的intent是强制字段。如果调用方不传、传空、或传了一个与密钥声明用途不兼容的值,请求在第一道关口就会被拒绝,根本不会触达密码运算层。
3.2 服务端校验流程
服务端在真正调用 HSM 做运算之前,应当执行一条不可绕过的校验链:
收到 operate 请求 → 1. 鉴权:X-App-Id 是否有该 keyId 的调用权限(最小权限) → 2. 租户校验:请求方 tenantId == 密钥 tenantId(多租户隔离) → 3. 状态校验:密钥 state == ACTIVE(已激活、未归档注销销毁) → 4. 用途校验:intent ∈ keyUsage ? —— 核心一步 → 5. 算法校验:声明的 encoding 与该密钥算法族一致 → 6. 频控/配额:该应用单位时间内调用是否超阈值 → 全部通过才下发到 HSM 执行;任意一步失败返回 4xx 并写审计第 4 步就是本文的主角。它的逻辑可以用一句伪代码概括:
if intent not in keyObject.keyUsage: reject(403, "KEY_USAGE_MISMATCH", "requested intent=%s not allowed for key=%s" % (intent, keyId)) audit.log(denied=True, reason="usage_mismatch") return这一步之所以要放在"真正运算之前",是为了做到默认拒绝(default deny):用途不匹配的请求永远走不到密码硬件面前。即便 HSM 本身有能力用这把密钥完成这个操作,业务网关也不允许它发生。
3.3 跨用途调用为什么必须被拒绝
我们不妨把"封装"和"签名"的越界作为具体例子展开。在信封加密体系里,KEK(封装密钥)的职责是保护 DEK。如果某服务错误地把一把"仅签名"的密钥拿去当 KEK 做封装,至少会产生两个问题:
- 语义混乱:签名密钥和封装密钥的安全模型完全不同。签名私钥一旦用于封装,意味着它被允许接触大量 DEK 材料,攻击面被人为放大。
- 审计失真:审计报表里会看到"这把签名密钥产生了大量封装操作",但业务上它根本不该做封装。等到密评或攻防演练时,这种不一致就是明确的扣分项,甚至可能被认为密钥管理失控。
因此,密钥服务在 API 层识别到intent=wrap而keyUsage=["sign"]时,必须返回拒绝并记录。这不是"多管闲事",而是把密码学正确的用法,用系统的手强制执行出来。
四、最小权限的密钥发放
用途约束解决的是"一把密钥能干什么",而最小权限解决的是"谁能用这把密钥"。两者必须配合,否则约束再严也会被人绕过——比如干脆给某个应用发一把"签名 + 封装 + 派生"全用途的万能密钥,这就回到了开头的混乱。
4.1 按应用、按接口发放
正确的发放模型是:每一个应用、每一种调用意图,都只拿到它真正需要的那一把(或那几把)限定用途的密钥。一个只负责验证签名的网关,不应该持有签名私钥;一个只做信封加密封装的服务,不应该拿到签名密钥。
应用 持有密钥 允许 intent 不允许 intent billing-signer ksp-sign-001 sign wrap / derive / verify(私钥) ticket-verifier ksp-verify-002(公钥) verify sign / wrap / derive dek-wrapper ksp-kek-003 wrap sign / derive session-deriver ksp-mk-004 derive sign / wrap这种矩阵式的发放,使得任何单一应用被攻陷时,攻击者获得的密钥能力都被限制在这一格子里,无法横向移动去干别的事。
4.2 与密钥生命周期联动
最小权限发放还要和密钥生命周期配合。密钥不是发下去就一成不变的:当billing-signer下线、或业务从签名切换到新的算法族时,对应的密钥应当进入"注销→销毁"流程,而不是留在系统里变成无人认领的隐患。一套合格的密钥管理系统,会把"发放—使用—轮换—注销"做成闭环,每一次状态变更都留下记录。
在国密密钥管理的语境下,生命周期管理还直接关系到合规:密钥生成、存储、激活、更新、归档、注销、销毁七个阶段都必须可追踪。用途标签和最小权限发放,是让这个生命周期"有章可循"的抓手——你不是笼统地管一把密钥,而是管它在每一个阶段"被允许做什么、被谁做"。
五、把"密钥实际用法"晒出来的审计报表
约束和发放做得再好,如果事后说不清"密钥到底被用在了哪里",在密评和攻防演练面前依然站不住脚。审计是密钥用途治理的最后一公里。
5.1 审计应当记录的最小字段
每一次密钥调用,无论成功还是被拒绝,都应当在审计日志里留下足够还原现场的信息:
audit_record: ts: 2026-03-12T10:23:11.482Z keyId: ksp-sign-001 appId: billing-signer tenantId: tenant-A intent: sign declaredUsage: ["sign"] result: ALLOW / DENY denyReason: (空) / "usage_mismatch" / "not_active" / "no_permission" opLatencyMs: 3.1 reqId: req-9f2c-...特别要强调的是,被拒绝的请求同样要记。很多系统的审计只记成功调用,这恰恰是盲区:攻击者探测式地尝试越权用途时,留下的就是一批 DENY 记录。这些记录是发现异常行为、乃至事后溯源的第一手材料。
5.2 审计报表的几个维度
把上面的原始日志聚合成报表,运维和安全负责人至少要看这几张图:
- 用途合规率:成功调用中,intent 与 keyUsage 完全匹配的比例。长期低于 100% 说明有调用方在用错密钥,或代码里有硬编码的误用。
- 跨用途尝试TOP:按 appId 聚合被拒绝的 usage_mismatch 次数,揪出"总想越权"的服务。
- 密钥调用热力图:哪把密钥被谁、在什么时间窗口高频调用,识别异常时段。
- 生命周期状态分布:有多少密钥处于 ACTIVE、归档、注销,防止"僵尸密钥"堆积。
下面是一张简化的用途合规日报示例:
| 应用 | 持有密钥数 | 成功调用 | 越权拒绝 | 用途合规率 | 风险提示 |
|---|---|---|---|---|---|
| billing-signer | 1 | 18203 | 0 | 100% | 正常 |
| ticket-verifier | 1 | 99021 | 0 | 100% | 正常 |
| dek-wrapper | 1 | 45120 | 3 | 99.99% | 排查 3 次 wrap 越权 |
| legacy-batch | 2 | 5120 | 117 | 97.78% | 高:疑似硬编码误用 |
最后一行legacy-batch的 117 次越权拒绝,就是典型的"老系统改造不彻底"信号——它仍在用一把签名密钥尝试做封装。审计报表把这个问题暴露出来,治理动作才有抓手。
六、用途治理如何支撑密评"密钥管理"控制项
落到合规层面,密钥用途约束与防误用治理,直接对应密评里关于密钥管理的一系列要求。我们以 GM/T 0051(智能密码钥匙应用接口规范所归属的密评相关标准体系)以及密评合规对密钥全生命周期的通用要求为参照,梳理对应关系:
密评关注点 用途治理的回应 ───────────────────────────────────────────────────────────── 密钥是否分类管理 keyUsage 显式声明签名/封装/派生/验证 密钥使用是否受控 API 层 intent 校验 + default deny 拒绝越权 是否遵循最小权限 按应用/接口发放限定用途密钥,禁发万能密钥 是否有使用审批与记录 每次调用(含拒绝)全量审计,可出具报表 密钥生命周期是否完整 生成→存储→激活→更新→归档→注销→销毁闭环 密钥存储是否安全 HSM 内密钥永不明文导出,多租户隔离可以看到,用途约束并不是孤立的一个功能点,而是把"分类管理—使用受控—最小权限—审计留痕—生命周期—安全存储"这几条要求串成了一条线。当评估人员问"你们怎么保证签名密钥不会被拿去加密"时,答案不再是"我们强调过规范",而是一句可验证的话:系统在 API 层强制校验 intent,跨用途调用会被拒绝并留痕。
这也是国密密钥管理落地时最容易被忽略、却最能拉开差距的一环。很多单位把 HSM 买回来、把密钥导进去,就认为密钥管理"完成了",但真正决定密评能否过、决定攻防能否扛住的,恰恰是这种"用途层面的强制执行"。
从更宏观的视角看,信封加密、透明数据加密、数据库字段加密等上层方案,最终都依赖底层的密钥管理系统提供"正确且受约束"的密钥。如果底层密钥可以被随意跨用途调用,上层再严密的加密方案也会在治理层面出现裂缝。因此,把用途约束做扎实,是整个密码应用合规的底座之一。
七、落地时的工程注意点
在真正把用途约束落到生产环境时,有几个容易踩的坑值得单独提一下。
第一,不要靠约定,要靠强制。最常见的失败模式是:架构文档里写"这把密钥只用于签名",但 API 不校验,结果三年后某个新同事在另一个服务里用它做了封装,系统毫无反应。约束必须写在代码路径里,写在每次调用的校验链里。
第二,用途标签不可在运行时扩张。密钥一旦激活,keyUsage 集合就应当冻结。确实需要新用途时,走"生成新密钥 + 灰度切换 + 注销旧密钥"的正规流程,而不是热修改属性。这既符合密钥生命周期管理,也避免了"悄悄提权"。
第三,公钥/私钥的用途要分开看。验证(verify)用的公钥本就可以广泛分发,它的"越权"风险远低于签名私钥。治理资源应该优先压在私钥、尤其是签名与封装类私钥上。
第四,审计要纳入拒绝事件。如前所述,只记成功不记拒绝,等于主动放弃了入侵探测能力。把 DENY 记录当成一等公民,是用途治理成熟度的分水岭。
第五,多租户环境下的隔离不能省。不同租户的密钥即便用途相同,也必须在 tenantId 层面隔离,防止 A 租户的应用以"相同用途"为借口触达 B 租户的密钥对象。
第六,短期密钥与长期密钥分级。派生类密钥往往是短期、高频轮换的,而签名根密钥是长期、高价值的。两者的用途约束策略应分层:长期密钥的用途集合要收得更紧、审批链更长;短期派生密钥可以允许在限定算法族内运作,但绝对不能与签名用途交叉。这种分级让安全资源集中在真正值钱的那几把密钥上。
第七,与算法族绑定而非仅与操作绑定。用途校验时还要顺便确认算法族一致,例如一把被声明为 SM2 的签名密钥,不应被调用方以 RSA 的编码方式发起请求。把"用途"和"算法族"两个维度叠在一起校验,能有效防止调用方用兼容性参数绕开用途限制。这一点在同时支持国密(SM1/SM2/SM3/SM4)与国际算法(AES/RSA/ECC/SHA)以及后量子算法(Kyber/Dilithium)的混合环境里尤为重要——不同算法族的同一"签名"语义,其安全假设并不相同。
方案参考
对于准备建设或审视自身密钥治理能力的团队,下面是一套可操作的参考方法,不依赖具体产品,可以对照落地:
先盘点,再约束。把当前系统里所有在用的密钥列一张清单,标注每把密钥"现在实际被用在了哪些地方"。这一步往往会暴露出大量"一把密钥多用途"的历史债务,是治理的起点。
定义用途词汇表。在团队内统一签名、封装、派生、验证四类(或结合业务扩展)用途的语义,并写成可被机器读取的元数据字段,避免口头约定。
在调用入口加校验。无论是自研的密钥服务还是引入的密钥管理系统,都要求每次调用显式声明 intent,并在服务端做强校验、默认拒绝。这是把约束从文档变成执行的关键一步。
按最小权限重新发放。以应用和接口为单位重做密钥发放矩阵,撤销一切"万能密钥",让每个调用方只持有它真正需要的限定用途密钥。
把审计做成日常。部署用途合规率、越权尝试 TOP、生命周期分布等报表,把它纳入常规的密码应用安全运营,而不是只在密评前临时抱佛脚。
纳入密钥生命周期闭环。确保生成、存储、激活、更新、归档、注销、销毁七个阶段都有记录可查,用途标签和发放关系随状态变更同步更新。
对照密评控制项自检。用"分类管理—使用受控—最小权限—审计留痕—生命周期—安全存储"这条主线,逐条核对自身是否具备可验证的证据,而不是只有口头说明。
上述方法的核心思想是:密钥的价值不只在于它保密,更在于它被严格限制在该被使用的用途上。把用途约束、跨用途拒绝、最小权限发放和审计报表这四件事做扎实,密钥管理从"藏好一把钥匙"升级为"管好每一次使用",密评合规与实战防护也就有了共同的、可验证的支点。