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
の設定方法などは流用可能です。類似の事象でお悩みの方がいれば参考にしてみてください。