DPoP証明の検証:jti・htm・htu・nonceで防げるリプレイと防げないリプレイ

OAuth 2.0のDPoP証明JWTを受け取ったサーバーが何を確認すべきか、jti・htm・htu・iat・nonceがリプレイをどこまで抑えるかを、RFC 9449だけを根拠に整理します。

oauthdpopjwtsecurityauthentication

DPoP(Demonstrating Proof of Possession)を使うクライアントは、リクエストごとにDPoPヘッダーで短いJWTを1つ送る。このJWTをDPoP証明(proof)と呼ぶ。証明はクライアントの秘密鍵で署名され、ヘッダーには対応する公開鍵が入っている。サーバーが署名だけを確かめて終わりにすると、証明からわかるのは「この鍵を持つ誰かが、いつかこのJWTを作った」ということだけだ。その証明がいまのこのリクエストのために作られたと言えるのは、jti、htm、htu、iat、nonceまで確認してからになる。

この記事の根拠はRFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)(2023年9月、Standards Track)の1本だけで、2026-10-28に原文を確認した。JWT、JWK、HTTPのセマンティクスなど、RFC 9449が参照する他の文書の細部は扱わない。特定のライブラリやサービスがこれらの規則をどう実装しているかも扱わない。

DPoPがすること、しないこと

RFC 9449のアブストラクトは、DPoPをアプリケーション層の所有証明によってOAuth 2.0トークンを送信者制約(sender-constraining)する仕組みと説明し、これによってアクセストークンとリフレッシュトークンのリプレイ攻撃を検知できると述べている。トークンが特定の公開鍵に結び付くため、トークンだけを盗んだ側はその鍵で署名した証明を作れない。

次の2つは最初から範囲外だ。

  • クライアント認証ではない。 §1は“DPoP itself is not used for client authentication”と書き、§3でもこの仕組みはクライアント認証の方式ではないと改めて述べている。
  • リクエストのボディやヘッダーの完全性は保証しない。 §11.7によれば、証明が対象にするのはHTTPメソッドとURIだけだ。メッセージの内容やヘッダーを書き換えられる攻撃者は、リクエストをリプレイできる場合もある。同じ節は通常TLSがこれを十分に防ぐとしており、§2はDPoPを常にHTTPSと組み合わせて使わなければならない(MUST)と定めている。

証明JWTに入るもの

§4.2が定める構造は次のとおり。JOSEヘッダーの3つのフィールドはすべて必須だ。

ヘッダーフィールド 要件
typ 値はdpop+jwt
alg 登録済みの非対称署名アルゴリズム。noneや対称(MAC)アルゴリズムの識別子であってはならない(MUST NOT)
jwk クライアントの公開鍵。秘密鍵を含んではならない(MUST NOT)

ペイロードのクレームは次のとおり。

クレーム 必須かどうか 内容
jti 必須 証明の一意な識別子
htm 必須 このJWTを付けたリクエストのHTTPメソッド
htu 必須 リクエストの対象URI。クエリとフラグメントは除く
iat 必須 JWTの作成時刻
ath アクセストークンと一緒に保護リソースへアクセスするときは必須 アクセストークン値のASCIIエンコードに対するSHA-256ハッシュをbase64urlでエンコードした値
nonce サーバーがDPoP-Nonceヘッダーでnonceを渡した場合は必須 サーバーが最近渡したnonce

jtiは、同じ文脈の別の証明と値が重なる確率を無視できるほど小さくしなければならない。§4.2は生成方法の例として、96ビット以上の擬似乱数データをbase64urlなどでエンコードする方法と、バージョン4のUUIDを挙げている。拡張やプロファイル、配備環境の要件に応じて、ほかのヘッダーパラメータやクレームを加えることも許されている(MAY)。

RFC 9449がトークンリクエストの例として示す証明をデコードすると次のようになる。署名は省略した。まずJOSEヘッダーだ。

{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
    "y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA",
    "crv": "P-256"
  }
}

続いてペイロード。

{
  "jti": "-BwC3ESc6acc2lTc",
  "htm": "POST",
  "htu": "https://server.example.com/token",
  "iat": 1562262616
}

アクセストークンを伴わないトークンエンドポイントへのリクエストで、サーバーもnonceを渡していないので、athとnonceはない。

サーバーが確認すべき12項目

