2021年10月1日以降、Let's Encrypt のサーバ x Unity x 古い Android で通信エラーになったら (代替となる ZeroSSL の利用方法)

2021-10-09

背景

2021 年のある日、自分が関わっているサービスの一部のアクセスで、通信エラーが報告されるようになりました。 調べるとエラーは 10 月 1 日から発生していて、Android の 7.1.1 未満の端末に限っていました。

「この感じはなんか SSL の証明書期限とかそういう類のやつでは…?」
と思い調査を進めた結果、自分は以下の問題が起きていると結論づけました:

  • 2021年10月1日以降 のタイミングで、
  • Let’s Encrypt で SSL 認証を行っているサーバUnity のクライアントから(UnityWebRequest で) 通信すると、
  • Android 7.1.1 未満の一部の古い Android 端末 で認証エラーが発生する

最終的に自分は Let’s Encrypt を使用している部分を別の認証局(ZeroSSL)に変更することで解決しました。 情報を集めるのに苦労し、対応に少々時間をとられてしまったのですが、 同じような問題が起こるケースは(特に Let’s Encrypt を使っている小規模なプロジェクトや個人開発のサービスでは) それなりにあるのではないかと思い、自分の調査内容・対応方法を参考情報として記録しておくことにします。

この記事を読むと得られるもの

  • 2021年10月1日以降に発生している Let’s Encrypt の潜在的な問題について技術的情報を知ることができます
  • Let’s Encrypt や ZeroSSL、その更新を自動化する ACME プロトコルなどの SSL に関する周辺知識が得られます
  • Let’s Encrypt の問題を回避するために、別の認証局である ZeroSSL を使う場合の、具体的な対応方法がわかります

2021年10月1日以降、SSL の認証エラーが発生。その技術的詳細

何が起こっていたか

筆者が関わっていたサービスで確認された事象や、手元で検証した結果について整理します。

  • 通信エラーは 2021-10-01 以降、Android 限定で OS が 7.1.1 未満の端末でのみ発生を確認
  • Let’s Encrypt が使われているドメイン限定の問題で、そうではない通信は問題なかった
  • 手元にある Android 端末では、8 系や 9 系は問題なく動いていたが、6 系の古い端末(ZenFone 3 Max / 2016)でのみ通信エラーが起きた

エラーの内容と問題の切り分けの検証

今回の問題は Unity クライアントからの接続で発生しているエラーでした。 問題が起きた Android 端末の実機ログ(adb logcat)を見てみると、Curl error 51 というエラーが吐かれていました:

10-01 17:29:57.243 15460 15536 E Unity   : Curl error 51: Cert verify failed: UNITYTLS_X509VERIFY_FLAG_EXPIRED
10-01 17:29:57.243 15460 15536 E Unity   : (Filename: ./Modules/UnityWebRequest/Implementations/TransportCurl.cpp Line: 799)

Unity より深いシステムレイヤーでの怪しそうなエラーは自分が見た範囲では見当たらず。 これは Unity クライアントに依存した問題なのか、Let’s Encrypt 環境だけで起きるのか、簡単なテストアプリを作って検証してみます。 Unity 2019.2 で特定の URL に UnityWebReqeust を投げるだけのシンプルなアプリを作って動作確認してみたところ、

  • 該当のサービスの API や、同じく Let’s Encrypt が使われていた筆者の個人サーバへのアクセスはエラー
  • Google.com など他の認証局が使われているサイトにはアクセス可能

という結果でした。また、問題が起きていた Android 端末の Chrome アプリからは、 該当のサーバのページは(認証エラーなど無く)普通に表示されました。 つまりこれは Unity アプリはダメでブラウザは OK という、クライアント依存の問題のようです。

Let’s Encrypt に関する技術情報

Let’s Encrypt は無料で SSL 対応(サイトの https 化)ができるソリューションとして、 2021 年現在では多くのサイト / サービスで利用されている認証局です。 自分もこのサイトの https 化対応をする際に利用しました。(その時の対応手順は以下:)

もともと、Let’s Encrypt には 「2021-09 から Android 7.1.1 未満で接続できなくなりそう」 という話があって、問題視されていました。

  • Let’s Encrypt は 2 つの証明書を使っていました:
    • DST Root CA X3 (有効期限 2000〜2021)
    • ISRG Root X1 (有効期限 2015〜2035)
  • このうち古参の DST が 2021-09 に期限切れとなるのですが、新参の ISRG は古いデバイス / クライアントで信頼されていない (信頼するルート証明書のリストに含まれていない)ため通らないというものです
  • iOS は iOS 10 (2016) 以降の証明書リストに ISRG が含まれています
  • iOS は基本的に古い OS が切り捨てられていく世界なのであまり問題視はされていない模様
  • Android はシステム更新の提供がメーカー依存だったりするので、そもそも OS のバージョンアップをするのが難しいという事情があります
  • Android 7.1.1 未満は Android 全体の 3 分の 1 程度とされており、影響範囲が大きくて困るということで、 なんやかやあって 最終的には 2024 年までは今まで通りアクセスできるようにする という結論に落ち着きました

