月曜日, 6月 9, 2025
- Advertisment -
ホームニューステックニュースVibe CodingでTLS 1.3を実装してみたら、思ったより人間が必要だった話

Vibe CodingでTLS 1.3を実装してみたら、思ったより人間が必要だった話



人間はコードを書かずに自然言語でLLMアプリに指示を与えることでソフトウェアを開発する、いわゆる「Vibe Coding」が普通になりました。プログラミング言語よりも自然言語のほうが得意っていう自分みたいな人間にとっては、いろいろ夢が広がります。

そんな夢の1つとして「プロトコルの実装」がありました。ネットワークの解説書を作る仕事をしているので、さまざまなプロトコルでやり取りされるメッセージの仕様に触れる機会はわりとあるほうです。それなのにプロトコルを実装したことがないのは実に残念な話だなと思っていました。

プロトコルの実装に必要なスキルは、プロトコルそのものの理解よりも、コンピューターの低レイヤの仕組みであるような気がします。仕組みを何となく知っていたとしても、そのためのコードを書くとなるとまたさらに別の話です。そういうわけで、プロトコルについては、「まったくの無知ではないけれど実装したことはない」という頭でっかちな状態に甘んじてきました。

そこでVibe Codingです。「自分でコードを書かないなら実装したとは言えないから、やはり頭でっかちなのでは?」と自問自答しつつも、「まあでも試しにやってみよう」ということで、やってみることにしました。題材はTLSプロトコルです。なぜなら、HTTPより実装したことがある人は少なそうだし、そこそこ知っているから。

方針

クライアントよりもサーバのほうが実装しやすそうなので(単なるイメージ)、「TLS 1.3サーバでクライアントからのハンドシェイクを完遂する」を目指すことにしました。利用するLLMアプリは、本格的にやるならDevinとかClineとかClaude Codeとかなんでしょうが、慣れているのでCursorのAgent Modeです。

Cursorに対する最初の指示もLLMアプリで書きます。これにはChatGPT 4oを使いました。理由はやはり「慣れているから」です。o3でもよかったんですが、「RFC 8446に基づいてTLS 1.3サーバの要件を絞り、実装のためにCursorに与える指示を生成する」みたいなタスクなら4oでいい気がします。

そのときのプロンプトはこれ(それぞれの後に4oの応答が挟まる)。

TLS1.3を試しに実装してみたいと考えています。言語はRustかHaskellで。

プロトコルの実装を試してみたい、というのが主な動機です。おそらくはサーバーを作るほうが手間がかからない気がしています。最低限のハンドシェイクと終了処理ができるようにしたいです。できるだけライブラリは使わずに実装することを考えています。

実装はCursorで行います。Cursorのエージェントに対する指示をTODOリストとし、それを読ませることで実装を進めていきます。実装とテスト(OpenSSLのクライアントから接続を試してみるので十分)をしながら目標を達成するというプロンプトを書いてください

生成されたCursorへの指示はこれでした。

生成されたCursorへの指示内容

Implement a minimal TLS 1.3 server that completes the full handshake and shutdown process, without delivering application data.
The server will be tested using openssl s_client with TLS 1.3.
External dependencies must be minimized—cryptographic primitives (e.g., HKDF, AES-GCM, X25519) can be borrowed from existing crates if necessary, but protocol logic must be implemented manually.


1. TCP Server

2. TLS Record Layer

3. ClientHello Parsing

4. Key Exchange & Key Schedule

5. ServerHello Construction

6. EncryptedExtensions

7. Finished Messages

8. Shutdown

9. Testing

  • Do not use full TLS libraries (e.g., rustls).
  • Use only low-level cryptographic crates (e.g., ring) where necessary.
  • Prefer manual parsing and message construction.

最初の実装

CursorのAgent Modeは、「ClientHelloを解析してServerHelloを返す」ところまではすんなり実装してくれました。

TLS 1.3では、ハンドシェイクの段階でも、ServerHello以降に送信するメッセージはすべて暗号化が必要です。この暗号化では、ClientHelloとServerHelloに基づいてサーバとクライアントがそれぞれ共有鍵を生成し、それを使います。この共有鍵を導出する処理は「鍵スケジュール」と呼ばれます。

CursorのAgent Modeは、この鍵スケジュールの処理を最後まで自力では書けませんでした。そもそもRustのringクレート(BoringSSLのコードに基づくRustの暗号ライブラリ)を正しく使えるようになるまでに、けっこう試行錯誤してました。それでもまあ、サーバからハンドシェイクで送る最後のメッセージのFinishedを送り付けるところまでは、なんとかCursorのAgent Modeだけでたどり着きました。

