TLSのセッション再開と0-RTT
接続再開の成功と、業務操作が一度だけ実行されることを別々に確認します。
説明
TLS 1.3では以前の接続から得たチケットを使い、PSKに基づいて接続を再開できます。再開した接続と0-RTTは同義ではありません。0-RTTで送る早期データには再送のリスクがあり、業務操作側でも扱いを決める必要があります。
固定の初回接続とチケットを使った再接続を比較します。PSK拡張と省略されるメッセージを確認した後、同じ注文が二度届くケースをSQLで実行し、要求ID付きの処理へ変更します。
接続再開の成功と、業務操作が一度だけ実行されることを別々に確認します。
1チケットと再開結果を確認する
初回のハンドシェイク後にチケットを受け取り、同じサーバーへ二度目の接続を行った記録です。
from sc09 import *
print(T["ticket"]受領したチケットの有無と寿命を読む)
print(T["first_reused"],T["second_reused"]再開が成立した結果を読む)初回はFalse、二度目はTrueです。チケットの存在だけではなく、TLS実装が接続を再開した結果も記録しています。
2ClientHelloのPSK拡張を比較する
pre_shared_keyは拡張41、early_dataは42です。ここでは存在を別々に確認します。
first = hello(hellos()[0])["extensions"]
second = hello(hellos("second")[0])["extensions"]
print(41 in first,41 in secondPSK拡張の有無を比較する)
print(42 in first,42 in second早期データの有無を別に調べる)二度目だけPSKがありますが、early_dataはどちらにもありません。この記録はチケットによる再開であり、0-RTTデータを送った接続ではありません。
3選ばれたPSKと鍵共有を読む
ServerHelloで選択したPSK identityの番号と、key_shareの有無を確認します。
selected = hello(hellos("second")[1],True)["extensions"]
print(selected[41]選択されたPSK identityを読む.hex(),51 in selected鍵共有があることを確認する)identity 0が選択され、鍵共有もあります。PSKと新しい鍵共有を組み合わせた再開です。
4省略されたメッセージを比較する
初回と再開時のサーバーハンドシェイクを復号します。
print([NAMES[m[0]] for m in server_messages()])
print([NAMES[m[0]] for m in server_messages("second")再開時のメッセージを取得する])
print(finished_check("second")再開時のFinishedを検証する)再開時にはCertificateとCertificateVerifyを送りませんが、PSKに基づくFinishedの確認は行います。認証は、チケットを受け取った元の接続での認証に基づきます。
5同じ注文が二度届いた場合を実行する
ここからは早期データなどで同じ要求が複数回届く場合の業務処理を調べます。注文を受けるたび100を加える実装です。
db = orders()
db.execute("UPDATE balance SET amount=amount+1001件分を加算する")
db.execute("UPDATE balance SET amount=amount+100")
print(db.execute("SELECT amount FROM balance二度実行した結果を確認する").fetchone())残高は200です。TLSを使った通信でも、同じ業務要求を二度実行すれば二度反映されます。
6要求IDと更新を一つの確定処理にする
比較用に残高を0へ戻します。apply_onceは要求IDを一意制約で記録し、初回だけ同じトランザクションで残高を更新します。
db.execute("UPDATE balance SET amount=0")
db.commit()
print(apply_once(db,"order-7",100)要求ID付きで初回の注文を確定する)
print(db.execute("SELECT * FROM seen").fetchall(),db.execute("SELECT amount FROM balance").fetchone())要求IDを記録して100を加算しました。記録だけ成功して業務更新が失敗する状態を、同じトランザクションで防ぎます。
7同じ要求と別の要求を再実行する
同じIDの再送と、新しい注文IDを比較します。
print(apply_once(db,"order-7",100)同じ要求を再送する)
print(db.execute("SELECT amount FROM balance").fetchone())
print(apply_once(db,"order-8",100)別の注文を受け付ける)
print(db.execute("SELECT amount FROM balance").fetchone())同じIDではFalseで100を保ち、別IDではTrueで200になります。IDの有効範囲、保存期間、同じIDで内容が異なる場合の拒否も実運用では設計対象です。
まとめ
チケットによる接続再開と0-RTTデータ送信は別の機能です。
再開でもFinishedによる交渉の検証を行います。
業務の重複実行は、操作の性質と要求IDを使った処理でも防ぎます。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc09 import *
print(T["ticket"])
print(T["first_reused"],T["second_reused"]){'has_ticket': True, 'lifetime': 7200}
False Truefirst = hello(hellos()[0])["extensions"]
second = hello(hellos("second")[0])["extensions"]
print(41 in first,41 in second)
print(42 in first,42 in second)False True False False
selected = hello(hellos("second")[1],True)["extensions"]
print(selected[41].hex(),51 in selected)0000 True
print([NAMES[m[0]] for m in server_messages()])
print([NAMES[m[0]] for m in server_messages("second")])
print(finished_check("second"))['EncryptedExtensions', 'Certificate', 'CertificateVerify', 'Finished'] ['EncryptedExtensions', 'Finished'] True
db = orders()
db.execute("UPDATE balance SET amount=amount+100")
db.execute("UPDATE balance SET amount=amount+100")
print(db.execute("SELECT amount FROM balance").fetchone())(200,)
db.execute("UPDATE balance SET amount=0")
db.commit()
print(apply_once(db,"order-7",100))
print(db.execute("SELECT * FROM seen").fetchall(),db.execute("SELECT amount FROM balance").fetchone())True
[('order-7',)] (100,)print(apply_once(db,"order-7",100))
print(db.execute("SELECT amount FROM balance").fetchone())
print(apply_once(db,"order-8",100))
print(db.execute("SELECT amount FROM balance").fetchone())False (100,) True (200,)
演習完了