§4.3は、証明を検証するサーバーが次のすべてを確認しなければならない(MUST)と定めている。確認の順序は問わないと明記されている。

  1. DPoPリクエストヘッダーが2つ以上ない。
  2. ヘッダーの値が、形式の正しいJWT 1つである。
  3. §4.2が求めるクレームがすべてそろっている。
  4. typがdpop+jwtである。
  5. algが登録済みの非対称署名アルゴリズムで、noneではなく、アプリケーションがサポートしており、ローカルポリシー上も許容される。
  6. jwkに含まれる公開鍵で署名が検証できる。
  7. jwkに秘密鍵が含まれていない。
  8. htmが現在のリクエストのHTTPメソッドと一致する。
  9. htuが、JWTを受け取ったリクエストのHTTP URIと一致する。クエリとフラグメントは無視する。
  10. サーバーがクライアントにnonceを渡していた場合、nonceクレームがその値と一致する。
  11. iat、またはnonceに含まれるサーバー管理のタイムスタンプから決まる作成時刻が、許容範囲に収まっている。
  12. アクセストークンと一緒に保護リソースへ提示された場合、athがそのアクセストークンのハッシュと等しく、アクセストークンが結び付いた公開鍵が証明の公開鍵と一致する。

htuの比較については、偽陰性を減らすためにRFC 3986 §6.2.2の構文ベースの正規化と§6.2.3のスキームベースの正規化を適用することが推奨されている(SHOULD)。署名検証(6)を通ったからといって残りを省けない理由もここにある。署名が示すのは鍵の所有だけで、証明がどのリクエストのためのものかを確かめるのは8から12だ。

4と5は、別の用途の署名付きJWTを証明として使い回される問題に関わる。§11.5は署名付き証明を受け付けるサーバーがtypがdpop+jwtであることを確認しなければならない(MUST)と改めて書き、§11.6はnoneアルゴリズムを許してはならない(MUST NOT)としている。algの検証全般についてはJWT検証でalgを信用してはいけない理由で扱った。

htmとhtuが防ぐもの

§11.1は、証明を手に入れた攻撃者が同じエンドポイントにそれをリプレイできる、というところから話を始める。htmとhtuは証明を特定のメソッドとURIに結び付ける。そのため、トークンエンドポイントに送ったPOSTの証明をリソースサーバーへのGETに付けて使い回すと、8と9で弾かれる。

逆に、メソッドとURIが同じリクエストどうしはhtmとhtuだけでは区別できない。htuはクエリを除いて比較するので、パスが同じでクエリだけが違うリクエストも、この段階では同じ対象に見える。この隙間を狭めるのが時間制限とjti、そしてnonceだ。

iatとjti:時間枠の中でだけ受け付ける

§11.1の最初の要件は時間に関するものだ。サーバーは作成から限られた時間内の証明だけを受け付けなければならない(MUST)。括弧書きで「できれば数秒から数分程度の短い期間」と添えられているが、RFCは具体的な数値を定めていない。クロックのずれを考慮して、iatがわずかに未来の証明を受け付けることは許されている(MAY)。

時間枠の中でのリプレイはjtiで扱う。§11.1によれば、サーバーは対象URIの文脈で、証明が受け付けられうる時間枠のあいだ各証明のjtiを保存しておき、同じ値が再び来たら拒否することができる(can)。これはMUSTではなく、取りうる方法として書かれている。同じ節は、厳密に運用すればこの一回限りのチェックはリプレイに対して非常に強い防御になるが、たとえば同じエンドポイントを受け持つ複数のサーバーが状態を共有していない環境では実現が難しい場合があると認めている。

jtiを追跡するサーバーは、不必要に長いjtiを拒否するか、ハッシュだけを保存するのがよいとも同じ節は助言している。この文は小文字のshouldで書かれており、規範的なキーワードではない。

まとめると、時間枠はRFCが求める最低線で、jtiの保存はその枠内のリプレイまで防ぎたいときにサーバーが選ぶ手段だ。jtiの保存先を持たないなら、時間枠の長さがそのままリプレイ可能な期間の上限になる。これは§11.1の記述から導いた解釈で、RFCにそう書かれているわけではない。

nonce:時間を決めるのはサーバー

iatはクライアントが決める値だ。§11.2は、クライアントを掌握した攻撃者がiatを未来の時刻にして証明を事前に作っておけると指摘している。サーバーが渡すnonceは、事前に作った証明を現在のnonceの有効期間を超えて使うことを防ぐ。nonceをいつ変えるかはサーバーが自分で決めるので、現在のnonceで事前に作った証明は、そのnonceの有効期間が終われば使えなくなる。ただし期間が終わるまでは使えるので、その間のリプレイには引き続きjtiによる処理が必要だ。

流れは、サーバーの種類によって応答コードが異なる。

  • 認可サーバー(§8)。 nonceが必要なのに証明に含まれていなければ、HTTP 400とuse_dpop_nonceエラー、そしてDPoP-Nonceヘッダーで応答する。クライアントはその値をnonceクレームに入れてリクエストし直す。
  • リソースサーバー(§9)。 HTTP 401とWWW-Authenticate: DPoP error="use_dpop_nonce"、そしてDPoP-Nonceヘッダーで応答する。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="use_dpop_nonce"
