Slide 1

Slide 1 text

PHPで TLSのプロトコルを実装してみる @PHPerKaigi 2026 ひがき

Slide 2

Slide 2 text

本セッションの目指すところ TLSに対して 「な〜んか知らんけど、裏側でええ感じに暗号化してくれるやつ」 「むずそう、、、」 「仕組みはわからんけど、使えてるしええか」 ↓ 「TLSおもろいじゃん!」 「意外と難しくないじゃん」 「うごくものは作れそうやな、俺 /私もやるか」 の気持ちになってもらう

Slide 3

Slide 3 text

話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)

Slide 4

Slide 4 text

話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)

Slide 5

Slide 5 text

TLSとは SSL/TLS はセッション層に位置するセキュアプロトコルで、通信の 暗号化、データ完全性の確保、サーバ(場合によりクライアント)の 認証を行うことができる。 ● 通信の暗号化 ● データ完全性の確保 ● サーバ(場合によりクライアント)の認証 引用:IPA TLS 暗号設定 ガイドライン [https://www.ipa.go.jp/security/crypto/guideline/gmcbt80000005ufv-att/ipa-cryptrec-gl-3001-3.1.1.pdf]

Slide 6

Slide 6 text

TLSの仕組み 参考:IPA TLS 暗号設定 ガイドライン [https://www.ipa.go.jp/security/crypto/guideline/gmcbt80000005ufv-att/ipa-cryptrec-gl-3001-3.1.1.pdf]

Slide 7

Slide 7 text

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]

Slide 8

Slide 8 text

TLS1.3の流れ

Slide 9

Slide 9 text

TLS1.3 やり取りはこんな流れ Client Server ApplicationData Finished Finished CertificateVerify Certificate EncryptedExtensions ServerHello ClientHello

Slide 10

Slide 10 text

ClientHello(Client → Server) TLSの最初のメッセージ ● 対応できる鍵交換・署名アルゴリズム・暗号スイート ● サポートversion(TLS1.2, TLS1.3) ClientHello やっほー、話したいんだけどさ。 暗号はこのへん使えるよ( cipher suites)。 署名はこのへんならいける( signature_algorithms)。 鍵交換のパーツも先にいくつか持ってきた( key_share)。 Client Server

Slide 11

Slide 11 text

ServerHello(Client ← Server) ClientHelloに対するサーバの応答 ● 使用する鍵交換方式・暗号スイート ● サポートversion(TLS1.3) ServerHello おっけー、じゃあこの暗号スイートと鍵交換方式でいこう。 こっちの鍵パーツも渡すね( key_share)。 Client Server

Slide 12

Slide 12 text

EncryptedExtensions(Client ← Server) ServerHelloの追加情報 ● ServerHelloで送信したExtension以外 ○ key_share ○ pre_shared_key ○ supported_versions EncryptedExtensions Client (お互い共通鍵を計算できるし、ここから暗号化して送るか) これ補足事項ね。 Server

Slide 13

Slide 13 text

Certificate(Client ← Server) 証明書のデータ ● 署名アルゴリズム ● 公開鍵 Certificate (Clientの署名いけるやつから選ぶか) ワイの証明書のデータはこれね。 Client Server

Slide 14

Slide 14 text

CertificateVerify(Client ← Server) デジタル署名のデータ ● サーバの秘密鍵で署名したデータを渡す Client Server CertificateVerify ちゃんと本人だよって署名もつけとく。

Slide 15

Slide 15 text

Finished(Client ← Server) サーバ側TLSハンドシェイク終わりのメッセージ ● 改ざん検知のためにHMACを付与する Client Server Finished ほい、ワイは準備OK で、本題(ApplicationData)なんやっけ?

Slide 16

Slide 16 text

Finished(Client → Server) クライアント側TLSハンドシェイク終わりのメッセージ ● 改ざん検知のためにHMACを付与する Client Server Finished 俺も準備OK

Slide 17

Slide 17 text

ApplicationData(Client ←→ Server) ● アプリケーション層のやり取り ○ HTTPとかそのあたり ■ GET / HTTP/1.1 Client Server ApplicationData (ApplicationDataは別の共通鍵で暗号化する) 本題(ApplicationData)は GET / HTTP/1.1

Slide 18

Slide 18 text

ApplicationData(Client ←→ Server) ● アプリケーション層のやり取り ○ HTTPとかそのあたり ■ GET / HTTP/1.1 Client Server ApplicationData …

Slide 19

Slide 19 text

まとめ(※0-RTTなどの説明省いている) Client Server ApplicationData Finished Finished CertificateVerify Certificate EncryptedExtensions ServerHello ClientHello

Slide 20

Slide 20 text

話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)

