そのDockerイメージは本物か? cosignによる署名検証 #docker #cosign #sigstore

はじめに
2026年3月のTrivyサプライチェーン攻撃とDockerイメージのバージョンピニングでは、タグではなくダイジェストでイメージを固定する方法を紹介しました。続く固定したバージョン、どう更新する? Renovateで始める自動依存関係管理では、固定したバージョンを Renovate で更新し、リリースから 7 日間は採用を待つ設定を紹介しました。
ただし、この二つを組み合わせても、Renovate が提案してきた新しいダイジェストが、本当に配布元の作ったDockerイメージのものかどうかはわかりません。ダイジェストはDockerイメージの内容から計算するハッシュ値なので、攻撃者が盗んだ認証情報でレジストリへ直接プッシュしたDockerイメージにも、正しいダイジェストが付くからです。

本稿では、Dockerイメージを誰が作ったかを示す、署名の検証を試してみます。
署名で何を検証するのか
Dockerイメージの署名は、配布元が「このダイジェストのDockerイメージは私たちが作った」と表明するものです。利用者は、署名が配布元のものであることを確かめれば、そのDockerイメージが配布元の手を経たものだとわかります。
まず、誰が署名したのかを確かめます。署名の方式には、配布元が管理する鍵で署名する方式と、鍵を保管せずにCIのジョブのIDで署名する方式(キーレス署名)があり、それぞれ確かめ方が異なります。
次に、何を対象に署名しているかを確かめます。署名はダイジェストに対して行いますが、Dockerイメージの名前やタグを署名の対象に含めるかどうかは配布元によって異なります。

ここからは cosign 3.1.1 を使い、実際に署名を検証してみます。
鍵による署名
公開鍵の取得
DHI (Docker Hardened Images)は、Docker社の鍵で署名されています。公開鍵のURLはDockerドキュメントに記載されています。
% curl -sO https://registry.scout.docker.com/keyring/dhi/latest.pub
この公開鍵そのものには署名がないので、暗号的な検証ができません。代わりにDocker社が用意している別の配布場所からも同じ鍵を取得し、内容が一致することを確かめます。公開鍵はdhi.io と docker-hardened-images/keyring リポジトリでも配布されています。
% curl -sS -o dhiio.pub https://dhi.io/keyring/latest.pub % curl -sS -o github.pub https://raw.githubusercontent.com/docker-hardened-images/keyring/main/publickey/dhi-latest.pub % sha256sum latest.pub dhiio.pub github.pub 1d02bbccf149283ae6288d96264dcad3fb23ee1911d90324a48eab28e4cb8a5f latest.pub 1d02bbccf149283ae6288d96264dcad3fb23ee1911d90324a48eab28e4cb8a5f dhiio.pub 1d02bbccf149283ae6288d96264dcad3fb23ee1911d90324a48eab28e4cb8a5f github.pub
3か所の鍵が一致しました。なお、3か所はいずれもDocker社が管理しているので、すべて一致したことは改竄されていないことの証明にはなりません。この照合で気付けるのは、一部の配布場所だけが改竄された場合です。
DHI Trivyイメージの検証
この公開鍵でDHIのTrivyのDockerイメージを検証します。Dockerイメージと署名を dhi.io から取得するので、事前に docker login dhi.io しておきます。また、DHI は OCI の referrers(署名などをイメージに関連付け、レジストリ上でイメージからたどれるようにする仕組み)で署名を管理しているので、--experimental-oci11 を付けます。なお、cosign 3.1.1 ではこのフラグに非推奨だという警告が出ますが、これは将来のバージョンで referrers が既定の動作になるという予告です。3.1.1 では、このフラグを付けないと署名が見つからず、検証に失敗します。
% cosign verify --key latest.pub --experimental-oci11 -o json \
dhi.io/trivy@sha256:aab064575567d6fa11fb35f4a5f104d93289cc33a1824b5bd337de1cfa7c4841 > trivy.json
Flag --experimental-oci11 has been deprecated, OCI referrers will be the default behavior in future versions
Verification for dhi.io/trivy@sha256:aab064575567d6fa11fb35f4a5f104d93289cc33a1824b5bd337de1cfa7c4841 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The signatures were verified against the specified public key
公開鍵で署名を検証できました。trivy.json には、署名の対象(payload)が出力されています。
% jq '.[0] | {critical, tags: .optional.tags}' trivy.json
{
"critical": {
"identity": {
"docker-reference": "index.docker.io/dhi/trivy"
},
"image": {
"docker-manifest-digest": "sha256:aab064575567d6fa11fb35f4a5f104d93289cc33a1824b5bd337de1cfa7c4841"
},
"type": "cosign container image signature"
},
"tags": "dhi/trivy:0.74.0-alpine3.24-dev,dhi/trivy:0.74-alpine3.24-dev,(略)"
}
なお、署名の検証だけでは不十分です。Dockerfileに dhi.io/trivy:0.74.0-alpine3.24-dev@sha256:… のようにタグとダイジェストを併記すると、Dockerはタグを無視してダイジェストのDockerイメージを取得します。そのため、タグはそのままでダイジェストだけを古いバージョンのTrivyのものに書き換えられると、古いTrivyが使われます。古いバージョンもDocker社が正しく署名しているので、署名の検証そのものには成功してしまいます。