DPoP-Nonce: <サーバーが決めた値>

nonceに関するおもな規則は次のとおり。

  • nonceの値は予測できないものでなければならない(MUST)。クライアントにとっては不透明な値だ(§8)。
  • 証明のnonceが、認可サーバーが最近渡したnonceと完全に一致しなければ、認可サーバーはリクエストを拒否しなければならない(MUST)(§8)。
  • サーバーは以後の応答のDPoP-Nonceヘッダーで新しいnonceを渡すことができ、クライアントは次のリクエストからその値を使わなければならない(MUST)。DPoP-Nonceを含む応答はキャッシュされないようにすることが推奨される(SHOULD)(§8.2)。
  • クライアントにnonceを渡したあとは、nonceクレームのない証明を受け付けてはならない(MUST NOT)(§11.3)。この規則がなければ、攻撃者はnonceを省いて送るだけでnonceの検査をすり抜けられる。
  • nonceは、それを発行したサーバーでだけ使うものだ(§9)。

nonceは一回限りの値というわけではない。§11.1は、クライアントが同じnonceを何度使ってもかまわないとしたうえで、jtiを追跡してnonceの有効期間中の重複を拒否していれば、トークンのリプレイのリスクが増えることはないと説明している。つまりnonceは証明の有効期間をサーバーの時計で区切る役目で、その期間内の重複はやはりjtiが受け持つ。§11.1は、サーバー時刻を埋め込んだnonceで証明の寿命を制限すれば、クライアントとサーバーのクロックのずれに関係なく同じ効果が得られるとも述べている。

nonceを使わない配備について、§11.2は寿命の長いDPoP制約付きアクセストークンを発行すべきでなく(SHOULD NOT)、短命のアクセストークンとリフレッシュトークンを使うよう勧めている。事前生成された証明はathがあるため、いずれにしてもそのアクセストークンの寿命内でしか使えない。トークンの寿命が短いほど、被害を受けうる期間も短くなる。

各仕組みがカバーする範囲

仕組み RFC 9449での要件の強さ 防ぐもの 残る隙間
署名 + jwk 検証はMUST(§4.3) 鍵を持たない側による証明の作成 鍵の所有を示すだけで、リクエストとの結び付きはない
htm、htu 一致の確認はMUST(§4.3) 別のメソッドやURIへの使い回し 同じメソッドとURIへのリプレイ、クエリの違い
iatの時間枠 限られた時間だけ受け付けるのはMUST(§11.1) 古い証明のリプレイ 枠内のリプレイ、未来のiatで事前に作った証明
jtiの保存 取りうる方法(can)(§11.1) 枠内での同じ証明のリプレイ 状態を共有しない複数のサーバー
サーバーのnonce 渡すかどうかは任意、渡したあとの確認はMUST(§8、§11.3) 現在のnonceの有効期間を超えた、事前に作った証明の使用。証明の寿命の制限がクライアントとサーバーのクロックの一致に依存しなくなる nonceの有効期間内の事前生成やリプレイは防げず、jtiで扱う必要がある
ath アクセストークンと使うときは必須(§4.2、§4.3) 別のアクセストークンとの組み合わせ 同じアクセストークンとのリプレイは防げない。時間枠とjtiの確認は引き続き必要

どの仕組みも、クライアントの中で攻撃者のコードが動いている状況までは防げない。§11.4は、その場合DPoPの保証は成り立たないと説明している。鍵をエクスポートできないようにすれば鍵そのものの窃取は防げるが、クライアントがオンラインのあいだに攻撃者が証明を作って使うことまでは止められない。

確認すること

  • 署名の検証で終わらせず、§4.3の12項目をすべて確認しているか。
  • algを証明の中の値ではなくサーバー側の許可リストと照合しているか。noneと対称アルゴリズムを拒否しているか。
  • htuを比較する前に、クエリとフラグメントを外してRFC 3986の正規化を適用しているか。
  • iatの許容範囲を数秒から数分程度に狭くとり、未来方向の許容幅をクロックのずれの分だけにしているか。
  • jtiの保存先を使うなら、対象URIと時間枠の単位で保持しているか。複数のサーバーが同じ保存先を見ているか。
  • サーバーがそのクライアントにnonceを渡している場合に、nonceのない証明を拒否しているか。
  • nonceを使わないなら、DPoP制約付きアクセストークンの寿命が短いか。

認可コードの段階の保護はOAuth 2.0のPKCEで扱った。PKCEはコードをトークンに交換する瞬間を守り、DPoPは発行されたトークンを鍵に結び付ける。

参考資料

2026-10-28に確認した。

ブログへ