もちろん鍵スケジュールが正しく実装されていないので、ServerHelloよりも後に送ったメッセージはすべてクライアントに受け付けてもらえません(クライアントはAlertというメッセージを返して終了する)。

https://x.com/golden_lucky/status/1927345378891854107?conversation=none

LLMアプリをClaudeCodeに移行して再挑戦

これ以上はCursor一本ではつらそうだったので、ここでClaudeCodeを試してみることにしました。年契約しているCursorと違い、ClaudeCodeを使うには課金が必要です。ひとまずUSD 5だけお小遣いを投入して作業を始めてもらいました。

プロンプトはこれ。

This is an implementation of TLS 1.3 server. It only do handshake without
any app data with openssl s_client. Server certificate is in the top
directory. tls13_server_todo.md is the TODO list. Now I keep getting an
error on saying “Failed to initialize key schedule: Failed to fill client
handshake key: ring::error::Unspecified”. Try fixing it and complete the
implementation.

はじめてClaudeCodeを使ったので、おっかなびっくりCLIモードでプロンプトを投入したのですが、かなりよい体験でした。どのモデルが使われたのかすら関知しないまま、こちらは特に何もしなくてもring関連のエラーがすべて潰されて、OpenSSLのクライアントアプリからエラーが返ることはないという状態になりました。

https://x.com/golden_lucky/status/1928439860072730941?conversation=none

まやかしだった

一見するとOpenSSLのクライアントアプリとの接続に成功したかに見えたClaudeCodeによる実装ですが、このときClaudeCodeが自分で検証に使っていたのはOpenSSL 1.1.1wでした(マシンのDebianがbullseyeなので)。OpenSSLの最新バージョンは3.5ですが、このバージョンのクライアントアプリからも接続を試してみると、この時点での実装にはまだバグがあることがわかりました。

具体的には、ServerHelloの直後に暗号化して送信すべきEncryptedExtensionsというメッセージを送らず、その時点でTCPを切断しているという状態でした。OpenSSL 1系では、このようなサーバーの挙動も許容される(SSL_OP_IGNORE_UNEXPECTED_EOF)のですが、OpenSSL 3系ではこれを明確に禁止するようになっていて、クライアントではAlertメッセージを返します。つまりTLSのハンドシェイクが完遂しません。

ChatGPT o3+Cursorで再挑戦

この時点で選択肢はいくつかありました。

  • さらにClaudeCodeに課金してVibe Codingを続ける
  • 自分でコードを調べて修正する
  • コードをLLMに調べさせて、Cursorに修正指示を出す

1は予算の関係でパス。2は、ClaudeCodeが生成したコードを自分で見始める気力はなかったので、やはりパス。残る選択肢は3です。いわば「半Vibe Coding」ですね。

既存のコードベースについて相談するならChatGPT o3だろうということで、ソース全体と実行時に出力していたデバッグ情報などを与えて、下記のように指示しました。

下記に、サーバのデバッグ出力、keylog.txt、クライアントのログを提示します。これらを見て問題を特定し、添付したソースの間違いを見つけなさい。ログでエラーになっている箇所や理由をいちいち説明する必要はありません。修正にあたっては下記を絶対に守ること。

  • ぜったい添付のソースのみを参照すること
  • 典型的な間違いによる憶測をしないこと
  • どこか一か所を直せば解消するという予断を持たずに、徹底的にログに現れている問題を引き起こしているソースの誤りを探すこと。pinpointの修正では解消しませんし、それを目指してはいけません
  • 修正箇所の指摘にはソース自体を使い、疑似コードは使わないこと。具体的な修正方法も提示すること。

(以下にサーバーとクライアントのデバッグ情報などが続く)

これに対するo3の応答から判明したのは、鍵スケジュールの実装に引き続きバグがあるという事実でした。それ自体は最初の実装の時点で判明していたことなので意外ではないのですが、先ほどのClaudeCodeでは問題が何も解決していなかったということなので、ちょっと拍子抜けです。

o3はわからずやだし、Cursorはあわてんぼう

o3が鍵スケジュールの実装について指摘してきたバグも、その指摘をCursorに伝えて直させるだけでは解決しませんでした。

