Code Day's Night

ichikawayのブログ

迂闊にTLS/SSLをPHPで実装してみたら最高だった件

この記事はTLS/SSLを実装してみたいという人が増えるといいな!という気持ちで書いています。実装の詳細は別記事で書こうかと思います。

 

数年前からいつかTLS/SSLのプロトコルをPHPで実装したいと思い、まずは本で知識を得ようかとラムダノートの「プロフェッショナルSSL/TLS」「徹底解剖TLS1.3」を買って読んでみましたが、なかなか頭に入らずに読んでは寝てしまうというパターンに。

 

やはり自分でTLSを実装してみないとなと思ってたところに、PHPカンファレンス福岡2024で hanhan1978 さんの「PHPでデータベースを作ってみた」を見て大いに刺激をもらい、ついにTLS実装に着手できました。

 

speakerdeck.com

この資料は本当によくて名言の宝庫です。たとえば、

「まじめに作ろうとすると大変な努力が必要になる。もっと迂闊につくりたい」

「不格好でもいいので、動く完成品を作ることは学びにとって実に良いことです」

「みんなも変なモノを作っていこうな!」

などなど。

あと資料の中はMySQLの通信をTCPダンプしてプロトコル解析しながら進めて、DBとしての振る舞いを作っていく過程が細かく書かれているので読んでも面白いと思います。

 

実装する前のあやふやな知識

TLS1.2のクライアント側をPHPで実装するか、と着手した時の私のTLSの知識は

  • 証明書を取得して接続先が正しいか確認
  • 公開鍵暗号で共通鍵を交換
  • 共通鍵を使ってデータを暗号化して通信
  • 暗号スイートはApache/Nginxの設定でなんとなく知ってる程度

ぐらいのぼんやりしたものでした。これぐらいぼんやりした知識しかなかったので、上記の書籍を読んでもなんとなくしか分からなかった、読み進めるのが辛かったのかもしれません。

 

実装したもの

github.com

はい、 俺のTLS(ore-no-tls-php)です。

今回は暗号処理(AESとRSA)にPHPのOpenSSL関数を利用しました。ハッシュはhash_hmac関数を利用。それ以外は素の?PHPで実装しています。

本当はOpenSSLに頼らずに暗号処理も自前で実装してみたかったのですが、それは将来のお楽しみということで。TLSのプロトコルの実装という意味では暗号処理だけOpenSSLに頼っても十分楽しめると思います。


テストケースも書いたので、今後同じように実装してみたくなった方はこのテストのテストデータを使うと楽に進められるのでぜひ!

 

今回は、

  • 暗号スイート「RSA_WITH_AES_128_GCM_SHA256」のみ対応
  • 証明書の認証局チェックはしていない

という不完全なものですが、Client Helloを送って暗号化した通信ができるまで体験できたので楽しかったですし、実装してみると自分の知識の無さを痛感しつつも、動かない箇所に出会うとRFCや色々なドキュメントや実装を調べる過程で多くの知識を得ました。

やはり実装しながら学ぶのは最高に楽しくていいですね。

退屈なRFCだって、実装で問題にぶつかったて読むと、ここに答え書いてある!って言いながらスラスラ読めました。

 

7月3日から始めて、平日1日30分程度(調査入れるともう少し時間は使ってる)で進めながら、7月27日にHTTPレスポンスを復号して表示までできました。趣味としては1ヶ月ぐらい楽しめるちょうど良い課題設定かなと思います。

 

進め方

TLS1.2についてはsatokenさんのこの記事がすごく良くまとまっていて、理解できなくてもまずは一つずつ進めていく形をとりました。すぐに理解できなくても実装したり調べたりしながら進めると最後には理解できるようになります。

私も最初は全然理解できないまま実装し、気付くと言っている意味や用語がわかるようになりました。

 

zenn.dev

 

大きくは次の4ステップ

ステップ1

まずはClient Helloを送ってServer Helloを取得するところだけ実装。ここが一番簡単で楽に結果が出るのでテンションが上がります。

ステップ2

Server Hello、Server Certificate、Server Hello Doneがまとめてサーバから送信されるので、それをパースして処理。Certificateのレコードの中から証明書データを取り出して、そこからRSA公開鍵を抽出。ここまでは、少し難易度が上がるだけなのでまだいけるかなという印象。

ステップ3

次に共通鍵の元となるプレマスターシクレットを生成して、Client Key Exchange、Chage Cipher Spec、Client Finishedの3つのメッセージを一気に作ってサーバに送信。サーバからAlertレコードが返ってこなければ成功。Alertが返ってきたら少ないエラーメッセージを元に試行錯誤することになります。

私はここでエラーが返ってきて10日ぐらい費やしましたw
もう嫌だと思いながらも、ここでどハマりしたおかげでRFCを読み込んだり、暗号方式について調べたり、色々な寄り道ができて後から考えると一番エキサイティングで成長できたポイントでした。

特にFinishedメッセージが一番難易度が高く、何が正解か、何が原因でエラーかがわからないまま進めることになります。(今回テストデータを作ったので、今後やりたいと思う人はこのテストデータを使ってテストをパスしてくとどの段階でダメだったかわかるようになると思います)

ステップ4

共通鍵が交換できた後は、Application DataとしてたとえばHTTPリクエストの文字列

"GET / HTTP/1.1\r\n\r\n";

を共通鍵で暗号化してサーバに送信します。

サーバからのレスポンスを復号してhtmlを表示します。

 

おもしろポイント

TLSも他のプロトコルも、仕様がRFCできっちり決まっていて、今回だと通信相手のサーバ(Apache)が正しい挙動をするという前提で進められました。
つまり、こちらが正しいクライアント実装をすればサーバから正解!と言ってもらえる状況です。正解と言ってもらうまで試行錯誤しながら色々と調べてトライしている時は、まるでパズルを解いているような感覚でした。

 

TLSは長い歴史の長でバージョンアップしてきており、色々なことが考慮された仕組みになっています。過去にDNSプロトコルを実装した時も思いましたが、長い歴史を支える仕組みに一度触れると面白いですね。ただ、実装中にエラーで抜け出せなかった時は、なんでこんなに面倒なことをしなければいけないのか?とは思いましたw

 

DNSパケットをいじっていた時は、ビット単位で色々なことが決まっていてビット操作をたくさんしていましたが、TLSはバイト単位で区切られて意味付けされているので、今回はバイト単位の操作だけですみました。
PHPだと hex2bin関数とbin2hex関数を多用して、string型の変数に16進数でデータを入れてそれをバイナリにして送信みたいにしていました。

 

最後に

TLS/SSLは別レイヤーとして隠蔽されたまま簡単に使えるため、別に自分で実装したり勉強する必要もないかもしれません。仕事の役にも立たないかもしれません。

それでも何かしら得るものはありますし、楽しいとか、ただやってみたかっただけでもいいじゃないですか。みなさんも迂闊にTLSを実装していきましょう!

 

俺のTLSを作り始めてから完成まで、Xで色々とつぶやいていたまとめスレ

https://x.com/cakephper/status/1808497060880306265

 

最初に読んで難解だった書籍、最終的にはめっちゃお世話になり読めるようになりました。日本語で色々と読めるのはありがたい。