ということで本来は 2024 年までは古い Android でも問題はないという話だったのですが、 別の問題として OpenSSL 1.0.2 下ではエラーが発生する という問題が報告されていました:

筆者の結論としては、今回筆者が遭遇した問題もこの OpenSSL 1.0.2 に起因する問題ではないかと見ています。

  • OpenSSL 1.0.2 で弾かれた場合のエラーは期限切れのような見え方になるようで、今回のケースと一致します
  • クライアント依存の問題なので、OpenSSL が原因だとすると 「Unity からの通信で最終的に Android OS 依存の認証処理が呼ばれ、それが古い Android だと OpenSSL 1.0.2 を使っている」 という話になってホントか? といまいち疑わしいですが、まあ状況証拠的にそういう話かなと…

いずれにせよ調査結果から 「Let’s Encrypt では問題が発生して、別の認証局では起きない」 ということがわかっていたので、 今回の通信エラーを回避するために Let’s Encrypt から別の認証局に乗り換える対応を行うことにしました。

代替手段となる認証局を探す

さて、では代わりにどの認証局を使うか、という話になります。 (AWS のようなクラウドサービス上だったら ACM のようにクラウド内で完結しますが、今回は個別のサーバでした)

結論から言うと、今回は Let’s Encrypt と同様に無料で利用可能な ZeroSSL を使うことにしました。

初めは普通にさくらインターネットとかの有料の SSL を年契約で購入してそれを使うか… と考えていたのですが、 毎年更新の決済をしてサーバを認証し直す作業をしたくないな、という話になりました。

  • Let’s Encrypt は無料で使えることに加え、 certbot などのコマンドラインベースの発行・更新が用意されていて、 一度仕込めば証明書を自動更新できる仕組みが簡単に作れました
  • ちなみに有料の証明書だと、昔は期間が長めで 2 年あるものもありましたが、 近年では Apple が長い期間の証明書を認めなくなったりした背景で、そういうものは無くなった様子:
  • Let’s Encrypt がコマンドだけで認証できている (本人確認的な作業を介さず、サーバに cron を仕込めば更新を自動化できる) のは無料ゆえに決済が挟まらないというのもありますが、ドメインが運用されているサーバ上でコマンドを実行することで 「コマンドの実行者 = ドメイン所有者」を確認できる ため、という理由があります
    • Let’s Encrypt の更新自動化のプロセスは ACME というプロトコルで規定されています:
    • Let’s Encrypt は一般的に certbot などのコマンドで証明書の発行や更新を行いますが、 これが内部で ACME プロトコルに則った処理をしています
    • 別の認証局で同様の更新自動化を行うには、これと同じようなことをする手段が必要になります

ちなみにこの acme.sh は、2021-08 から使用するデフォルトの認証局が Let’s Encrypt から ZeroSSL に変更になったとのこと:

ZeroSSL と、その更新自動化について

ZeroSSL は Let’s Encrypt と比べるとマイナーですが、Let’s Encrypt と同等に無料で使える認証局のひとつです。 (かくいう自分も、今回の件で調査するまで ZeroSSL は存在を知りませんでした)


  • ZeroSSL はブラウザでアカウント登録をしてポチポチやるだけなら、簡単に証明書を発行できてユーザフレンドリーなサービスです。 が、この方法だと証明書の更新時に前述した手動の更新作業が発生してしまいます。 Let’s Encrypt と同様にコマンドベースで発行・更新をしたいところです
  • ZeroSSL 公式のリポジトリに、Let’s Encrypt の certbot をラップして ZeroSSL に向けるための zerossl-bot というスクリプトがあります。公式サイトでも紹介されている自動化手段のようです:
  • が、どうもリポジトリの更新が途絶えていて機能していない模様?
    • スクリプト自体は短いものですが、中身を覗くと渡す変数名が違っているぽかったりして怪しいです
    • そういう指摘の Pull Request も出ているようですがマージされていないまま
    • ためしに個人サーバでスクリプトを修正しつつ動かしてみたりもしましたが、 どうも ZeroSSL を見に行っていない感じの挙動でよくわからず…

Linux サーバに ZeroSSL を適用し、更新を自動化する方法

前置きが長くなりましたが、以下に実際に自分がやった、サーバに ZeroSSL を適用する方法を示します。 今回は Let’s Encrypt からの切り替えですが、まっさらな状態から https 対応をする場合でも同様の手順になります。

サーバの環境は CentOS 6 系、Web サーバは Apache です。

※「CentOS 6 ってもうサポート切れてるじゃん」みたいなのは今回は置いといてね!

※ 別の Linux ディストリビューションや nginx などのサーバでも、やることは大体同じですので参考にしてください。

