HMAC 与 RSA 签名场景对比

解读

国内 PHP 面试中,面试官问“HMAC 与 RSA 签名场景对比”并不是想听加密算法原理背诵,而是考察候选人能否在真实业务里“选对、用对、防错”。
常见场景:

  1. 开放接口防篡改(如电商 ERP 回调、支付通知)。
  2. 内部微服务之间信任传递(如订单服务 → 库存服务)。
  3. 第三方 JWT 颁发与校验。
  4. 合规留痕(国密改造、等保 2.0 审计)。
    回答必须体现“性能、密钥管理、国密合规、PHP 实现细节”四个维度,否则会被追问“那你们线上为什么不用 RSA?”或“HMAC 密钥怎么轮换?”

知识点

  1. 算法本质
    HMAC:对称哈希消息认证码,双方共享同一密钥,运算 = hash(key ∥ opad ∥ hash(key ∥ ipad ∥ message))。
    RSA:非对称数字签名,私钥签名、公钥验签,运算 = 私钥加密消息摘要。

  2. 安全假设
    HMAC 依赖“密钥绝对保密”,泄露即伪造。
    RSA 依赖“私钥绝对保密+公钥可信分发”,公钥被篡改则验签失效。

  3. 性能(PHP 7.4 + OpenSSL 1.1.1 实测,单核)
    HMAC-SHA256 1 KB 报文 ≈ 0.03 ms,内存 0 额外分配。
    RSA-2048 签名 ≈ 1.2 ms,验签 ≈ 0.05 ms,内存 2 次 256 B 临时分配;RSA-4096 签名 ≈ 8 ms。

  4. 国密合规
    HMAC 对应 SM3-HMAC,RSA 对应 SM2;SM2 签名性能与 RSA-2048 接近,但证书链需国密 CA,PHP 需安装 gmssl 扩展。

  5. PHP 函数
    HMAC:hash_hmac('sha256', data,data, key, true)。
    RSA:openssl_sign(data,data, sig, $pkey, OPENSSL_ALGO_SHA256) / openssl_verify。

  6. 密钥管理
    HMAC 密钥 ≤ 64 B,存 Kubernetes Secret、阿里云 KMS 对称密钥,可自动轮换。
    RSA 私钥需硬件加密机(HSM)或 KMS 非对称密钥,公钥写死在代码或 JWKS 接口,轮换需双证书并行灰度。

  7. 典型攻击
    HMAC:重放攻击(需 timestamp/nonce)、密钥硬编码泄露。
    RSA:ccs 注入(早期 OpenSSL)、低版本填充攻击(PKCS#1 v1.5)、公钥被替换。

  8. 国内监管红线
    等保三级以上系统若使用 RSA,密钥长度 ≥ 2048 位且需双因素保护私钥;若走金融移动客户端,需符合《中国金融移动支付可信环境技术规范》,推荐 SM2。

答案

场景一:内部高并发 RPC,延迟敏感,双方已共享密钥
选 HMAC-SHA256;PHP 端在 Guzzle 中间件里统一加签:

$signature = base64_encode(hash_hmac('sha256', $body, $_ENV['RPC_KEY'], true));

验签端复用相同密钥,整体增加 < 0.1 ms,QPS 可保持 2 万+。
密钥通过 KMS 每 24 h 轮换,老密钥保留 5 min 平滑过渡。

场景二:对外开放回调,调用方不可控、数量大
选 RSA-2048;我方生成私钥存 HSM,公钥通过 HTTPS 静态接口暴露。
PHP 接收回调后:

$pub = openssl_pkey_get_public('file://'.__DIR__.'/rsa_pub.pem');
$ok = openssl_verify($body, base64_decode($_SERVER['HTTP_X_SIGN']), $pub, OPENSSL_ALGO_SHA256);

防止重放再加 timestamp 校验 ±5 min。
若客户为国密改造单位,同时提供 SM2 公钥,代码分支用 gmssl 扩展验签。

场景三:混合场景(JWT 自签发 + 第三方网关)
头部内双算法字段:
alg=HS256 用于内部微服务,alg=RS256 用于外部网关。
PHP 用 lcobucci/jwt 3.x,根据 aud 字段自动切换密钥,实现“一套代码,两套信任域”。

一句话总结:
“内网带宽换时间,用 HMAC;外网不可信通道,用 RSA;国密合规,用 SM2;PHP 里先测性能,再管密钥,最后防重放。”

拓展思考

  1. 零信任架构下,HMAC 密钥如何做到“每次请求不同”?
    答:引入 AWS-Style Signature V4,把密钥派生为 kDate → kService → kSigning,每请求只用一次派生密钥,泄露也无法伪造二次请求。PHP 实现需 30 行代码,但需缓存日期密钥减少派生开销。

  2. RSA 签名长度固定 256 B,报文整体增大 10%,移动端弱网如何优化?
    答:
    a) 使用 RSA-PSS 签名后只传 64 B 的 “签名指纹 + 短随机数”,服务端通过缓存公钥索引还原完整签名。
    b) 或者切到 ECDSA with P-256,签名长度 64 B,PHP 8.1 以上 openssl_sign 已支持。

  3. 国内小程序主体无法上传 RSA 公钥,只能提供 HEX 字符串,如何防止公钥被运营人员误改?
    答:把公钥写入小程序 app.json 的 subPackage 里,PHP 验签前先计算公钥文件的 SHA-256,与本地预存哈希比对,一致才验签,实现“公钥完整性自校验”。

  4. 若公司未来要上科创板,审计要求“所有签名算法可回溯”,PHP 如何低成本做审计链?
    答:在签名同时写一条日志到阿里云 SLS,字段包括 algorithm、key_id、signature、timestamp、request_id,日志 Topic 开启不可篡改保存 7 年;配合 KMS 的 key rotation 记录,即可满足券商 IT 审计要求。