🐵

Python の [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] を解決する

2024/10/14に公開1

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 設定を見てみます

https://github.com/python/cpython/blob/v3.12.7/Modules/_ssl.c#L173

@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_hostnameSNI というやつです

 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 を追加

この辺の文字列はこちらを参照のこと

https://docs.openssl.org/1.1.1/man1/ciphers/#cipher-strings

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]>

無事成功

成果物

https://github.com/sharl/yamato-tracking

最後に

上記のようなアダプタをマウントすることでデフォルトの ciphers をオーバーライドできるので活用してみてください

Enjoy!!

Discussion