3.10以降のPythonで特定のサイトにTLS接続できなかったので利用する暗号化スイートを明示的に指定してみた
CX事業本部@大阪の岩田です。
Python3.12からrequestsを使って特定のサイトにアクセスした際、ssl.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] ssl/tls alert handshake failure (_ssl.c:1000)
というエラーが発生してアクセスに失敗しました。最初はエラーメッセージから相手先のサーバーがSSL3しかサポートしていないのかと勘違いしたのですが、Python3.10以後からデフォルトでサポートする暗号化スイートが変更されたことが原因のようです。明示的に暗号化スイートを指定して対策してみたので手順をご紹介します。
環境
今回利用した環境です
- Python: 3.12.3
- urllib3: 2.2.1
- requests: 2.31.0
- OpenSSL: 3.3.0 9 Apr 2024
urllib3のバージョン1系と2系で挙動が異なるのでご注意ください。
やってみる
普通にrequests.getしてみる
まずは普通にrequests.get('https://対象サイト')
を試みます。すると以下のようなエラーが発生しました。
>>> requests.get('https://') Traceback (most recent call last): File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/site-packages/urllib3/connectionpool.py", line 467, in _make_request self._validate_conn(conn) File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/site-packages/urllib3/connectionpool.py", line 1099, in _validate_conn conn.connect() File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/site-packages/urllib3/connection.py", line 653, in connect sock_and_verified = _ssl_wrap_socket_and_match_hostname( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/site-packages/urllib3/connection.py", line 806, in _ssl_wrap_socket_and_match_hostname ssl_sock = ssl_wrap_socket( ^^^^^^^^^^^^^^^^ File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/site-packages/urllib3/util/ssl_.py", line 465, in ssl_wrap_socket ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/site-packages/urllib3/util/ssl_.py", line 509, in _ssl_wrap_socket_impl return ssl_context.wrap_socket(sock, server_hostname=server_hostname) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/ssl.py", line 455, in wrap_socket return self.sslsocket_class._create( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/ssl.py", line 1042, in _create self.do_handshake() File "/Users/iwata.tomoya/.anyenv/envs/pyenv/versions/3.12.3/lib/python3.12/ssl.py", line 1320, in do_handshake self._sslobj.do_handshake() ssl.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] ssl/tls alert handshake failure (_ssl.c:1000)
ネゴシエーション処理の詳細を確認
SSLV3_ALERT_HANDSHAKE_FAILURE
というエラーから「もしやサーバー側がSSL3しかサポートしていない?」と疑ってopensslのs_clientコマンドでハンドシェイクの詳細を確認してみました。
openssl s_client -connect 対象のサーバー:443 -trace
出力は以下のようになりました。
Received TLS Record Header: Version = TLS 1.2 (0x303) Content Type = Handshake (22) Length = 57 ServerHello, Length=53 server_version=0x303 (TLS 1.2) Random: ...略 --- New, TLSv1.2, Cipher is AES128-GCM-SHA256 Server public key is 2048 bit Secure Renegotiation IS supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : AES128-GCM-SHA256
opensslのs_clientコマンドだと接続に成功し、かつTLS1.2で接続していることが分かります。ということはサーバー側がサポートしているTLSバージョンの問題ではなさそうです。さらに原因を切り分けるためWiresharkでパケットをキャプチャしながら再度requests.get
を実行してみたところ、Client Helloの後のサーバーからのレスポンスでAlert (Level: Fatal, Description: Handshake Failure)
のエラーが発生していることが分かりました。
Client Helloの際に提示している暗号化スイートは以下の通りでした。
Cipher Suites (18 suites) Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303) Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9) Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027) Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f) Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e) Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 (0x006b) Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (0x0067) Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)
先ほどopensslコマンドで接続する際に利用していたAES128-GCM-SHA256
は含まれていないことが分かります。その後サーバー側が対応している暗号化スイートを詳細に確認していくと対応しているのは以下の6種ということが分かりました。
- AES128-GCM-SHA256
- AES128-SHA
- AES128-SHA256
- AES256-GCM-SHA384
- AES256-SHA
- AES256-SHA256
非デフォルトの暗号化スイートを指定してrequests.getを実行してみる
ここまででエラーの原因が切り分けられました。requestsから対象サーバーにアクセスする際の暗号化スイートとして対象サーバーがサポートしている暗号化スイートを指定できれば良さそうです。調べたところ以下のissueで暗号化スイートの指定方法が紹介されていたので、このissueを参考にしつつ実装してみました。
How can I change the allowed SSL ciphers for a requests session?
import requests from urllib3 import ssl ssl_context = ssl.create_default_context() ssl_context.set_ciphers("DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK") session = requests.session() adapter = requests.adapters.HTTPAdapter() adapter.init_poolmanager(1, 1, ssl_context=ssl_context) session.adapters.pop("https://", None) session.mount("https://", adapter) res = session.get('https://対象サイトのURL') print(res)
指定している暗号化スイートですが、今回はPython3.9以前の挙動と合わせるためにDEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK
を指定しています。
CPythonのコミット履歴を確認したところPython3.9からPython3.10のバージョンアップ時に暗号化スイートがDEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK
から@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM
に更新されているようです。
このスクリプトを実行すると無事に200OKで対象サイトにアクセスできました
python test.py <Response [200]>
パケットキャプチャの結果を確認するとClient Helloで提示している暗号化スイートは以下のようになっていました
Cipher Suites (31 suites) Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303) Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f) Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9) Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8) Cipher Suite: TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xccaa) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028) Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 (0x006b) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027) Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (0x0067) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014) Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039) Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013) Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033) Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA256 (0x003d) Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA256 (0x003c) Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)
Server Helloのレスポンスは以下の通りでした。ネゴシエーションの結果、暗号化スイートはTLS_RSA_WITH_AES_128_GCM_SHA256
が採用されたようです。opnesslのs_clientコマンドで接続確認した際と同じですね
Handshake Protocol: Server Hello Handshake Type: Server Hello (2) Length: 53 Version: TLS 1.2 (0x0303) Random: b928b590b5dd0acc8781d1be156fac4d8188f407ce64ea423b2ecb5230dfb6ea Session ID Length: 0 Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) ...略
まとめ
requestsを利用する際に暗号化スイートを指定する方法をご紹介しました。今回はrequestsを利用する場合のコードを紹介していますが、urllib3を直接利用する場合やurllibを利用する場合もssl.SSLContext
の設定方法などは流用可能です。類似の事象でお悩みの方がいれば参考にしてみてください。