Digest 認証

3 行まとめ
Section titled “3 行まとめ”- Digest 認証は Basic 認証と同じ HTTP 認証フレームワーク (RFC 9110 §11) 上のスキームで、仕様は RFC 7616(RFC 2617 を廃止)。
- サーバーが渡した
nonceを混ぜたハッシュを送るので、パスワードそのものは経路に出ない。使い回しもnc(nonce カウント)で防ぐ。 - それでも実務で選ばれない。サーバーは
H(A1)を保存しなければならず、これがパスワード等価だから。ハッシュ化保存の利点が消える。
HTTP 認証フレームワークの中の位置づけ
Section titled “HTTP 認証フレームワークの中の位置づけ”枠組みは Basic とまったく同じ。401 + WWW-Authenticate でチャレンジし、Authorization で応答する。違うのは「Authorization に何を載せるか」だけ。
flowchart TB
subgraph FW["HTTP 認証フレームワーク (RFC 9110 §11)"]
direction TB
C["チャレンジ<br/>401 + WWW-Authenticate"]
R["クレデンシャル送信<br/>Authorization ヘッダ"]
C --> R
end
FW --> S1["Basic (RFC 7617)<br/>パスワードを Base64 で載せる"]
FW --> S2["Digest (RFC 7616)<br/>nonce を混ぜたハッシュを載せる"]
FW --> S3["Bearer (RFC 6750)<br/>トークンを載せる"]
style S2 fill:#4054b2,color:#fff
Basic が「毎回パスワードを送る」のに対し、Digest は「パスワードを知っていることを証明する値を送る」。この置き換えだけで、経路の盗聴に対しては強くなる。ただし後述のとおり、代償としてサーバー側の保存が弱くなる。
Basic と違い、サーバーから nonce をもらうまでクレデンシャルを組み立てられない。したがって先回り送信ができず、最初の往復が必ず発生する。
sequenceDiagram
autonumber
participant B as ブラウザ
participant S as サーバー
B->>S: GET /private/ (Authorization なし)
S-->>B: 401 Unauthorized<br/>WWW-Authenticate: Digest<br/>realm, qop, nonce, algorithm
Note over B: nonce を受け取って初めて<br/>計算材料が揃う
B->>B: cnonce を生成し nc=00000001 に
B->>B: H(A1) と H(A2) を求め<br/>response を計算
B->>S: GET /private/<br/>Authorization: Digest<br/>username, nonce, nc, cnonce, response
S->>S: 保存済みの H(A1) で<br/>同じ計算をして突き合わせる
alt 一致し nonce も有効
S-->>B: 200 OK<br/>Authentication-Info: rspauth, nextnonce
Note over B: rspauth を検証すると<br/>サーバー側も本物だと分かる
else nonce の期限切れ
S-->>B: 401 + stale=true
Note over B: 利用者に再入力させず<br/>新しい nonce で再送する
else 不一致
S-->>B: 401 (再チャレンジ)
end
stale=true の分岐が Digest 特有。nonce には寿命があるので、正しいパスワードでも期限切れで弾かれる。このとき stale=true を付けると、クライアントはダイアログを出さずに新しい nonce で再計算する。付け忘れると、利用者には「正しいパスワードを入れたのに再入力を求められた」ように見える。
チャレンジとクレデンシャルのパラメータ
Section titled “チャレンジとクレデンシャルのパラメータ”チャレンジ側: WWW-Authenticate
Section titled “チャレンジ側: WWW-Authenticate”WWW-Authenticate: Digest realm="Staging Area", qop="auth", algorithm=SHA-256, nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS", charset=UTF-8, userhash=true| パラメータ | 必須 | 意味 |
|---|---|---|
realm |
実質必須 | 保護空間の名前。H(A1) の計算に入るので、変えると全パスワードが無効になる |
nonce |
実質必須 | サーバーが毎回変える使い捨ての値。リプレイ防止の核 |
qop |
必須 | auth または auth-int。RFC 7616 で必須になった(RFC 2617 では任意) |
algorithm |
任意 | MD5 / SHA-256 / SHA-512-256 と各 -sess 版。省略時は MD5 |
opaque |
任意 | サーバーの状態。クライアントは中身を解釈せずそのまま返す |
stale |
任意 | true なら「nonce が古いだけ。パスワードは合っている」 |
domain |
任意 | この認証が通用する URI の一覧 |
charset |
任意 | 指定できる値は UTF-8 のみ |
userhash |
任意 | true なら利用者名をハッシュ化して送ってよい |
クレデンシャル側: Authorization
Section titled “クレデンシャル側: Authorization”Authorization: Digest username="demo", realm="Staging Area", uri="/private/", algorithm=SHA-256, nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", nc=00000001, cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ", qop=auth, response="3d78f6dba3c48ccb424d50225bac0f815d8466a521cb96d23020a63948cc84b1", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS", userhash=false| パラメータ | 必須 | 意味 |
|---|---|---|
response |
必須 | 計算したハッシュ。これが認証の本体 |
username / username* |
必須 | 利用者名。非 ASCII は username*、隠すなら userhash |
realm / uri / nonce |
必須 | チャレンジで受け取った値をそのまま返す |
qop |
サーバーが指定したら必須 | auth か auth-int |
cnonce |
必須 | クライアント側の使い捨て値。サーバーだけに値を選ばせない |
nc |
必須 | 同じ nonce で送った回数。16 進 8 桁固定で 00000001 から |
opaque |
受け取ったら必須 | そのまま返す |
データフローを追う
Section titled “データフローを追う”Digest でわかりにくいのは「何をどう混ぜて response にするか」。仕組みを一言でいうとこうなる。

