LoginSignup
18
16

More than 1 year has passed since last update.

【Python】【requests】プロキシ環境下でSSLErrorになる原因と対処法

Last updated at Posted at 2023-02-16

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が生じます。

image.png

引用元:Proxyサーバ - ネットワークスペシャリスト - SE娘の剣 -

したがって、解決法は以下の2通りになります。

1. サーバー証明書の検証をしない

requestsライブラリであれば、requests.get('https://hogehoge.com', verify=False)のように、検証をしない設定にします。ただし、これはセキュリティ的によろしくないので、あくまで手元の検証作業などのときに使用します。

また、コードをいじらず、環境変数REQUESTS_CA_BUNDLEまたはCURL_CA_BUNDLEを空文字にするとverify=Falseに出来るとの情報がありますが、コード内でverify=Trueとされている場合はこの方法が使えないようです。

requests/requests/sessions.py
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で設定されています。

requests/requests/utils.py
from . import certs
DEFAULT_CA_BUNDLE_PATH = certs.where()
requests/requests/adapters.py
    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ライブラリのコードはApache2.0ライセンスです。
https://github.com/psf/requests/blob/main/LICENSE

18
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
16