Slide 21

Slide 21 text

話すこと ①TLSの説明 ②最初の一歩( Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)

Slide 22

Slide 22 text

注意事項 ● TLS1.3のサーバ側を実装。 ● パフォーマンスは考慮できていない。 ○ いろいろ漏れはあるけど、動くものを実装。 ● 特定の鍵交換・暗号方式しか対応していない。 ○ 鍵交換:ECDHE(X25519) ○ 暗号化:TLS_AES_256_GCM_SHA384 ○ 署名:ECDSA ● OpenSSL使用。 ● 0-RTT未対応。 ● 1プロセス1リクエスト。 ○ 複数リクエストはタイミングによって壊れる ● 表示されているプログラムは抜粋してます

Slide 23

Slide 23 text

https://github.com/higaki-takanori/phigaki-tls

Slide 24

Slide 24 text

● 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 参考

Slide 25

Slide 25 text

● TCPソケットを作成し、リクエストを受け取る ● リクエストが Handshake or ApplicationDataを判定 ● (Handshakeの場合) ○ ClientHelloの内容を読み取る ○ ServerHello 作成 → そのままレスポンスとして返す ○ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ○ … ○ Finished 作成 → 暗号化してレスポンスとして返す ● (ApplicationDataの場合) ○ 復号化する ○ リクエストの内容に応じたレスポンスを返す 流れ

Slide 26

Slide 26 text

● TCPソケットを作成し、リクエストを受け取る ● リクエストが Handshake or ApplicationDataを判定 ● (Handshakeの場合) ○ ClientHelloの内容を読み取る ○ ServerHello 作成 → そのままレスポンスとして返す ○ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ○ … ○ Finished 作成 → 暗号化してレスポンスとして返す ● (ApplicationDataの場合) ○ 復号化する ○ リクエストの内容に応じたレスポンスを返す 最初の一歩

Slide 27

Slide 27 text

● TCPソケットを作成し、リクエストを受け取る ● リクエストが Handshake or ApplicationDataを判定 ● (Handshakeの場合) ○ ClientHelloの内容を読み取る ○ ServerHello 作成 → そのままレスポンスとして返す ○ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ○ … ○ Finished 作成 → 暗号化してレスポンスとして返す ● (ApplicationDataの場合) ○ 復号化する ○ リクエストの内容に応じたレスポンスを返す 流れ

Slide 28

Slide 28 text

$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);

Slide 29

Slide 29 text

リクエストをWiresharkで見てみる

Slide 30

Slide 30 text

リクエストをWiresharkで見てみる

Slide 31

Slide 31 text

リクエストをWiresharkで見てみる TCPの世界のbinary (16進数表記) TLSの世界のbinary (16進数表記)

Slide 32

Slide 32 text

16 03 01 06 b2 01 00 06 ae 03 03 a6 c0 23 df 11 … このbinaryから意味を見出していく リクエストのParse

Slide 33

Slide 33 text

TLSの通信単位(RFC8446で設定) TLSメッセージ (Handshakeの内容・暗号化されているTLSメッセージ) Type legacy_record_version (固定値) Length

Slide 34

Slide 34 text

TLSの通信単位(RFC8446で設定) 暗号化されていないHandshakeの内容 0x16 (Handshake) 0x0303 (TLS1.2) 0x06b2 (1714)

Slide 35

Slide 35 text

TLSの通信単位(RFC8446で設定) 暗号化されているTLSメッセージ 0x17 (ApplicationData) 0x0303 (TLS1.2) 0x0203 (531)

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

ClientHelloのフォーマット(RFC8446で設定) 固定値 (0x16) (Handshake) 固定値(0x0301 or 0x0303) TLSRecordの長さ 固定値 (0x01) (ClientHello) Handshakeの長さ 固定値(0x0303) ランダム値 対応できる暗号スイート一覧 固定値(0x00) 拡張 レガシーセッションID

Slide 38

Slide 38 text

ClientHelloのフォーマット(RFC8446で設定) 固定値 (0x16) (Handshake) 固定値(0x0301 or 0x0303) TLSRecordの長さ 固定値 (0x01) (ClientHello) Handshakeの長さ 固定値(0x0303) ランダム値 対応できる暗号スイート一覧 固定値(0x00) 拡張 レガシーセッションID 対応できる署名アルゴリズム 鍵交換のパーツ

Slide 39

