エラーメッセージ
Signature verification failed
PyJWT (Python) — jwt.exceptions.InvalidSignatureError
このエラーの意味
PyJWT はトークンの構造をデコードして署名のチェックを実行しましたが、そのチェックに失敗しました。署名が鍵とペイロードに一致しません。InvalidSignatureError は DecodeError のサブクラスなので、範囲の広すぎる except DecodeError では、これを解析の問題と誤って扱うおそれがありますが、実際には鍵と内容の不一致です。ES256 のトークンにはもう 1 つ注意点があります。JWS の署名は 64 バイトの生の r‖s 形式でなければならないため、ASN.1 DER を出力する自作の署名処理で作られたトークンは、鍵が正しくてもここで失敗します。
発生する理由
発行者が使ったものとは別の鍵
よくある鍵ペアをローテーションしたのに検証者が古い公開鍵を使い続けている場合、間違った PEM をコピーして貼り付けた場合、2 つのサービスがシークレットマネージャーから別々のバージョンのシークレットを読み込んでいる場合などです。JWKS を使う発行者では、kid の不一致も同じ結果になります。
自作の署名処理が誤った署名を生成した
ときどき生の r‖s ではなく DER を使うこと以外にも、ES256 のヘッダーなのに SHA-384 でハッシュする自作の署名処理や、base64url の header.payload のテキストではなく JSON に署名する処理は、形式は正しくても決して検証に成功しない署名を生成します。設定の受け渡しの過程で壊れた鍵は、ここには該当しません。リテラルの \n シーケンスを含む PEM は「Unable to load PEM file」で読み込みに失敗し、正常な PEM の前後にある引用符やインデントは許容されます。
署名後にトークンが変更された
ときどきペイロードの JSON を再シリアライズしたり、1 文字でも変えたりするミドルウェアは、署名されたバイト列を変えてしまいます。署名は 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")