Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
PHPで TLSのプロトコルを実装してみる
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
higaki
March 20, 2026
Programming
0
22
PHPで TLSのプロトコルを実装してみる
PHPerKaigi2026 2026/03/21 11:35〜Track B の登壇資料です。
higaki
March 20, 2026
Tweet
Share
More Decks by higaki
See All by higaki
PHPでResult型を なるべく使いやすい形で実装する
higaki_program
2
350
PHPでResult型やってみよう
higaki_program
1
580
aspidaで型安全にREST APIのバックエンドを呼び出したい
higaki_program
0
780
Other Decks in Programming
See All in Programming
「やめとこ」がなくなった — 1月にZennを始めて22本書いた AI共創開発のリアル
atani14
0
400
今からFlash開発できるわけないじゃん、ムリムリ! (※ムリじゃなかった!?)
arkw
0
110
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
640
Claude Code の Skill で複雑な既存仕様をすっきり整理しよう
yuichirokato
1
410
Goの型安全性で実現する複数プロダクトの権限管理
ishikawa_pro
2
440
Swift ConcurrencyでよりSwiftyに
yuukiw00w
0
270
ロボットのための工場に灯りは要らない
watany
10
3k
PHP 7.4でもOpenTelemetryゼロコード計装がしたい! / PHPerKaigi 2026
arthur1
1
110
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
680
Claude Codeログ基盤の構築
giginet
PRO
7
3.4k
Claude Codeセッション現状確認 2026福岡 / fukuoka-aicoding-00-beacon
monochromegane
4
430
Claude Code Skill入門
mayahoney
0
400
Featured
See All Featured
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
360
30k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.1k
Money Talks: Using Revenue to Get Sh*t Done
nikkihalliwell
0
180
Google's AI Overviews - The New Search
badams
0
930
Music & Morning Musume
bryan
47
7.1k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
790
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
How to build a perfect <img>
jonoalderson
1
5.3k
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.2k
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
470
A Tale of Four Properties
chriscoyier
163
24k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.4k
Transcript
PHPで TLSのプロトコルを実装してみる @PHPerKaigi 2026 ひがき
本セッションの目指すところ TLSに対して 「な〜んか知らんけど、裏側でええ感じに暗号化してくれるやつ」 「むずそう、、、」 「仕組みはわからんけど、使えてるしええか」 ↓ 「TLSおもろいじゃん!」 「意外と難しくないじゃん」 「うごくものは作れそうやな、俺 /私もやるか」
の気持ちになってもらう
話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)
話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)
TLSとは SSL/TLS はセッション層に位置するセキュアプロトコルで、通信の 暗号化、データ完全性の確保、サーバ(場合によりクライアント)の 認証を行うことができる。 • 通信の暗号化 • データ完全性の確保 •
サーバ(場合によりクライアント)の認証 引用:IPA TLS 暗号設定 ガイドライン [https://www.ipa.go.jp/security/crypto/guideline/gmcbt80000005ufv-att/ipa-cryptrec-gl-3001-3.1.1.pdf]
TLSの仕組み 参考:IPA TLS 暗号設定 ガイドライン [https://www.ipa.go.jp/security/crypto/guideline/gmcbt80000005ufv-att/ipa-cryptrec-gl-3001-3.1.1.pdf]
TLS1.2とTLS1.3の大きな違い(補足スライド) 参考:RFC8446 Major Differences from TLS 1.2 [https://datatracker.ietf.org/doc/html/rfc8446#section-1.2] • ServerHello
以降の通信が暗号化 • 1-RTTでハンドシェイクが完結 • 0-RTTモードが追加 • 鍵交換は DHE、ECDHE、PSK のみが規定され、いずれかの利用が必須になった • HKDF-Expand, HKDF-Extractを使った鍵導出 に変更 • 共通鍵暗号は AES-GCM、AES-CCM、ChaCha20-Poly1305 のみが規定された • 署名は RSA-PSS、RSASSA-PKCS1-v1_5、ECDSA が必須になった 参考:IPA TLS 暗号設定 ガイドライン [https://www.ipa.go.jp/security/crypto/guideline/gmcbt80000005ufv-att/ipa-cryptrec-gl-3001-3.1.1.pdf]
TLS1.3の流れ
TLS1.3 やり取りはこんな流れ Client Server ApplicationData Finished Finished CertificateVerify Certificate EncryptedExtensions
ServerHello ClientHello
ClientHello(Client → Server) TLSの最初のメッセージ • 対応できる鍵交換・署名アルゴリズム・暗号スイート • サポートversion(TLS1.2, TLS1.3) ClientHello
やっほー、話したいんだけどさ。 暗号はこのへん使えるよ( cipher suites)。 署名はこのへんならいける( signature_algorithms)。 鍵交換のパーツも先にいくつか持ってきた( key_share)。 Client Server
ServerHello(Client ← Server) ClientHelloに対するサーバの応答 • 使用する鍵交換方式・暗号スイート • サポートversion(TLS1.3) ServerHello おっけー、じゃあこの暗号スイートと鍵交換方式でいこう。
こっちの鍵パーツも渡すね( key_share)。 Client Server
EncryptedExtensions(Client ← Server) ServerHelloの追加情報 • ServerHelloで送信したExtension以外 ◦ key_share ◦ pre_shared_key
◦ supported_versions EncryptedExtensions Client (お互い共通鍵を計算できるし、ここから暗号化して送るか) これ補足事項ね。 Server
Certificate(Client ← Server) 証明書のデータ • 署名アルゴリズム • 公開鍵 Certificate (Clientの署名いけるやつから選ぶか)
ワイの証明書のデータはこれね。 Client Server
CertificateVerify(Client ← Server) デジタル署名のデータ • サーバの秘密鍵で署名したデータを渡す Client Server CertificateVerify ちゃんと本人だよって署名もつけとく。
Finished(Client ← Server) サーバ側TLSハンドシェイク終わりのメッセージ • 改ざん検知のためにHMACを付与する Client Server Finished ほい、ワイは準備OK
で、本題(ApplicationData)なんやっけ?
Finished(Client → Server) クライアント側TLSハンドシェイク終わりのメッセージ • 改ざん検知のためにHMACを付与する Client Server Finished 俺も準備OK
ApplicationData(Client ←→ Server) • アプリケーション層のやり取り ◦ HTTPとかそのあたり ▪ GET /
HTTP/1.1 Client Server ApplicationData (ApplicationDataは別の共通鍵で暗号化する) 本題(ApplicationData)は GET / HTTP/1.1
ApplicationData(Client ←→ Server) • アプリケーション層のやり取り ◦ HTTPとかそのあたり ▪ GET /
HTTP/1.1 Client Server ApplicationData <html> … </html>
まとめ(※0-RTTなどの説明省いている) Client Server ApplicationData Finished Finished CertificateVerify Certificate EncryptedExtensions ServerHello
ClientHello
話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)
話すこと ①TLSの説明 ②最初の一歩( Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)
注意事項 • TLS1.3のサーバ側を実装。 • パフォーマンスは考慮できていない。 ◦ いろいろ漏れはあるけど、動くものを実装。 • 特定の鍵交換・暗号方式しか対応していない。 ◦
鍵交換:ECDHE(X25519) ◦ 暗号化:TLS_AES_256_GCM_SHA384 ◦ 署名:ECDSA • OpenSSL使用。 • 0-RTT未対応。 • 1プロセス1リクエスト。 ◦ 複数リクエストはタイミングによって壊れる • 表示されているプログラムは抜粋してます
https://github.com/higaki-takanori/phigaki-tls
• RFC8446 ◦ https://datatracker.ietf.org/doc/html/rfc8446 • TLS 暗号設定 ガイドライン ◦ https://www.ipa.go.jp/security/crypto/guideline/gmcbt80000005ufv-att/ipa-cryptrec-gl-3001-3.1.1.pdf
• エムスリーテックブック8 ◦ https://techbookfest.org/product/b94hFWewG7fVRLgqEmSjT1?productVariantID=dWQKDmPPqwY 3dfr28X7j4L • SSL/TLS実践入門 ◦ https://gihyo.jp/book/2024/978-4-297-14178-3 • pizzacatさんのtails(HaskellのTLS実装) ◦ https://github.com/pizzacat83/tails • nsfisisさんのphpcon-kagawa-2025 ◦ https://github.com/nsfisis/nil.ninja/tree/main/vhosts/t/phpcon-kagawa-2025 • ichikawaさんの迂闊にTLS/SSLをPHPで実装してみたら最高だった件 ◦ https://blog.ichikaway.com/entry/20240801/ore-no-tls 参考
• TCPソケットを作成し、リクエストを受け取る • リクエストが Handshake or ApplicationDataを判定 • (Handshakeの場合) ◦
ClientHelloの内容を読み取る ◦ ServerHello 作成 → そのままレスポンスとして返す ◦ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ◦ … ◦ Finished 作成 → 暗号化してレスポンスとして返す • (ApplicationDataの場合) ◦ 復号化する ◦ リクエストの内容に応じたレスポンスを返す 流れ
• TCPソケットを作成し、リクエストを受け取る • リクエストが Handshake or ApplicationDataを判定 • (Handshakeの場合) ◦
ClientHelloの内容を読み取る ◦ ServerHello 作成 → そのままレスポンスとして返す ◦ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ◦ … ◦ Finished 作成 → 暗号化してレスポンスとして返す • (ApplicationDataの場合) ◦ 復号化する ◦ リクエストの内容に応じたレスポンスを返す 最初の一歩
• TCPソケットを作成し、リクエストを受け取る • リクエストが Handshake or ApplicationDataを判定 • (Handshakeの場合) ◦
ClientHelloの内容を読み取る ◦ ServerHello 作成 → そのままレスポンスとして返す ◦ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ◦ … ◦ Finished 作成 → 暗号化してレスポンスとして返す • (ApplicationDataの場合) ◦ 復号化する ◦ リクエストの内容に応じたレスポンスを返す 流れ
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_bind($socket, '0.0.0.0', 443); socket_listen($socket, 5);
$sock = socket_accept($socket); $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock, $chunk, 8192, 0);
リクエストをWiresharkで見てみる
リクエストをWiresharkで見てみる
リクエストをWiresharkで見てみる TCPの世界のbinary (16進数表記) TLSの世界のbinary (16進数表記)
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … このbinaryから意味を見出していく リクエストのParse
TLSの通信単位(RFC8446で設定) TLSメッセージ (Handshakeの内容・暗号化されているTLSメッセージ) Type legacy_record_version (固定値) Length
TLSの通信単位(RFC8446で設定) 暗号化されていないHandshakeの内容 0x16 (Handshake) 0x0303 (TLS1.2) 0x06b2 (1714)
TLSの通信単位(RFC8446で設定) 暗号化されているTLSメッセージ 0x17 (ApplicationData) 0x0303 (TLS1.2) 0x0203 (531)
ClientHelloのフォーマット(RFC8446で設定) (TLSRecord) type legacy_record_version (TLSRecord) length msg_type (Handshake) length legacy_version
random cipher_suites legacy_compression_methods extensions legacy_session_id
ClientHelloのフォーマット(RFC8446で設定) 固定値 (0x16) (Handshake) 固定値(0x0301 or 0x0303) TLSRecordの長さ 固定値 (0x01)
(ClientHello) Handshakeの長さ 固定値(0x0303) ランダム値 対応できる暗号スイート一覧 固定値(0x00) 拡張 レガシーセッションID
ClientHelloのフォーマット(RFC8446で設定) 固定値 (0x16) (Handshake) 固定値(0x0301 or 0x0303) TLSRecordの長さ 固定値 (0x01)
(ClientHello) Handshakeの長さ 固定値(0x0303) ランダム値 対応できる暗号スイート一覧 固定値(0x00) 拡張 レガシーセッションID 対応できる署名アルゴリズム 鍵交換のパーツ
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] この通信は Handshake(0x16 = 22)
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] 固定値(0x0301 or 0x0303)
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] TLSPlaintext長さは 1714(0x06b2)
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] Handshakeの中身
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] ClientHello(1)
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] Handshakeの長さ 1710(0x0006ae)
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] ClientHelloの中身
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
16 03 01 06 b2 01 00 06 ae 03
03 a6 c0 23 df 11 … 省略するが流れは同じ リクエストのParse 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
... $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock,
$chunk, 8192, 0); :/ リクエストをParse $tlsRecord = TlsRecord::from($chunkHex); ここから補助スライド
... $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock,
$chunk, 8192, 0); :/ リクエストをParse $tlsRecord = TlsRecord::from($chunkHex); TLS通信における データの送受信単位
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } }
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } 16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 …
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] 16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 …
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } 16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] この通信は Handshake(0x16 = 22)
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } 16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] 固定値(0x0301 or 0x0303)
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } 16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] TLSPlaintext長さは 1714(0x06b2)
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } 16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 … 0x16(22)は Handshake 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } 16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
final readonly class TlsRecord { public function :_construct( public TlsPlaintext|TlsCiphertext|HexString
$element, ) {} public static function from(HexString $hex): self { $contentType = ContentType::fromHex($hex:>sub(0, 1)); $_legacyRecordVersion = ProtocolVersion::fromHex($hex:>sub(1, 2)); $_length = HexLength::from($hex:>sub(3, 2)); $element = match ($contentType) { ContentType::Handshake :> TlsPlaintext::new($contentType, Handshake::from($hex:>sub(5))), ContentType::ApplicationData :> TlsCiphertext::new($hex:>sub(5)), default :> $hex:>sub(5), }; return new self( element: $element, ); } } ちなみに、暗号化されると全てApplicationDataになる
• TCPソケットを作成し、リクエストを受け取る • リクエストが Handshake or ApplicationDataを判定 • (Handshakeの場合) ◦
ClientHelloの内容を読み取る ◦ ServerHello 作成 → そのままレスポンスとして返す ◦ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ◦ … ◦ Finished 作成 → 暗号化してレスポンスとして返す • (ApplicationDataの場合) ◦ 復号化する ◦ リクエストの内容に応じたレスポンスを返す 流れ
• TCPソケットを作成し、リクエストを受け取る • リクエストが Handshake or ApplicationDataを判定 • (Handshakeの場合) ◦
ClientHelloの内容を読み取る ◦ ServerHello 作成 → そのままレスポンスとして返す ◦ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ◦ … ◦ Finished 作成 → 暗号化してレスポンスとして返す • (ApplicationDataの場合) ◦ 復号化する ◦ リクエストの内容に応じたレスポンスを返す 流れ
final readonly class Handshake { public function :_construct( public HandshakeType
$msgType, public HexLength $length, public ClientHello|HandshakeResponder $body, ) {} … } public static function from(HexString $hex): self { $msgType = HandshakeType::fromHex($hex:>sub(0, 1)); $length = HexLength::from($hex:>sub(1, 3)); $body = match ($msgType) { HandshakeType::ClientHello :> ClientHello::from($hex:>sub(4)), default :> throw new \RuntimeException('not implemented'), }; return new self( msgType: $msgType, length: $length, body: $body, ); }
final readonly class Handshake { public function :_construct( public HandshakeType
$msgType, public HexLength $length, public ClientHello|HandshakeResponder $body, ) {} … } public static function from(HexString $hex): self { $msgType = HandshakeType::fromHex($hex:>sub(0, 1)); $length = HexLength::from($hex:>sub(1, 3)); $body = match ($msgType) { HandshakeType::ClientHello :> ClientHello::from($hex:>sub(4)), default :> throw new \RuntimeException('not implemented'), }; return new self( msgType: $msgType, length: $length, body: $body, ); } 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] 01 00 06 ae 03 03 a6 c0 23 df 11 …
final readonly class Handshake { public function :_construct( public HandshakeType
$msgType, public HexLength $length, public ClientHello|HandshakeResponder $body, ) {} … } public static function from(HexString $hex): self { $msgType = HandshakeType::fromHex($hex:>sub(0, 1)); $length = HexLength::from($hex:>sub(1, 3)); $body = match ($msgType) { HandshakeType::ClientHello :> ClientHello::from($hex:>sub(4)), default :> throw new \RuntimeException('not implemented'), }; return new self( msgType: $msgType, length: $length, body: $body, ); } 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
final readonly class Handshake { public function :_construct( public HandshakeType
$msgType, public HexLength $length, public ClientHello|HandshakeResponder $body, ) {} … } public static function from(HexString $hex): self { $msgType = HandshakeType::fromHex($hex:>sub(0, 1)); $length = HexLength::from($hex:>sub(1, 3)); $body = match ($msgType) { HandshakeType::ClientHello :> ClientHello::from($hex:>sub(4)), default :> throw new \RuntimeException('not implemented'), }; return new self( msgType: $msgType, length: $length, body: $body, ); } 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
final readonly class Handshake { public function :_construct( public HandshakeType
$msgType, public HexLength $length, public ClientHello|HandshakeResponder $body, ) {} … } public static function from(HexString $hex): self { $msgType = HandshakeType::fromHex($hex:>sub(0, 1)); $length = HexLength::from($hex:>sub(1, 3)); $body = match ($msgType) { HandshakeType::ClientHello :> ClientHello::from($hex:>sub(4)), default :> throw new \RuntimeException('not implemented'), }; return new self( msgType: $msgType, length: $length, body: $body, ); } 01 00 06 ae 03 03 a6 c0 23 df 11 … 0x01(1)は ClientHello 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
final readonly class Handshake { public function :_construct( public HandshakeType
$msgType, public HexLength $length, public ClientHello|HandshakeResponder $body, ) {} … } public static function from(HexString $hex): self { $msgType = HandshakeType::fromHex($hex:>sub(0, 1)); $length = HexLength::from($hex:>sub(1, 3)); $body = match ($msgType) { HandshakeType::ClientHello :> ClientHello::from($hex:>sub(4)), default :> throw new \RuntimeException('not implemented'), }; return new self( msgType: $msgType, length: $length, body: $body, ); } 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
final readonly class Handshake { public function :_construct( public HandshakeType
$msgType, public HexLength $length, public ClientHello|HandshakeResponder $body, ) {} … } public static function from(HexString $hex): self { $msgType = HandshakeType::fromHex($hex:>sub(0, 1)); $length = HexLength::from($hex:>sub(1, 3)); $body = match ($msgType) { HandshakeType::ClientHello :> ClientHello::from($hex:>sub(4)), default :> throw new \RuntimeException('not implemented'), }; return new self( msgType: $msgType, length: $length, body: $body, ); } 01 00 06 ae 03 03 a6 c0 23 df 11 … 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] 省略しますが、流れは同じです。 ここまで補助スライド
話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)
話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やって いることはシンプル)
EncryptedExtensions(Client ← Server) ServerHelloの追加情報 • ServerHelloで送信したExtension以外 ◦ key_share ◦ pre_shared_key
◦ supported_versions EncryptedExtensions Client (お互い共通鍵を計算できるし、ここから暗号化して送るか) これ補足事項ね。 Server
EncryptedExtensions(Client ← Server) ServerHelloの追加情報 • ServerHelloで送信したExtension以外 ◦ key_share ◦ pre_shared_key
◦ supported_versions EncryptedExtensions Client (お互い共通鍵を計算 できるし、ここから暗号化して送るか) これ補足事項ね。 Server
TLS1.3から HKDF-Expand, HKDF-Extractを使った鍵導出 に変更 鍵導出
1. ClientHelloで「Secretの元」を受け取る 2. 「Secretの元」を加工して、「Secret(鍵の元)」を生成 3. 「Secret(鍵の元)」から「鍵」と「IV」を生成 鍵導出流れ
Secret(鍵の元)を導出 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
Secretから共通鍵とIV(暗号化の初期値)を導出 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
Secretから共通鍵とIV(暗号化の初期値)を導出 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] TLS1.3が対応している暗号化にkeyとivが必要
使用する関数 • HKDF-Extract • HKDF-Expand • HKDF-Expand-Label • Derive-Secret
HMAC-based Extract-and-Expand Key Derivation Function HMACを用いた鍵導出を行う関数 HKDF(HKDF-Extract, HKDF-Expand)
HMACは平文メッセージに鍵を結合した状態でハッシュ関数に通すと いうシンプルな考え方です。 Keyed-Hashing for Message Authentication 暗号ハッシュ関数を用いたメッセージ認証の仕組み HMAC 参考:RFC2104 [https://datatracker.ietf.org/doc/html/rfc2104]
引用:SSL/TLS実践入門 CMACとHMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]
HMAC 引用:SSL/TLS実践入門 HMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]
HMAC 引用:SSL/TLS実践入門 HMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]
HMAC 引用:SSL/TLS実践入門 HMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]
HMAC
HMAC hash_hmac( algo: 'sha384', data: $メッセージ, key: $共通鍵, );
ハッシュ関数は元のメッセージと 1対1に対応した「ハッシュ値」を生成するため、ハッシュ値が変わら ないなら、メッセージも変化していないこと(完全性)が分かります。しかし、それは誰が行っても結果 は同じであり、通信のようにハッシュ値を生成する人と検査する人が異なる環境では、メッセージと ハッシュ値が同時に改ざんされている可能性を否定できません。 MACはメッセージを送信した主体が計算した値であることを担保する「認証( Authentication)」の機 能も加えることで、通信環境における完全性検証を実現していると考えると理解しやすいでしょう。 MACはハッシュと異なり、たとえ入力が同じであっても使用する共通鍵によって全く異なる出力結果 になります。そのため、メッセージと一緒に受信した
MACが正しいことを検証するには MACを作成し た送信者と同じ鍵を受信者も共有している必要があります。鍵の要素が加わることにより、メッセージ の完全性と真正性の両方を確認することができるのです注 24(図2.9)。 MAC と ハッシュの違い(補足スライド) 引用:SSL/TLS実践入門 CMACとHMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]
MAC と ハッシュの違い 引用:SSL/TLS実践入門 CMACとHMAC 図2.9[https://gihyo.jp/book/2024/978-4-297-14178-3]
MACは同じ鍵を持っていないと同じMAC値にならない → 同じMAC値になったってことは同じ共通鍵を持っている相手!! Hash 「このデータが壊れていないか」(完全性) MAC 「このデータが壊れていないか」+「正しい相手が作ったか」(完全性 + 認証) MAC
と ハッシュの違い
HKDF-Extract 引用:RFC5869 [https://datatracker.ietf.org/doc/html/rfc5869]
HMAC(再掲)
HKDF-Extract
HKDF-Expand 引用:RFC5869 [https://datatracker.ietf.org/doc/html/rfc5869]
HKDF-Expand
HKDF-Expand
HKDF-Expand
HKDF-Expand
HKDF-Expand HKDF-Expand の返り値
HKDF-Expand-Label 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
HKDF-Expand(再掲)
HKDF-Expand-Label
HKDF-Expand-Label
HKDF-Expand-Label
HKDF-Expand-Label HKDF-Expand-Label の返り値
Derive-Secret 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
HKDF-Expand-Label(再掲)
Derive-Secret
Derive-Secret Derive-Secret の返り値
使用する関数 • HKDF-Extract • HKDF-Expand • HKDF-Expand-Label • Derive-Secret
• 今回はHashアルゴリズムは「sha384」にします。 ◦ Hashで得られる長さは48byte • 共通鍵の長さは「256bit(32byte)」にします。 • 初めてTLSコネクションをはる場合にします。 ◦ PSK=0
実際に共通鍵を導出する
実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
実際に共通鍵を導出する
実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
実際に共通鍵を導出する
実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
実際に共通鍵を導出する
実際に共通鍵を導出する 鍵交換でClientとServerで 同じ値が計算できる (DHE・ECDHE)
実際に共通鍵を導出する 鍵交換でClientとServerで 同じ値が計算できる (DHE・ECDHE) openssl_pkey_derive($client鍵のパーツPem, $server秘密鍵Pem);
実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] Clientのリクエストを 暗号化・復号化する共通鍵の元
実際に共通鍵を導出する
実際に共通鍵を導出する 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
実際に共通鍵を導出する
実際に共通鍵を導出する
実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] Serverのリクエストを 暗号化・復号化する共通鍵の元
実際に共通鍵を導出する
実際に共通鍵を導出する 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]
実際に共通鍵を導出する
実際に共通鍵を導出する
所感
• ブラウザに画面が表示された時は感動🥹 • バイナリに意味を見出すのが、白黒のものに色が付いていくみたいで気 持ち良い • ブラウザがURL叩いて表示される短い時間に色んなことしてて、それを知 れてめちゃ楽しかった⭐ • 全てRFCに記載されている
所感
• OpenSSLの部分はまだ自作できてない ◦ OpenSSLなしで実装していきたい ◦ hmac_hashの部分も自作したい • Server側作ったら、Client側も作りたくなる ◦ @cakephper
さんはPHP Conference Japan 2024でTLS Client側 の登壇されてた 今後
まとめ
まとめ 「TLSおもろいじゃん!」 「意外と難しくないじゃん」 「うごくものは作れそうやな、俺/私もやるか」 の気持ちになれましたでしょうか? もっと迂闊にいろんなものを実装するきっかけになれば嬉しいです
(時間あれば)DEMO
自己紹介
ひがき 株式会社リンケージ 🍊PHPConference愛媛2026 実行委員長🐘 PHPConference愛媛2026開催!! • 2026年10月3日 • 愛媛大学 城北キャンパス
自己紹介
ご静聴ありがとうございました
Appendix
stringの扱い方
• PHPはバイナリもstring型として扱われる。 • (IMO)TLS自作は16進数の文字列に変換して見ると便利。 →「16進数の文字列」のクラスを作成 stringの扱い方
stringの扱い方 16 03 01 06 b2 01 00 06 リクエストで
受け取るデータ
stringの扱い方 16 03 01 06 b2 01 00 06 "�"
var_dump() リクエストで 受け取るデータ
stringの扱い方 16 03 01 06 b2 01 00 06 "�"
var_dump() リクエストで 受け取るデータ 😭
stringの扱い方 0b00010110 0b00000011 0b00000001 0b00000110 0b10110010 0b00000001 0b00000000 0b00000110 "�"
var_dump() リクエストで 受け取るデータ
stringの扱い方 0b00010110 0b00000011 0b00000001 0b00000110 0b10110010 0b00000001 0b00000000 0b00000110 "�"
var_dump() 0b00110001 0b00110110 0b00110000 0b00110011 0b00110000 0b00110001 0b00110000 0b00110110 0b01100010 0b00110010 0b00110000 0b00110001 0b00110000 0b00110000 0b00110000 0b00110110 bin2hex()
stringの扱い方 0b00010110 0b00000011 0b00000001 0b00000110 0b10110010 0b00000001 0b00000000 0b00000110 "�"
var_dump() 0b00110001 0b00110110 0b00110000 0b00110011 0b00110000 0b00110001 0b00110000 0b00110110 0b01100010 0b00110010 0b00110000 0b00110001 0b00110000 0b00110000 0b00110000 0b00110110 bin2hex() "16030106b2010006" var_dump()
stringの扱い方 0b00010110 0b00000011 0b00000001 0b00000110 0b10110010 0b00000001 0b00000000 0b00000110 "�"
var_dump() 0b00110001 0b00110110 0b00110000 0b00110011 0b00110000 0b00110001 0b00110000 0b00110110 0b01100010 0b00110010 0b00110000 0b00110001 0b00110000 0b00110000 0b00110000 0b00110110 bin2hex() "16030106b2010006" var_dump() ☺
stringの扱い方 0b00010110 0b00000011 0b00000001 0b00000110 0b10110010 0b00000001 0b00000000 0b00000110 "�"
var_dump() 0b00110001 0b00110110 0b00110000 0b00110011 0b00110000 0b00110001 0b00110000 0b00110110 0b01100010 0b00110010 0b00110000 0b00110001 0b00110000 0b00110000 0b00110000 0b00110110 bin2hex() "16030106b2010006" var_dump() 全部string型 😳
• 型の縛りがないので、bin2hexの前後がわからなくなる • 今どの形式の文字列なのかわからなくなる 全部string型だと辛いポイント 無限 bin2hex() ができちゃう string string
🤔
クラス分け 0b00110001 0b00110110 0b00110000 0b00110011 0b00110000 0b00110001 0b00110000 0b00110110 0b01100010
0b00110010 0b00110000 0b00110001 0b00110000 0b00110000 0b00110000 0b00110110 "16030106b2010006" 0b00010110 0b00000011 0b00000001 0b00000110 0b10110010 0b00000001 0b00000000 0b00000110 "�" string HexString
クラス分け HexString string (Request) string (Response) 今回の自作TLSではこのクラスを ベースとする
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_bind($socket, $this:>host, $this:>port) socket_listen($socket, 5)
$sock = socket_accept($socket); $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock, $chunk, 8192, 0); :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex = HexString::from($chunk);
... socket_listen($socket, 5) $sock = socket_accept($socket); $chunk = ''; :/
$chunk にリクエストが格納される $bytes = socket_recv($sock, $chunk, 8192, 0); :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex = HexString::from($chunk);
... $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock,
$chunk, 8192, 0); :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex = HexString::from($chunk);
... :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex =
HexString::from($chunk);
... :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex =
HexString::from($chunk); :/ リクエストをParse $tlsRecord = TlsRecord::from($chunkHex);
• 16進数変換の部分はBinaryStringクラスを作成でもよかったかも ◦ Debugしやすさから16進数にしてた ◦ __debugInfo とか __toString 実装で事足りていたかも 時間ないから省略したけど、Appendixの振り返り