🐵
Python の [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] を解決する
Python の requests で ↑ こんなエラーが
困るのだが!!
最近の傾向から SSLv3 なぞ使っているわけがないので
「💡 こやつめ デフォルトの ciphers にないプロトコル使ってんな」
というわけで調査してみます
きっかけ
リクエストに失敗する
PS C:\Users\sharl> python -c 'import requests; requests.get("https://toi.kuronekoyamato.co.jp")'
...
requests.exceptions.SSLError: HTTPSConnectionPool(host='toi.kuronekoyamato.co.jp', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)')))
ちなみに
PS C:\Users\sharl> python -V
Python 3.12.7
PS C:\Users\sharl> python -c 'import ssl; print(ssl.OPENSSL_VERSION)'
OpenSSL 3.0.15 3 Sep 2024
という環境です
調査開始
Python のデフォルト ciphers 設定を見てみます
@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM
これを明示的に設定した SSL socket を作って確認
sample.py
import socket
import ssl
hostname = 'toi.kuronekoyamato.co.jp'
ctx = ssl.create_default_context()
ctx.set_ciphers('@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM')
with socket.create_connection((hostname, 443)) as sock:
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
print(ssock.version())
server_hostname
は SNI というやつです
PS C:\Users\sharl> python .\sample.py
Traceback (most recent call last):
File "C:\Users\sharl\sample.py", line 10, in <module>
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\ssl.py", line 455, in wrap_socket
return self.sslsocket_class._create(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\ssl.py", line 1041, in _create
self.do_handshake()
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\ssl.py", line 1319, in do_handshake
self._sslobj.do_handshake()
ssl.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)
同じようにハンドシェイクで失敗してる
ということでサーバとのハンドシェイクが成功してるのはどの cipher なのかを見てみる
$ openssl s_client -connect toi.kuronekoyamato.co.jp:443 -servername toi.kuronekoyamato.co.jp < /dev/null 2> /dev/null | grep Cipher
New, TLSv1.2, Cipher is AES128-GCM-SHA256
Cipher : AES128-GCM-SHA256
AES128-GCM-SHA256
ですか
さきほどの sample.py
をちょっといじって
sample.py
import socket
import ssl
hostname = 'toi.kuronekoyamato.co.jp'
ctx = ssl.create_default_context()
# ctx.set_ciphers('@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM')
# add AESGCM, !PSK
ctx.set_ciphers('@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:AESGCM:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM:!PSK')
with socket.create_connection((hostname, 443)) as sock:
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
print(ssock.version())
-
ECDH
を使わないAESGCM
はデフォルトにないのでAESGCM
を追加 - pre-shared key なんぞ使わない通常のアクセスなので
!PSK
を追加
この辺の文字列はこちらを参照のこと
PS C:\Users\sharl> python .\sample.py
TLSv1.2
無事成功
【小ネタ】サポートしている cipher の確認
$ openssl ciphers -v '@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM'
TLS_AES_256_GCM_SHA384 TLSv1.3 Kx=any Au=any Enc=AESGCM(256) Mac=AEAD
TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_AES_128_GCM_SHA256 TLSv1.3 Kx=any Au=any Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(256) Mac=SHA384
ECDHE-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(256) Mac=SHA384
ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256
ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256
DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(256) Mac=AEAD
DHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(128) Mac=AEAD
DHE-RSA-AES256-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AES(256) Mac=SHA256
DHE-RSA-AES128-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AES(128) Mac=SHA256
以上をふまえて実装
requests
モジュールを使った実装は以下のようになります
kuronekoyamato.py
# -*- coding:utf-8 -*-
import ssl
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager import PoolManager
class YamatoAdapter(HTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=False):
ctx = ssl.create_default_context()
# add AESGCM, !PSK
ctx.set_ciphers('@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:AESGCM:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM:!PSK')
self.poolmanager = PoolManager(
num_pools=connections,
maxsize=maxsize,
block=block,
ssl_version=ssl.PROTOCOL_TLSv1_2,
ssl_context=ctx,
)
if __name__ == '__main__':
session = requests.Session()
session.mount('https://', YamatoAdapter())
url = 'https://toi.kuronekoyamato.co.jp'
with session.get(url) as r:
print(r)
PS C:\Users\sharl> python .\kuronekoyamato.py
<Response [404]>
無事成功
成果物
最後に
上記のようなアダプタをマウントすることでデフォルトの ciphers をオーバーライドできるので活用してみてください
Enjoy!!
Discussion
AESGCM
の脆弱性には特に触れません