詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

中間証明書のないサーバにアクセスする

不特定多数のウェブサイトにアクセスするアプリケーションを書いていると、ときおり SSL 証明書の検証エラーとなる URL に行き当たることがある。が、確認のためブラウザでアクセスしてみると、普通に見れてしまったりもする。そんな事例のひとつ、タイトルの通り中間CA証明書のないサーバについて。

https://incomplete-chain.badssl.com/ というわかりやすい例がある。これを curl してみると:

% docker run -it --rm buildpack-deps:buster bash
root@22f1788d53c7:/# curl --version
curl 7.64.0 (x86_64-pc-linux-gnu) libcurl/7.64.0 OpenSSL/1.1.1d zlib/1.2.11 libidn2/2.0.5 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.36.0 librtmp/2.3
Release-Date: 2019-02-06
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
root@22f1788d53c7:/# curl -sS https://www.example.com/ > /dev/null # 正常系として
root@22f1788d53c7:/# curl -sS https://incomplete-chain.badssl.com/ > /dev/null
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

となるわけだけど、手元のブラウザではたぶんとくにエラーもなく開ける(はず)。

この URL では、サーバはあえて中間証明書を配信していない。にもかかわらずブラウザで見えてしまうのは、badssl.com の説明によると、

  • ブラウザが中間証明書をキャッシュしている
  • または、ブラウザが AIA (= Authority Information Access) fetching に対応している

から、ということらしい。AIA は SSL 証明書の拡張仕様で、サーバの証明書の発行者の証明書へのアクセス方法が記述されるもの。ブラウザで証明書の情報を見てみると確認できるはず。見てみると証明書の URL が http: で記述されていることに最初おどろいたけど、平文で取得した証明書も検証されるので、安全性には問題ないのだった。それを自動的に取得してくるのが AIA fetching(なのだと思う)。AIA fetching は最近のメジャーなブラウザだと Firefox 以外は対応しているのだというがあったが、公式にアナウンスしているものはすぐには見つからなかった。ソースを読まないといけないだろう。

中間証明書をまとめて取得する

で、ブラウザで見られるページはアプリケーションからも取得できたい、というのは普通にありえる需要で、これをどのように実現するのか、という話。

AIA fetching を実装してしまう、というのも正攻法ではあって、そういう実装もある(未検証)。使えるライブラリがない場合や実装したくない場合に、すべてのサイトを尽くすことはできないかもしれないけど、多くをカバーできる方法として、あらかじめ中間証明書を手元に取得しておく、という手をここでは考える。

つまり、ルートCA証明書によって署名されている中間証明書をあらかじめダウンロードしておけばいいわけだ。そして、都合のいいことにそういうリストが作られている。

https://wiki.mozilla.org/CA/Intermediate_Certificates

Mozilla による Common CA Database (CCADB) プロジェクト のリソースのひとつで、Mozilla が信頼しているルート CA によって署名された中間証明書の一覧らしい。このページの権威は自分には確認できなかったけど、どのみちすでに信頼されている手元のルート証明書で検証するので、それっぽいものが網羅されていれば出自はなんでもいいのである。

先述のページから中間証明書を取得し、すでに手元にあるルート証明書で検証できたものだけ保存することにする。Dockerfile にするとこういう感じになる。楽したかったので、apt で入る csvtool というのを使ってみた。

FROM buildpack-deps:buster

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update && \
    apt-get install -y csvtool
RUN mkdir -p /opt/intermediate-certs/certs && \
    curl -sL https://ccadb-public.secure.force.com/mozilla/PublicAllIntermediateCertsWithPEMCSV | \
    csvtool namedcol 'PEM Info' - | \
    csvtool drop 1 - | \ # ヘッダを落とす
    csvtool call "printf '%q\n'" - | \ # 改行が含まれているので1行に
    while IFS= read -r e; do \
        pem=$(eval "echo $e" | sed "s/'//"); \ # ' を除く
        if echo "$pem" | openssl verify; then \ # 既存のルート証明書で検証
            echo "$pem" | \
                tee -a /opt/intermediate-certs/ca-bundle.crt \ # 証明書バンドルを作る
                > /opt/intermediate-certs/certs/"$(echo "$pem" | openssl x509 -subject -noout | perl -pe 's/\W/_/g')".pem; \ # 個別の証明書も保存する
        fi; \
    done && \
    c_rehash /opt/intermediate-certs/certs

openssl verify して成功したものだけ保存する。今気づいたけど、中間証明書によって署名されている中間証明書はここで跳ねられてしまうな……。

証明書バンドルは、単にテキストとして並べて記述すればよい(opensslRFC)。取り回しがしやすいように、すべての証明書を1ファイルにまとめたものとそれぞれ別々のファイルにしたものとを作っている。

今回取得した中間証明書をシステムにそのまま組み込んでしまいたいなら、システムのルート(debian なら /etc/ssl/certs)に置いてしまえばいいのだけど、アプリケーション側で制御したいことがほとんどだと思うので、別の場所に置いておくのだいいだろう。

さっそく試してみる。--cacert がポイント。--capath /opt/intermediate-certs/certs:/etc/ssl/certs としてもいい。

% docker run --rm -it $(docker build -q .) bash
root@39401ca59f18:/# curl --version
curl 7.64.0 (x86_64-pc-linux-gnu) libcurl/7.64.0 OpenSSL/1.1.1d zlib/1.2.11 libidn2/2.0.5 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.36.0 librtmp/2.3
Release-Date: 2019-02-06
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
root@39401ca59f18:/# curl --cacert /opt/intermediate-certs/ca-bundle.crt -sS https://www.example.com/ > /dev/n
ull
root@39401ca59f18:/# curl --cacert /opt/intermediate-certs/ca-bundle.crt -sS https://incomplete-chain.badssl.com/ > /dev/null

やったね! 成功です。

ちなみに

ブラウザの確認だけで済ませてしまうと、こういう設定漏れに気づきにくいみたいです。Mackerel のようなサービスを使えば、証明書の期限なども含めて監視できます。わたしも使っています! どうぞご利用下さい。

mackerel.io

f:id:motemen:20200422224025p:plain

参考文献

はてなで一緒に働きませんか?