パスワードは手元から出ず、経路に出るのは混ぜた結果だけ。 しかも混ぜる材料に使い捨ての値が入っているので、その結果を盗んでも次のリクエストには使えない。以下、実物の値で追う。
1. response ができるまで
Section titled “1. response ができるまで”username = demo / realm = Staging Area / password = P@ssw0rd、GET /private/、algorithm=SHA-256、qop=auth の場合。
| # | 段階 | 値 |
|---|---|---|
| 1 | A1 |
demo:Staging Area:P@ssw0rd |
| 2 | H(A1) |
008dc6f7c71911eabf2c1d6b55873886a6e432f18c06d2dc3f067c3135760966 |
| 3 | A2 |
GET:/private/ |
| 4 | H(A2) |
84a5bbac9197aaeffe57f4a0309811e8a5d08b82f42d9c54181f15d923076ab7 |
| 5 | 連結 | H(A1):nonce:nc:cnonce:qop:H(A2) |
| 6 | response |
3d78f6dba3c48ccb424d50225bac0f815d8466a521cb96d23020a63948cc84b1 |
flowchart TB
IN1["username<br/>realm<br/>password"] --> A1["A1<br/>demo:Staging Area:P@ssw0rd"]
A1 --> HA1["H(A1)<br/>008dc6f7...0966"]
IN2["method<br/>request-uri"] --> A2["A2<br/>GET:/private/"]
A2 --> HA2["H(A2)<br/>84a5bbac...6ab7"]
IN3["nonce<br/>nc<br/>cnonce<br/>qop"] --> J["連結してハッシュ化"]
HA1 --> J
HA2 --> J
J --> RES["response<br/>3d78f6db...84b1"]
style IN1 fill:#7a2222,color:#fff
style RES fill:#1f5c33,color:#fff
押さえどころは 3 つ。
- パスワードは
A1の中にしか出てこない。 経路に出るのはresponseだけで、そこからパスワードを逆算することは(ハッシュなので)できない。 nonceとncとcnonceが入るので、同じパスワードでもリクエストごとにresponseが変わる。 盗んだresponseの再利用が効かない。A2にメソッドと URI が入る。 つまりresponseは「この URI にこのメソッドで」という文脈に縛られる。別の URI に付け替えても通らない。
2. サーバー側は同じ計算を繰り返すだけ
Section titled “2. サーバー側は同じ計算を繰り返すだけ”flowchart TB
A["Authorization: Digest ... を受信"] --> B{"nonce は自分が発行したもので<br/>まだ有効か"}
B -->|"期限切れ"| ST["401 + stale=true<br/>再入力は求めない"]
B -->|"不正・未知"| NG["401 (再チャレンジ)"]
B -->|"有効"| C{"この nonce で<br/>その nc は未使用か"}
C -->|"使用済み"| RP["401<br/>リプレイとして拒否"]
C -->|"未使用"| D["保存済みの H(A1) を取り出す"]
D --> E["受け取った nonce nc cnonce qop と<br/>自分で作った H(A2) を連結"]
E --> F["ハッシュを計算し<br/>response と定数時間で比較"]
F -->|"不一致"| NG
F -->|"一致"| G["200 OK<br/>Authentication-Info を付ける"]
style D fill:#7a2222,color:#fff
style G fill:#1f5c33,color:#fff
赤で示した H(A1) の取り出しが Digest の弱点。サーバーはこの値を持っていなければ検証できず、しかもこの値だけで認証を突破できる(後述)。
3. 手元で確かめる
Section titled “3. 手元で確かめる”import hashlib
def H(s: str) -> str: return hashlib.sha256(s.encode()).hexdigest()
user, realm, pw = "demo", "Staging Area", "P@ssw0rd"method, uri = "GET", "/private/"nonce = "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"cnonce = "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"nc, qop = "00000001", "auth"
ha1 = H(f"{user}:{realm}:{pw}") # 008dc6f7c719...0966ha2 = H(f"{method}:{uri}") # 84a5bbac9197...6ab7print(H(f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}"))# => 3d78f6dba3c48ccb424d50225bac0f815d8466a521cb96d23020a63948cc84b1# --digest を付けると curl が 401 を受けてから計算して再送する。# Basic の -u と違い、先回り送信はできない(nonce が必要なため)curl -v --digest -u demo:'P@ssw0rd' https://example.com/private/
# 実際に 2 往復していることを確認するcurl -sv --digest -u demo:'P@ssw0rd' https://example.com/private/ 2>&1 | grep -E '^(> GET|< HTTP)'# > GET /private/ HTTP/1.1# < HTTP/1.1 401 Unauthorized# > GET /private/ HTTP/1.1# < HTTP/1.1 200 OK4. nonce のライフサイクル
Section titled “4. nonce のライフサイクル”stateDiagram-v2
[*] --> Issued
Issued : 401 で発行
Active : nc を数えて再利用
Stale : 期限切れ
Issued --> Active : 初回の応答が届く
Active --> Active : nc を 1 進める
Active --> Stale : 有効期限が過ぎる
Stale --> Issued : stale で再発行
Active --> [*] : サーバーが破棄
サーバーは nonce ごとに「どの nc まで使われたか」を覚えておく必要がある。 ここが Basic との決定的な運用差で、Digest はステートフルになる。
- 複数台構成なら nonce の状態を共有しないと、リクエストが別のサーバーに当たった瞬間に落ちる
- Apache の
mod_auth_digestは共有メモリを使うため、APR が共有メモリをサポートするプラットフォームでしか正しく動かない - 寿命を短くするとリトライが増え、長くするとリプレイの窓が広がる
RFC 7616 は nonce の作り方の例として time-stamp H(time-stamp ":" ETag ":" private-key) を挙げている。時刻を埋め込んでおけば、期限判定にサーバー側の記憶が要らないという発想。
5. サーバーも名乗る: Authentication-Info
Section titled “5. サーバーも名乗る: Authentication-Info”Digest には相互認証がある。サーバーは Authentication-Info(RFC 7615)で rspauth を返せる。
Authentication-Info: qop=auth, rspauth="824315e53af346f6e67277510146e73b78904532dccd2b3603b0c83eb7d95b29", cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ", nc=00000001, nextnonce="次のリクエストで使う nonce"rspauth は response とほぼ同じ計算で、A2 の method 部分を空にする(A2 = ":" request-uri)。これを検証できれば、相手が H(A1) を知っていること、つまり本物のサーバーであることが確認できる。
- Basic にはこの仕組みがない。 偽サーバーに繋がっても気づけない
nextnonceを使うと、クライアントは次の往復で 401 を食わずに済む- ただしクライアント側で
rspauthを検証している実装は少ない。ブラウザは検証しない
6. 経路に流れるものを Basic と並べる
Section titled “6. 経路に流れるものを Basic と並べる”| Basic | Digest | |
|---|---|---|
| 経路に出る値 | base64(user:pass) = パスワードそのもの |
response = ハッシュ |
| 逆算できるか | できる(Base64 は可逆) | できない |
| 盗んだ値の再利用 | できる(有効期限がない) | できない(nonce と nc で防ぐ) |
| 先回り送信 | できる | できない(nonce が必要) |
| リクエストの改竄検知 | なし | URI とメソッドは縛られる。本文は auth-int のときだけ |
| サーバーの真正性確認 | なし | rspauth で可能 |
経路のセキュリティだけを見れば Digest は明確に上。これは押さえておく。問題は次の節。
パスワード保存の非対称性
Section titled “パスワード保存の非対称性”ここが Digest の評価を決める。 試験でも「Digest のほうが安全か」を問う形で出る。結論を絵にするとこうなる。