そこで、署名の対象に含まれているDockerイメージの名前(docker-reference)とタグの一覧(tags)も確かめます。
% jq -r '.[].critical.identity."docker-reference"' trivy.json index.docker.io/dhi/trivy % jq -r '.[].optional.tags' trivy.json | tr , '\n' | grep -x 'dhi/trivy:0.74.0-alpine3.24-dev' dhi/trivy:0.74.0-alpine3.24-dev
名前とタグも使いたいものと一致しました。
キーレス署名
キーレス署名とは
キーレス署名では、Dockerイメージの配布元は署名の鍵を保管しません。代わりにSigstoreプロジェクトの、CIのジョブのIDを確かめて証明書を発行する認証局のFulcioと、署名を記録する透明性ログのRekor の2つのサービスを利用します。これらのサービスは誰でも無料で使えますが、自前で運用することもできます。 cosign は特に指定をしなければパブリックのサービスを利用します。
Dockerイメージの配布元のCIのジョブは、署名のたびに使い捨ての鍵を作ります。そして、CIがジョブごとに発行するIDトークン(OIDC)をFulcioに提示し、ジョブのIDを記載した有効期間の短い証明書を受け取って、その鍵で署名します。
この署名はRekorに記録されます。Rekorは追記専用のログで、一度記録した内容は後から書き換えたり消したりできません。
利用者は、証明書に記載されたIDが期待する配布元のCIのものかを確かめます。証明書の有効期間は10分ほどしかないので、署名した時刻が有効期間内だったこともRekorの記録で確かめます。

kanikoイメージの検証
例として、Dockerイメージのビルドツールであるkaniko(osscontainertools/kaniko)のイメージを検証します。kanikoのDockerイメージはGitHub Actionsのリリースワークフローが署名しています。
% cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity https://github.com/osscontainertools/kaniko/.github/workflows/images.yaml@refs/tags/v1.29.0 \
--certificate-github-workflow-repository osscontainertools/kaniko \
--certificate-github-workflow-trigger push \
ghcr.io/osscontainertools/kaniko@sha256:350929376b8a69afb1d8b362086c809e4ef6799fa1bcb317c2806ae1447b3845 > /dev/null
Verification for ghcr.io/osscontainertools/kaniko@sha256:350929376b8a69afb1d8b362086c809e4ef6799fa1bcb317c2806ae1447b3845 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The code-signing certificate was verified using trusted certificate authority certificates
--certificate-oidc-issuer は、IDトークンの発行者がGitHub Actionsであることを指定しています。 --certificate-identity は、署名したのが osscontainertools/kaniko リポジトリの images.yaml というワークフローで、 v1.29.0 のタグで実行されたことを指定しています。 --certificate-github-workflow-repository はそのワークフローを起動したリポジトリ、 --certificate-github-workflow-trigger は起動のトリガーが push であることを指定しています。
証明書に記載されたIDにはタグが含まれるので、タグとの対応も確かめられます。 v1.29.0 のダイジェストを v1.28.0 のタグのワークフローが署名したものとして検証すると、失敗します。
% cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity https://github.com/osscontainertools/kaniko/.github/workflows/images.yaml@refs/tags/v1.28.0 \
ghcr.io/osscontainertools/kaniko@sha256:350929376b8a69afb1d8b362086c809e4ef6799fa1bcb317c2806ae1447b3845 > /dev/null
Error: no matching attestations: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "https://github.com/osscontainertools/kaniko/.github/workflows/images.yaml@refs/tags/v1.28.0", got "https://github.com/osscontainertools/kaniko/.github/workflows/images.yaml@refs/tags/v1.29.0"
(略)
署名で確かめられないこと
配布元の正規のパイプラインが悪用された場合と、検証に使う公開鍵や cosign 自体が正しいかどうかは、署名の検証では確かめられません。
正規のパイプラインが悪用された場合
署名の検証で確かめられるのは「配布元の正規のパイプラインが作った」ことまでで、そのパイプラインが悪用されたかどうかはわかりません。
2026年3月のTrivyのサプライチェーン侵害事件(Trivy ecosystem supply chain temporarily compromised)では、正規のパイプラインを経ずに作られた 0.69.5 、 0.69.6 と、正規のパイプラインが悪用されて作られた 0.69.4 の両方がありました。
TrivyのDockerイメージは、リリースワークフローがキーレス署名しています。 0.69.5 と 0.69.6 は、攻撃者が盗んだ認証情報を使って直接Docker Hubにプッシュしたもので、正規のリリースワークフローによる署名はされていなかったはずです。そのため利用者は署名を検証することで、これらの侵害されたDockerイメージを拒否できたはずです。
一方 0.69.4 は、攻撃者が悪意のあるコミットを正規のパイプラインを経てリリースしたものです。Dockerイメージの中身には悪意のあるコードが含まれていますが、正規のパイプラインが署名しているので、署名の検証では検出できません。

