Сообщение об ошибке
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») с собственной ошибкой алгоритма: `crypto/ecdsa: verification error` для ES*, `crypto/rsa: verification error` для RS*/PS*, `signature is invalid` для HS*. Байты токена и ключ просто не соответствуют друг другу.
Почему это происходит
Keyfunc возвращает не тот ключ
частоКлюч из другого окружения, жёстко прописанный в коде ключ из документации или Keyfunc, которая игнорирует `kid` и всегда возвращает первый ключ из набора ключей, где их несколько.
Издатель провёл ротацию ключа подписи
частоСервисы, которые загружают открытый ключ один раз при запуске, после ротации продолжают проверять подписи старым ключом. Все новые токены не проходят проверку, а старые закешированные — по-прежнему проходят, пока сервис не перезагрузит ключ.
Подписант создал подпись ECDSA в кодировке DER
иногдаgolang-jwt ожидает подпись ES256 в виде 64 байт в формате raw (r‖s, RFC 7518). Токен, подписанный через KMS или `ecdsa.SignASN1` без преобразования, содержит DER-подпись длиной около 71 байта, и она не проходит проверку даже с правильным ключом.
Токен изменили после подписания
иногдаПовторно закодированная полезная нагрузка, изменённые пробельные символы в сегменте заголовка или полезной нагрузки либо токен, собранный из частей двух разных токенов. Подпись охватывает именно этот текст base64url, символ в символ.
Как исправить
- 1.
Выбирайте ключ из JWKS по kid
Пусть 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 errors). Отвечайте на ошибку подписи статусом 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 }