PythonのTLS/SSL通信をWiresharkでデコードする

PythonではTLS/SSLを用いたセキュア通信を比較的簡単に記述出来る。
今回は、TLS/SSL通信を行う簡単なクライアント/サーバをPythonで動かし、その通信をWiresharkでデコード出来るようにする。

スポンサーリンク

秘密鍵と証明書の準備

opensslコマンドを使って秘密鍵と証明書を準備する。ここで用意する証明書は自己署名証明書(俗に言う「オレオレ証明書」)と呼ばる。

UbuntuなどOpenSSLがインストールされた環境で以下のコマンドを実行。色々と聞かれるが、正確な情報を入れても無意味(第三者認証局に証明書への署名依頼するわけじゃないし)なのでただEnterを押すだけでOK(デフォルト値で設定される)。

$ openssl req -x509 -newkey rsa:2048 -days 1825 -nodes -out cert.pem -keyout key.pem

opensslの最初の引数reqを指定する場合、通常は秘密鍵とともに認証局に署名を依頼する際に使われる証明書署名要求が生成されるが、-x509オプションを付けることによって証明書署名要求のかわりに自己署名証明書が生成される。-days 1825は有効期間1825日(5年)の指定。その他の引数の意味はOpenSSLの公式ページを参照。

コマンド一発で生成されたファイルはkey.pemが秘密鍵、cert.pemが証明書で、これらはTLS/SSL通信で実際に利用可能だ。とりあえず実験用としては問題なく使えるが「自己署名」の意味を理解せずこれらをサーバ等に使うとマズい。使い方によってはセキュリティ上問題が発生するので注意(こちらの記事などが参考になる)。

Wiresharkでデコード可能な暗号スイートの選択

以前の記事に書いた通りWiresharkでSSL通信の中身をデコードして見る事が出来るのだが、暗号スイートとして適切な物を指定しないと秘密鍵を使ったデコードが出来ない。

システムが対応している暗号スイートは以下のコマンドで確認出来る。

$ openssl ciphers -v 

Wiresharkでデコードするためにはこの中で、Kx=RSA(鍵交換がRSA)かつAu=RSA(認証方法がRSA)の固定鍵を使う設定にする必要がある。
それ以外の場合は仮に秘密鍵にRSA鍵を使っていたとしてもDH鍵共有と呼ばれる方法で鍵交換がされるためセッション鍵をログに記録する必要が出てくる(以前の記事でchromeにログ出力させたのがセッション鍵)。自分が調べた限り、Pythonでセッション鍵を簡単にログに記録する手段は無いようだ。

さて、暗号スイートの絞り込み方だが、Pythonでも有効な書式がSSLの公式ドキュメントに記載されている。いまいちわかりづらいのだが、どうやら’kRSA’を指定すれば良いようだ。以下のコマンドで絞り込まれた暗号スイートの一覧が確認出来る。

$ openssl ciphers -v kRSA

ところが、TLS1.3では上記コマンドで表示される一覧の一番上(つまり優先度が最も高い)暗号スイートがTLS_AES_256_GCM_SHA384はKx=any,Au=anyとなって具合が悪い。なんでだろう?と思って調べたところ、そもそもTLS1.3では固定鍵を用いる暗号スイートがプロトコルから削除された模様(RFC8466)。自分の環境ではPythonもTLS1.3対応がされており、実際デフォルト設定だと’kRSA’指定で暗号スイートの絞り込みをしてもDH鍵共有がされる。このため、固定鍵で通信させるためにはPython側でTLS1.2を指定する必要がある。具体的な方法は後述。

PythonでTLS/SSL通信のクライアント/サーバを作る

前回記事でTCP通信を試したコードをベースに、SSL対応化を行った。といっても、SSLのためのコードを少し追加するだけで済んでしまった。

サーバ側のソースは以下。プログラムの先頭で、上記で作成した証明書と秘密鍵のファイルをload_cert_chainメソッドで指定するなど、SSLコンテクストを準備している。
そして、MyTCPHandlerクラスのhandleメソッドの中でクライアントに接続したTCPソケットをSSLソケットにラップしている。

sslserver.py

import socketserver,ssl

# SSLコンテクストの準備
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile="cert.pem", keyfile="key.pem")

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    サーバのリクエストハンドラクラス

    サーバへの接続があるたびにインスタンスが生成される。
    クライアントとの通信のためにhandle()メソッドがオーバーライドされなくてはならない。
    """
    def handle(self):
        # クライアントに接続したTCPソケットself.requestをSSLソケットにラップ
        srequest = context.wrap_socket(self.request, server_side=True)

        self.data = srequest.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # 単に大文字にして返すだけ
        srequest.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "127.0.0.1", 12345

    # localhostのポート12345にバインドしてサーバ開始
    with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
        # Ctrl-Cで止めるまでサーバとして動作
        server.serve_forever()

クライアント側のソースは以下。

sslclient.py

import socket, ssl

HOST, PORT = "127.0.0.1", 12345

data = "Hello Python TLS/SSL Server!"

# SSLコンテクストの準備
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.options |= ssl.OP_NO_TLSv1_3        # TLS1.3を禁止する
context.check_hostname = False              # ホスト名のチェックをしない
context.load_verify_locations('cert.pem')    # 証明書を指定
context.set_ciphers('kRSA')                 # 暗号スイートの指定

# ソケットを作成しSSLソケットにラップ
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssock = context.wrap_socket(sock)

# サーバへ接続してデータを送る
ssock.connect((HOST,PORT))
ssock.sendall(bytes(data + "\n", "utf-8"))

# サーバからのデータを受信して終了
received = str(ssock.recv(1024), "utf-8")
ssock.close()

print("Sent:     " + data)
print("Received: " + received)

ソース中のコメントにあるように、クライアント側ではSSLコンテクストcontextに対して以下のような設定を行っている。

  1. TLS1.3を禁止
  2. ホスト名のチェックをしない
  3. 証明書に上記で作成したcert.pemを指定
  4. 暗号スイートにkRSAを指定

この中で、1.と4.はWiresharkでSSLデコードしなければ不要。
2.はサーバにホスト名ではなくIPアドレスをじか指定している関係でこうしているが、本番環境でこれをやるとリスク要因になるので注意。

クライアント側の実行結果は以下。期待通り、サーバ側から送ったデータが大文字になって返ってきている。

$ python3 sslclient.py
Sent:     Hello Python TLS/SSL Server!
Received: HELLO PYTHON TLS/SSL SERVER!

Wiresharkで通信をデコードする

何はともあれtcpdumpでパケットキャプチャ。localhostの通信なのでinterfaceにloを指定している。

$ sudo tcpdump -i lo -s 0 -w pyssl.pcap

WiresharkのTLSプロトコル設定で”RSA key list”に上記key.pemを設定したうえで、キャプチャデータをWiresharkで開いた様子。サーバからの応答パケット(Frame 11)を選択すると、一番下のペインに”Decrypted TLS”と書かれたタブが現れて、クリックすると通信内容がデコード出来ているのがわかる。

コメント