AWS / PHP / Python ちょいメモ

amazon web service , PHP, Python を使ったときのメモ。日本語でググってもわからなかった事を中心に。

Python が 参照する証明書ストアを掘ってみた (Ubuntu 版)

Python で作ったプログラムが、あるサイトをのぞきに行けなったのだが、証明書ストアに中間証明書がなかったことが原因だった。という話の、まとめ。

結論としては、Ubuntuの証明書ストア管理には、ca-certificates という仕組みが用意されているので、それを使って、足りない中間証明書を追加してやった。という内容になります。


こんな環境に対して、つらつら書きます。

  • Ubuntu 14.04.5
  • Python 3.5.3
  • urllib.request で 該当サイトを見に行く (Requests ライブラリなどは未使用)

起こったこと

Python

Python で作ったプログラムが、ある時からサイトに接続できなくなった。エラーを見ると、次の内容で、SSL関連。どうやら、新たに追加されたサブドメインで、これまでとは異なる証明書が設定されているらしい。

SSLError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:720)'),)

curl

適切かどうか(最終的には、半分適切だったが)わかんないけど、curl でも見に行ってみると、こんなエラー。

curl: (60) Peer certificate cannot be authenticated with known CA certificates
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.


curl のバージョン。 https に対応していて OpenSSL が使われてる模様:

$ curl -V
curl 7.35.0 (x86_64-pc-linux-gnu) libcurl/7.35.0 OpenSSL/1.0.1f zlib/1.2.8 libidn/1.28 librtmp/2.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smtp smtps telnet tftp
Features: AsynchDNS GSS-Negotiate IDN IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP

openSSL の バージョン:

$ openssl version
OpenSSL 1.0.1f 6 Jan 2014

調査

OpenSSL で確認

OpenSSLを使うと、該当サイトの証明書の詳細が確認できるので、やってみた。

$ openssl s_client -connect <DOMAIN>:443 -showcerts
 もしくは、
$ openssl s_client -port 443 -showcerts -host <DOMAIN>


まずは、該当サイト:

$ openssl s_client -connect kilasdaerah.kompas.com:443 -showcerts
CONNECTED(00000003)
depth=0 CN = *.kompas.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = *.kompas.com
verify error:num=27:certificate not trusted
verify return:1
depth=0 CN = *.kompas.com
verify error:num=21:unable to verify the first certificate
verify return:1

...

 -----END CERTIFICATE-----
 ---
Server certificate
subject=/CN=*.kompas.com
issuer=/C=US/O=thawte, Inc./OU=Domain Validated SSL/CN=thawte DV SSL CA - G2
...


正常につながるサブドメイン側:

CONNECTED(00000003)
 ---
Certificate chain
 0 s:/CN=*.kompas.com
   i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=Thawte RSA CA 2018
 -----BEGIN CERTIFICATE-----

...

 -----END CERTIFICATE-----
 1 s:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=Thawte RSA CA 2018
   i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA
 -----BEGIN CERTIFICATE-----

...

 -----END CERTIFICATE-----
 ---
Server certificate
subject=/CN=*.kompas.com
issuer=/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=Thawte RSA CA 2018
...

開発MacOSのブラウザで確認

該当サイトも、正常につながるサブドメインも、どちらも閲覧が可能。

もちろん、送ってくる証明書と、ルート証明書までのチェーンは異なる。

ただ、どちらも3階層の証明書であることは共通。あれ? OpenSSLのログからだと、該当サイトは証明書本体のみの送信で、正常につながるサブドメインについては、証明書+中間証明書の組み合わせが送信されているもよう。



わかったこと

この段階でわかったことは、4つ:

  • Python 3.5.3 の urllib.request で見に行けない HTTPS サイトがある
  • 上記のサイトには curl でも見に行けない
  • 該当サイトからは証明書のみが送られてきている
  • 該当サイトの証明書は、正常につながるサブドメインとは、ルート証明書も異なる

