Goはどのようにサプライチェーン攻撃を軽減するか
How Go Mitigates Supply Chain Attacks by Filippo Valsorda
現代のソフトウェアエンジニアリングは共同作業によって成り立っており、オープンソースソフトウェアの再利用を基盤としています。これにより、依存関係を侵害することでソフトウェアプロジェクトを攻撃する、サプライチェーン攻撃の標的にさらされることになります。
どのようなプロセスや技術的対策を講じたとしても、あらゆる依存関係は避けられない信頼関係です。しかし、Goのツールと設計は、さまざまな段階でこのリスクを軽減する助けとなります。
すべてのビルドは「ロック」されている
依存関係の新しいバージョンが公開されるといった外部世界での変化が、Goのビルドに自動的に影響を与える手段はありません。
多くの他のパッケージマネージャーのファイルとは異なり、Goモジュールには、制約を記述する別のリストと、特定のバージョンを固定するロックファイルという分離した仕組みはありません。あるGoのビルドに寄与するすべての依存関係のバージョンは、メインモジュールのgo.modファイルによって完全に決定されます。
Go 1.16以降、この決定性はデフォルトで強制されており、go.mod が不完全な場合、ビルドコマンド(go build、go test、go install、go run など)は失敗するようになっています。go.mod(ひいてはビルド)を変更する可能性があるコマンドは go get と go mod tidy だけです。これらのコマンドは自動的にあるいはCI上で実行されることを想定していないため、依存関係のツリーへの変更は意図的に行われ、コードレビューを経る機会が確保されます。
これはセキュリティ上重要です。CIシステムや新しいマシンが go build を実行するとき、チェックインされたソースこそが、何がビルドされるかについての最終的かつ完全な真実の情報源(source of truth)だからです。第三者がそれに影響を与える手段はありません。
さらに、go get で依存関係を追加すると、その推移的な依存関係(transitive dependencies)は、最小バージョン選択(Minimal version selection)のおかげで、最新バージョンではなく、その依存関係自身の go.mod ファイルで指定されたバージョンで追加されます。go install example.com/cmd/devtoolx@latest の呼び出しでも同様のことが起こります。いくつかのエコシステムにおける同等の操作はバージョン固定を迂回してしまいます。Goでは、example.com/cmd/devtoolx の最新バージョンが取得されますが、そのすべての依存関係はそのモジュール自身の go.mod ファイルによって設定されます。
あるモジュールが侵害され、悪意のある新しいバージョンが公開されたとしても、そのモジュールを利用している人が明示的にその依存関係を更新するまで、誰にも影響が及びません。これにより、変更内容をレビューする機会と、エコシステムがその出来事を検知するための時間が確保されます。
バージョンの内容は決して変化しない
第三者がビルドに影響を与えられないようにするために必要な、もう一つの重要な性質は、モジュールのあるバージョンの内容が不変(immutable)であることです。もし依存関係を侵害した攻撃者が既存のバージョンを再アップロードできてしまうなら、それに依存するすべてのプロジェクトを自動的に侵害できてしまうでしょう。
これがgo.sumファイルの役割です。go.sumには、ビルドに寄与する各依存関係の暗号学的ハッシュ値の一覧が含まれています。ここでも、go.sum が不完全であればエラーになり、これを変更できるのは go get と go mod tidy だけなので、go.sum への変更は必ず意図的な依存関係の変更に伴って発生します。それ以外のビルドでは、完全なチェックサムの集合が保証されています。
これはほとんどのロックファイルに共通する機能です。Goはさらにその先を行き、チェックサムデータベース(Checksum Database、略してsumdb)というものを用意しています。これは、go.sumのエントリからなる、グローバルで追記専用のかつ暗号学的に検証可能な一覧です。go get が go.sum ファイルにエントリを追加する必要があるとき、sumdbの完全性の暗号学的な証明とともに、そのエントリをsumdbから取得します。これにより、あるモジュールのすべてのビルドが同じ依存関係の内容を使うだけでなく、世の中に存在するすべてのモジュールが同じ依存関係の内容を使うことまでもが保証されます!
sumdbのおかげで、侵害された依存関係や、あるいはGoogleが運用するGoのインフラストラクチャでさえも、特定の依存先を狙って改変された(たとえばバックドアが仕込まれた)ソースを標的にすることは不可能です。たとえば example.com/modulex のv1.9.2を使っている他の誰もが使っていて、かつレビュー済みであるのとまったく同じコードを使っていることが保証されています。
最後に、私がsumdbの中で特に気に入っている点を挙げます。モジュールの作者側に鍵管理を一切要求しないこと、そしてGoモジュールの分散的な性質とシームレスに協調して動作することです。
VCSこそが真実の情報源である
ほとんどのプロジェクトは何らかのバージョン管理システム(VCS)を通じて開発され、その後、他のエコシステムではパッケージリポジトリにアップロードされます。これは、侵害されうるアカウントが2つ存在することを意味します。VCSホストとパッケージリポジトリです。後者はより使用頻度が低く、見落とされやすいものです。また、これは、リポジトリにアップロードされるバージョンに悪意のあるコードを隠しやすいことも意味します。特に、たとえば縮小(minimize)のためなど、アップロードの一環としてソースが日常的に変更されている場合はなおさらです。
Goには、パッケージリポジトリのアカウントというもの自体が存在しません。パッケージのインポートパスには、go mod download がモジュールをVCSから直接取得するために必要な情報が埋め込まれており、VCS上ではタグがバージョンを定義しています。
Go Module Mirrorは確かに存在しますが、それは単なるプロキシに過ぎません。モジュールの作者は、アカウントを登録することも、プロキシにバージョンをアップロードすることもありません。このプロキシは、go ツールが使うのと同じロジックを使ってバージョンを取得しキャッシュしています。実際、プロキシは内部で go mod download を実行しています。チェックサムデータベースが、あるモジュールのバージョンについてソースツリーがただ一つしか存在しえないことを保証しているため、プロキシを使う人は誰でも、プロキシを介さずVCSから直接取得する人と同じ結果を目にすることになります。(もしそのバージョンがすでにVCS上で入手できなくなっていたり、その内容が変更されていたりすると、直接取得は失敗する一方で、プロキシからの取得はそれでも成功することがあり、可用性を高め、エコシステムを「left-pad」問題から守ることにもつながります。)
クライアント側でVCSツールを実行することは、かなり大きな攻撃対象領域(attack surface)にさらされることを意味します。ここでもGo Module Mirrorが役に立ちます。プロキシ上の go ツールは堅牢なサンドボックスの中で動作し、あらゆるVCSツールをサポートするように設定されている一方、デフォルトでは主要な2つのVCSシステム(gitとMercurial)のみをサポートしています。プロキシを使えば、デフォルトでは無効になっているVCSシステムを使って公開されたコードも引き続き取得できますが、ほとんどのインストール環境では攻撃者はそのコードに到達できません。
コードをビルドしても実行はされない
コードの取得もビルドも、それがたとえ信頼できない悪意のあるコードであっても、そのコードを実行させることはない、というのがGoツールチェインの明確なセキュリティ設計上のゴールです。これは他の多くのエコシステムとは異なる点であり、それらの多くはパッケージ取得時にコードを実行する機能を第一級のものとしてサポートしています。こうした「post-install」フックは、過去において、侵害された依存関係を侵害された開発者マシンへと変える最も手軽な方法として、またモジュールの作者を経由してワームのように広がる方法として使われてきました。
公平を期すために言うと、コードを取得するのは多くの場合、開発者マシン上のテストの一部として、あるいは本番環境のバイナリの一部として、その直後に実行するためであることが多いので、post-installフックがないことは攻撃者を遅らせるだけに過ぎません。(ビルドの内部にセキュリティ境界はありません。ビルドに寄与するどのパッケージも init 関数を定義できます。)しかし、実行しようとしているバイナリやテストしようとしているパッケージが、そのモジュールの依存関係のうち一部のサブセットしか使っていない場合もあるため、これは意味のあるリスク軽減策になりえます。たとえば、macOS上で example.com/cmd/devtoolx をビルドして実行する場合、Windows専用の依存関係や example.com/cmd/othertool の依存関係が手元のマシンを侵害する手段はありません。
Goでは、ある特定のビルドにコードを提供しないモジュールは、そのビルドに対してセキュリティ上の影響を一切持ちません。
「少しのコピーは、少しの依存関係よりまし」
Goのエコシステムにおける最後の、そしておそらく最も重要なソフトウェアサプライチェーンのリスク軽減策は、最も技術的でないものです。それは、Goには大きな依存関係のツリーを拒み、新しい依存関係を追加するよりも少しのコピーを好む、という文化があるということです。これは、Goの格言の一つ、「少しのコピーは、少しの依存関係よりましだ(a little copying is better than a little dependency)」にまで遡ります。「ゼロ依存(zero dependencies)」というラベルは、高品質で再利用可能なGoのモジュールが誇りを持って掲げるものです。あるライブラリを必要としているとき、それによって他の作者や所有者による何十もの別のモジュールへの依存を抱え込むことにはならない、ということに気づくはずです。
これは、HTTPスタックやTLSライブラリ、JSONエンコーディングといった、よく使われる高レベルの構成要素を提供する、豊富な標準ライブラリと追加モジュール群(golang.org/x/... のもの)によっても可能になっています。
これらすべてを合わせると、ごく少数の依存関係だけで、豊かで複雑なアプリケーションを構築できるということを意味します。ツールがどれほど優れていても、コードを再利用することに伴うリスクを完全になくすことはできません。だからこそ、常に最も強力な軽減策は、小さな依存関係ツリーであり続けるのです。
By Filippo Valsorda