アカウント登録・認証・復旧
何を証拠として誰を確認したか、要素が独立しているか、復旧後にどの利用状態を止めるかを説明します。
説明
登録時の本人確認と、登録後に認証器を持っていることの確認は別の手続です。復旧で別人へアカウントを渡してしまうと、通常のログインを強くしても保護が崩れます。また、入力項目が二つあるだけで二要素認証になるわけではありません。
固定の登録・ログイン・復旧記録をSQLiteで比較します。認証要素の種類と依存する端末を調べ、最後に公開試料のバックアップコードを一度だけ消費し、既存セッションを失効させます。
何を証拠として誰を確認したか、要素が独立しているか、復旧後にどの利用状態を止めるかを説明します。
1手続ごとの証拠
registrationはperson-7の本人確認記録、loginはaliceの認証器利用記録です。この資料の識別子は固定の例示値です。
from sc04 import accounts
import hashlib
db = accounts()独立した証拠・認証器・復旧資料を作る
print(db.execute("SELECT * FROM evidence ORDER BY stage手続と証拠を並べる").fetchall())登録・ログイン・復旧でstageと証拠が異なります。同じアカウント名があるだけで、確認できた内容まで同じとは判断できません。
2復旧時の主体を比較する
登録時と復旧時のsubjectを同じaccountで結合します。
print(db.execute("SELECT a.account,a.subject,r.subject,a.subject=r.subject登録と復旧の主体を照合する FROM evidence a JOIN evidence r USING(account) WHERE a.stage='registration' AND r.stage='recovery'").fetchall())person-7とperson-9は異なり、比較値は0です。この復旧記録だけで元の登録主体だと判断することはできません。
3知識情報を二つ使っても二要素認証にはならない
passwordとquestionは両方knowledgeです。種類の異なる要素を数えます。
print(db.execute("SELECT name,kind二つの認証手段の種類を読む FROM factors WHERE name IN ('password','question') ORDER BY name").fetchall())
print(db.execute("SELECT COUNT(DISTINCT kind)異なる要素の種類を数える FROM factors WHERE name IN ('password','question')").fetchone())入力項目は2個でも種類は1です。パスワードと秘密の質問を並べただけで、所持要素を追加したことにはなりません。
4別の方式でも同じ端末へ依存する
otpとpushはどちらもphoneへ依存する設定です。
print(db.execute("SELECT name,kind,dependency同じ端末へ依存する認証手段を読む FROM factors WHERE dependency='phone' ORDER BY name").fetchall())方式名は別でも、同じ端末の侵害や喪失の影響を受けます。種類だけでなく、共通の故障・侵害要因も調べる必要があります。
5復旧用コードを照合する
ここからは、登録時に発行して別に保管したバックアップコードを使う復旧方式の例です。サーバーはハッシュと使用済み状態を保持します。
candidate = hashlib.sha256提示された試料コードのハッシュを求める(b"public-backup-code-7").hexdigest()
print(db.execute("SELECT account,used FROM recovery WHERE hash=?対応アカウントと未使用状態を読む", (candidate,)).fetchone())
print(db.execute("SELECT * FROM sessions ORDER BY id復旧前のセッションを読む").fetchall())aliceの未使用コードが見つかり、s1とs2は有効です。実用のコードには十分な乱数強度と試行制限も必要ですが、ここでは固定試料で一回性を調べます。
6コード消費とセッション失効を確定する
未使用のalice用コードを1件だけ使用済みにし、成功した場合に既存セッションを止めます。同じトランザクションで処理します。
with db:コード消費と失効をまとめて確定する
consumed = db.execute("UPDATE recovery SET used=1未使用コードだけを消費する WHERE account=? AND hash=? AND used=0", ("alice", candidate)).rowcount
if consumed:
db.execute("UPDATE sessions SET active=0既存セッションを止める WHERE account=?", ("alice",))
print(consumed)消費件数を読む
print(db.execute("SELECT * FROM sessions ORDER BY id失効後の状態を読む").fetchall())消費1件、s1とs2は無効になります。復旧だけ成功させて古いセッションを残すと、以前の利用状態が継続する場合があります。
7同じコードを再利用する
同じ条件付きUPDATEを繰り返します。
print(db.execute("UPDATE recovery SET used=1同じコードの再利用を試す WHERE account=? AND hash=? AND used=0", ("alice", candidate)).rowcount)0件です。コードの文字列が正しくても、使用済み状態の検査で再利用を拒否できます。
まとめ
登録時の本人確認と認証器の利用、復旧時の主体確認を分けます。
認証要素の種類と、依存する端末の共通性を確認します。
復旧ではコードの一回性と既存セッションの失効も設計します。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc04 import accounts
import hashlib
db = accounts()
print(db.execute("SELECT * FROM evidence ORDER BY stage").fetchall())[('alice', 'login', 'alice', 'password+otp'), ('alice', 'recovery', 'person-9', 'different-person'), ('alice', 'registration', 'person-7', 'checked-identity-record')]print(db.execute("SELECT a.account,a.subject,r.subject,a.subject=r.subject FROM evidence a JOIN evidence r USING(account) WHERE a.stage='registration' AND r.stage='recovery'").fetchall())[('alice', 'person-7', 'person-9', 0)]print(db.execute("SELECT name,kind FROM factors WHERE name IN ('password','question') ORDER BY name").fetchall())
print(db.execute("SELECT COUNT(DISTINCT kind) FROM factors WHERE name IN ('password','question')").fetchone())[('password', 'knowledge'), ('question', 'knowledge')]
(1,)print(db.execute("SELECT name,kind,dependency FROM factors WHERE dependency='phone' ORDER BY name").fetchall())[('otp', 'possession', 'phone'), ('push', 'possession', 'phone')]candidate = hashlib.sha256(b"public-backup-code-7").hexdigest()
print(db.execute("SELECT account,used FROM recovery WHERE hash=?", (candidate,)).fetchone())
print(db.execute("SELECT * FROM sessions ORDER BY id").fetchall())('alice', 0)
[('s1', 'alice', 1), ('s2', 'alice', 1)]with db:
consumed = db.execute("UPDATE recovery SET used=1 WHERE account=? AND hash=? AND used=0", ("alice", candidate)).rowcount
if consumed:
db.execute("UPDATE sessions SET active=0 WHERE account=?", ("alice",))
print(consumed)
print(db.execute("SELECT * FROM sessions ORDER BY id").fetchall())1
[('s1', 'alice', 0), ('s2', 'alice', 0)]print(db.execute("UPDATE recovery SET used=1 WHERE account=? AND hash=? AND used=0", ("alice", candidate)).rowcount)0
演習完了