Kerberos認証の仕組み
どのチケットをどの相手へ示し、どの鍵と時刻の条件を確認するか説明します。
説明
KerberosではKDCが発行するチケットを使います。TGTはサービス用チケットを求めるためのもので、各サービスへ提示するチケットとは宛先が違います。サービスはチケットとAuthenticatorを組み合わせ、相手と時刻などを検査します。
固定の交換記録と復号後フィールドの資料をSQLで調べます。TGTからHTTPサービスのチケットへ進む順序、サービス名と鍵の世代、期限、Authenticatorの再利用を追います。
どのチケットをどの相手へ示し、どの鍵と時刻の条件を確認するか説明します。
1認証からサービスへの交換順序
exchangeは交換記録の要約です。encrypted_forは各段階で着目するチケットまたはAuthenticatorの保護鍵で、メッセージ全体が一つの鍵で暗号化されるという意味ではありません。
from kerberos import evidence
db = evidence()
print(db.execute("SELECT seq,kind,client,target,encrypted_for交換順序と着目する鍵を並べる FROM exchange ORDER BY seq").fetchall())AS交換でTGTを得て、TGS交換でHTTPサービスのチケットを得てからAP-REQで提示します。AS-REPやTGS-REPにはチケット以外にクライアント向けの暗号化部分もあります。
2TGTとサービスチケットを比較する
ここでは復号後の主要フィールドをまとめた資料表を使います。時刻は基準時刻からの秒です。
print(db.execute("SELECT id,client,service,etype,kvno,start,endチケットの主体・宛先・暗号方式・期間を読む FROM tickets ORDER BY start").fetchall())TGTの宛先はkrbtgt/LAB.TEST、httpの宛先はHTTP/web.lab.testです。etype 18はAES256-CTS-HMAC-SHA1-96、kvnoは保護に使う鍵の世代を表します。
3HTTPサービス用の鍵を照合する
サービス名・暗号方式・鍵の世代が一致する鍵記録を探します。これは復号する鍵を選ぶ条件であり、SQLの一致が暗号検証を代替するわけではありません。
print(db.execute("SELECT t.id,k.service,k.kvno FROM tickets t JOIN keys k ONサービス名・方式・世代で鍵を照合する t.service=k.service AND t.etype=k.etype AND t.kvno=k.kvno ORDER BY t.id").fetchall())httpだけが対応します。TGTをHTTPサービスの鍵で処理する構成ではありません。
4サービスの鍵だけを更新した場合
サービスの鍵台帳を世代4へ進め、世代3の鍵を残さなかった場合を調べます。
db.execute("UPDATE keys SET kvno=4鍵台帳だけを新しい世代へ進める")
print(db.execute("SELECT t.id FROM tickets t JOIN keys k ON t.service=k.service AND t.etype=k.etype AND t.kvno=k.kvno").fetchall())
db.execute("UPDATE keys SET kvno=3元の条件へ戻す")対応する鍵がなくなります。鍵更新では既発行チケットの寿命と旧世代鍵の扱いを調整する必要があります。比較後は試料を世代3へ戻します。
5チケットの期限を照合する
HTTPチケットは時刻10から300までの資料です。この例の受付条件はstart<=now<endとします。
print(db.execute("SELECT id,start<=299 AND 299<end期限直前を調べる,start<=300 AND 300<endこの例の期限境界を調べる FROM tickets WHERE id='http'").fetchone())299は1、300は0です。実装で許容する時計ずれを含め、クライアント・KDC・サービスの時計を管理する必要があります。
6Authenticatorの時刻と主体を調べる
Authenticatorはチケット内のセッション鍵で保護し、主体と時刻を含みます。同じチケットを使ってもAuthenticatorは要求ごとに用意します。
print(db.execute("SELECT a.id,a.client=t.clientチケットとAuthenticatorの主体を照合する,a.service=t.service,a.ctime,a.cusec秒とマイクロ秒の値を読む FROM authenticators a JOIN tickets t ON t.id='http' ORDER BY a.id").fetchall())a1とa2は同じ主体・サービスですが、マイクロ秒値が7と8です。資料表で一致しただけで暗号検証が済むわけではなく、受信側は保護された内容を確認してこの条件を評価します。
7同じAuthenticatorを再記録する
replay表には主体・サービス・時刻の組に一意制約があります。暗号検証と時刻検査を通過した後の、再利用の記録処理を比較します。
sql = "INSERT OR IGNORE INTO replay同じ組を重複記録しない SELECT client,service,ctime,cusec FROM authenticators WHERE id=?"
print(db.execute(sql,("a1",)).rowcount最初のAuthenticatorを記録する)
print(db.execute(sql,("a1",)).rowcount)
print(db.execute(sql,("a2",)).rowcount別のAuthenticatorを記録する)
print(db.execute("SELECT COUNT(*) FROM replay").fetchone())a1の初回は1、再提示は0、別のa2は1です。チケットの期限だけでなく、Authenticatorの再利用を検出する状態も必要です。
まとめ
TGTとサービスチケットは宛先と用途が異なります。
サービス名・暗号方式・鍵の世代を対応付けます。
時刻の検査とAuthenticatorの再利用検知を組み合わせます。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from kerberos import evidence
db = evidence()
print(db.execute("SELECT seq,kind,client,target,encrypted_for FROM exchange ORDER BY seq").fetchall())[(1, 'AS-REQ', 'alice@LAB.TEST', 'krbtgt/LAB.TEST', '-'), (2, 'AS-REP', 'alice@LAB.TEST', 'krbtgt/LAB.TEST', 'krbtgt-key'), (3, 'TGS-REQ', 'alice@LAB.TEST', 'HTTP/web.lab.test', 'krbtgt-session-key'), (4, 'TGS-REP', 'alice@LAB.TEST', 'HTTP/web.lab.test', 'http-key'), (5, 'AP-REQ', 'alice@LAB.TEST', 'HTTP/web.lab.test', 'http-session-key')]
print(db.execute("SELECT id,client,service,etype,kvno,start,end FROM tickets ORDER BY start").fetchall())[('tgt', 'alice@LAB.TEST', 'krbtgt/LAB.TEST', 18, 2, 0, 600), ('http', 'alice@LAB.TEST', 'HTTP/web.lab.test', 18, 3, 10, 300)]print(db.execute("SELECT t.id,k.service,k.kvno FROM tickets t JOIN keys k ON t.service=k.service AND t.etype=k.etype AND t.kvno=k.kvno ORDER BY t.id").fetchall())[('http', 'HTTP/web.lab.test', 3)]db.execute("UPDATE keys SET kvno=4")
print(db.execute("SELECT t.id FROM tickets t JOIN keys k ON t.service=k.service AND t.etype=k.etype AND t.kvno=k.kvno").fetchall())
db.execute("UPDATE keys SET kvno=3")[]
print(db.execute("SELECT id,start<=299 AND 299<end,start<=300 AND 300<end FROM tickets WHERE id='http'").fetchone())('http', 1, 0)print(db.execute("SELECT a.id,a.client=t.client,a.service=t.service,a.ctime,a.cusec FROM authenticators a JOIN tickets t ON t.id='http' ORDER BY a.id").fetchall())[('a1', 1, 1, 60, 7), ('a2', 1, 1, 60, 8)]sql = "INSERT OR IGNORE INTO replay SELECT client,service,ctime,cusec FROM authenticators WHERE id=?"
print(db.execute(sql,("a1",)).rowcount)
print(db.execute(sql,("a1",)).rowcount)
print(db.execute(sql,("a2",)).rowcount)
print(db.execute("SELECT COUNT(*) FROM replay").fetchone())1 0 1 (2,)
演習完了