Slide 39 text

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]

Slide 40

Slide 40 text

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)

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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)

Slide 43

Slide 43 text

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の中身

Slide 44

Slide 44 text

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]

Slide 45

Slide 45 text

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)

Slide 46

Slide 46 text

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)

Slide 47

Slide 47 text

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の中身

Slide 48

Slide 48 text

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]

Slide 49

Slide 49 text

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]

Slide 50

Slide 50 text

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]

Slide 51

Slide 51 text

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]

Slide 52

Slide 52 text

... $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock, $chunk, 8192, 0); :/ リクエストをParse $tlsRecord = TlsRecord::from($chunkHex); ここから補助スライド

Slide 53

Slide 53 text

... $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock, $chunk, 8192, 0); :/ リクエストをParse $tlsRecord = TlsRecord::from($chunkHex); TLS通信における データの送受信単位

Slide 54

Slide 54 text

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, ); } }

Slide 55

Slide 55 text

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 …

Slide 56

Slide 56 text

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 …

Slide 57

Slide 57 text

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)

Slide 58

Slide 58 text

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)

Slide 59

Slide 59 text

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)

Slide 60

Slide 60 text

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]

Slide 61

Slide 61 text

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]

Slide 62

Slide 62 text

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になる

Slide 63

Slide 63 text

● TCPソケットを作成し、リクエストを受け取る ● リクエストが Handshake or ApplicationDataを判定 ● (Handshakeの場合) ○ ClientHelloの内容を読み取る ○ ServerHello 作成 → そのままレスポンスとして返す ○ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ○ … ○ Finished 作成 → 暗号化してレスポンスとして返す ● (ApplicationDataの場合) ○ 復号化する ○ リクエストの内容に応じたレスポンスを返す 流れ

Slide 64

Slide 64 text

● TCPソケットを作成し、リクエストを受け取る ● リクエストが Handshake or ApplicationDataを判定 ● (Handshakeの場合) ○ ClientHelloの内容を読み取る ○ ServerHello 作成 → そのままレスポンスとして返す ○ EncryptedExtensions 作成 → 暗号化してレスポンスとして返す ○ … ○ Finished 作成 → 暗号化してレスポンスとして返す ● (ApplicationDataの場合) ○ 復号化する ○ リクエストの内容に応じたレスポンスを返す 流れ

Slide 65

Slide 65 text

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, ); }

Slide 66

Slide 66 text

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 …

Slide 67

Slide 67 text

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]

Slide 68

Slide 68 text

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]

Slide 69

Slide 69 text

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]

Slide 70

Slide 70 text

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]

Slide 71

Slide 71 text

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] 省略しますが、流れは同じです。 ここまで補助スライド

Slide 72

Slide 72 text

話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やってい ることはシンプル)

Slide 73

Slide 73 text

話すこと ①TLSの説明 ②最初の一歩(Parse) ③Secretから共通鍵導出(むずそうに見えるけど、やって いることはシンプル)

Slide 74

Slide 74 text

EncryptedExtensions(Client ← Server) ServerHelloの追加情報 ● ServerHelloで送信したExtension以外 ○ key_share ○ pre_shared_key ○ supported_versions EncryptedExtensions Client (お互い共通鍵を計算できるし、ここから暗号化して送るか) これ補足事項ね。 Server

Slide 75

Slide 75 text

EncryptedExtensions(Client ← Server) ServerHelloの追加情報 ● ServerHelloで送信したExtension以外 ○ key_share ○ pre_shared_key ○ supported_versions EncryptedExtensions Client (お互い共通鍵を計算 できるし、ここから暗号化して送るか) これ補足事項ね。 Server

Slide 76

Slide 76 text

TLS1.3から HKDF-Expand, HKDF-Extractを使った鍵導出 に変更 鍵導出

Slide 77

Slide 77 text

1. ClientHelloで「Secretの元」を受け取る 2. 「Secretの元」を加工して、「Secret(鍵の元)」を生成 3. 「Secret(鍵の元)」から「鍵」と「IV」を生成 鍵導出流れ

Slide 78

Slide 78 text

Secret(鍵の元)を導出 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 79

Slide 79 text

Secretから共通鍵とIV(暗号化の初期値)を導出 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 80

Slide 80 text

Secretから共通鍵とIV(暗号化の初期値)を導出 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] TLS1.3が対応している暗号化にkeyとivが必要

Slide 81

Slide 81 text

使用する関数 ● HKDF-Extract ● HKDF-Expand ● HKDF-Expand-Label ● Derive-Secret

