一覧SC-09-3 · TLSの保護範囲

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 / 公開試料と固定時刻

1
from sc09 import * print(T["ticket"]) print(T["first_reused"],T["second_reused"])
{'has_ticket': True, 'lifetime': 7200}
False True
2
first = 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
3
selected = hello(hellos("second")[1],True)["extensions"] print(selected[41].hex(),51 in selected)
0000 True
4
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
5
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,)
6
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,)
7
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,)

演習完了