o3は、最初のうちはわりと素直に「ああ、確かにおかしいね」というバグを指摘してくれるのですが、鍵スケジュールのような複雑な処理では、「何か1つを解決すれば万事OK」とはなりにくいものです。そのため「修正後に改めて実行結果を見せる」を何回か繰り返すわけですが、だんだん「もともと何をしていたのか」を忘れてきます(o3が)。そのうち「その指摘は、すでに無意味だと判明している修正と本質的に同じでしょ」みたいなやり取りが増えていき、何も解決しない。

またo3はすぐにコードの現物を無視しようとします。修正後のコードを見せているのに過去に提供したコードだけを見て回答したり、場合よっては「よくある実装ミス」みたいな回答をでっちあげてきます。

これらを防止すべく、プロンプトにも先のような注意書きを含めるようにしているのですが、やり取りが長くなるとこれも忘れられがちです。

CursorもCursorで、o3が指摘したバグを認識しつつも、まったく関係ない箇所まで同時に直して別のバグを混入させたり、自分がコード中に書いたコメントに惑わされて以前の修正を勝手にリバートしようとしたり、わりとよく手が付けられなくなります。指示の際に「まず落ち着いて過去の修正を振り返ってから作業をしてください」などと言っておかないと暴走しがち。

以下、この方法によるデバッグで苦労したところ。

Server Handshake Traffic Secretが一致しない

TLS 1.3の処理でもっとも基本になるのは、暗号に使う共通の鍵をサーバーとクライアントが独立に生成することです。サーバ側から送信するメッセージの暗号に使う鍵は、Server Handshake Traffic Secretという値から計算します。このServer Handshake Traffic Secretなどを求める一連の処理が鍵スケジュールで、これをCursorでもClaudeCodeでもバグなしに実装できませんでした。

この鍵スケジュールのデバッグでは、o3に相談しつつ、『プロフェッショナルTLS&PKI』の2.5.2項を何度も読み返すことになりました。最終的に、ここまでの実装の何がバグっていたかというと、以下の2点です。

  • クライアントから提示されてサーバーで選択したハッシュ関数を使わず、最初にCursorが実装でハードコードしたSHA-256が固定的に使われていた
  • ring::hkdfに渡すトランスクリプトハッシュの計算が雑だった

1つめの問題は、クライアントとのやり取りで使われた実際の値を使って鍵スケジュールの計算ステップを1つずつo3に辿らせているときに判明しました。気づいたのはもちろん人間のほうです。o3は、クライアントのログを与えているにもかかわらず、「計算ステップは合っている」という事実に縛られてまったく関係ない主張(後述するトランスクリプトハッシュが違うという可能性)を繰り返すだけでした。

2つめは、鍵スケジュールの材料として使う「トランスクリプトハッシュ」と呼ばれる値の導出方法です。ClientHelloとServerHelloを連接したものにハッシュ関数を施して求めるのですが、最初の実装でハッシュ関数が二重に施される状態になっていることにLLMアプリが誰も気づいてくれませんでした(仕様でHKDF-Expand-LabelおよびDerive-Secretと呼ばれている関数の実装が単純でないからだと思う)。

トランスクリプトハッシュは、ハンドシェイク中にどんどん変化していって、適切なタイミングで適切なデータを使う必要があります。o3は、鍵スケジュールの導出過程にあからさまなバグがなくなった後はこの点に異常に執着し、いつまでも「トランスクリプトハッシュが正しい値から計算されていることを確認せよ」と繰り返すだけでとてもつらかったです。計算に使っている値は正しいけど、あなたたちの実装でクライアントと合意したハッシュ関数を使ってなかったのが問題だったんだよ!

EncryptedExtensionsには何も入れてはいけない

鍵スケジュールに成功したら、その鍵を使ってサーバーが最初に送るのはEncryptedExtensionsというメッセージです。このメッセージには、クライアントから要求されたTLS拡張のうち、サーバが対応しているものを入れます。拡張がなければ、EncryptedExtensionsを表すサブタイプを指定した空のレコードを返します。今回の実装では拡張には一切対応しないので、この場合に該当します。

しかし、どこかの時点でChatGPTかCursorのチャットで「OpenSSLのクライアントでは、TLS 1.3に対応していることを表す拡張を要求するんです!」という嘘情報があり、それでsupported_versionsという種類の拡張をずっと指定していました。「これ、もしかして不要なのでは?」という仮定に基づいた試行も、人間が指示しないといつまでも気づかなかったままだったような気がします。

