错误信息
token signature is invalid: crypto/ecdsa: verification error
也可能输出为
- token signature is invalid: crypto/rsa: verification error
- token signature is invalid: signature is invalid
- token signature is invalid: ed25519: verification error
- crypto/ecdsa: verification error
golang-jwt v4 只返回内部文本,例如“crypto/ecdsa: verification error”。应使用 `errors.Is(err, jwt.ErrTokenSignatureInvalid)`(v5),而不是匹配字符串。
golang-jwt/jwt (Go) — jwt.ErrTokenSignatureInvalid joined with the algorithm's verification error
含义
golang-jwt 解析了令牌、接受了它的算法,也从你的 `Keyfunc` 拿到了密钥,但用这个密钥验证签名没有通过。在 v5 中,这个错误由 `jwt.ErrTokenSignatureInvalid`(“token signature is invalid”)和算法自身的错误组合而成:ES* 为 `crypto/ecdsa: verification error`,RS*/PS* 为 `crypto/rsa: verification error`,HS* 为 `signature is invalid`。令牌的字节和这个密钥根本不是一对。
原因
Keyfunc 返回了错误的密钥
常见来自其他环境的密钥、从文档中照搬的硬编码密钥,或者忽略 `kid`、总是返回多密钥集合中第一个密钥的 Keyfunc。
颁发者轮换了签名密钥
常见只在启动时加载一次公钥的服务,在密钥轮换后仍继续用旧密钥验证。在服务重新加载密钥之前,每个新令牌都会验证失败,而缓存的旧令牌仍能通过。
签名方生成的是 DER 编码的 ECDSA 签名
偶见golang-jwt 按 64 字节的原始格式(r‖s,RFC 7518)解析 ES256 签名。由 KMS 或 `ecdsa.SignASN1` 签名且未经转换的令牌,带的是约 71 字节的 DER 签名,即使密钥正确也会验证失败。
令牌在签名后被修改过
偶见重新编码过的载荷、头部或载荷段中的空白字符变化,或者用两个不同令牌的片段拼成的令牌。签名覆盖的是原样的 base64url 文本。
修复方法
- 1.
按 kid 从 JWKS 中查找密钥
让 Keyfunc 从颁发者的密钥集中选出令牌头部指定的密钥,并在轮换时刷新密钥集(例如使用 `github.com/MicahParks/keyfunc`)。始终用 `WithValidMethods` 固定允许的算法。
go import ( "context" "github.com/MicahParks/keyfunc/v3" "github.com/golang-jwt/jwt/v5" ) // Resolve the verification key from the issuer's JWKS by the token's kid. k, err := keyfunc.NewDefaultCtx(ctx, []string{"https://issuer.example.com/.well-known/jwks.json"}) if err != nil { return err } token, err := jwt.Parse(raw, k.Keyfunc, jwt.WithValidMethods([]string{"ES256"}), jwt.WithIssuer("https://issuer.example.com/"), jwt.WithAudience("https://api.example.com")) - 2.
证明双方使用的是同一个密钥
对验证方使用的公钥和颁发者私钥对应的公钥,分别计算其 DER 编码的哈希。哈希不一致,问题就查清了。
bash # Same key on both sides? Compare SPKI fingerprints. openssl pkey -pubin -in verifier-public.pem -outform DER | sha256sum openssl pkey -in issuer-private.pem -pubout -outform DER | sha256sum - 3.
按错误类别处理,而不是按字符串
golang-jwt v5 提供了哨兵错误(sentinel error)。把签名验证失败映射为 401,不要退而尝试其他密钥,也不要跳过验证。
go switch { case errors.Is(err, jwt.ErrTokenSignatureInvalid): return http.StatusUnauthorized // wrong key or tampered token: never retry with another key blindly case errors.Is(err, jwt.ErrTokenExpired): return http.StatusUnauthorized // client should refresh case errors.Is(err, jwt.ErrTokenMalformed): return http.StatusBadRequest }