错误信息
Signature verification failed
PyJWT (Python) — jwt.exceptions.InvalidSignatureError
含义
PyJWT 解码了令牌的结构并执行了签名检查,但检查没有通过:签名与密钥和载荷不匹配。InvalidSignatureError 是 DecodeError 的子类,因此范围过宽的 except DecodeError 可能会把它误标为解析问题——实际上这是密钥与内容不匹配。ES256 令牌还有一个额外的细节:JWS 签名必须是 64 字节的原始 r‖s 格式,所以由自行编写、输出 ASN.1 DER 的签名代码生成的令牌,即使密钥正确,也会在这一步验证失败。
原因
所用密钥与颁发者使用的不同
常见密钥对已轮换而验证方仍保留着旧公钥、复制粘贴了错误的 PEM,或者两个服务从机密管理器中读取了不同版本的机密。对于基于 JWKS 的颁发者,kid 不匹配也会造成同样的结果。
自行编写的签名代码生成了错误的签名
偶见除了用 DER 代替原始 r‖s 之外,如果自制的签名代码在 ES256 头部下用 SHA-384 做哈希,或者签名的是 JSON,而不是由 base64url 头部和载荷组成的 header.payload 文本,生成的签名格式正确,却永远无法通过验证。在配置传递过程中被损坏的密钥不会导致这个错误:含有字面 \n 序列的 PEM 在加载时就会报错“Unable to load PEM file”,而完整 PEM 周围的引号或缩进是可以容忍的。
令牌在签名后被修改
偶见中间件只要重新序列化载荷 JSON,或者改动哪怕一个字符,就会改变被签名的字节。签名覆盖的是原样的 base64url 文本——看似无关紧要的改动并非无关紧要。(添加的 base64 填充会更早被拒绝,报错为“Invalid payload padding”;被截断的令牌则报“Not enough segments”。)
修复方法
- 1.
在两端分别计算密钥指纹
输出验证方加载的公钥的 SHA-256 指纹,并与颁发者的指纹比较。不一致,问题就查清了;一致,则说明问题出在令牌的字节上。
python import hashlib from cryptography.hazmat.primitives import serialization public_key = serialization.load_pem_public_key(pem_bytes) # the key the verifier uses spki = public_key.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) print(hashlib.sha256(spki).hexdigest()) # compare with the issuer's - 2.
先检查 kid,再怀疑令牌损坏
读取未经验证的头部,确认你用来验证的正是令牌所指定的密钥。如果颁发者有多个密钥,就通过 PyJWKClient 按 kid 查找。
python import jwt header = jwt.get_unverified_header(token) print(header) # {'alg': 'ES256', 'kid': '...'} from jwt import PyJWKClient key = PyJWKClient("https://issuer.example/.well-known/jwks.json") \ .get_signing_key_from_jwt(token).key payload = jwt.decode(token, key, algorithms=["ES256"], audience="api://orders")