3. 签名与加密算法选型

JWT 的头部字段 alg 决定了令牌的签名或加密方式,它不是一个可以随意填写的装饰字段,而是整个令牌信任链的起点。选错算法,或者让验证方无条件信任令牌自带的 alg,都会把签名机制从“防篡改”变成“可伪造”。本节从数学原理出发,对比四种主流算法的安全强度与适用场景,并重点剖析 none 算法漏洞与算法混淆攻击的成因和防御方法。

一、四种算法的数学原理

HS256(HMAC-SHA256)属于对称签名。签名方与验证方共享同一个密钥 K,签名值 Signature = HMAC-SHA256(K, base64url(header) + "." + base64url(payload))。HMAC 的安全性建立在 SHA-256 的抗碰撞性和密钥保密性之上,只要密钥足够长且不泄露,攻击者无法在不知道 K 的情况下伪造签名。它的优点是计算快、实现简单;缺点是密钥必须被多方共享,任何持有密钥的一方都能签发令牌,无法区分“谁签的”。

RS256(RSASSA-PKCS1-v1_5 + SHA256)属于非对称签名。签名方用私钥 d 对消息摘要做 RSA 运算,验证方用公钥 (n, e) 验证。其数学基础是大整数分解难题:已知 n = p × q,在 p、q 足够大时无法在可行时间内分解出私钥。RS256 使用 PKCS#1 v1.5 填充,结构确定、兼容性极好,但填充方式较老,理论上存在特定条件下的侧信道与填充相关攻击面。

PS256(RSASSA-PSS + SHA256)同样基于 RSA,但使用 PSS(Probabilistic Signature Scheme)填充。PSS 在填充中引入随机盐值,使同一消息每次签名结果不同,安全性证明更接近随机预言机模型,抗选择消息攻击能力优于 PKCS#1 v1.5。代价是签名长度与 RS256 相同,但实现复杂度略高,部分老旧库支持不完整。

ES256(ECDSA P-256 + SHA256)基于椭圆曲线离散对数难题。在 256 位素域曲线上,其安全强度约等于 3072 位 RSA。签名由 (r, s) 两个整数组成,长度远小于 RSA,非常适合移动端、IoT 和高并发网关。但 ECDSA 对随机数 k 极度敏感:若两次签名复用同一 k,私钥可被直接解出(索尼 PS3 事件即为此类)。因此实现必须使用密码学安全的随机源。

JWT 签名算法选型与攻击防御全景 四种算法对比 HS256 · 对称 HMAC 密钥:共享密钥 K(双方持有) 强度:依赖密钥长度与保密性 场景:单体服务、内部可信调用 RS256 · RSA PKCS#1 v1.5 密钥:私钥签名 / 公钥验证 强度:2048 位 ≈ 112 位安全 场景:开放平台、多服务验证 PS256 · RSA-PSS 密钥:私钥签名 / 公钥验证 强度:随机盐,抗选择消息攻击 场景:高安全要求、新系统 ES256 · ECDSA P-256 密钥:私钥签名 / 公钥验证 强度:256 位 ≈ 3072 位 RSA 场景:移动端、IoT、高并发 两类典型攻击 ① none 算法漏洞 攻击者将 alg 改为 "none" 并删除签名段 若验证方未强制校验 alg 则直接接受伪造令牌 后果:任意身份伪造 ② 算法混淆攻击 服务端预期 RS256 攻击者改为 HS256 用公开的 RSA 公钥 当作 HMAC 密钥签名 后果:公钥泄露即伪造 防御措施 白名单校验 alg 服务端硬编码允许的算法 绝不使用令牌自带的 alg 决策 拒绝 none 显式禁用 none 与空签名 验证签名段必须存在且非空 密钥类型隔离 HMAC 密钥与 RSA 公钥分离 禁止公钥参与 HMAC 运算 库与版本管理 使用维护活跃的 JWT 库 及时修复已知算法混淆 CVE 密钥轮换与审计 定期轮换签名密钥 记录 alg 异常并告警