あと、拡張を何もしていないときでもEncryptedExtensions自体のメッセージ長は「2」になるという事実も、人間に言われて調べさせられるまでLLMアプリたちは気づかなかったっぽい。

CertificateVerifyがクライアントで受け付けてもらえない

Server Handshake Traffic Secretが正しく導出できて、その鍵で暗号化したEncryptedExtensionsをクライアントで受け取ってもらえるようになったら、次は証明書のやり取りによる認証のフェーズになります。ここでも重要になるのはトランスクリプトハッシュなんですが、CursorとClaudeCodeによる実装はそのためにトランスクリプトハッシュを正しく更新していないという雑なものでした。

これはすぐに人間が気づいて修正させたのですが、その修正がまた雑で、これで少しハマりました(こっちも「修正させた」気になっていてうかつだった)。具体的に言うと、トランスクリプトハッシュの計算対象は暗号化する前の平文のメッセージなんですが、やつらが修正したやり方は暗号化された値を連接してハッシュにかけるという杜撰なものでした。

あと、ここでも最初の実装でハードコードされていたハッシュ関数の問題にハマりました。

クライアントからのFinishedを復号できない

CertificateVerifyの後は、サーバーとクライアントによるFinishedのやり取りです。ここまでにだいぶ人間がコードに馴染んだこともあり、サーバーからのFinishedについては簡単な修正指示で一発で通りました。

問題はクライアントからのFinishedの処理です。サーバーではここではじめて復号処理が必要になります。そのための鍵はServer Handshake Traffic Secretとは別のClient Handshake Traffic Secretから生成し、さらに復号処理そのもの正しく実装しなければなりません。

ここでハマったのは、シーケンス番号の扱いでした。TLS 1.3による暗号化と復号では認証付き暗号(AEAD)を使うわけですが、その処理にはnonceが必要です。このnonceの使いまわしを避けるため、TLS 1.3ではシーケンス番号を生成に利用することになっているのですが、これは送受信のメッセージで別々に管理することになっています。Cursorはこれを知らなかったので、シーケンス番号を送受信両方のメッセージでカウントするという実装になっており、ちょっとハマりました。

シーケンス番号については、レコードプロトコルのメッセージではないChangeCipherSpecで加算すべきかもよくわからず、ChatGPTに聞いたら「加算するよ」というのでそう実装させたのですが、これもまた嘘でした。ChangeCipherSpecはトランスクリプトハッシュで考慮しないというのは知っていたので、これは怪しいと思い、事なきを得ました。

おわりに

最終的な実装はこちら。OpenSSL 3.5のクライアントからTLS 1.3で接続してハンドシェイクを完遂できるようになっています。

https://github.com/k16shikano/my-tls-server

こうしてふりかえってみると、けっこういろいろな箇所でハマってますが、出来上がった実装を眺めると何もかもが自明に見えるので不思議です。プロトコルの実装、おもしろい。とくに暗号プロトコルは「クライアントにおける挙動と合わせる」ために鍵の共有という処理が必要なので、そのデバッグがけっこう面白かったです。

一方、Vibe Codingの体験談としては、けっこうモヤモヤするところもあります。コードを手で書いた箇所は、実際のところほとんどないんですが、RustのringクレートをCursorが最後まで使いこなせず、特にfillの処理でエラーを引き起こすたびに無駄なあがきを始めるので困りました。そのたびに、「それは用意しているバッファの長さが間違ってるから実行時にエラーが起きてるんだよ」と教える必要がありました。型エラーを直すのはわりと得意に見えるんですが、こういう実行エラーの原因を推察して直すのは得意ではないようです。

もっと課金して高いモデルを使い、全部をClaude Codeでやれば、また違った体験になるのかもしれません。上述したような今回のハマりポイントは、高いモデルだと回避できるものなのだろうか。

とはいえ、趣味だとこれくらいがちょうどいいという見方もありそうです。特に自分は、こうして生成AIにコードを書かせたことで、TLS 1.3やOpenSSLの動作に対する理解がだいぶ深まりました。

それにしても『プロフェッショナルTLS&PKI』はよくできた本でした(宣伝)。第2章「TLS 1.3」は本当に焦点がうまく絞られていて、いまのIT技術書に求められるであろう「生成AIに指示するのに必要な知見が得られる」に適った内容であることを再確認できました。TLS 1.3の実装をしてみたいという夢がある人はぜひ読んでみてください!

https://www.lambdanote.com/products/tls-pki-2



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -