一覧SC-04-1 · 本人確認と認証器

アカウント登録・認証・復旧

何を証拠として誰を確認したか、要素が独立しているか、復旧後にどの利用状態を止めるかを説明します。

説明

登録時の本人確認と、登録後に認証器を持っていることの確認は別の手続です。復旧で別人へアカウントを渡してしまうと、通常のログインを強くしても保護が崩れます。また、入力項目が二つあるだけで二要素認証になるわけではありません。

固定の登録・ログイン・復旧記録を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 / 公開試料と固定時刻

1
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')]
2
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)]
3
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,)
4
print(db.execute("SELECT name,kind,dependency FROM factors WHERE dependency='phone' ORDER BY name").fetchall())
[('otp', 'possession', 'phone'), ('push', 'possession', 'phone')]
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', 0)
[('s1', 'alice', 1), ('s2', 'alice', 1)]
6
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)]
7
print(db.execute("UPDATE recovery SET used=1 WHERE account=? AND hash=? AND used=0", ("alice", candidate)).rowcount)
0

演習完了