(1) ZeroSSL にアカウント登録して EAB credential を発行

  • ZeroSSL の公式サイトで任意のメールアドレスを入力してアカウントを作成します
  • ZeroSSL のサイトにログインし、Developer の項目で EAB Credentials for ACME Clients の Generate ボタンを押します
    • ここで表示される KID と HMAC Key をメモっておきます

(2) サーバに入って acme.sh のインストール

  • 該当のサーバに ssh でログインします。
  • sudo でもどうにかなりそうですが、root になっちゃうのが無難でしょう (acme.sh のドキュメントでも root 推奨と書かれていました)
su -
  • acme.sh を取得します。メールアドレスには ZeroSSL アカウントのものを指定しておきましょう
curl https://get.acme.sh | sh -s email=xxxx@xxxx.xxx
  • これにより /root/.acme.sh/ 以下に関連ファイルが生成されます
    • (また、後述しますがこの時点で自動更新用の cron も指定されています)
  • .bashrc にパスを通す処理が追加されているので root で入り直して適用します
exit
su -

(3) acme.sh で使う認証局に ZeroSSL を指定

  • acme.sh でアカウント登録を行います。 xxxx の部分には先程メモっておいた KID / Key を指定します
acme.sh --register-account --server zerossl \
  --eab-kid xxxxxxxxxxxx \
  --eab-hmac-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • 無事 Registered 的なことを言われたら、使う認証局に ZeroSSL を指定します (現在はもともと ZeroSSL がデフォルトらしいが一応)
acme.sh --set-default-ca --server zerossl
  • 証明書を発行します。 xxxx の部分には適用するドメイン(このサイトで言うと tatsuya-koyama.com)を指定します
acme.sh --issue -d xxxx.xxxx.xxxx --apache

途中、処理待ちに 15 秒待たされたりしますが、無事発行されれば .acme.sh/ 以下に証明書ファイルが生成されます。

# 取得したファイルの確認
ls -al .acme.sh/xxxx.xxxx.xxxx/

(4) Apache 向けに証明書をインストール

  • インストールを行うと先程生成した証明書関連のファイルから、.pem ファイルが作られます
    • 先に保存先を用意しておきます。どこでもよいですが今回は Apache 用なので /etc/httpd/ あたりにディレクトリを切ります
mkdir /etc/httpd/ssl
  • acme.sh で証明書をインストールします。(指定したパスに .pem ファイルが生成されます)
    • reloadcmd には発行・更新後に実行するサーバの再起動コマンドを指定します
acme.sh --install-cert \
  --domain xxxx.xxxx.xxxx \
  --cert-file /etc/httpd/ssl/cert.pem \
  --key-file  /etc/httpd/ssl/key.pem \
  --fullchain-file /etc/httpd/ssl/fullchain.pem \
  --reloadcmd "sudo service httpd restart"
  • pem ファイルが作られたことを確認したら、これを見るように Apache の conf を書き換えます
# あなたのサーバの Apache の conf ファイルを開く
vim /etc/httpd/conf.d/xxxxxx.conf

# 以下のように編集
---------------------------------------------------
SSLEngine on
SSLCertificateFile      /etc/httpd/ssl/cert.pem
SSLCertificateKeyFile   /etc/httpd/ssl/key.pem
SSLCertificateChainFile /etc/httpd/ssl/fullchain.pem
---------------------------------------------------
  • サーバを再起動して適用します
service httpd restart
  • Web サイトなどであればブラウザでアクセスして証明書を確認してみましょう(URL 欄の左の鍵マークをクリック)
    • 以下のように ZeroSSL という表記があれば ZeroSSL で認証できています:

(5) 自動更新処理の確認と cron の処理時間の調整

  • 証明書の有効期限は 90 日間なので、期限が切れる前に更新する必要があります
  • 実は acme.sh のインストール時点でそのための cron が追加されています
# cron の確認
$ crontab -l
40 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
  • これが「更新が必要なタイミングだったら更新する」という内容の処理になっているので、 すでに更新自動化は施されています。 acme.sh は気が利きますね
    • 上記の cron は毎日 0 時 40 分に実行する設定です。 違う時間帯が良ければ crontab -e で編集しましょう
  • この処理が正常に作動するか確認したい場合は、 --force をつけると強制実行ができます
    • (期限が近づいていなくても証明書の更新処理を行います)
"/root/.acme.sh"/acme.sh --cron --force --home "/root/.acme.sh"
  • インストール時と同様に 15 秒くらい待たされて、pem ファイルが更新されサーバが再起動すれば問題ありません

以上が ZeroSSL の対応手順となります。

おわりに

今回の障害は

  • 2021-10-01 以降 / Let’s Encrypt / Unity からの通信 / 一部の古い Android

という、限定的な状況で発生する障害ではありましたが、 該当する環境で遭遇している人(特に個人開発者の人)は多少は居そうだなというのと、 同様のトラブルシューティングに関する情報が Web 上に無さそうだったので、 マニアックな障害ではありましたが今回の記事を書くことにしました。

同様の問題で困っている人を 1 人でも救えたら幸いです。