Python requests で HTTPS リクエスト時の certificate verify failed: self signed certificate in certificate chain エラーに対応する
Python の requests
パッケージで https インターネットアクセスする際に、TLS インスペクションが入っている環境で発生する certificate verify failed: self signed certificate in certificate chain
エラーに対応する方法を紹介します。
やりたいこと
Python で http リクエストを送る際に requests
パッケージを使用することはよくあると思います。
ところが企業の環境等で TLS インスペクションが入っている環境において requests
パッケージでインターネットアクセスする際に
certificate verify failed: self signed certificate in certificate chain
というエラーが発生することがあります。このエラーの対応方法を紹介します。
環境
- OS
- Microsoft Windows 10 22H2
- Python
- 3.11.2
- requests
- 2.32.3
事象
例えば TLS インスペクションがある環境で以下のようなコードで Yes か No を返してくれる YESNO API を叩いてみます。
import requests
res = requests.get("https://yesno.wtf/api")
print(res.content)
すると以下のようなエラーが発生します。
requests.exceptions.SSLError: HTTPSConnectionPool(host='xxx.com', port=443): Max retries exceeded with url: /api (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:992)')))
このエラーは TLS インスペクションが入っていることにより証明書検証でエラーとなっていることがほとんどです。 TLS インスペクションの簡単な説明と、後ほど使用する証明書の特定、取得方法を こちらの記事 で紹介していますので必要であればご参照ください。
対応方法
証明書検証をオフにする (非推奨)
requests
では requests.request()
メソッドのキーワード引数 verify
に False
を渡すことで証明書検証をオフにすることができます。ただしあらゆる HTTPS における証明書検証がなされなくなるので、この方法はおすすめしません。
なお、キーワード引数 verify
は requests.request()
のラップメソッドである requests.get()
や requests.post()
などでも同様に使用できます。
verify – (optional) Either a boolean, in which case it controls whether we verify the server’s TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to True. - Developer Interface - Requests 2.32.3 documentation
verify=False
として実行すると、
import requests
res = requests.get("https://yesno.wtf/api", verify=False)
print(res.content)
以下のワーニングが表示されますが、
InsecureRequestWarning: Unverified HTTPS request is being made to host 'xxx.xxx'. Adding certificate verification is strongly advised.
API のレスポンスは受け取れていることがわかります。
{
"answer": "yes",
"forced": false,
"image": "https://yesno.wtf/assets/yes/0-c44a7789d54cbdcad867fb7845ff03ae.gif"
}
自己署名証明書を指定する
本質的にはプロキシの自己署名証明書を追加することがあるべき姿です。
requests.request()
メソッドのキーワード引数 verify
の公式ドキュメントの記載を再掲しますが、 verify
は Boolean 値以外に証明書パスを指定できます。繰り返しになりますが、追加する証明書の特定、取得方法を こちらの記事 で紹介していますので必要であればご参照ください。
verify – (optional) Either a boolean, in which case it controls whether we verify the server’s TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to True. - Developer Interface - Requests 2.32.3 documentation
キーワード引数 verify
に証明書パスを指定して実行するとワーニングも出ずに API のレスポンスが取得出来ると思います。
import requests
res = requests.get("https://yesno.wtf/api", verify="C:/path/to/cert.pem")
print(res.content)
環境変数で自己署名証明書を指定する
requests
では環境変数 REQUESTS_CA_BUNDLE
で証明書を指定することもできます。サードパーティーのパッケージの中には http リクエストを行うために requests
を組み込んでいるものが多くあります。例えば kagglehub
パッケージの model_download()
メソッドはその一つですが、model_download()
の引数に verify
のようなものは用意されていません。そのような場合、環境変数で証明書をしていすることで外から証明書パスを指定することができます。
This list of trusted CAs can also be specified through the
REQUESTS_CA_BUNDLE
environment variable. - Advanced Usage - Requests 2.32.3 documentation
環境変数 REQUESTS_CA_BUNDLE
に証明書パスを指定して実行すると問題なく API レスポンスが取得できます。
import os
import requests
os.environ["REQUESTS_CA_BUNDLE"] = "C:/path/to/cert.pem"
res = requests.get("https://yesno.wtf/api")
print(res.content)
ここでは Python で環境変数を設定していますが、PowerShell やコマンドプロンプトで設定することも可能です。
certificate verify failed: Missing Authority Key Identifier
エラー
Python 3.13 での変更点
Python 3.13 で ssl.create_default_context()
で作成される ssl.SSLContext
インスタンスの verify_flags
のデフォルト値に ssl.VERIFY_X509_STRICT
と ssl.VERIFY_X509_PARTIAL_CHAIN
が加わりました。
バージョン 3.13 で変更: The context now uses
VERIFY_X509_PARTIAL_CHAIN
andVERIFY_X509_STRICT
in its default verify flags.
- ssl — ソケットオブジェクト用の TLS/SSL ラッパー — Python 3.13.7 ドキュメント
Python 3.12 までは ssl.create_default_context()
で作成される ssl.SSLContext
インスタンスの verify_flags
のデフォルト値は ssl.VERIFY_X509_TRUSTED_FIRST
のみでした。
# Python runtime version 3.12.6
import ssl
ssl.create_default_context().verify_flags
# <VerifyFlags.VERIFY_X509_TRUSTED_FIRST: 32768>
ところが Python 3.13 では ssl.VERIFY_X509_STRICT
と ssl.VERIFY_X509_PARTIAL_CHAIN
が加わっています。
# Python runtime version 3.13.7
import ssl
ssl.create_default_context().verify_flags
# <VerifyFlags.VERIFY_X509_STRICT|VERIFY_X509_TRUSTED_FIRST|VERIFY_X509_PARTIAL_CHAIN: 557088>
ssl.VERIFY_X509_STRICT
がデフォルトで設定されたことにより、証明書が厳格に検証されるようになり、通常自己署名証明書にセットしていない認証局識別子 (Authority Key Identifier) がないと怒られるようになります。具体的には certificate verify failed: Missing Authority Key Identifier
エラーが発生します。
requests
の調査
requests
には ssl.SSLContext.verify_flags
を変更する API は用意されていません。requests
は urllib3
パッケージを内部的にしようしていますが、何とか requests
の API から urllib3
に ssl.SSLContext
を渡す術がないかソースも含め調べたところ、requests.Session.mount()
に requests.adapters
モジュールの HTTPAdapter
を継承したカスタムクラスを渡すことでやや無理やり ssl.SSLContext
を渡せることがわかりました。
以下は HTTPAdapter
の init_poolmanager()
メソッドのコード抜粋ですが、urllib3.poolmanager
モジュールの PoolMagager
クラスに **pool_kwargs
を渡しています。
def init_poolmanager( self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs ): # (中略) self.poolmanager = PoolManager( num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs, )
- requests/src/requests/adapters.py at main · psf/requests · GitHub
urllib3
のドキュメントを見ると **connection_pool_kw
は urllib3.connectionpool.ConnectionPool
の作成に使用すると記載があります。
class urllib3.PoolManager(num_pools=10, headers=None, **connection_pool_kw)
(中略)
Parameters:
(中略)
- **connection_pool_kw (Any) – Additional parameters are used to create fresh
urllib3. connectionpool.ConnectionPool
instances.
そして urllib3.connectionpool.ConnectionPool
を継承した urllib3.HTTPConnectionPool
を見てみると **conn_kw
は urllib3.connection.HTTPSConnection
インスタンス作成に使われると記載があります。
class urllib3.HTTPConnectionPool(host, port=None, timeout=_TYPE_DEFAULT.token, maxsize=1, block=False, headers=None, retries=None, _proxy=None, _proxy_headers=None, _proxy_config=None, **conn_kw)
(中略)
Parameters:
(中略)
- **conn_kw (Any) – Additional parameters are used to create fresh
urllib3.connection.HTTPConnection
,urllib3.connection.HTTPSConnection
instances.
ついに urllib3.connection.HTTPSConnection
が ssl_context
というキーワード引数で ssl.SSLContext
を受け取ることが確認できました。
class urllib3.connection.HTTPSConnection(host, port=None, *, timeout=_TYPE_DEFAULT.token, source_address=None, blocksize=16384, socket_options=[(6, 1, 1)], proxy=None, proxy_config=None, cert_reqs=None, assert_hostname=None, assert_fingerprint=None, server_hostname=None, ssl_context=None, ca_certs=None, ca_cert_dir=None, ca_cert_data=None, ssl_minimum_version=None, ssl_maximum_version=None, ssl_version=None, cert_file=None, key_file=None, key_password=None)
(中略)
Parameters:
(中略)
- ssl_context (ssl.SSLContext | None)
requests
で ssl.VERIFY_X509_STRICT
を外す
さて、前置きが長くなりましたが Python 3.13 で ssl.create_default_context()
で作成される ssl.SSLContext
インスタンスの verify_flags
のデフォルト値に ssl.VERIFY_X509_STRICT
が加わったことによる requests
で自己署名証明書を使用時に発生する certificate verify failed: Missing Authority Key Identifier
エラーに対応していきます。
import ssl
import requests
from requests.adapters import HTTPAdapter
class CustomSSLAdapter(HTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
ctx = ssl.create_default_context()
ctx.verify_flags = ssl.VERIFY_X509_TRUSTED_FIRST | ssl.VERIFY_X509_PARTIAL_CHAIN
ctx.load_verify_locations(cafile="C:/path/to/cert.pem")
pool_kwargs["ssl_context"] = ctx
return super().init_poolmanager(connections, maxsize, block, **pool_kwargs)
session = requests.Session()
session.mount("https://", CustomSSLAdapter())
res = session.get("https://yesno.wtf/api")
print(res.content)
HTTPAdapter
を継承した CustomSSLAdapter
を作り、init_poolmanager()
メソッドをオーバーライドして自前で ssl.SSLContext
を用意し、verify_flags
に ssl.VERIFY_X509_STRICT
を外した値を設定しています。そして自前の ssl.SSLContext
を **pool_kwargs
に混ぜ込んでスーパークラスの init_poolmanager()
に渡します。
この CustomSSLAdapter
を requests.Session
に mount()
することで、その requests.Session
インスタンスでリクエスト実行時に自前の ssl.SSLContext
が使用されるようになります。
これにより無事に certificate verify failed: Missing Authority Key Identifier
エラーが発生しなくなりました。
あとがき
TLS インスペクションが入った環境化における Python の requests
パッケージで HTTPS リクエスト時に発生する certificate verify failed: self signed certificate in certificate chain
エラーの対応方法について紹介しました。まだまだ OS の証明書ストアを見てくれないものが多いので個別に対応する必要があり面倒ですね。
Python 3.13 の変更を踏まえた certificate verify failed: Missing Authority Key Identifier
エラーへの対応方法も紹介しましたが、面倒になってみんな verify=False
を使うんじゃないかとか思ってしまいますが、一旦 Python 3.12 に落として実行するのもありですね。