こんにちは!ITIの吉葉です!
今回はTLSの理解を深める資料として、TLS通信の基本フローを手で動かすハンズオン資料を作ったので紹介します。
ColabNotebook配布
今回作成したColabNotebookは以下で共有しています。 よければコピーしてお使いください!

詳しくは弊社リポジトリをご参照ください github.com
なぜ作ろうと思ったか
私の所属するAIチームでは去年の秋頃から自己研鑽の一環として輪読会をしています。
その種本として結城浩著の『暗号技術入門』を選びました。
『暗号技術入門』の終盤に出てきたのがSSL/TLSでした。
SSL/TLSというと、HTTPSという形でブラウザで見ることが多いんじゃないかなと思います。
HTTPSはHTTPとTLSを合わせた技術で、通信相手の真正性、通信内容の機密性、データの完全性を保証してくれます。
さて、このSSL/TLS、かなり複雑な技術です。 対称鍵、公開鍵、署名、乱数と今まで習った全てを活用します。 習った全てがこれに収束するんだ!!というカタルシスはありますが、複雑は複雑。 正直、何をやっているのか理解しづらい面も多々あるかなと感じました。
そこで、今回はTLSは実際にはどんなことをやっているのか、そして本当に安全な通信ができるのか体験できるハンズオン資料を作成しました。
事前知識:SSL/TLSってなあに?
本題に入る前にSSL/TLSについて軽く説明しましょう。 SSL/TLSは両方ともインターネット上の通信を暗号化するためのプロトコル(取り決め)です。ざっくり簡単に言うとSSLが古くて、TLSが新しい規格ですね。 SSL/TLSの神髄は、「正しい相手に」「安全に」情報が送れるところにあります。例えば通販サイトになりすましたサーバーがあったとしても、SSL/TLSを使えばなりすましを見抜くことができますし、正規のサイトとの通信経路が盗聴されていても情報は暗号化されているのでクレカ情報などは盗まれません。 SSL/TLSによりクレジットカード情報や個人情報などセンシティブな情報を安全に、正しい相手に送ることができるわけです。
コンセプト:Discordを「ネットワーク」に見立てる
SSL/TLSの章を読んだ時、以下のことを思いました。
- 各ステップが複雑で結局何が起きてるかわからない
- 今何のために何をしているかわからない
- これで本当に安全なやり取りができてる実感がない
そこで考えたのが次のコンセプトです。
- ステップバイステップでコマンドを実行する
- やりとりのパケットを衆目に晒してなお解読できないことを体験させる
これを以下の形で具体化しました。
- GoogleColabを使い、コマンドを手入力
- 各ステップでどんな処理を行うのか意識できる
- クライアント役とサーバー役の2人が、Discordのチャンネルで「パケット」をやりとりする
- 観客は全員そのチャンネルを見ている
- つまり全パケットを「傍受」できる状態
目指すは「各ステップにどんな暗号技術が必要か」、「全部見えてるのに読めない」というのが実感できる体験です。