選択肢

  • insecure として扱う -> 公開サイト見に行くのに、それはちょっとなぁ。本当に InSecure のサイトとの区別つけづらくなる
  • 正常なサイトとして扱いたい -> 該当サイトの証明書を、信頼するルート証明書からチェーンできるようにすればいい

後者として扱うための方策を、各種実施していきました。

対応

Ubuntu の証明書ストア

Ubuntu の 証明書ってどこで管理されているのだろうか?いろいろググってたら発見。

/etc/ssl

誰が入れたのかを調べてみると、opensslっぽい。

$ apt-file search "/etc/ssl"
dkimproxy: /etc/ssl/private/dkimproxy.key
fsvs: /usr/share/doc/fsvs/examples/debian/etc/ssl/servers
openssl: /etc/ssl/openssl.cnf
python-neutron: /usr/lib/python2.7/dist-packages/neutron/tests/unit/bigswitch/etc/ssl/ca_certs/README
python-neutron: /usr/lib/python2.7/dist-packages/neutron/tests/unit/bigswitch/etc/ssl/combined/README
python-neutron: /usr/lib/python2.7/dist-packages/neutron/tests/unit/bigswitch/etc/ssl/host_certs/README
python-ubuntu-sso-client: /etc/ssl/certs/UbuntuOne-Go_Daddy_CA.pem
python-ubuntu-sso-client: /etc/ssl/certs/UbuntuOne-Go_Daddy_Class_2_CA.pem
python-ubuntu-sso-client: /etc/ssl/certs/UbuntuOne-ValiCert_Class_2_VA.pem
uoa-integration-tests: /etc/ssl/certs/uoa-test-server.pem


でも、どうやら上記のフォルダを直接触るのではなく、 ca-certificates という仕組みの乗っ取るらしい。

独自のルートCA証明書を追加する方法(Ubuntu, CentOS 7) | cloudpack.media


どれどれ。確かに、山盛り crt ファイルを /usr/share/ca-certificates/ にインストールした上で /etc/ssl/certs も気にしてる。

$ dpkg -L ca-certificates
...
/usr/share/ca-certificates/mozilla/Security_Communication_EV_RootCA1.crt
/usr/share/ca-certificates/mozilla/COMODO_Certification_Authority.crt
...
/usr/sbin
/usr/sbin/update-ca-certificates
/etc
/etc/ca-certificates
/etc/ca-certificates/update.d
/etc/ssl
/etc/ssl/certs
$ apt-cache depends ca-certificates
ca-certificates
  依存: openssl
 |依存: debconf
  依存: <debconf-2.0>
    cdebconf
    debconf
  破壊: ca-certificates-java
  破壊: <ca-certificates-java:i386>
  拡張: openssl

Ubuntu の証明書ストア に 必要な中間証明書をインストール

今回必要だった中間証明書は、公式からダウンロード

それを thawte_SHA2SHA1_intermediateSSL123.crt として保存。次のコマンドで、証明書ストアに保管した。

$ sudo cp thawte_SHA2SHA1_intermediateSSL123.crt /usr/share/ca-certificates

/etc/ca-certificates.confに、/usr/share/ce-certificetsからの相対パスで、ファイル名を追記する。

$ sudo vim /etc/ca-certificats.conf

update-ca-certificetsを実行する。

$ sudo update-ca-certificates

無事 pem ファイルがストアに準備された。

$ ls /etc/ssl/certs/ | grep thawte
thawte_Primary_Root_CA.pem
thawte_Primary_Root_CA_-_G2.pem
thawte_Primary_Root_CA_-_G3.pem
thawte_SHA2SHA1_intermediateSSL123.cer.pem

その後

以上の対応により、 python でも curl でも、該当サイトをエラーなく取得しに行くことが可能になりました。


その他

requests ライブラリを使っている場合には、こちらを参照して certifi ライブラリをチェックするといいかもしれません

MacOSの証明書ストアは、「キーチェーンアクセス」で確認できます。