macOS用公式インストーラーのPython 3.6でCERTIFICATE_VERIFY_FAILEDとなる問題

この記事の内容はインストーラーの**大切な情報**にすべて書いてありますが、気づかないとハマるので残しておきます。

<img width="732" alt="Pythonインストーラーの大切な情報" src="https://qiita-image-store.s3.amazonaws.com/0/4240/bb1bcbc7-c232-f4ba-00f2-82141c1d140f.png">


## 問題

python.orgで配布されているmacOS用の公式インストーラーでインストールしたPython 3.6を使い、`urllib.request.urlopen()`で`https://`のWebページを取得しようとすると、以下のエラーが発生します。

```
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/request.py", line 1318, in do_open
    encode_chunked=req.has_header('Transfer-encoding'))
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/client.py", line 1239, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/client.py", line 1285, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/client.py", line 1234, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/client.py", line 1026, in _send_output
    self.send(msg)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/client.py", line 964, in send
    self.connect()
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/client.py", line 1400, in connect
    server_hostname=server_hostname)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ssl.py", line 401, in wrap_socket
    _context=self, _session=session)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ssl.py", line 808, in __init__
    self.do_handshake()
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ssl.py", line 1061, in do_handshake
    self._sslobj.do_handshake()
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ssl.py", line 683, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:749)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/request.py", line 223, in urlopen
    return opener.open(url, data, timeout)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/request.py", line 526, in open
    response = self._open(req, data)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/request.py", line 544, in _open
    '_open', req)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/request.py", line 504, in _call_chain
    result = func(*args)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/request.py", line 1361, in https_open
    context=self._context, check_hostname=self._check_hostname)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/request.py", line 1320, in do_open
    raise URLError(err)
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:749)>
```

他にもsslモジュールを使うモジュールではサーバー証明書の検証時に同様のエラーが発生するはずです。

## 原因

macOSに標準でインストールされているOpenSSLが古すぎるため、Python 3.6以降ではmacOS用のインストーラーにはOpenSSLが同梱され、システムのOpenSSLは参照されなくなりました。

これによって、OSにインストールされたルート証明書も参照されず、インストール直後の状態ではルート証明書が含まれていません[^1]。このため、TLSサーバー証明書の検証に失敗します。

## 対処法

以下のコマンドを実行すると、[certifi](https://pypi.python.org/pypi/certifi)モジュールをダウンロードしそこに含まれるルート証明書を参照するようになります。

```
$ /Applications/Python\ 3.6/Install\ Certificates.command
```

実行前:

```
$ ls -l /Library/Frameworks/Python.framework/Versions/3.6/etc/openssl/
```

実行後:

```
$ ls -l /Library/Frameworks/Python.framework/Versions/3.6/etc/openssl/
total 8
lrwxr-xr-x  1 orange  admin  52  3 22 23:00 cert.pem -> ../../lib/python3.6/site-packages/certifi/cacert.pem
```

この場合、ルート証明書の更新に応じて適切にアップデートするために、certifiプロジェクトのメーリングリストを購読しましょうと書かれています。


## 今後

ユーザーが個別に証明書をアップデートしなければならない今の状態は望ましくないので、[DSAS開発者の部屋:最近の Python-dev (2017-03)](http://dsas.blog.klab.org/archives/2017-03/python-dev-201703.html)にあった、OpenSSL以外のTLS実装を使ってOSの証明書を利用できるようにする[PEP 543](https://www.python.org/dev/peps/pep-0543/)の話につながっていくのだと思います。

## 参考

* [Tell HN: Python 3.6 on OS X requires a post-install step for SSL to work | Hacker News](https://news.ycombinator.com/item?id=13626273)
* [Python Release Python 3.6.1 | Python.org](https://www.python.org/downloads/release/python-361/)
* [DSAS開発者の部屋:最近の Python-dev (2017-03)](http://dsas.blog.klab.org/archives/2017-03/python-dev-201703.html)
* [PEP 543 -- A Unified TLS API for Python | Python.org](https://www.python.org/dev/peps/pep-0543/)

[^1]: pipはルート証明書を同梱しているので、`pip install`は動きます。