SSL通信を復号化してみた(Python/Wireshark)

最近browser-useを使う際にパケットダンプをする機会があり、その際にSSL通信を復号化しないと判断できないケースもありました。（OpenAIに通信は成功しているのに、browser-useのAgentでは失敗してるなど）ということで、Pythonにおける復号化手順になります。

# 前提
sslモジュールを利用

# 1. パケット復号化に利用する設定

create_default_contextというメソッドと、keylog_filenameというプロパティを利用します。以下、それぞれの意味となります。

:::note info
ssl.create_default_context
keylog_filename がサポートされており、環境変数 SSLKEYLOGFILE が設定されている場合、create_default_context() はキーの記録を有効にする。
:::

:::note info
SSLContext.keylog_filename
・TLSキーが生成または受信されるたびに、キーをキーログファイルに書き込みむ。
・このキーログファイルはデバッグ専用に設計されている。
・ファイル形式はNSSによって規定されており、Wiresharkなどの多くのトラフィック解析ツールで使用可能
:::

参考URL

https://docs.python.org/ja/3.13/library/ssl.html#ssl.create_default_context

コードにすると以下のような感じになります。

```python
import os
import ssl
import certifi
from langchain_openai import AzureChatOpenAI
from browser_use import Agent
import asyncio

ssl_keylogfile = "tls_keys_20250213-001.log"  
os.environ["SSLKEYLOGFILE"] = ssl_keylogfile    

context = ssl.create_default_context()
context.keylog_filename = ssl_keylogfile

llm=AzureChatOpenAI(
    openai_api_version="2024-xx-xx",
    azure_endpoint="https://xxxxxxxxxxxxx.openai.azure.com/",
    azure_deployment="gpt-4o",
    validate_base_url=False,
    model="gpt-4o",
    api_key="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
)

async def main():
    agent = Agent(
        task="Qiitaのnw-engineerサイトでいいねの数が多いトピックを5件表示してください。",
        llm=llm
    )
    result = await agent.run()
    print(result)

    print(f"TLS セッションキーを {ssl_keylogfile} に保存しました。")

asyncio.run(main())
```

以下関連コードの説明です。

:::note info
関連コード
```bash
ssl_keylogfile = "tls_keys_20250213-001.log"  
os.environ["SSLKEYLOGFILE"] = ssl_keylogfile    
```
SSLKEYLOGFILEという環境変数にtls_keys_20250213-001.logをセットします。これがキーのロギングを有効にするためのトリガーになります。

```bash
context = ssl.create_default_context()
context.keylog_filename = ssl_keylogfile
```
次にssl.create_default_context()を呼び出すことで、TLSのデフォルト設定を持つ SSLContextオブジェクト(context)が作成されます。

最後に、context.keylog_filenameにssl_keylogfile（実際はtls_keys_20250213-001.log)を代入しています。
:::

# 2. Wiresharkの設定
スクリプト実行の前にパケットキャプチャを開始し、スクリプト動作終了と同時にキャプチャを停止します。

このとき、ssl_keylogfileファイルが保存されていますので、これをWiresharkに利用する形となります。@gaichi 様の記事を参考にさせて頂きました。

https://qiita.com/gaichi/items/364879b3c4cb68830652#wireshark%E3%81%A7tls%E3%82%92%E5%BE%A9%E5%8F%B7

# 3. 実行結果

BrowserUseのエラー出力では判断できなった際のダンプになります。ただしく復号化されています。(SourceとDestinationは非表示にしています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335933/1c86c829-fc20-431a-9dde-bb0390c9edc1.png)

※「429 Too Many Requests , JavaScript Object Notation」がでていたので、AzureOpenAIの無料枠では１分間のレートが足かせになっているという事にここで気が付きました。。。。（重量課金＋レートを挙げて無事解消）

# 4. その他
意外と簡単にSSL通信を復号化できるため、今後も利用していきたいと思います。


# 20250216 追記
コメントにて復号化しているのはWiresharkでは？というありがたいご意見をいただきましたのでタイトルを修正させていただきました。

