エラーメッセージ
signature verification failed
jose (JavaScript) — JWSSignatureVerificationFailed, code ERR_JWS_SIGNATURE_VERIFICATION_FAILED
このエラーの意味
jose は JWS を正常に解析し、暗号学的なチェックを実行しましたが、選択した鍵では署名を検証できませんでした。構造、アルゴリズム、鍵の種類はいずれも問題ありませんでした。数学的に一致しなかっただけであり、原因は鍵か、署名対象のバイト列の改変にあると考えられます。
発生する理由
このトークンに合わない鍵
よくある検証者が持っている鍵が、発行者が署名に使った鍵と異なります。古い kid がなくなったローテーション後の JWKS、更新されずに古くなったハードコードの鍵、環境の取り違え(ステージングの検証者と本番のトークン)などが該当します。
署名者と検証者の間で署名対象のバイト列が変わった
よくあるJWS が署名するのは、ヘッダーとペイロードを base64url で表した header.payload のテキストそのものです。これらのセグメントを再エンコードする処理(base64 のパディングを正規化するゲートウェイ、JSON の再シリアライズ、転送中の文字の欠落)があると、デコードしたペイロードが同一に見えても検証は失敗します。
自作の ES256 署名処理が DER を出力した
ときどきNode の crypto.sign() は、ECDSA ではデフォルトで ASN.1 DER を出力しますが、RFC 7518 では ES256 の JWS 署名は生の r‖s(ちょうど 64 バイト)でなければなりません。3 番目のセグメントをデコードすると 0x30 で始まる約 70〜72 バイトになるトークンは、誤った形式で署名されています。
修正方法
- 1.
kid をもとに発行者の JWKS から鍵を取得する
1 つの鍵を手動で固定するのではなく、トークンごとに正しい鍵を jose に選ばせてください。createRemoteJWKSet は鍵をキャッシュし、ローテーションのたびに更新します。
js import { createRemoteJWKSet, jwtVerify } from "jose"; const jwks = createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: "https://issuer.example", }); - 2.
node:crypto で署名する場合は生の r‖s を出力する
独自の署名処理では IEEE P1363 形式の出力を指定し、署名が JWS の要求する 64 バイトの形式になるようにしてください。あるいは、3 番目のセグメントの長さを調べて、署名処理が出力したものを確認してください。
js import { sign } from "node:crypto"; const sig = sign("sha256", Buffer.from(signingInput), { key: privateKey, dsaEncoding: "ieee-p1363", // raw r||s, 64 bytes for P-256 }); - 3.
署名セグメントの形を確認する
トークンの 3 番目のセグメントをデコードしてください。64 バイトなら生の r‖s(ES256 として正しい形式)、0x30 で始まる約 70 バイトなら DER です。下にある DER と生の形式の変換ツールは、r と s の値を表示し、2 つの形式を相互に変換します。