macOS版cURLはcURLと証明書検証の仕様が異なる
目次
はじめに
2023年12月に「cURLの"--cacert"オプション利用時の挙動がmacOSとLinuxで異なる」という内容のissueが立ち、2024年3月にcURLの開発者であるDaniel Stenberg氏が「THE APPLE CURL SECURITY INCIDENT 12604」というタイトルで記事を公開しました。
この記事では本件に関する詳細の説明と検証、また独自の掘り下げを行い、macOS版cURLとオリジナルのcURLの違いについて解説します。ただし、本件は明らかになっていない部分が多くあるため、本記事には推測や仮説が含まれます。ご了承ください。
--cacertオプションにおけるmacOSのcURLとオリジナルのcURLの違い
まずはDaniel Stenberg氏の記事で述べられた内容を元に、"--cacert"オプションにおけるmacOS版cURLとオリジナルのcURLの違いについて説明します。cURLコマンドには"--cacert"オプションを利用して証明書検証に利用する証明書リストをこちらから指定することができます。このオプションを使うと、その他の証明書リストは利用されないようになります。しかし、記事によるとmacOSのcURLでは"--cacert"オプションを利用していても、証明書の検証に失敗した場合、フォールバックしてシステムの証明書ストアを使って再度検証を行うようになっているようです。
挙動検証
まずは検証に用いる証明書を作成します。
# Country Nameなどを聞かれますがすべてエンターで問題ないです
$ openssl genrsa -out key.pem 2048 && openssl req -new -x509 -days 365 -key key.pem -out cert.pem
次にこの証明書を利用してUbuntuとmacOSの両環境にてリクエストを行ってみます。先程生成した証明書はどこのCAの証明書でもなく、ただの文字列が書かれたファイルと同じです。これを使って証明書チェーンの検証を行おうものなら当然失敗するはずです。
# Ubuntu
$ curl --cacert cert.pem --capath . -v https://www.example.com -o /dev/null
...
* CAfile: cert.pem
* CApath: .
...
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.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.
# macOS
$ curl --cacert cert.pem --capath . -v https://www.example.com -o /dev/null
...
* CAfile: cert.pem
* CApath: .
...
SSL certificate verify ok.
...
HTTP/2 200
出力が多いので詳細は省略していますが、結果を見るとUbuntuでのリクエスト時は検証に失敗し、macOSの場合は証明書の検証に成功して200のレスポンスを受け取っていることが確認できます。
オプションを利用しない場合の挙動も異なる
Daniel Stenberg氏の記事では"--cacert"オプションを利用した場合の挙動について述べられていますが、実際には"--cacert"オプションを利用しない場合でもmacOS版cURLとオリジナルのcURLの挙動に違いがあることが確認できました。(なおこの挙動については、curlのオンラインマニュアルにも記載されています)
cURLは"--cacert"オプションを利用しない場合、組み込みの証明書リストを利用して証明書検証を行います。OSによって場所が異なるようですが、macOSの場合は"/etc/ssl/cert.pem"がCA証明書のデフォルトの場所となっており、Ubuntuの場合は"/etc/ssl/certs/ca-certificates.crt"がデフォルトの場所となっています。デフォルトの場所は"v"オプションを用いてリクエストを行うと確認することができます。
# Ubuntu
$ curl -v https://www.example.com
...
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
...
# macOS
$ curl -v https://www.example.com
...
* CAfile: /etc/ssl/cert.pem
* CApath: none
...
しかし、この"/etc/ssl/cert.pem"を先程作成した証明書で上書きしても、macOS版cURLでは証明書の検証に失敗せず、リクエストが成功してしまうことが確認できます。
# バックアップを取っておく
$ sudo mv /etc/ssl/cert.pem /etc/ssl/cert.pem.bak
# 先程作成したcert.pemを移動させる
$ sudo mv cert.pem /etc/ssl/cert.pem
# 何故か検証に成功する
$ curl -v https://www.example.com -o /dev/null
...
* CAfile: /etc/ssl/cert.pem
* CApath: none
...
* SSL certificate verify ok.
この挙動についてはcurlのmanにおいて"--cacert"オプションの項目に記載されています。
中間証明書が設定されていないWebサイトへのリクエスト時の挙動の違い
上記に関連して挙動の異なる部分がもう1つあります。中間証明書が正しく設定されていないWebサイトへアクセスした場合の挙動です。
※ TLSに関する証明書チェーンの話は別の記事で詳しく書くので省略させていただきます。
中間証明書が正しく設定されていないWebサイトに対して、UbuntuのcURLコマンドやcURLコンテナを利用してリクエストした場合では証明書に関連したエラーが発生しました。しかし、macOS版のcURLでは中間証明書が設定されていないサーバーへリクエストを行っても証明書エラーが発生しませんでした。
以下のようにvオプションを利用して、リクエストを行った際のハンドシェイクのログを確認しました。するとUbuntuではサーバー証明書の発行元を検証する際にエラーが発生していますが、macOSでは「SSL certificate verify ok.」と表示されており、証明書の検証に成功していることがわかります。
※ 中間証明書が正しく設定されていないWebサイトのドメインを"missed.example.com"とします
# Ubuntuでの検証
$ curl -v https://missed.example.com
...
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
} [2 bytes data]
* SSL certificate problem: unable to get local issuer certificate
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.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.
# macOSでの検証
$ curl -v https://missed.example.com -o /dev/null
...
* CAfile: /etc/ssl/cert.pem
* CApath: none
...
* SSL certificate verify ok.
...
本来はエラーになるのが正しい挙動であるため、macOS版cURLはオリジナルのcURLと異なる挙動をしていると言えます。
何故挙動が異なるかの考察
この挙動はcURLの挙動の違いというより、証明書検証に利用されるシステムの違いによる影響だと思われます。先ほど「オプションを利用しない場合の挙動も異なる」にてmacOSのcURLが恐らくキーチェーン等の証明書に関するシステムを利用している可能性について述べましたが、このシステムが中間証明書の自動取得を行う仕組み、または中間証明書の信用リストのようなものを持っている可能性があります。
本来中間証明書がサーバーから送信されない場合は証明書検証に失敗するのですが、ブラウザ等では中間証明書を正しく設定していないサイトでもエラーにならないように中間証明書の信用リストを持っていたり、AIA fetchingといって中間証明書を補完する仕組みがあります。このどれかがmacOSのキーチェーン等の証明書に関連したシステムにも実装されている可能性があります。
後述しますがmacOSのcURLではLibreSSLを利用しており、AppleはこのLibreSSLに対してパッチを当てている可能性が高いです。上記で述べた話がLibreSSLに実装されている可能性もありますし、キーチェーンに実装されており、それをLibreSSLから呼び出すことでmacOS版cURLが中間証明書の補完を行っている可能性もあります。この辺はさらなる調査が必要です。
セキュリティ的な問題点
本来想定していた動作をしないという問題点
「本来このように動作すると思い込んでいたら実は違った」ということはセキュリティの問題に繋がります。開発環境ではmacOSを使っており、「テストしても問題なかったので本番環境にデプロイしたら各所で問題が発生した」となる可能性もあるでしょう。特にWebサーバーにおける中間証明書の設定忘れはよく見かけます。macOSのcURLを使っていると、中間証明書の設定を忘れてもエラーが発生しないため、証明書の設定漏れに気付かない事があります。最近のブラウザでは中間証明書の補完が行われるため、このような状況でも問題なくアクセスできますが、プロトコルに準拠したソフトウェアではエラーが発生してしまい、ビジネスに影響がでる可能性も考えられます。
LibreSSLへパッチを当てている可能性
GitHubのissueにてAppleはどうやらLibreSSLにパッチを当てているようであることが指摘されています。Appleは一部のソフトウェアをOSSとして公開しておりcURLも同様に公開されています。cURLに独自のパッチを当てていれば本来リポジトリから変更内容が確認できるはずです。しかし、今回のAppleがcURLに対してどのような変更を行っているかはソースコードレベルでは明らかになっていません。というのも、AppleはcURLが内部で利用しているLibreSSLというOpenSSLの互換性を持つライブラリにパッチを当ててている可能性が高いようです。実際に以下のように確認すると、macOS版cURLはLibreSSLを利用していることがわかります。
$ curl --version
curl 8.6.0 (x86_64-apple-darwin23.0) libcurl/8.6.0 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.61.0
Release-Date: 2024-01-31
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM NTLM_WB SPNEGO SSL threadsafe UnixSockets
もしかするとmacOS上で「最初からインストールされていて、LibreSSLを内部で利用しているコマンドやシステム等」も本来と異なる動作をしてしまう可能性があるかもしれません。現状はそういった事案は見かけていませんが、このような事例があることを頭の片隅においておくほうがよいかもしれません。
解決方法
MacPortsやHomebrewからインストールする
MacPortsやHomebrewなどのパッケージ管理システムを利用してcURLをインストールすると、オリジナルのcURLがインストールされるため、macOS版cURLの問題を回避することができます。
curl --version
curl 8.8.0 (aarch64-apple-darwin23.4.0) libcurl/8.8.0 (SecureTransport) OpenSSL/3.3.1 zlib/1.2.12 brotli/1.1.0 zstd/1.5.6 libidn2/2.3.7 libssh2/1.11.0 nghttp2/1.61.0 librtmp/2.3 OpenLDAP/2.6.8
Release-Date: 2024-05-22
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz MultiSSL NTLM SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd
LibreSSLではなくOpenSSLが利用されていることも確認できます。
環境変数を設定する
GitHubリポジトリのissueに解決方法のコメントが記載されていました。以下のように環境変数に設定すると、挙動が異なる部分が解消されるようです。実際に試してみると、これまでエラーにならなかった動作が軒並み証明書のエラーを出してくれるようになりました。
$ export OPENSSL_X509_TEA_DISABLE=1
挙動の違いで困りたくない人は".zshrc"などの設定ファイルに記述しておき、シェル起動時に自動で設定されるようにしておくと良いでしょう。
ソースコードからビルドする
cURLのソースコードをダウンロードして自前でビルドすることで、macOS版cURLの問題を回避することができます。
終わりに
調査をするにつれて記事がだんだん長くなってしまいました。調査は今後も行うので、新たな情報があれば追記していきたいと思います。この記事がどこかの誰かの役に立てれば幸いです。また、本記事を書いているとTECH+にて日本語の記事が掲載されていることを確認し、参考にさせていただきました。こちらも良ければご覧ください。