また、いただきましたコメントからLinuxだけでキャプチャしたファイルを復号化する方法は？という疑問が浮かび(こういった気づきを得られた点もコメントに感謝です)、調べてみたところ便利なモジュールがあったので試してみました。まずは、tsharkとpysharkをインストールします。

```bash
apt install tshark -y
pip3 install pyshark
```

ここで前回のコードをそのまま利用したいところなのですが、私の環境では、TLS1.3では復号化できませんでした。おそらくtsharkのバージョンの問題だとおもいますが、SSLKEYLOGFILE に記録されるシークレットがCLIENT_RANDOMでない場合、正しく復号化でないようです。そのため、TLS 1.2を使用して試験しました。以下外部通信用のスクリプトを新たに作成します。

```python
import os
import ssl
import http.client

ssl_keylogfile = "/tmp/tls_keys_20250216-001.log"
os.environ["SSLKEYLOGFILE"] = ssl_keylogfile

context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.options |= ssl.OP_NO_TLSv1_3
context.options |= ssl.OP_NO_TLSv1_1
context.options |= ssl.OP_NO_TLSv1
context.verify_mode = ssl.CERT_REQUIRED
context.keylog_filename = ssl_keylogfile
context.load_default_certs()

conn = http.client.HTTPSConnection("qiita.com", context=context)
conn.request("GET", "/")
res = conn.getresponse()
print(f"HTTP Status: {res.status}")
```

続いて、取得済みのキャプチャーとキーログファイルを読み込ませ解析するためのスクリプトを作成します。
```python:analysis.py
import pyshark

PCAP_FILE = "./*****.pcap"        # キャプチャファイル
SSLKEYLOGFILE = "./*****.log"     # キーログファイル

def analyze_pcap(pcap_file, ssl_key_file):
    capture = pyshark.FileCapture(
        pcap_file,
        display_filter="tls or http",
        override_prefs={'ssl.keylog_file': ssl_key_file}
    )

    for packet in capture:
        try:
            if 'TLS' in packet:
                print(f"[TLS] {packet.number}: {packet.tls}")
            if 'HTTP' in packet:
                print(f"[HTTP] {packet.number}: {packet.http}")
        except AttributeError:
            pass

    capture.close()

analyze_pcap(PCAP_FILE, SSLKEYLOGFILE)
```

pyshark.FileCaptureで利用しているオプションを以下に記載します。

:::note info
**pyshark.FileCaptureの利用オプション**

pcap_file： tcpdumpで取得たdumpファイル
display_filter： 適用するフィルター(ここではTLSもしくはHTTPにぼっている）
override_prefs： ssl.keylog_fileとしてSSLKEYLOGFILEを設定
:::

詳細は以下をご確認ください。

https://github.com/KimiNewt/pyshark/blob/master/src/pyshark/capture/file_capture.py

以下実行時の結果（抜粋）です。
```bash
(browser_use) root@xxxxxxx:~# python3 analysis.py |more

[TLS] 13: Layer TLS
:       TLSv1.2 Record Layer: Application Data Protocol: Hypertext Transfer Protocol
        Content Type: Application Data (23)
        Version: TLS 1.2 (0x0303)
        Length: 86
        Encrypted Application Data: 3a4fe6697346938eb969c0cf8be62d5f5ce465d41911e9e92ae9bc4dbaac07a4fd0fe1982a25a8296866ec84479ba0e496603c6f6cf72647167dd267f797f9d0843974389775cec0ab563869d44f948ad1988d85eabc
        Application Data Protocol: Hypertext Transfer Protocol

[HTTP] 13: Layer HTTP
:       GET / HTTP/1.1\r\n
        Expert Info (Chat/Sequence): GET / HTTP/1.1\r\n
        GET / HTTP/1.1\r\n
        Severity level: Chat
        Group: Sequence
        Request Method: GET
        Request URI: /
        Request Version: HTTP/1.1
        Host: qiita.com\r\n
        Accept-Encoding: identity\r\n
        Full request URI: https://qiita.com/
        HTTP request 1/1
        \r\n
```
正しく復号化できていますね。