同じ「ハッシュを保存している」ように見えて、流出したときに起きることが正反対になる。
flowchart TB
subgraph BASIC["Basic 認証"]
direction TB
B1["サーバーが受け取るのは<br/>平文のパスワード"]
B2["受け取ってから<br/>Argon2 や bcrypt でハッシュ化"]
B3["保存するのは<br/>ソルト付き低速ハッシュ"]
B4["流出しても<br/>そのままでは認証できない"]
B1 --> B2 --> B3 --> B4
end
subgraph DIGEST["Digest 認証"]
direction TB
D1["サーバーが受け取るのは<br/>response だけ"]
D2["検証には H(A1) が必要<br/>H(username:realm:password)"]
D3["保存するのは<br/>ソルトなし高速ハッシュ"]
D4["流出したら<br/>H(A1) だけで認証できる"]
D1 --> D2 --> D3 --> D4
end
style B4 fill:#1f5c33,color:#fff
style D3 fill:#7a4a12,color:#fff
style D4 fill:#7a2222,color:#fff
Digest の検証には H(A1) が必要で、H(A1) はパスワードと等価になる。RFC 7616 §5.2 も次のように書いている。
パスワードファイルは、平文のパスワードを含んでいるものとして保護しなければならない。その realm の文書にアクセスする目的においては、実質的にそうなっているからである。
帰結はこうなる。
- Argon2 や bcrypt が使えない。 ソルトも段数もない
H(username:realm:password)を保存するしかない H(A1)が漏れたら、パスワードを知らなくても認証を通せる。 ハッシュを保存している意味がないrealmを変えると全ユーザーのパスワードが無効になる。H(A1)にrealmが入っているため。逆に言えば、realm 違いへの横展開は防げる- MD5 は GPU で高速に総当たりできるので、Apache 自身のドキュメントが「受動的な盗聴者は今日のグラフィックスハードウェアでパスワードを総当たりできる」と警告している
アルゴリズム
Section titled “アルゴリズム”RFC 7616 で登録されているのは 3 つ。それぞれに -sess 版がある。
algorithm |
ハッシュ長 | 位置づけ |
|---|---|---|
MD5 |
128 bit | RFC 2617 からの互換用。新規に選ばない |
SHA-256 |
256 bit | RFC 7616 で追加。実質これを選ぶ |
SHA-512-256 |
256 bit | SHA-512 を 256 bit に切り詰めたもの |
algorithm を省略すると MD5 扱いになる。書き忘れが即ダウングレードになるので、必ず明示する。
-sess 版は何が違うか
Section titled “-sess 版は何が違うか”-sess を付けると A1 の作り方が変わり、セッション最初の nonce と cnonce が焼き込まれる。
A1 (通常) = username ":" realm ":" passwordA1 (-sess) = H(username ":" realm ":" password) ":" nonce ":" cnonceH(A1) が nonce ごとに変わるので、長期間同じ値を使い続けずに済む。ただし Apache の mod_auth_digest は MD5-sess を正しく実装していないとドキュメントに明記されている。仕様にあることと使えることは別だと分かる例。
サーバーは複数のチャレンジを並べられる
Section titled “サーバーは複数のチャレンジを並べられる”RFC 7616 は、サーバーが WWW-Authenticate を複数返して好ましい順に並べることを認めている。クライアントは自分が対応する中で最も強いものを選ぶ。
WWW-Authenticate: Digest realm="Staging Area", qop="auth", algorithm=SHA-256, nonce="..."WWW-Authenticate: Digest realm="Staging Area", qop="auth", algorithm=MD5, nonce="..."これは同時にダウングレードの窓でもある。 中間者が SHA-256 の行を削れば、クライアントは MD5 を選ぶ。TLS がなければこの操作は自由にできる。
他の認証方式との比較
Section titled “他の認証方式との比較”| 方式 | 経路に出る値 | サーバーの保存 | 失効・ログアウト | ステートフルか |
|---|---|---|---|---|
| Basic | パスワード(Base64) | ソルト付き低速ハッシュにできる | 仕組みがない | ステートレス |
| Digest | ハッシュ(nonce 混ぜ) |
H(A1)(パスワード等価) |
仕組みがない | nonce と nc を持つ |
| Bearer | トークン | トークンのハッシュ | 有効期限・失効リストで可能 | トークンストア次第 |
| Cookie セッション | セッション ID | セッションストア + パスワードは低速ハッシュ | サーバー側で破棄できる | セッションを持つ |
| mTLS | 証明書(TLS 層) | 公開鍵のみ | 失効リスト / 短命証明書 | ステートレス |
TLS を前提にすると、Digest が Basic に勝つ点はほぼ消える。 経路が暗号化されていれば「パスワードを平文で送らない」利点は意味を失い、「サーバーにパスワード等価の値を置く」欠点だけが残る。
flowchart TB
A["Digest を検討している"] --> B{"TLS を使えるか"}
B -->|"使える"| C{"パスワードを<br/>Argon2 などで保存したいか"}
C -->|"したい"| D["Basic + TLS<br/>または Cookie セッション"]
C -->|"H(A1) 保存を受け入れる"| E{"それでも Digest を<br/>選ぶ理由があるか"}
E -->|"既存機器との互換性"| F["Digest を使う"]
E -->|"ない"| D
B -->|"どうしても使えない"| G{"相手は自分で<br/>作ったクライアントか"}
G -->|"はい"| H["アプリ層で署名する<br/>HMAC など"]
G -->|"いいえ"| F
style D fill:#1f5c33,color:#fff
style F fill:#7a4a12,color:#fff
使ってよいケースと使ってはいけないケース
Section titled “使ってよいケースと使ってはいけないケース”Digest は「壊れている」のではなく、選ぶ理由がほぼ残っていない方式。だから判断は「安全か」ではなく「Basic + TLS ではなく Digest にする理由があるか」で切る。
成立の前提条件
Section titled “成立の前提条件”-
H(A1)の保存を受け入れられる。 つまりパスワード等価の値がデータベースに載ってよい - パスワードは使い捨てか、他サービスと共用していない。 流出時の被害を realm 内に閉じられる
-
algorithm=SHA-256以上を、相手のクライアントが確実に扱える(後述のブラウザ事情) - nonce の状態を共有できる。 単一サーバー、または nonce に時刻を埋め込んで検証を無状態にしてある
- ログアウトとパスワード再設定の UI が不要
- TLS が使えないか、Basic が明確に拒否される事情がある
ひとつでも欠けたら Basic + TLS か Bearer にする。とくに 1 つ目を飲めないなら、そこで判断は終わる。

