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 事件即为此类)。因此实现必须使用密码学安全的随机源。
二、安全强度与性能的量化对比
下表给出常见参数下的安全强度与性能参考。表中“等效安全强度”以对称密码的比特数表示,数值为业界公认的估算,非实测结果;签名长度与耗时属于典型量级示例,实际值受库实现、硬件和消息长度影响。
| 算法 | 密钥类型 | 典型密钥/曲线 | 等效安全强度 | 签名长度(示例) | 验证性能 |
|---|---|---|---|---|---|
| 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 把信任放在私钥持有者上。理解这一点,才能在具体系统中做出既安全又高效的决策。