二、安全强度与性能的量化对比

下表给出常见参数下的安全强度与性能参考。表中“等效安全强度”以对称密码的比特数表示,数值为业界公认的估算,非实测结果;签名长度与耗时属于典型量级示例,实际值受库实现、硬件和消息长度影响。

算法 密钥类型 典型密钥/曲线 等效安全强度 签名长度(示例) 验证性能
HS256 对称共享密钥 ≥256 位随机密钥 约 256 位(密钥保密时) 32 字节 极快
RS256 非对称 RSA 2048 / 3072 约 112 / 128 位 256 / 384 字节 较慢
PS256 非对称 RSA 2048 / 3072 约 112 / 128 位 256 / 384 字节 较慢
ES256 非对称 P-256 曲线 约 128 位 64 字节 快

从表中可以看出:HS256 在密钥完全保密且仅限可信内部使用时性能最优,但一旦密钥泄露或需要多方验证,风险急剧上升。RS256 与 PS256 的签名长度相同,PS256 在安全性证明上更强,新系统应优先选择 PS256。ES256 以最短的签名长度提供约 128 位安全强度,是移动端和资源受限场景的首选,但必须确保随机数 k 的生成质量。

三、none 算法漏洞的成因与防御

none 算法本意是用于“不签名”的场景,例如某些内部调试或已由其他层保证完整性的令牌。问题在于,如果验证方直接读取令牌头部的 alg 字段并据此选择验证逻辑,攻击者只需把 alg 改为 none、删除签名段,就能构造出被接受的令牌。例如:

// 攻击者构造的令牌(header.payload. 末尾为空签名)
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.
eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.

防御的核心是:验证方必须维护一份允许算法的白名单,并且只使用白名单中的算法进行验证,绝不根据令牌自带的 alg 动态选择。同时显式拒绝 none,要求签名段必须存在且非空。以下是一个校验逻辑的示例:

ALLOWED_ALGS = {"RS256", "PS256", "ES256"}

def verify_token(token, jwks):
    header = decode_header(token)
    alg = header.get("alg")
    if alg not in ALLOWED_ALGS:
        raise InvalidAlgorithmError(f"algorithm {alg} not allowed")
    if alg == "none":
        raise InvalidAlgorithmError("none algorithm is forbidden")
    key = select_key(jwks, header.get("kid"))
    return jwt.decode(token, key, algorithms=[alg])

四、算法混淆攻击的防御

算法混淆攻击的典型场景是:服务端预期使用 RS256,验证时却根据令牌头部的 alg 选择了 HS256,并把 RSA 公钥当作 HMAC 密钥使用。由于 RSA 公钥是公开的,攻击者可以用它计算 HMAC 签名,从而伪造令牌。防御要点有三:第一,验证方固定算法,不信任令牌中的 alg;第二,HMAC 密钥与 RSA/EC 公钥在存储和使用上严格隔离,禁止公钥参与 HMAC 运算;第三,使用维护活跃的 JWT 库,并及时修复已知的算法混淆 CVE。

关键提醒:算法选型不是“越强越好”,而是要与信任模型匹配。对称算法适合单一信任域,非对称算法适合多验证方场景。无论选择哪种算法,验证方都必须以服务端配置为准,而不是以令牌自述为准。

五、选型建议

  • 单体服务、内部调用:HS256,密钥长度至少 256 位,定期轮换。
  • 开放平台、多服务验证:PS256 优先,RS256 作为兼容备选,公钥通过 JWKS 端点分发。
  • 移动端、IoT、高并发:ES256,签名短、验证快,但必须保证随机数质量。
  • 任何场景:禁用 none,固定算法白名单,隔离密钥类型,监控 alg 异常。

算法选型的本质是信任边界的设计。HS256 把信任放在共享密钥上,RS256/PS256/ES256 把信任放在私钥持有者上。理解这一点,才能在具体系统中做出既安全又高效的决策。