署名は正しい。ただしその鍵はこちらが失効させた
一言でいうと
署名が保証するのは、「このバイト列がその鍵で署名された」ということだけです。残りの半分、その鍵を今も信頼しているかは、署名アルゴリズムの外で別に管理する必要があります。
なぜ必要なのか
「成果物に署名を付けました」という文は、思ったより何も語っていません。保証は、署名を付ける場所ではなく、検証が失敗する場所で生まれるからです。そのため、署名の仕組みを導入するときに最初にやることは、失敗するべき場合をすべて書き出して、1つずつ実際に失敗させてみることです。
一覧を書き出してみると、たいてい4行になります。成果物が変わった場合、署名が自分たちの鍵ではない場合、自分たちの鍵だが取り消した、または期限が切れた場合、そしてそもそも知らない鍵である場合です。前の2つは、署名アルゴリズムが自動で見つけてくれます。後ろの2つは見つけてくれません。生の鍵ペアには期限がなく、「この鍵は私たちのものか」という質問に答えるための資料が、鍵の中に入っていないからです。
どう動くのか
署名は、ファイル全体ではなくダイジェストに掛けます。ハッシュを先に求めて、その値に秘密鍵で署名するので、成果物が1バイト違うだけでもハッシュがまるごと変わり、同じ署名では合わせられません。そのため、改ざんの検知は、事実上ただで付いてきます。
問題は鍵です。鍵を自分たちで持っていると、その鍵を誰がどこに置いたのか、漏えいしたらどう気づくのか、交換するときに何を再署名する必要があるのかが、すべて私たちの宿題になります。sigstoreがこの宿題を別の方法で解いた理由が、ここにあります。基本の流れであるIDベースの署名では、メモリの中で一時的な鍵ペアを作り、認証局がOpenID ConnectのIDトークンを確認したあとで、そのIDを公開鍵に結び付けた短命な証明書を発行します。署名が終わるとすぐに秘密鍵は破棄され、証明書も期限が切れます。検証する側は、破棄された鍵の代わりに、透明性ログに残った、時刻が記録された項目を見て判断します(sigstoreの署名の概要)。
鍵を自分で管理したいなら、cosignはKMSを通してそれができるようにしています。AWS KMS、GCP KMS、Azure Key Vault、HashiCorp Vault、Kubernetes Secretのようなプロバイダーを、awskms://、gcpkms://、k8s://のようにURIで指し、cosign public-key --key <URI>で公開鍵を取り出して、検証に使います(cosignの鍵管理)。検証の側では、cosign verifyが、署名だけでなく、署名の証明書が信頼する認証局から出たものかまで確認すると、ドキュメントに書かれています(cosignの検証)。
どの道を行っても、残る原則は同じです。「この署名は正しいか」と「この鍵を今信頼しているか」は別の質問であり、両方を問う必要があります。自分で管理するなら、どの鍵をいつからいつまで信頼するかを書いたキーリングが必要で、sigstoreを使うなら、証明書の寿命と透明性ログが、その代わりになります。
現場での姿
最も高くつく失敗は、検証はするのに、失敗を握りつぶすことです。パイプラインで検証コマンドの終了コードを見ない、あるいはエラーをログに残すだけで次のステップに進む形です。この状態は、署名を付けていないより悪いです。誰もが保護されていると信じているからです。
2つ目は、鍵の交換のときに表に出ます。新しい鍵に移りながら、古い鍵をキーリングから消さないと、取り消した鍵で署名された成果物が、通り続けます。反対に、古い鍵をそのまま消してしまうと、まだデプロイされていない以前のリリースが、すべて止まります。そのため、消す代わりに期限を書くほうが、運用で生き残ります。
3つ目は、署名の対象がぼやけていることです。何に署名したかが署名ファイルの中に入っていないと、検証する側は、「この署名があのファイルのものだ」という事実を、ファイル名で推測することになります。リリースが複数あるディレクトリでは、この推測はすぐに外れます。そのため、実務では、署名を成果物に直接掛けるよりも、対象とそのダイジェストを書いたドキュメントに署名して、そのドキュメントを一緒に配布する方向に行きます。次のモジュールの来歴証明が、まさにそのドキュメントです。
4つ目は、秘密鍵をビルドの段階で読める状態に置くことです。ビルドスクリプトが鍵に触れられるなら、そのビルドで動くどんなコードでも鍵に触れられ、依存1つが汚染された瞬間に、署名そのものが無意味になります。鍵をKMSの裏に置く、あるいはそもそも一時的な鍵を使う設計が、この問題を正面から狙ったものです。
5つ目は、署名を作ったあとに検証を一度もしてみないことです。署名のコマンドはたいてい静かに成功するので、ハッシュアルゴリズムを別々に指定していたり、見当違いの鍵で署名していたりしても、その場では何も起きません。問題は、数週間後のデプロイパイプラインで、それも急ぐ日に表に出ます。署名した直後に、同じ場所で検証まで実行してみる1行が、この事故をまるごとなくします。
次のラボですること
鍵ペアを作って成果物に署名し、検証が失敗するべき4つの場合を自分で作ってみます。改ざんしたもの、取り消した古い鍵で作った問題のない署名、キーリングにない鍵、そして正常な成果物です。最後に、キーリングを読んで期限まで確認する検証器を作り、4つの場合を一度に分けます。