判断の分岐はこの順に辿る。
flowchart TB
S["Digest 認証を使いたい"] --> Q1{"H(A1) の保存を<br/>受け入れられるか"}
Q1 -->|"受け入れられない"| NG1["使わない<br/>Basic + TLS / Bearer"]
Q1 -->|"受け入れる"| Q2{"相手は SHA-256 の Digest を<br/>確実に扱えるか"}
Q2 -->|"MD5 に落ちる"| NG2["使わない<br/>総当たりに耐えられない"]
Q2 -->|"扱える"| Q3{"nonce の状態を<br/>共有できるか"}
Q3 -->|"できない"| NG3["使わない<br/>複数台で落ちる"]
Q3 -->|"できる"| Q4{"Basic + TLS では<br/>済まない理由があるか"}
Q4 -->|"ない"| NG4["使わない<br/>交換が割に合わない"]
Q4 -->|"既存機器の互換性"| OK1["使える<br/>組み込み機器 / 既存 API"]
Q4 -->|"TLS を張れない経路"| OK2["使える<br/>ただし暫定策として"]
style OK1 fill:#1f5c33,color:#fff
style OK2 fill:#1f5c33,color:#fff
style NG1 fill:#7a2222,color:#fff
style NG2 fill:#7a2222,color:#fff
style NG3 fill:#7a2222,color:#fff
style NG4 fill:#7a2222,color:#fff
使ってよいケース
Section titled “使ってよいケース”| ケース | 成立する理由 | 一緒にやること |
|---|---|---|
| 既存の組み込み機器・ネットワーク機器 | 相手のファームウェアが Digest しか話さない。選択肢がない | 管理ネットワークを分離し、パスワードを使い捨てにする |
| RFC 2617 時代の既存 API の維持 | 移行コストが見合わない。動いているものを壊さない | TLS を必須にし、algorithm を明示する。移行計画を持つ |
| TLS を張れない閉域の暫定策 | 平文よりはよい。攻撃者が経路にいる前提を少しだけ緩められる | 暫定であることを期限付きで明文化する |
| 相互認証が本当に要る自前クライアント | rspauth でサーバーの真正性を確認できる |
クライアント側の rspauth 検証を実装する(既製品はしない) |
「なぜ組み込み機器では成立するのか」を分解する。 Digest の弱点は「サーバーの保存が弱い」「ログアウトできない」「総当たりに弱い」だが、機器の管理画面では利用者が 1 人か数人で、パスワードは機器固有の使い捨て、ログアウトの概念もない。弱点が刺さる前提そのものが存在しないため、互換性という 1 点だけで選択が正当化される。
使ってはいけないケース
Section titled “使ってはいけないケース”| ケース | できないこと |
|---|---|
| 新規に作る Web アプリの認証 | パスワードを Argon2 で保存できない。ログアウトも 2 要素認証も作れない |
| パスワードを他サービスと共用する利用者がいる場面 | H(A1) の流出が realm 外へ波及する(総当たりで元のパスワードが割れる) |
| 複数台のサーバーで負荷分散する構成 | nonce の状態を共有しないとリクエストごとにランダムに落ちる |
| TLS の代わりとして | 本文は保護されない。auth-int を使っても実装がない。中間者はダウングレードできる |
| リクエスト本文の完全性を守りたい場面 | qop=auth-int は主要実装が未対応。Apache も未実装 |
| MD5 しか話せない相手 | GPU で総当たりされる。ハッシュが速すぎる |
| 一般利用者向けのログイン | Basic と同じ問題(ログアウト不能、再設定 UI なし、2FA を挟めない) |
Apache — mod_auth_digest
Section titled “Apache — mod_auth_digest”主要サーバーで Digest を標準搭載しているのは Apache だけ。ただし実装は RFC 2617 準拠で、RFC 7616 の SHA-256 には対応していない。
<Location "/private/"> AuthType Digest AuthName "Staging Area" AuthDigestDomain "/private/" AuthDigestProvider file AuthUserFile "/etc/httpd/.digest_pw" Require valid-user</Location># htpasswd ではなく htdigest。realm を引数に取るのがポイントhtdigest -c /etc/httpd/.digest_pw "Staging Area" demo
# 中身は user:realm:MD5(user:realm:password) の3列cat /etc/httpd/.digest_pw# demo:Staging Area:20435eaf6289f145538bcd35f4fd3c51この 3 列目がそのまま H(A1)。ファイルを見れば分かるとおり、ソルトもなく段数もない。htpasswd で作る Basic 用のファイル(htpasswd -B なら bcrypt)と比べると、保存の強度が段違いに低い。
AuthDigestDomain を書き忘れると、認証が通用する範囲がクライアントに伝わらず、余計な往復が増える。
nginx — 標準では使えない
Section titled “nginx — 標準では使えない”nginx に Digest 認証のモジュールは同梱されていない。 ngx_http_auth_digest というサードパーティモジュールがあるが、RFC 2617 準拠で、作者自身が「アルファ扱いで、疑ってかかってほしい」と書いている。
location /private/ { auth_digest "Staging Area"; auth_digest_user_file /etc/nginx/.digest_pw;}nginx を使っているなら、Digest を入れるために再ビルドする前に「Basic + TLS で済まないか」を必ず確認する。
自前で検証する(Python)
Section titled “自前で検証する(Python)”import hashlibimport hmac
def H(s: str, alg: str = "sha256") -> str: return hashlib.new(alg, s.encode()).hexdigest()
def verify(ha1: str, params: dict, method: str, alg: str = "sha256") -> bool: """ha1 は保存済みの H(username:realm:password)""" ha2 = H(f"{method}:{params['uri']}", alg) expected = H( ":".join([ ha1, params["nonce"], params["nc"], params["cnonce"], params["qop"], ha2, ]), alg, ) # 文字列比較ではなく定数時間比較にする return hmac.compare_digest(expected, params["response"])自前実装で間違えやすい点。
nonceとncの消費を記録しないとリプレイを防げない。 計算が合っていても、同じ(nonce, nc)の 2 回目は拒否するncは 16 進 8 桁の文字列としてそのまま連結する。 整数に変換して桁が変わると一致しないuriはクライアントが送ってきた値を使う。 サーバー側で正規化した URI で計算すると、クライアントの計算と食い違うalgorithmを省略されたら MD5 として扱うのが仕様。ただし MD5 を受け付けるかどうかは設計判断- 比較は定数時間で。
==はタイミング攻撃の入り口になる
運用とセキュリティで押さえる点
Section titled “運用とセキュリティで押さえる点”ブラウザの実装状況が揃っていない
Section titled “ブラウザの実装状況が揃っていない”RFC 7616 の一番の価値は SHA-256 の追加だが、ブラウザの対応がまだ揃っていない。
| ブラウザ | SHA-256 の Digest |
|---|---|
| Firefox | 93 以降で対応(2021 年) |
| Chromium | 実装が進行中(Intent to Ship が出ている) |
| WebKit / Safari | 標準化ポジションの issue が未回答のまま(2023 年 6 月に提起) |
相手がブラウザなら、SHA-256 が通らずに MD5 へ落ちる可能性を前提に設計する。 落ちて困るなら Digest を選んではいけない。自前のクライアントやツール(curl など)なら SHA-256 を指定できる。
ダウングレード攻撃
Section titled “ダウングレード攻撃”中間者は WWW-Authenticate を書き換えられる。
- SHA-256 の行を削って MD5 だけ残す → 総当たりが現実的になる
WWW-Authenticate: Basicに差し替える → クライアントはパスワードを平文で送る。Apache のドキュメントも「中間者はブラウザを Basic に降格させられる」と明記している
どちらも TLS があれば起きない。 つまり Digest の安全性は結局 TLS に依存する。
H(A1) はソルトなしの 1 回ハッシュなので、辞書攻撃・レインボーテーブルが効く。パスワードファイルが漏れた場合の耐性は、bcrypt や Argon2 と比べて桁違いに低い。
- パスワードを十分に長いランダム文字列にする(人間が覚える前提を捨てる)
- ファイルの権限を最小にし、ドキュメントルート外に置く
- 漏洩時は realm ごと変える(
H(A1)に realm が入るため、realm を変えれば既存の値は使えなくなる)
キャッシュとプロキシ
Section titled “キャッシュとプロキシ”Basic と同じで、Authorization 付きリクエストへの応答は共有キャッシュに保存してはならない(RFC 9111 §3.5)。プロキシ向けには 407 / Proxy-Authenticate / Proxy-Authorization を使い、qop や nonce の扱いは同じ。
Basic とまったく同じで、標準の手段がない。 ブラウザは H(A1) の材料をメモリに保持し、閉じるまで送り続ける。stale=true を返しても再入力は求められないので、むしろログアウトから遠ざかる。
引っかかりやすいポイント
Section titled “引っかかりやすいポイント”- Digest の仕様は RFC 7616(RFC 2617 を廃止)。フレームワークは RFC 9110 §11。
-
response = H( H(A1) : nonce : nc : cnonce : qop : H(A2) )。 -
A1 = username : realm : password、A2 = method : request-uri(authのとき)。 -
-sess版はA1 = H(username:realm:password) : nonce : cnonceと形が変わる。 -
qop=auth-intのときだけA2に 本文のハッシュが入る。実装はほぼない。 -
ncは 16 進 8 桁固定で00000001から。桁を詰めると失敗する。 -
cnonceとncは RFC 7616 では必須。qopもチャレンジ側で必須になった。 -
algorithm省略時はMD5。 書き忘れがダウングレードになる。 - 登録アルゴリズムは MD5 / SHA-256 / SHA-512-256 と各
-sess版。 -
stale=trueは「nonce が古いだけ」の意味。再入力を求めさせない。 - サーバーは
H(A1)を保存しなければならず、これはパスワード等価。 Argon2 は使えない。 -
realmはH(A1)に入る。realm を変えると全パスワードが無効になる。 -
rspauth(Authentication-Info)でサーバー側の真正性を確認できる。Basic にはない。 - 先回り送信ができない。 nonce が必要なので最初の 401 が必ず入る。
- Digest はステートフル。 nonce と nc の消費をサーバーが覚える必要がある。
- TLS があるなら Basic + TLS のほうが総合的に安全。 頻出の判断ポイント。
- nginx は標準で Digest に対応していない。Apache の実装は RFC 2617 止まり。
Digest 認証の response はどう計算するか。
A1 = username:realm:password、A2 = method:request-uri を作り、それぞれをハッシュ化する。そのうえで H(A1):nonce:nc:cnonce:qop:H(A2) を連結してもう一度ハッシュ化した値が response。
nonce と cnonce はそれぞれ何のためにあるか。
nonce はサーバーが発行する使い捨ての値で、盗んだ response の再利用を防ぐ。cnonce はクライアントが生成する使い捨ての値で、サーバーだけに計算の材料を選ばせないためにある。両方が入ることで、どちらか一方が値を仕込んでも攻撃が成立しない。
nc は何を数えていて、書式の決まりは。
同じ nonce でそのクライアントが送ったリクエスト数(今回を含む)。16 進 8 桁固定で、最初は 00000001。サーバー側はどの nc まで使われたかを覚えておき、同じ値の 2 回目をリプレイとして拒否する。
stale=true が返ってきたらクライアントは何をすべきか。
パスワードは合っていて nonce が期限切れなだけなので、利用者に再入力を求めず、新しい nonce で response を計算し直して再送する。サーバー側で stale を付け忘れると、利用者には「正しいパスワードなのに弾かれた」ように見える。
Digest 認証は Basic 認証より安全か。
経路については明確に安全。パスワードそのものが流れず、リプレイもできない。しかしサーバー側の保存は Basic より弱い。検証に H(A1) が必要で、これはパスワード等価かつソルトなしの 1 回ハッシュなので、Argon2 や bcrypt が使えない。TLS がある前提では、Basic + TLS のほうが総合的に安全になる。
なぜ Digest ではパスワードをソルト付きハッシュで保存できないのか。
サーバーが response を検証するには、クライアントとまったく同じ H(A1) = H(username:realm:password) を計算できなければならない。ソルトを足したり段数を増やしたりすると値が変わって一致しなくなる。つまり保存できるのは H(A1) そのものだけで、それが漏れれば認証を突破できる。
realm を変更すると何が起きるか。
realm は H(A1) の計算に入っているため、保存済みの全ユーザーの値が無効になる。パスワードを配り直す必要がある。裏を返せば、同じパスワードを使っている別 realm へ流出が波及しない利点でもある。
algorithm を省略すると何が使われるか。
MD5。RFC 7616 で SHA-256 と SHA-512-256 が追加されたが、省略時の既定は互換のため MD5 のまま。書き忘れがそのままダウングレードになるので必ず明示する。
-sess 付きのアルゴリズムは通常版と何が違うか。
A1 の作り方が変わる。通常版は username:realm:password だが、-sess 版は H(username:realm:password):nonce:cnonce になり、セッション最初の nonce と cnonce が焼き込まれる。同じ H(A1) を長期間使い回さずに済む。ただし主要実装の対応は乏しく、Apache の mod_auth_digest は MD5-sess を正しく実装していないと明記している。
qop=auth と qop=auth-int の違いは。
auth は A2 = method:request-uri で、メソッドと URI だけを保護する。auth-int は A2 = method:request-uri:H(entity-body) となり、リクエスト本文の完全性も保護する。ただし主要実装がほぼ対応しておらず、Apache も未実装。
Authentication-Info の rspauth は何を証明するか。
サーバーが H(A1) を知っていること、つまり本物のサーバーであることを証明する。計算は response とほぼ同じで、A2 のメソッド部分を空にする(A2 = ":" request-uri)。Basic にはこの相互認証の仕組みがない。ただしブラウザは rspauth を検証しない。
Digest 認証で先回り送信ができないのはなぜか。
response の計算にサーバーが発行した nonce が必要なため。したがって Basic と違い、最初の 401 を受け取るまでクレデンシャルを組み立てられず、必ず 2 往復になる。Authentication-Info の nextnonce を使えば、2 回目以降の 401 は省ける。
Digest 認証を負荷分散構成で使うと何が問題になるか。
Digest はステートフルで、サーバーは発行した nonce と消費された nc を覚えておく必要がある。複数台でこの状態を共有しないと、リクエストが別のサーバーに当たった瞬間に「未知の nonce」として弾かれる。nonce に時刻を埋め込んで検証を無状態にする手はあるが、それだけではリプレイ検知ができない。