Slide 82

Slide 82 text

HMAC-based Extract-and-Expand Key Derivation Function HMACを用いた鍵導出を行う関数 HKDF(HKDF-Extract, HKDF-Expand)

Slide 83

Slide 83 text

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]

Slide 84

Slide 84 text

HMAC 引用:SSL/TLS実践入門 HMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]

Slide 85

Slide 85 text

HMAC 引用:SSL/TLS実践入門 HMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]

Slide 86

Slide 86 text

HMAC 引用:SSL/TLS実践入門 HMAC[https://gihyo.jp/book/2024/978-4-297-14178-3]

Slide 87

Slide 87 text

HMAC

Slide 88

Slide 88 text

HMAC hash_hmac( algo: 'sha384', data: $メッセージ, key: $共通鍵, );

Slide 89

Slide 89 text

ハッシュ関数は元のメッセージと 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]

Slide 90

Slide 90 text

MAC と ハッシュの違い 引用:SSL/TLS実践入門 CMACとHMAC 図2.9[https://gihyo.jp/book/2024/978-4-297-14178-3]

Slide 91

Slide 91 text

MACは同じ鍵を持っていないと同じMAC値にならない → 同じMAC値になったってことは同じ共通鍵を持っている相手!! Hash 「このデータが壊れていないか」(完全性) MAC 「このデータが壊れていないか」+「正しい相手が作ったか」(完全性 + 認証) MAC と ハッシュの違い

Slide 92

Slide 92 text

HKDF-Extract 引用:RFC5869 [https://datatracker.ietf.org/doc/html/rfc5869]

Slide 93

Slide 93 text

HMAC(再掲)

Slide 94

Slide 94 text

HKDF-Extract

Slide 95

Slide 95 text

HKDF-Expand 引用:RFC5869 [https://datatracker.ietf.org/doc/html/rfc5869]

Slide 96

Slide 96 text

HKDF-Expand

Slide 97

Slide 97 text

HKDF-Expand

Slide 98

Slide 98 text

HKDF-Expand

Slide 99

Slide 99 text

HKDF-Expand

Slide 100

Slide 100 text

HKDF-Expand HKDF-Expand の返り値

Slide 101

Slide 101 text

HKDF-Expand-Label 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 102

Slide 102 text

HKDF-Expand(再掲)

Slide 103

Slide 103 text

HKDF-Expand-Label

Slide 104

Slide 104 text

HKDF-Expand-Label

Slide 105

Slide 105 text

HKDF-Expand-Label

Slide 106

Slide 106 text

HKDF-Expand-Label HKDF-Expand-Label の返り値

Slide 107

Slide 107 text

Derive-Secret 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 108

Slide 108 text

HKDF-Expand-Label(再掲)

Slide 109

Slide 109 text

Derive-Secret

Slide 110

Slide 110 text

Derive-Secret Derive-Secret の返り値

Slide 111

Slide 111 text

使用する関数 ● HKDF-Extract ● HKDF-Expand ● HKDF-Expand-Label ● Derive-Secret

Slide 112

Slide 112 text

● 今回はHashアルゴリズムは「sha384」にします。 ○ Hashで得られる長さは48byte ● 共通鍵の長さは「256bit(32byte)」にします。 ● 初めてTLSコネクションをはる場合にします。 ○ PSK=0 実際に共通鍵を導出する

Slide 113

Slide 113 text

実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 114

Slide 114 text

実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 115

Slide 115 text

実際に共通鍵を導出する

Slide 116

Slide 116 text

実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 117

Slide 117 text

実際に共通鍵を導出する

Slide 118

Slide 118 text

実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 119

Slide 119 text

実際に共通鍵を導出する

Slide 120

Slide 120 text

実際に共通鍵を導出する 鍵交換でClientとServerで 同じ値が計算できる (DHE・ECDHE)

Slide 121

Slide 121 text

実際に共通鍵を導出する 鍵交換でClientとServerで 同じ値が計算できる (DHE・ECDHE) openssl_pkey_derive($client鍵のパーツPem, $server秘密鍵Pem);

Slide 122

Slide 122 text

実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] Clientのリクエストを 暗号化・復号化する共通鍵の元

Slide 123

Slide 123 text

実際に共通鍵を導出する

Slide 124

Slide 124 text

実際に共通鍵を導出する 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 125

Slide 125 text

実際に共通鍵を導出する

Slide 126

Slide 126 text

実際に共通鍵を導出する

Slide 127

Slide 127 text

実際に共通鍵を導出する 一部抜粋:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446] Serverのリクエストを 暗号化・復号化する共通鍵の元

Slide 128

Slide 128 text

実際に共通鍵を導出する

Slide 129

Slide 129 text

実際に共通鍵を導出する 引用:RFC8446 [https://datatracker.ietf.org/doc/html/rfc8446]

Slide 130

Slide 130 text

実際に共通鍵を導出する

Slide 131

Slide 131 text

実際に共通鍵を導出する

Slide 132

Slide 132 text

所感

Slide 133

Slide 133 text

● ブラウザに画面が表示された時は感動🥹 ● バイナリに意味を見出すのが、白黒のものに色が付いていくみたいで気 持ち良い ● ブラウザがURL叩いて表示される短い時間に色んなことしてて、それを知 れてめちゃ楽しかった⭐ ● 全てRFCに記載されている 所感

Slide 134

Slide 134 text

● OpenSSLの部分はまだ自作できてない ○ OpenSSLなしで実装していきたい ○ hmac_hashの部分も自作したい ● Server側作ったら、Client側も作りたくなる ○ @cakephper さんはPHP Conference Japan 2024でTLS Client側 の登壇されてた 今後

Slide 135

Slide 135 text

まとめ

Slide 136

Slide 136 text

まとめ 「TLSおもろいじゃん!」 「意外と難しくないじゃん」 「うごくものは作れそうやな、俺/私もやるか」 の気持ちになれましたでしょうか? もっと迂闊にいろんなものを実装するきっかけになれば嬉しいです

Slide 137

Slide 137 text

(時間あれば)DEMO

Slide 138

Slide 138 text

自己紹介

Slide 139

Slide 139 text

ひがき 株式会社リンケージ 🍊PHPConference愛媛2026 実行委員長🐘 PHPConference愛媛2026開催!! ● 2026年10月3日 ● 愛媛大学 城北キャンパス 自己紹介

Slide 140

Slide 140 text

ご静聴ありがとうございました

Slide 141

Slide 141 text

Appendix

Slide 142

Slide 142 text

stringの扱い方

Slide 143

Slide 143 text

● PHPはバイナリもstring型として扱われる。 ● (IMO)TLS自作は16進数の文字列に変換して見ると便利。 →「16進数の文字列」のクラスを作成 stringの扱い方

Slide 144

Slide 144 text

stringの扱い方 16 03 01 06 b2 01 00 06 リクエストで 受け取るデータ

Slide 145

Slide 145 text

stringの扱い方 16 03 01 06 b2 01 00 06 "�" var_dump() リクエストで 受け取るデータ

Slide 146

Slide 146 text

stringの扱い方 16 03 01 06 b2 01 00 06 "�" var_dump() リクエストで 受け取るデータ 😭

Slide 147

Slide 147 text

stringの扱い方 0b00010110 0b00000011 0b00000001 0b00000110 0b10110010 0b00000001 0b00000000 0b00000110 "�" var_dump() リクエストで 受け取るデータ

Slide 148

Slide 148 text

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()

Slide 149

Slide 149 text

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()

Slide 150

Slide 150 text

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() ☺

Slide 151

Slide 151 text

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型 😳

Slide 152

Slide 152 text

● 型の縛りがないので、bin2hexの前後がわからなくなる ● 今どの形式の文字列なのかわからなくなる 全部string型だと辛いポイント 無限 bin2hex() ができちゃう string string 🤔

Slide 153

Slide 153 text

クラス分け 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

Slide 154

Slide 154 text

クラス分け HexString string (Request) string (Response) 今回の自作TLSではこのクラスを ベースとする

Slide 155

Slide 155 text

$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);

Slide 156

Slide 156 text

... 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);

Slide 157

Slide 157 text

... $chunk = ''; :/ $chunk にリクエストが格納される $bytes = socket_recv($sock, $chunk, 8192, 0); :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex = HexString::from($chunk);

Slide 158

Slide 158 text

... :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex = HexString::from($chunk);

Slide 159

Slide 159 text

... :/ $chunk を HexString に変換、$chunkHex を Parseしていく $chunkHex = HexString::from($chunk); :/ リクエストをParse $tlsRecord = TlsRecord::from($chunkHex);

Slide 160

Slide 160 text

● 16進数変換の部分はBinaryStringクラスを作成でもよかったかも ○ Debugしやすさから16進数にしてた ○ __debugInfo とか __toString 実装で事足りていたかも 時間ないから省略したけど、Appendixの振り返り