4. HMAC与数字签名:完整性验证与身份伪造检测
说到RBAC密钥越权检测,有个基础问题绕不开:你怎么知道消息是没被改过的?你怎么确定发消息的人就是声称的那个角色?
有人觉得,只要用了HTTPS就够了。嗯,我在项目里见过太多"外面安全里面裸奔"的情况——内部服务间调用根本不走TLS,全靠应用层自己扛。这时候,HMAC和数字签名就成了最后一道防线。
4.1 从哈希到HMAC:带上密钥的完整性校验
先聊哈希。SHA-256算出来的摘要,能证明数据没被篡改。但有个致命问题:谁都能算。
举个例子,攻击者把请求里的"role=admin"改成"role=user",再重新算一遍哈希附上去。接收方验哈希,发现对得上——因为攻击者也拿到了算法。单纯的哈希,防的是传输错误,防不了恶意篡改。
HMAC就是给哈希加了一把锁。
HMAC(Hash-based Message Authentication Code)在计算摘要时掺入了一个共享密钥。你想想看,没有密钥的人,就算知道算法,也算不出正确的MAC值。我在审计一个金融系统时发现,他们用HMAC-SHA256给内部API请求做签名,但密钥居然是"123456"写死在代码里……嗯,这个后面会讲怎么避免。
HMAC的计算过程(RFC 2104),核心就两步:
- 把密钥做两次异或填充(ipad/opad)
- 先算 (key ⊕ ipad) || message 的哈希,再算 (key ⊕ opad) || 上一步结果的哈希
说白了,就是两层哈希把密钥和安全地混进去了。简单,但有效。
# Python示例:用HMAC-SHA256给RBAC请求签名
import hmac
import hashlib
import base64
def sign_request(user_id, role, action, secret):
message = f"{user_id}:{role}:{action}".encode('utf-8')
sig = hmac.new(secret.encode('utf-8'), message, hashlib.sha256).digest()
return base64.b64encode(sig).decode()
# 验证端
def verify_request(user_id, role, action, secret, received_sig):
expected = sign_request(user_id, role, action, secret)
# 注意:用hmac.compare_digest防时序攻击
return hmac.compare_digest(expected, received_sig)
我曾经审计过一段代码,用
if expected == received_sig: 做验证。攻击者可以通过计时差异猜出签名。必须用 hmac.compare_digest 或类似的恒定时间比较函数。
4.2 数字签名:非对称场景下的身份锚定
HMAC有个天然缺陷:密钥必须共享。你和我都知道同一个secret。万一服务端被拖库,密钥泄露,攻击者就能伪造任意角色的请求。
数字签名用公私钥对解决了这个问题。私钥签名,公钥验签。私钥只有一方持有,服务器只需保存公钥。就算公钥被改,也伪造不了签名。
在RBAC场景里,我习惯这样设计:每个服务(或每个角色)生成一对RSA-2048或ECDSA密钥。服务A用私钥对请求内容签名,服务B拿服务A的公钥验签。这样即便B被攻破,攻击者也拿不到A的私钥,伪造不了A发来的越权请求。
# 用cryptography库演示数字签名(简化版)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
# 服务A:签名
def sign_message(private_key_pem, message):
private_key = serialization.load_pem_private_key(private_key_pem, password=None)
signature = private_key.sign(
message.encode(),
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
hashes.SHA256()
)
return signature.hex()
# 服务B:验签
def verify_message(public_key_pem, message, signature_hex):
public_key = serialization.load_pem_public_key(public_key_pem)
try:
public_key.verify(
bytes.fromhex(signature_hex),
message.encode(),
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
hashes.SHA256()
)
return True
except Exception:
return False
4.3 在RBAC越权检测中的具体应用
我在实际审计中总结了一套判断流程:
- 查签名覆盖范围——签名要覆盖关键字段:user_id、role、action、timestamp。漏掉一个就可能被篡改。
- 查密钥存储——千万不要硬编码。我在某云厂商的日志里见过用户代码里直接写secret,git历史都翻得到。
- 查重放防护——只有签名不够,必须要有timestamp + nonce,或者用序列号防重放。
- 查验证失败处理——签名失败不能只打日志然后放行。我见过一个项目,验签抛异常就直接返回200,因为开发图方便……
| 检测项 | HMAC | 数字签名 |
|---|---|---|
| 密钥管理复杂度 | 低(共享密钥) | 中(需管理公私钥对) |
| 抗伪造能力 | 高(密钥不泄露时) | 极高(私钥不外泄) |
| 典型越权场景 | 内部服务间调用 | 跨信任域、第三方调用 |
| 防重放推荐做法 | timestamp+HMAC+nonce | timestamp+签名+序列号 |
| 性能 | 快(对称哈希) | 慢(非对称运算) |
4.4 身份伪造检测:不只是验签名
拿到签名,验过了,就万事大吉?
不对。我踩过一个坑:有一次审计发现,某服务虽然验了签名,但签名里只包含action,没包含user_id。攻击者只要截获一个签名(比如"delete_user"的签名),就能把自己的请求里的user_id改成别人,然后用同一个签名去删除其他用户。
这就是签名截断重放攻击。签名必须绑定到具体的主体和资源,即:谁(user_id)对什么(resource_id)做了什么(action)。
签名负载 = 用户身份 + 角色 + 操作 + 资源 + 时间戳
任何一项缺失,都可能被利用。
另外,在实际检测中我常用一个技巧:查看签名失败日志。如果你发现某个user_id的签名连续失败几次,很可能是有人在尝试伪造该用户的权限。可以联动告警,甚至临时锁定该账号。
4.5 本章知识体系总览
下面这张图总结了HMAC与数字签名在RBAC越权检测中的定位和关系:
4.6 常见越权伪造套路与防御
我这些年审计过不少系统,几乎每个都会遇到下面几种伪造手法:
- 签名剥离——攻击者把签名参数删掉,服务端如果没有强制验证,就直接通过了。解决办法:所有需要鉴权的接口,签名必须为必选参数,缺失即拒绝。
- 签名继承——攻击者用自己的合法身份(比如"viewer"角色)得到签名,然后拼接到高权限请求上。我见过一个SaaS平台,同一个secret对所有角色共享。防御:签名要包含角色字段,密钥按角色级别隔离。
- HMAC密钥猜解——弱密钥比如"password123"、"secret",或者密钥长度太短(少于32字节)。建议至少用256位随机密钥,定期轮换。
拿到一份代码后,我习惯先grep搜索"hmac"、"signature"、"verify"关键字,然后看密钥从哪里加载。如果看到
secret = "xxx" 或 load_from_env("SECRET", default="xxx"),基本就有问题。