Python urllib.request.urlopen() で HTTPS リクエスト時の urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain エラーに対応する

Python ビルトインの urllib パッケージの urllib.request.urlopen() メソッド で https インターネットアクセスする際に、TLS インスペクションが入っている環境で発生する urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain エラーに対応する方法を紹介します。

やりたいこと

企業の環境等で TLS インスペクションが入っている環境において Python ビルトインの urllib パッケージの urllib.request.urlopen() メソッドで https インターネットアクセスする際に urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain エラーが発生することがあります。このエラーの対応方法を紹介します。

なお Python で http リクエストを送る場合、ビルトインの urllib ではなく、サードパーティーの requests パッケージを使うことのほうが多いと思います。requests パッケージでの対応方法は こちらの記事 で紹介しています。

環境

OS
Microsoft Windows 11 23H2
Python
3.13.7

事象

例えば TLS インスペクションがある環境で以下のようなコードで Yes か No を返してくれる YESNO API を叩いてみます。

from urllib import request

res = request.urlopen("https://yesno.wtf/api")
data = res.read().decode('utf-8')
print(data)

すると以下のようなエラーが発生します。

urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Missing Authority Key Identifier>

このエラーは TLS インスペクションが入っていることにより証明書検証でエラーとなっていることがほとんどです。 TLS インスペクションの簡単な説明と、後ほど使用する証明書の特定、取得方法を こちらの記事 で紹介していますので必要であればご参照ください。

対応方法

証明書検証をオフにする (非推奨)

urllib.request.urlopen() メソッドではキーワード引数 context に様々な SSL オプションを設定できる ssl.SSLContext を渡すことができます。

urllib.request.urlopen(url, data=None, [timeout, ]*, context=None)
(中略)
context を指定する場合は、様々な SSL オプションを記述する ssl.SSLContext インスタンスでなければなりません。
urllib.request — URL を開くための大規模なライブラリ — Python 3.13.7 ドキュメント

ssl.SSLContextverify_mode プロパティを持っており、ssl.CERT_NONE を設定することで証明書検証エラーを無視するようになります。ただしあらゆる HTTPS における証明書検証がなされなくなるので、この方法はおすすめしません。

SSLContext.verify_mode
接続先の証明書の検証を試みるかどうか、また、検証が失敗した場合にどのように振舞うべきかを制御します。この属性は CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED のうちどれか一つでなければなりません。
ssl — ソケットオブジェクト用の TLS/SSL ラッパー — Python 3.13.7 ドキュメント

ssl.SSLContext.verify_modessl.CERT_NONE に設定したコード例が以下です。

from urllib import request
from ssl import SSLContext, PROTOCOL_TLS_CLIENT, CERT_NONE

context = SSLContext(PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = CERT_NONE

res = request.urlopen("https://yesno.wtf/api", context=context)
data = res.read().decode('utf-8')
print(data)

API レスポンスを受け取ることができました。

{
  "answer":"yes",
  "forced":false,
  "image":"https://yesno.wtf/assets/yes/15-3d723ea13af91839a671d4791fc53dcc.gif"
}

ここで ssl.SSLContext.verify_modessl.CERT_NONE に設定する前に ssl.SSLContext.check_hostnameFalse にしていますが、これは ssl.SSLContext.check_hostnameTrue の状態で ssl.SSLContext.verify_modessl.CERT_NONE に設定しようとすると以下のエラーが発生するためです。

ValueError: Cannot set verify_mode to CERT_NONE when check_hostname is enabled.

自己署名証明書を指定する

本質的にはプロキシの自己署名証明書を追加することがあるべき姿です。

先ほどの ssl.SSLContextssl.SSLContext.load_verify_locations() メソッドで CA 証明書を追加することも可能です。

SSLContext.load_verify_locations(cafile=None, capath=None, cadata=None)
verify_modeCERT_NONE でない場合に接続先の証明書ファイルの正当性検証に使われる “認証局” (CA=certification authority) 証明書ファイル一式をロードします。
ssl — ソケットオブジェクト用の TLS/SSL ラッパー — Python 3.13.7 ドキュメント

PEM 形式の場合はキーワード引数 cafile に証明書のファイルパスを指定します。

cafile を指定する場合は、PEM フォーマットで CA 証明書が結合されたファイルへのパスを指定してください。
ssl — ソケットオブジェクト用の TLS/SSL ラッパー — Python 3.13.7 ドキュメント

ssl.SSLContext.load_verify_locations() メソッドで PEM 形式の証明書を追加した例が以下のコードです。

from urllib import request
from ssl import SSLContext, PROTOCOL_TLS_CLIENT

context = SSLContext(PROTOCOL_TLS_CLIENT)
context.load_verify_locations(cafile="C:/path/to/cert.pem")
print(context.get_ca_certs())

res = request.urlopen("https://yesno.wtf/api")
data = res.read().decode('utf-8')
print(data)

問題なく API レスポンスが取得できると思います。

また、証明書が DER 形式の場合はキーワード引数 cadata にバッファを渡します。

cadata オブジェクトを指定する場合は、PEM エンコードの証明書一つ以上の ASCII 文字列か、DER エンコードの証明書の bytes-like object オブジェクトのどちらかを指定してください。
ssl — ソケットオブジェクト用の TLS/SSL ラッパー — Python 3.13.7 ドキュメント

ssl.SSLContext.load_verify_locations() メソッドで DER 形式の証明書を追加した例が以下のコードです。

from urllib import request
from ssl import SSLContext, PROTOCOL_TLS_CLIENT

context = SSLContext(PROTOCOL_TLS_CLIENT)

with open('C:/path/to/cert.der', 'rb') as f:  
  context.load_verify_locations(cadata=f.read())

res = request.urlopen("https://yesno.wtf/api", context=context)
data = res.read().decode('utf-8')
print(data)

なお、他にも ssl.SSLContext.load_default_certs() メソッドで、OS の証明書ストアからロードすることも可能です。ブラウザではアクセスできるのに urllib.request.urlopen() ではエラーとなる場合はこれが一番手っ取り早いです。

SSLContext.load_default_certs(purpose=Purpose.SERVER_AUTH)
デフォルトの場所から “認証局” (CA=certification authority) 証明書ファイル一式をロードします。Windows では、CA 証明書はシステム記憶域の CAROOT からロードします。
ssl — ソケットオブジェクト用の TLS/SSL ラッパー — Python 3.13.7 ドキュメント

あとがき

TLS インスペクションが入った環境化における Python ビルトインの urllib.request.urlopen() メソッドで HTTPS リクエスト時に発生する urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain エラーの対応方法について紹介しました。調べた限り urllib.request.urlopen() は証明書を環境変数では指定することができないようで、ラップされて ssl.SSLContext が外から渡せなくなっているようなモジュールがあると詰みそうです。基本 urllib.request.urlopen() ではなく requests パッケージを使うでしょうから問題はなさそうですが。