パスワード認証と試行回数の制限
保存値・応答・試行記録・発行セッションを対応付けてログイン処理を点検します。
説明
ログイン処理では、パスワードの保存方法に加え、失敗応答と連続試行への対応を確認します。失敗文面から利用者の存在が分かるか、試行回数に上限があるかを調べます。
架空のアカウントをSQLiteへ保存し、PBKDF2の照合とセッション発行を実行します。不正なパスワードと存在しない利用者、試行上限、期限後の成功を比較します。
保存値・応答・試行記録・発行セッションを対応付けてログイン処理を点検します。
1パスワードの保存形式を読む
比較用のPBKDF2-HMAC-SHA256を使います。1200回は試料の小さな計算値であり、実運用に推奨する設定ではありません。
from sd05 import *
app = Login()独立した認証台帳を作る
print(app.db.execute("SELECT name,hex(salt),length(hash)平文ではなく導出値の長さを確認する FROM users ORDER BY name").fetchall())保存しているのはアカウントごとのsaltと32バイトの導出値です。試料のpublic-passwordは公開用の固定値です。
2誤ったパスワードと存在しない利用者
利用者の存在で応答文面が変わらないか比較します。
print(app.login("alice","wrong")存在する利用者で失敗する)
print(app.login("nobody","wrong")存在しない利用者と比較する)
print(app.db.execute("SELECT COUNT(*) FROM sessions").fetchone())両方401・login_failedで、セッションは0件です。文面の一致だけで応答時間など他の差がないことまで証明したわけではありません。
3失敗時にも導出処理を通す
不明な利用者にもダミーの保存値を用意し、照合の導出処理を行います。試行上限で止める場合は別です。
print(app.cost_calls照合用の導出を通った回数を確認する)
print(app.db.execute("SELECT name,failures,until失敗数と制限区間を読む FROM attempts ORDER BY name").fetchall())両要求で照合処理へ進み、失敗を1回ずつ記録しています。時間特性の調整には実測と負荷・制限方針も必要です。
4同じアカウントで失敗を重ねる
この例は30秒の区間で3回失敗すると、以降の試行を制限します。
print(app.login("alice","wrong",now=12回目の失敗を記録する))
print(app.login("alice","wrong",now=23回目の失敗を記録する))
print(app.db.execute("SELECT failures,until FROM attempts WHERE name='alice'").fetchone())失敗数3、制限期限32です。値はこの演習の方針であり、すべてのシステムへそのまま適用する上限ではありません。
5制限中は正しいパスワードでも発行しない
時刻3では制限期限前です。
before = app.cost_calls
print(app.login("alice","public-password",now=3)制限中の正しい認証情報を試す)
print(app.cost_calls == before重い照合へ進んでいないことを確認する)
print(app.db.execute("SELECT COUNT(*) FROM sessions").fetchone())429で新しいセッションはありません。アカウント単位の制限には、第三者が正当な利用者を妨げる影響もあるため、段階的な待機や他の信号も検討します。
6制限期限後にログインする
時刻32で同じ認証情報を使います。セッションIDは画面の再現用の公開値です。
print(app.login("alice","public-password",now=32制限期限に到達してから認証する))
print(app.db.execute("SELECT * FROM sessions発行先の主体を確認する").fetchall())
print(app.db.execute("SELECT * FROM attempts WHERE name='alice'").fetchall())200でaliceのセッションを1件発行し、その失敗記録を除きます。運用のセッションIDは予測できない値として生成する必要があります。
7別アカウントの状態を保つ
aliceの成功が、不明な利用者の失敗記録やbobの保存値を変えていないか確認します。
print(app.db.execute("SELECT name,failures他の試行記録が保持されたことを確認する FROM attempts ORDER BY name").fetchall())
print(app.db.execute("SELECT name,length(hash) FROM users ORDER BY name").fetchall())
app.db.close()nobodyの失敗記録と両アカウントの保存値は残っています。制限と解除の集計キーが、意図した範囲に対応していることを確認します。
まとめ
パスワードの導出値とパラメーターを保存し、照合します。
失敗応答・重い照合処理・制限の集計キーを点検します。
成功時に発行したセッションの主体と、解除した記録を照合します。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sd05 import *
app = Login()
print(app.db.execute("SELECT name,hex(salt),length(hash) FROM users ORDER BY name").fetchall())[('alice', '616C6963652D7075626C69632D73616C74', 32), ('bob', '626F622D7075626C69632D73616C74', 32)]print(app.login("alice","wrong"))
print(app.login("nobody","wrong"))
print(app.db.execute("SELECT COUNT(*) FROM sessions").fetchone())(401, {'error': 'login_failed'})
(401, {'error': 'login_failed'})
(0,)print(app.cost_calls)
print(app.db.execute("SELECT name,failures,until FROM attempts ORDER BY name").fetchall())2
[('alice', 1, 30), ('nobody', 1, 30)]print(app.login("alice","wrong",now=1))
print(app.login("alice","wrong",now=2))
print(app.db.execute("SELECT failures,until FROM attempts WHERE name='alice'").fetchone())(401, {'error': 'login_failed'})
(401, {'error': 'login_failed'})
(3, 32)before = app.cost_calls
print(app.login("alice","public-password",now=3))
print(app.cost_calls == before)
print(app.db.execute("SELECT COUNT(*) FROM sessions").fetchone())(429, {'error': 'login_failed'})
True
(0,)print(app.login("alice","public-password",now=32))
print(app.db.execute("SELECT * FROM sessions").fetchall())
print(app.db.execute("SELECT * FROM attempts WHERE name='alice'").fetchall())(200, {'session': 'public-session-1'})
[('public-session-1', 'alice', 1)]
[]print(app.db.execute("SELECT name,failures FROM attempts ORDER BY name").fetchall())
print(app.db.execute("SELECT name,length(hash) FROM users ORDER BY name").fetchall())
app.db.close()[('nobody', 1)]
[('alice', 32), ('bob', 32)]演習完了