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() メソッドのキーワード引数 verifyFalse を渡すことで証明書検証をオフにすることができます。ただしあらゆる HTTPS における証明書検証がなされなくなるので、この方法はおすすめしません。

なお、キーワード引数 verifyrequests.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_STRICTssl.VERIFY_X509_PARTIAL_CHAIN が加わりました。

バージョン 3.13 で変更: The context now uses VERIFY_X509_PARTIAL_CHAIN and VERIFY_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_STRICTssl.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 は用意されていません。requestsurllib3 パッケージを内部的にしようしていますが、何とか requests の API から urllib3ssl.SSLContext を渡す術がないかソースも含め調べたところ、requests.Session.mount()requests.adapters モジュールの HTTPAdapter を継承したカスタムクラスを渡すことでやや無理やり ssl.SSLContext を渡せることがわかりました。

以下は HTTPAdapterinit_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_kwurllib3.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.

Pool Manager - urllib3 2.5.0 documentation

そして urllib3.connectionpool.ConnectionPool を継承した urllib3.HTTPConnectionPool を見てみると **conn_kwurllib3.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.

Connection Pools - urllib3 2.5.0 documentation

ついに urllib3.connection.HTTPSConnectionssl_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)

Connections - urllib3 2.5.0 documentation

requestsssl.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_flagsssl.VERIFY_X509_STRICT を外した値を設定しています。そして自前の ssl.SSLContext**pool_kwargs に混ぜ込んでスーパークラスの init_poolmanager() に渡します。

この CustomSSLAdapterrequests.Sessionmount() することで、その 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 に落として実行するのもありですね。