Pythonのrequests
ライブラリをプロキシ環境下で使用しているとしばしば次のエラーに出会います。
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)
During handling of the above exception, another exception occurred:
(略)
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='hogehoge.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))
During handling of the above exception, another exception occurred:
(略)
requests.exceptions.SSLError: HTTPSConnectionPool(host='hogehoge.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1,
'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))
これはSSL Cert Verificationに失敗しているために生じます。
SSL Cert Verificationとは、HTTPS リクエストの SSL 証明書の検証のことです。証明書を検証できない場合、SSLError が発生します。
SSL 証明書の検証とは、クライアント側が接続先のサーバーを信頼してよいかを判断することです。SSL 証明書の検証では主に下記のような点をチェックします。
- サーバー証明書がどの認証局で発行されたかを確認する。
- 発行した認証局が信頼できるか確認する。これは、発行した認証局の上位の認証局の上位の認証局の・・・と辿っていき、クライアント側で元々信頼すると設定していた認証局の証明書(主にルート認証局の証明書)に辿り着ければ信頼できるとします。
- 証明書が改ざんされていないか確認します。
- 証明書の有効期限や、無効化されていないか確認します。
詳しく正確な話は別の記事に譲ります。
で、本題なのですが、アクセスしようとしているサイトが信頼できるようなサイトで、HTTPS通信を可能にしていれば、普通SSLErrorは起きません。SSLErrorが起きる場合はサーバー証明書の有効期限が切れているといった場合でしょうか。または、クライアント側のライブラリのバージョンが古すぎて、ルート証明書が古いといった場合でしょうか。ともあれ、あまり見かけることはありません。
ですが、プロキシを介する通信ではSSLErrorが起きます。プロキシを介する通信では、プロキシサーバの証明書を検証する場合があります。プロキシサーバは組織内のみで使用されるため、自己署名証明書(上位の認証局が自分自身)が入っている場合があります。自己署名証明書は信頼できない証明書(=クライアント側で元々信頼すると設定していない証明書)であるため、SSLErrorが生じます。
引用元:Proxyサーバ - ネットワークスペシャリスト - SE娘の剣 -
したがって、解決法は以下の2通りになります。
1. サーバー証明書の検証をしない
requests
ライブラリであれば、requests.get('https://hogehoge.com', verify=False)
のように、検証をしない設定にします。ただし、これはセキュリティ的によろしくないので、あくまで手元の検証作業などのときに使用します。
また、コードをいじらず、環境変数REQUESTS_CA_BUNDLE
またはCURL_CA_BUNDLE
を空文字にするとverify=False
に出来るとの情報がありますが、コード内でverify=True
とされている場合はこの方法が使えないようです。
if verify is True or verify is None:
verify = (
os.environ.get('REQUESTS_CA_BUNDLE') or
os.environ.get('CURL_CA_BUNDLE') or
verify
)
2. プロキシサーバの証明書をクライアント側で設定する
requests
ライブラリが参照する証明書はcertifi
ライブラリで管理している証明書であり、OSに設定した証明書ではありません。ライブラリ内では変数DEFAULT_CA_BUNDLE_PATH
で設定されています。
from . import certs
DEFAULT_CA_BUNDLE_PATH = certs.where()
def cert_verify(self, conn, url, verify, cert):
"""Verify a SSL certificate. This method should not be called from user
code, and is only exposed for use when subclassing the
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
:param conn: The urllib3 connection object associated with the cert.
:param url: The requested URL.
:param verify: 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
:param cert: The SSL certificate to verify.
"""
if url.lower().startswith("https") and verify:
cert_loc = None
# Allow self-specified cert location.
if verify is not True:
cert_loc = verify
if not cert_loc:
cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
if not cert_loc or not os.path.exists(cert_loc):
raise OSError(
f"Could not find a suitable TLS CA certificate bundle, "
f"invalid path: {cert_loc}"
)
conn.cert_reqs = "CERT_REQUIRED"
if not os.path.isdir(cert_loc):
conn.ca_certs = cert_loc
else:
conn.ca_cert_dir = cert_loc
else:
conn.cert_reqs = "CERT_NONE"
conn.ca_certs = None
conn.ca_cert_dir = None
if cert:
if not isinstance(cert, basestring):
conn.cert_file = cert[0]
conn.key_file = cert[1]
else:
conn.cert_file = cert
conn.key_file = None
if conn.cert_file and not os.path.exists(conn.cert_file):
raise OSError(
f"Could not find the TLS certificate file, "
f"invalid path: {conn.cert_file}"
)
if conn.key_file and not os.path.exists(conn.key_file):
raise OSError(
f"Could not find the TLS key file, invalid path: {conn.key_file}"
)
したがって、プロキシサーバの証明書はOSのインストール機能ではなく、requests
ライブラリが指定する方法で設定する必要があります。
コード中に仕込む場合は、requests.get('https://hogehoge.com', verify='/path/to/certfile')
のように証明書を含むフォルダまたは証明書そのもののパスを指定します(フォルダだとうまくいかない)。
環境変数でも指定する事ができ、requests
ライブラリの場合はREQUESTS_CA_BUNDLE
またはCURL_CA_BUNDLE
のどちらかに証明書を含むフォルダまたは証明書そのもののパスを指定します。
証明書の入手は組織のシステム管理者から入手するか、OSにインストールされていればエクスポート機能で出力し、どこか(例えばC:直下)に配置しておきます。
参考:Python requestsライブラリは認証局の証明書をどう管理する?
上記の方法でverify
にパスを設定した場合、デフォルトのcertifi
による証明書設定は外れますので注意してください。
certifi
の証明書も加えたい場合は、certifi.where()
で証明書の場所を確認し、cacert.pem
ファイルをコピーします。拡張子を.cer
に変更します。cacert.cer
ファイルをテキストエディタで開き、プロキシサーバの証明書の中身を最後尾に追加します。
あと当たり前ですが、プロキシサーバの情報をセットできているかはチェックしましょう。
import os
import requests
os.environ['http_proxy'] = 'http://user:pass@proxyip.0.0.1:80'
os.environ['https_proxy'] = 'http://user:pass@proxyip.0.0.1:80'
res = requests.get('https://hogehoge.com/')
import requests
proxies = {
'http' : 'http://user:pass@proxyip.0.0.1:80',
'https' : 'http://user:pass@proxyip.0.0.1:80',
}
res = requests.get('https://hogehoge.com/', proxies=proxies)
ほか参考
- requests でプロキシサーバーを通してリクエストを投げる
- Python requests で SSLError が起きて毎回ググってるのでまとめた
- Pythonのrequestsでverify=Falseを使わずにSSLの検証を無効化する
以上 誤り等あればコメント下さい。
フォルダ指定だとうまくいかないので、原因が分かる方は教えてください。
requests
ライブラリのコードはApache2.0ライセンスです。
https://github.com/psf/requests/blob/main/LICENSE