Python requestsライブラリは認証局の証明書をどう管理する?

Python でHTTP(S)通信をするようなプログラムを書く場合、requests ライブラリを直接・間接的に使っていることが非常に多いかと思います。

HTTPS通信の際には、サーバーの証明書が信頼できる認証局によって発行されたものかクライアントはチェックします。 このチェックのために、クライアントは信頼できる認証局の一覧(認証局の証明書リスト)を保持しています。

requests ライブラリが信頼できる証明書リストをどこから持ってきているのか調べる機会が有りましたので、ご紹介します。

前提

現時点の最新版は 2017/08/15 にリリースされた 2.18.4 です。 あまりに古すぎるバージョンから歴史を追うと大変なため、2013/09/24 にリリースされた 2.0.0 以降を対象とします。

証明書リスト取得の変遷

ライブラリが利用する証明書リストは

  • OS 標準
  • ライブラリにバンドル

のどちらかであることが多いですが、requests は後者です。

もともとはライブラリにベタ埋めされた証明書リストが certifi という名前で外部ライブラリとして切り出され、バンドルとcertifiの並行運用を経て、最近になって certifi に一本化されています。

バージョン2.0.0 〜

ライブラリにバンドルされたリスト

証明書の更新方法

ライブラリのアップデート

バージョン2.4.0 〜

certifi がインストールされていれば certifi。なければライブラリにバンドルされたリスト

証明書の更新方法

ライブラリの更新 OR certifi の新規インストール OR インストール済み certifi のアップデート

バージョン2.16.0〜

certifi(requests インストール時に一緒にインストール)

証明書の更新方法

certifi のアップデート

certifi について

過去の requests ライブラリでは証明書リストがバンドルされていたため、requests ライブラリを特定のバージョンに固定して運用すると、いつまでも証明書一覧が古いままになります。 アプリケーションの動作は同じまま、証明書だけを更新できるように certifi ライブラリとして切り出されました。

certifi は慎重にキュレートされたルート証明書リストで Mozilla Included CA Certificate List がベースになっています。

生ファイルとして提供されている他、様々なプログラミング言語向けにライブラリ化されています。

URL http://certifiio.readthedocs.io/en/latest/

Python 版 certifi を使っている場合

$ pip install -U certifi

とするだけで、証明書リストをアップデート出来ます。

certifi 以外の証明書の指定方法

証明書リストのパスを環境変数(REQUESTS_CA_BUNDLE)で指定することも出来ます。

例えば、ライブラリやライブラリを利用しているアプリケーションを修正せずに、証明書リストをOS標準のものに変えたい場合などに利用出来ます。

証明書リストが古いままだと証明書の verification に引っかかるサイトがあるとします。

$ python -c 'import requests; requests.get("https://good.sca1a.amazontrust.com/")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib/python2.7/dist-packages/requests/api.py", line 55, in get
    return request('get', url, **kwargs)
  File "/usr/lib/python2.7/dist-packages/requests/api.py", line 44, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/lib/python2.7/dist-packages/requests/sessions.py", line 335, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/lib/python2.7/dist-packages/requests/sessions.py", line 438, in send
    r = adapter.send(request, **kwargs)
  File "/usr/lib/python2.7/dist-packages/requests/adapters.py", line 331, in send
    raise SSLError(e)
requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)

certifi の最新の証明書リストを利用して再チャレンジしてみましょう。

$ curl -o newest.pem https://mkcert.org/generate/
$ REQUESTS_CA_BUNDLE=newest.pem  python -c 'import requests; requests.get("https://good.sca1a.amazontrust.com/")'
$ 

無事アクセス出来ましたね。

補足:curl/openssl で証明書リストを環境変数から変更

curl/openssl も環境変数で証明書リストを指定できます。スクリプト内で TLS 通信している場合に、スクリプトを極力書き換えずに証明書リストだけを変更することができます。

curl は CURL_CA_BUNDLE を利用

$ CURL_CA_BUNDLE=newest.pem curl -I https://good.sca1a.amazontrust.com/
HTTP/1.1 200 OK
Server: nginx/1.10.1
Date: Fri, 10 Nov 2017 20:26:54 GMT
Content-Type: text/html
Content-Length: 3051
Last-Modified: Thu, 22 Dec 2016 21:04:31 GMT
Connection: keep-alive
ETag: "585c3fdf-beb"
Strict-Transport-Security: max-age=31556952; includeSubdomains; preload
Accept-Ranges: bytes

openssl は SSL_CERT_FILE を利用

$ SSL_CERT_FILE=newest.pem openssl s_client -connect good.sca1a.amazontrust.com:443 -quiet
depth=2 C = US, O = Amazon, CN = Amazon Root CA 1
verify return:1
depth=1 C = US, O = Amazon, OU = Server CA 1A, CN = Amazon
verify return:1
depth=0 1.3.6.1.4.1.311.60.2.1.3 = US, 1.3.6.1.4.1.311.60.2.1.2 = Delaware, businessCategory = Private Organization, serialNumber = 5846743, C = US, ST = Washington, L = Seattle, O = Amazon Trust Services, CN = good.sca1a.amazontrust.com
verify return:1
^C

証明書のチェックを無視したい場合

やんごとなき理由により、 requests ライブラリで証明書のverifyを無効化する場合、get メソッドの verify 引数を False にします。

$ python -c 'import requests; requests.get("https://good.sca1a.amazontrust.com/", verify = False)'
$ 

無効化は自己責任でお願いします。

まとめ

Python で非常によく利用される requests ライブラリを利用していると、TLS 通信時にサーバー証明書のチェックで引っかかることが稀に有ります。

今回は requests ライブラリはどのようにルート証明書リストを管理しているのか紹介した上で

  • certifi ライブラリを使った証明書の管理・更新
  • 環境変数を使った証明書リストの変更方法
  • 証明書チェックを無視する方法

を紹介しました。

それでは、セキュアな TLS ライフを。

参考