0.69.4 のような悪意あるイメージが正規のパイプラインを経て署名されてリリースされた場合の備えとなるのが、リリースされてから実際に利用するまで一定の期間を待つ、クールダウン期間の設定です。もし悪意あるリリースがあったとしても、利用する前に取り下げられることが期待できます。
署名の検証とクールダウン期間の設定は別々の経路の改竄に対応しているので、どちらか片方では不十分です。
信頼の起点をどこに置くか
署名の検証は、検証に使う公開鍵と cosign が正しいことを前提にしています。
今回のDHIの検証では、複数の配布場所の公開鍵を照合しましたが、すべての配布場所が侵害されていれば気付けません。最終的には、配布場所をDocker社が十分に管理していることを信頼することになります。
cosign についても、それ自体が改竄されていれば検証結果が信頼できません。 cosign もSigstoreで署名されていますが、自分自身を検証しても正しいことの裏付けにはなりません。例えば、どんな検証にも成功と答えるように改竄された cosign ならば、自分自身の検証にも成功と答えてしまうでしょう。
そうすると cosign を検証する別のツールが必要で、そのツールを検証するにはさらに別のツールが必要…となるときりがありません。どこかで検証を打ち切り、その時点を信頼の起点にするしかありません。例えば次のようなものが考えられます。
cosignをOSのパッケージなど、別の署名の仕組みを持つ経路で入手する。信頼の起点はOSの配布元の署名用の鍵などになる。- 別の実装のSigstoreクライアント(sigstore-python など)で
cosignの署名を検証する。二つの実装が同時に侵害されない限り、改竄を検出できる。
どちらも、より侵害されにくいと判断できる場所を、信頼の起点とするものです。
まとめ
本稿では、他人が作ったDockerイメージについて、改竄されていないことを署名で確かめる方法を見てきました。
ダイジェストによる固定に加えて署名を検証すると、そのダイジェストのDockerイメージが配布元の正規のパイプラインで作られたことを確かめられます。その際は、誰が署名したのかに加えて、署名の対象にDockerイメージの名前とタグが含まれるかを確かめる必要があります。
署名の検証は、正規のパイプラインを経ずに作られたDockerイメージを拒否できますが、正規のパイプラインの悪用は検出できないので、リリースから一定の期間を待つクールダウン期間の設定などで対応します。
また、公開鍵や cosign そのものが改竄されていないか確認するには、どこか別のものを信頼の起点とする必要があります。
なお、署名で確かめられるのは「誰がどこからビルドしたか」という出自であり、「中身が安全か」ではありません。悪意のあるコードがソースコードや依存パッケージに含まれていたとしても、配布元の正規のパイプラインを通すことができれば、正しい署名がなされてしまいます。中身の安全性は脆弱性スキャンなどで対策する必要があります。
本稿がDockerイメージの改竄といったサプライチェーン攻撃への対策を考える一助となれば幸いです。