何を簡略化して、何を残したか
TLSをそのまま再現しようとすると、途端に難しくなります。PRF(疑似乱数関数)、PKCS#1パディング、レコード層のフォーマット……細部を全部再現すれば正確ですが、体験の妨げになります。
判断基準は「エッセンスが伝わるか」の一つにしました。
残したもの
| 要素 | 理由 |
|---|---|
| RSA暗号化(openssl pkeyutl) | 「公開鍵で暗号化、秘密鍵で復号」はTLSの核心 |
| 証明書検証(openssl verify) | CAチェーンの概念を体験させたい |
| AES-128-CBC | 実際の暗号化を体感させる |
| HMAC-SHA256(MAC値) | 改ざん検知の概念を伝える |
簡略化したもの
PRF → SHA256の繰り返し
本物のTLS 1.2では、鍵導出にP_SHA256というHMACベースの関数を使います。これを実装すると28行のPythonスクリプトが必要になります。しかし、本質は「同じ素材から同じランダムな値を作る」ことなので、SHA256を数回繰り返す形に置き換えました。
base = bytes.fromhex(pms + client_random + server_random) h1 = hashlib.sha256(base + b"ms1").digest() h2 = hashlib.sha256(base + b"ms2").digest() master_secret = (h1 + h2)[:48]
特に鍵生成を
client_write_MAC = hashlib.sha256(ms_base + b"client_write_MAC").digest()[:32].hex()
のように鍵の名前をseedにすることで今何の鍵を作っているのか分かりやすくしました。
余談:ついでに実際のTLS1.2では
# 擬似乱数関数(PRF)にマスターシークレット、 # "key expansion"、サーバーランダムクライアントランダムを渡す km=PRF(MS, "key expansion", ServerRandom + ClientRandom) client_write_MAC = km[:32].hex()
のように一括で鍵を計算します。 また、疑似乱数の中では、MasterSecretを鍵としたハッシュ値を計算するため、HMACが用いられます。
バイナリフォーマット → テキスト
実際のTLSレコード層は複雑なバイナリフォーマットを持ちますが、今回はApplication Dataを
平文: ランチは12時に渋谷で MAC値: a3f9c2...
というテキスト形式にしました。復号結果がそのまま読めるので、「暗号文が平文に戻った」という体験が分かりやすくなります。
Finishedメッセージ → hs_hashの暗号化
本物のFinishedはPRFで計算したverify_dataを送りますが、今回はDiscordのハンドシェイクログをSHA256でハッシュ化したものをそのままセッション鍵で暗号化する形にしました。
「今までのやりとりをまとめてセッション鍵で封印する」という意味は保たれています。
3役に分離したCA設計
最初はサーバー役が自分でCA証明書も作っていましたが、CA(認証局)とをサーバーが兼任するのは不要な複雑さを産むことに気づきました。本物のTLSではブラウザにルートCA証明書があらかじめ組み込まれています。これを再現するために、主催者がCA役を担う設計にしました。
【事前】主催者(CA役)→ ca.crt を全員に配布
↓(ブラウザの組み込みルートCA証明書ストアに相当)
【当日】サーバー役 → server.crt をDiscordに投稿
クライアント役 → openssl verify -CAfile ca.crt server.crt
server.crt: OK と表示された瞬間が、ブラウザの錠前マークに相当します。
Claudeと壁打ちしながら作った
このハンズオンはClaudeとの会話を通じて開発しました。最初は「openssl コマンドを叩いてTLSを再現するハンズオン」というざっくりした要件から始まり、形式が何度か変わっています。
最初はシェルスクリプトで「サーバー役スクリプト」「クライアント役スクリプト」を作っていました。対話形式で進行を自動化するイメージです。でも「各手順のコマンドさえ分かればいい」という方向転換で、HTMLのコマンド早見表に変わりました。
しかし、自分で試していくうちに取り扱うファイルが多く、ややこしいと感じたこと、そして(輪読会の)資料よりもずっと複雑だと感じたことから、0からGoogleColab上にハンズオン資料を手作業で制作しました。
制作物をClaudeに共有し、既存のノートブックのバグ(変数名のtypo、グローバル変数が効かない問題、ca_crtをserver.crtに書き出していたミス)を指摘しつつクライアント版を作ってもらう、という流れで完成しました。
やってみて気づいたこと
面白かったところ
作りながら一番面白いと思ったのは、今まで習った暗号技術の総復習ができる点です。
TLSは暗号技術の総合芸術です。
例えば、メッセージの復号には共通する対称鍵を使います。そして、対称鍵を求めるには秘密の値を安全に運ぶ必要があります。このために公開鍵暗号を使用しますが、自分の受け入れた公開鍵が本当に正しいものかは分かりません。そこで、CAが公開鍵(とメタ情報)をハッシュ化して公開鍵にくっつけた署名を検証します。
この流れで
- 共通鍵暗号
- 公開鍵暗号
- 署名
- ハッシュ化
といった技術が登場します。これらは全て『暗号技術入門』で習ったものであり、今回のハンズオンを作りながら「ああ、こんなことやったな」と思い出していました。
改善点
鍵生成のロジックを手書きさせるのが冗長かもしれない
共通鍵を生成する部分は例示以外、自分でコードを補完させるような作りにしました。めんどうですが、集中力を強制的に投じてもらえるので、「ああ、鍵ごとにseedと長さが違うのか」など気付いてもらえるかな、と。
実際、それは功を奏しましたが、一方で先述のような理解をしたあとのタスクは徒労感があるのも事実です。
単純にseedと読み込みバイト数を書き換えるだけで楽しくない作業になってしまうので、気付きと手軽さをどう両立すればいいかなと考えています。
コマンド打ち込みが結局コピペになりがち
全体的に見られた傾向です。今回のハンズオンではコードセルにはほとんどコマンドを入れていません。例示したコメントを打ち込むことで、集中力を割いて欲しかったからです。 しかし、実際はコピペ実行が多く、今自分が何をやっているかを意識させるには至らなかったかなと思います。
変数の取り扱い
相手から送られてきたランダムやBase64などはColabのフォーム機能で変数に格納するようになっています。
これは「相手からの値を保存したぞ!」感を出すためです。
逆に実際にコマンドを叩くときには、「この値を使うぞ!」感のために値をそのまんま貼り付けるようにしました。つまり、変数を使わない選択をしました。
ただ、今振り返ると、保存した変数を使った方が「あの時の値を使うぞ!」感が出る上、手戻りもないのでいいんじゃないかと考えています。
最後に
今回はTLSのざっくりとした流れを見るためのハンズオンとその工夫について紹介しました。
再度リンクを掲載しておくので、ぜひ遊んでみてください!














