|
午前2時、静かな部屋。子供は眠り、パートナーは随分前から寝ているのに、まだソファであなたを待っています。テレビの明かりはまだチラチラと目の中で揺れています。あなたはすっかり疲れ果てています。今夜の進捗に満足し、過去数時間の成果を含むコードをコミットします。「[master 2e4fd96] セキュリティ脆弱性 CVE-123 を修正しました」。夜明け後の重要なリリース前に、他のユーザーが変更内容を確認したりコメントしたりできるように、更新内容をホストサーバーにアップロードします。コンピューターをスタンバイ状態にし、パートナーを揺り起こして寝かしつけます。電気を消そうとしましたが、寝室でおもちゃにつまずいてしまい、お気に入りのおもちゃの音が聞こえたので、子供のためにミルクを作らなければなりません。 ひどい睡眠不足の4時間があっという間に過ぎ、スマホのバイブレーションで目が覚めた。顔を撫でながらアラームのことを考え、ベッドから起き上がり、ベッドサイドテーブルを閉めた。(しまった!また子供たちを起こしてしまったかも。)スマホを手に取ると、イカれた同僚が「変更したコードをマージしたんだけど、この場所にラベルが必要だよ」と声をかけてきた。うわあ! あなたを起こしたパートナーは、泣いている子供の面倒を見てあげるように言い(まあ、順調に進んでいるんだけどね!)、よろめきながらパソコンに向かい、苦労してパスワードを入力し、目をこすって変更をダウンロードした。 目を細めて周囲を見渡すと、まるで洪水のように変化が押し寄せてくる。部屋は子供の甲高い泣き声で満たされ、パートナーは弱々しい力で自制しようと奮闘する。`git log --pretty=short...` すべて順調に見える。同僚とあなたのコミットがブランチにマージされた。テストパッケージを実行すると、すべてパスする。準備は万端のようだ。`git tag -s 1.2.3 -m '重大なCVE-123を含むさまざまな修正' && git push --tag`。秘密鍵にパスワードを入力するのに苦労した後、ゆっくりと椅子から立ち上がり、子供たちの世話を手伝いに駆け出す(なんてことだ、このソースコードはどこにしまったんだ?)。あとはCIシステムが処理してくれる。 2か月早送りします(2か月後)。 CVE-123パッチは長らく適用され、正常に展開されてきました。しかし、同僚から怒りの返信が届きました。どうやら、あなたの主要ユーザーの一人に重大なセキュリティ脆弱性があるようです。同僚が問題を調査した結果、根本原因が判明しました。ログ履歴によると、この脆弱性はあなたが作成したバックドアを悪用するものでした!何だって?こんなこと、思いもよらなかった。さらに悪いことに、あなたはGPGキーを使ってパッチ1.2.3に署名し、タグ付けが有効で準備が整っていることも確認しました。「3-bc-4-2-b、この野郎」と同僚は皮肉を込めて言いました。「どうもありがとう」 いや、それは意味がない。あなたはすぐにコミット履歴を確認する。`git log --pat ch 3bc42b. 「X、Y、Zの不足しているドキュメント文字列を追加してください。」` 困惑した表情で手を挙げ、特に期待もせずにスペースバーを軽く数回タップする。案の定、いくつかの小さなドキュメント文字列が変更され、ごく些細なコード行も修正され、認証システムにバックドアが追加されていた。コミットメッセージには、このことがはっきりと示されており、特に警告は出ていない。なぜ確認しなかったんだ? それに、コミットの作者はあなただ! 頭の中は混乱しています!どうしてこんなことが起きたのでしょう?コミットはあなたの名前で行われたのに、変更を加えた記憶がありません。そして、そのコード行は変更していないような気がしますが、どうも腑に落ちません。同僚があなたにコミットを仕組んだのでしょうか?それとも、同僚のシステムが不正アクセスされたのでしょうか?それとも、あなたのコンピュータが不正アクセスされたのでしょうか?コミットはあなたのローカルストレージには存在しません。コミットは明らかにマージ部分であり、2ヶ月前のあの朝まで遡るまではローカルストレージには存在しませんでした。 何が起こったにせよ、極めて恐ろしく明白なことが一つあります。それは、今、責められているのはあなただということです。 あなたは誰を信じますか? 好きなように仮説を立ててみてください。リポジトリが侵害された原因が何なのか、永遠にわからないかもしれません。上記の話は完全に架空のものですが、あり得る話です。では、リポジトリを参照したりクローンしたりするプログラマー、そしてリポジトリをダウンロードする可能性のある人(例えば、リポジトリから作成された圧縮ファイルなど)にとって、リポジトリが安全であることを、時間のある時にどのように保証できるでしょうか? Gitは分散型バージョン管理システムです。簡単に言えば、誰でもあなたのリポジトリのプライベートコピーを持ち、オフライン作業に利用できます。各自のリポジトリにバージョンをコミットし、互いにプッシュ・プルできます。分散型バージョン管理システムにとって中央リポジトリは必須ではありませんが、他のプログラマーが作業内容を送信したりクローンしたりできる「公式」中央リポジトリとして機能します。つまり、プロジェクトXの広く流通しているリポジトリには悪意のあるコードが含まれている可能性があり、誰かがあなたにプロジェクトリポジトリを提供したからといって、それを実際に使用すべきとは限りません。 問題は「誰を信頼できるか?」ではなく、「誰を信頼するのか?」です。あるいは、あなたはリポジトリを使い慣れていないので、そのことに気づいていないかもしれません。上記の事例を含め、ほとんどのプロジェクトでは、多くの個人や組織が、十分に検討されていない意思決定の枝葉に無意識のうちに信頼を置いています。
つい最近(2012年3月4日)、GitHubの公開鍵セキュリティの脆弱性が、Egor Homakovというロシア人によって悪用されました。この脆弱性により、彼はGitHub上のRuby on Railsフレームワークのメインブランチにコードをコミットすることができました。
ネットワークが安全で「クリーン」だと仮定しましょう。例えば、腹を立てた従業員が、傲慢で迷惑な同僚の名前/メールアドレスを入手し、それを使って何かを送信したとします。もしあなたがマネージャーやプロジェクトリーダーだったら、誰を信じますか?誰を疑いますか?
コンピュータは開発だけでなく、Webの閲覧やソフトウェアのダウンロードにも使われます。すべての開発者がオペレーティングシステムのセキュリティを深く理解しているわけではありません。さらに、GNU/Linuxやその他の*NIXベースのシステムを使用しているだけでは、潜在的な脅威から逃れられるわけではありません。 もう少し深く掘り下げるために、世界最大のオープンソースソフトウェアプロジェクトであるLinuxカーネルの開発者、リーナス・トーバルズ氏が信頼の問題にどのように対処したかを考えてみましょう。2007年にGoogleで行われた講演で、彼は自身と他者(彼は「アシスタント」と呼んでいました)との間にどのように信頼関係を築いたかを語りました。リーナス氏自身は膨大な量のコードを管理することは不可能だったため、カーネルの一部については他者が作業を手伝いました。これらの「アシスタント」たちは要件の大部分を処理し、それをリーナス氏に提出しました。リーナス氏はそれを自身のブランチにマージしました。このようにして、彼はアシスタントたちの作業を信頼していました。アシスタントたちはすべてのパッチを注意深くレビューし、実際、リーナス氏のパッチの多くはアシスタントたちから生まれたものでした。 アシスタントからLinusへのパッチの伝達方法が理解できません。考えられるのは、これらの高レベルかつ検証済みのパッチが彼の「アシスタント」の手に渡り、Linusにメールで送信され、各アシスタントが自身のGPG/PGP鍵を使って確認・署名する、というものです。これは、信頼ネットワークの実行が署名検証によって確認されていることを示唆しています。Linusは自身のプライベートリポジトリ(前述の通り、セキュリティ確保には最大限の努力を払っています)を安全に管理しており、そこには彼が個人的に信頼するデータのみが含まれています。彼のリポジトリは安全であり、彼の知る限り、安心して使用できる状態です。 この時点で、Linusの信頼ネットワークが正しく検証されていると仮定した場合、彼はどのようにしてこれらの信頼の変更を他の人に自信を持って渡すことができるのでしょうか?彼は自分のコミットを確実に把握していますが、他の人はどのようにしてそのコミットが実際に「Linus Torvalds」によって署名されていると知ることができるのでしょうか?この仮定を証明するシナリオは、この記事の冒頭で述べたとおりです。誰でもLinusを名乗ることができます。攻撃者がリポジトリのクローン版にアクセスし、Linusの名前でコミットしたとしても、誰も違いに気づきません。幸いなことに、秘密GPG鍵(git tag -s)を提供することで、署名タグをバイパスすることができます。タグは特定のコミットを指し示し、そのコミットは履歴全体の導入に依存します。つまり、署名されたコミットはSHA-1でハッシュ化されます。SHA-1にセキュリティ上の欠陥がないと仮定すると、特定の履歴コミットの状態は常にその信頼されたタグを指します。 確かにそれは役に立ちますが、すべてのコミットとそれに続くタグ(ルートを含む次のタグが追加されるまで)を検証するのに役立つわけではありません。また、過去のすべてのコミットが正しいことを必ずしも保証するわけでもありません。これは、Linusにとって最善であり、信頼ツリーが信頼できることを示しているだけです。注:この架空の話では、Linusの秘密鍵を使ってタグに署名してきました。残念ながら、Linusは被害者になります。これはよくあることですが、人為的ミスです。Linusは「信頼できる」同僚を本当に完全に信頼できるのでしょうか?たとえ人為的ミスをこの方程式から排除したとしても、完璧なのでしょうか? 信頼の保護 あるコミットが「Mike Gerwitz」という人物によって送信されたことを検証する方法があるとして、実際には私自身が自分のメールアドレスを使って送信した(つまり、秘密鍵を使ってタグ付けした)場合、タグの署名を主張できるでしょうか?はい、私たちは自分が誰であるかを証明しようとしているのです。もしあなたが自分の識別子をプロジェクトの作者/メンテナーにのみ提供しているのであれば、何らかの合理的な方法で身元を証明する必要があります。例えば、あなたの作業が同じ社内ネットワークで行われている場合、社内IPアドレスからセキュリティを検証できます。メールでの送信であれば、GPGキーを使ってパッチに署名できます。しかし残念ながら、これでは信頼レベルは作者/メンテナーにまでしか拡張されず、他のユーザーは存在しません!あなたのリポジトリをクローンして履歴を見た場合、特にそのリポジトリがユーザーから頻繁にパッチやマージリクエストを受け取っている場合、そのコミットが「Foo Bar」からのものであり、Foo Barからの信頼できるコミットであるとどうやって判断できるでしょうか?どうすれば信頼性を保証できるでしょうか? 以前はGPGはタグ付けにのみ使用されていました。幸いなことに、Git v1.7.9では個々のコミットに対するGPG署名がサポートされていると報告されています。これは私が待ち望んでいた機能です。この記事の冒頭で説明したシナリオを考えると、各コミットに署名すると、次のようなものになります。
上記の例の「-S」に注目してください。これは、変更されたコードをコミットする際にあなたのGPG公開鍵を使用するようにGitに指示するものです(「-s」と「-S」の違いに注意してください)。あなたが毎回GPG公開鍵を使ってコードをコミットしていれば、あなた(あるいは他の誰か)は、変更が実際にあなた自身によってコミットされたとほぼ確信できます。上記の例で言えば、あなたのコードはあなたのGPG公開鍵で署名されているため、バックドアコードはあなた自身によってコミットされたのではないと指摘することで、あなた自身を守ることができます。(もちろん、他の人はこれを言い訳にして、あなたが故意に署名しなかったと主張するかもしれません。この問題については後ほど簡単に説明します。)
注目すべきは、上記の出力におけるスラッシュの後の16進数値です。(実際の出力は上記のものとは大きく異なる可能性があります。4096Rが表示されていなくても心配しないでください。)複数の公開鍵がある場合は、そのうちの1つを署名として選択してください。この16進数値は、Git環境変数 `user.signingkey` に設定する必要があります。
次に、署名付きコミットを試してみましょう。まず、テストコードリポジトリを作成し、後の記事で実験を行います。
このコミットと署名なしのコミットとの唯一の違いは、-S オプションが追加されていることです。これは、GPG 署名付きのコミットを要求していることを示します。すべてが正しく設定されていれば、キーに対応するパスワードの入力を求められます(gpg エージェントを実行している場合はプロンプトは表示されません)。コミットは期待通りに実行され、最終結果は上記の出力のようになります(GPG の詳細と SHA-1 ハッシュは異なります)。 デフォルトでは(少なくともGitバージョン1.7.9では)、`git log`は署名の一覧表示や検証を行いません。コミットの署名を表示するには、次のように`--show-signature`オプションを使用します。
ここでの最大の違いは、コミッターとこのコミットに対応する署名が、それぞれ異なる人物を指す場合があることです。言い換えれば、コミット署名は概念的には -s オプションに似ています。つまり、コミットに署名を追加します。これは、コミットに署名したことを保証しますが、コミットした内容が変更内容と完全に一致することを意味するわけではありません。これを説明するために、「John Doe」からパッチを受け取って適用したい場合を考えてみましょう。コードリポジトリのポリシーでは、すべてのコミットは信頼できる個人によって署名されている必要があり、それ以外のコミットはすべてプロジェクトマネージャーによって拒否されます。実際のパッチを簡単に実装する方法を説明すると、以下のようになります。
ここで疑問が生じます。自分のGPGキーを使ってコミットに署名する人に対して、私たちはどう対処すべきでしょうか? これを2つの観点から分析する必要があります。まず、プロジェクトマネージャーの観点から、サードパーティの貢献者の身元を慎重に確認する必要があるのか、それとも単に彼らのコードを受け入れるだけでいいのか? これは具体的な状況によって異なります。次に、法的な観点から、身元確認が必要になる場合もありますが、すべてのユーザーがGPGキーを持っているわけではありません。次のようなシナリオを考えてみましょう。ある人が、身元確認を必要とせずにいくつかのコミットに署名するためだけにキーを作成し、その後そのキーを破棄(または忘れてしまう)します。そして、検証のためにそれ以上の情報を提供しません。(実際、PGPの基本的な理念は、自分のキーを使って署名する人が本当に誰なのかを証明できる信頼できるサイトを作ることです。したがって、重要なのは目的ではなく、ユースケースです。)したがって、パッチを提供するすべての人に厳格な署名ポリシーを適用すると、失敗する可能性があります。LinuxとGitは、法的要件を満たすためにコミットで「署名」を使用します。つまり、作成者は「オリジナルの開発者証明書」を使用することに同意するということです。これは、作成者がコミットに含まれるコードの法的所有権を有していることを実質的に示しています。次のステップは、信頼できるサイト以外の第三者からのパッチをいつ受け入れるかを決定することです。 パッチにこのアプローチを使用する場合、作成者は GPG で署名する必要はなく、次の操作を行う必要があります。
このようなパッチを受け取った場合は、GPGで「-S」(大文字のS)を付けて署名し、コミットすることができます。これにより、作成者のsigned-off-by署名行が保持されます。プルリクエストの場合は、コミットを変更することで署名できます(git commit -S --amend)。ただし、この操作を行うとコミットのSHA-1ハッシュが変更されることに注意してください。 プルリクエストを作成した人の署名行を保持したい場合はどうすればよいでしょうか?コミットを変更することはできません。変更すると相手の署名が無効になってしまうため、二重署名は現実的ではありません(Gitは将来的に二重署名をサポートする予定ですが)。ただし、マージされたコードに署名することは検討できます。これについては次のセクションで説明します。 #p# 大規模合併の管理 ここまでは、パッチの適用や個々のコミットのマージについて説明してきました。さて、機能または脆弱性パッチのプルリクエストが300件のコミット数(これは全く普通のことです)で届いたとします。どうすればよいでしょうか?以下のいずれかの方法を選択できます。
3つの方法のどれを選択するかは、特定のプロジェクトにおいてどの要素が最も重要かつ実現可能かによって決まります。具体的には、以下の点が挙げられます。
上記の方法 1 を使用すると、前のセクションで説明した問題を簡単に解決できます。 (方法2) 方法2は、`git merge`に-Sパラメータを渡すだけです。マージが非常に高速に実行される場合(つまり、すべてのコミットがマージを実行せずにHEADのみを変更する場合)、`--no-ff`オプションを使用してマージコミットを強制する必要があります。
ログを確認すると、次のようになります。
マージコミットには署名が含まれていますが、マージに関係する2つのコミット(031f6eeとce77088)には署名が含まれていません。これが問題です。もしコミット031f6eeに、記事冒頭で述べたバックドアが含まれていたらどうなるでしょうか?このコミットはあなたが承認したとみなされるかもしれませんが、署名がないため、誰でも承認できます。さらに、コミットce77088に、コミット031f6eeから既に削除されている悪意のあるコードが含まれていた場合、2つのコードブランチの差分比較を行っても悪意のあるコードは検出されません。しかし、これはセキュリティ戦略で対処する必要がある別の問題です。各コミットをレビューしていますか?もしそうであれば、潜在的な問題はコミットレビュー中に発見され、各コミットに個別に署名する必要はありません。マージ自体が、「はい、各コミットを個別にレビューし、これらの変更に問題は見つかりませんでした」ということを意味します。 各提出物を個別に確認すると責任が重すぎる場合は、方法 1 の使用を検討してください。 (方法3) 方法3は上記の通り、各コミットの確認が必須です。一方、方法2はコミットをざっと確認するだけで済むか、全く確認する必要もありません。つまり、方法3でも各コミットに自動的に署名することで方法2と同じことができますが、そうすると方法3が全く不要に思えてしまうかもしれません。どちらの方法を使用するかはあなた次第です。 方法3をリモートで実行可能にする唯一の方法は、特にコミット数が多い場合は、コミットごとにキーに対応するパスワードを再入力する必要がないようにすることです。そのためには、gpg-agentを使用する必要があります。gpg-agentはパスワードをメモリ内に安全に保存し、後続のリクエストで使用します。gpg-agentを使用することで、パスワードの入力を求められるのは一度だけです。gpg-agentの起動方法に応じて、完了後にプロセスを強制終了する方法を変更する必要があります。 各コミットに署名する方法は数多くあります。一般的に、コミットに署名することは完全に新しいコミットを意味するため、署名方法の選択はある程度重要になります。例えば、各コミットを選択し、それぞれに `-S --amedn` オプションを追加することもできますが、これらのコミットはマージとはみなされず、(マージが非常に高速に実行されない限り)ブランチ履歴を表示する際に非常に混乱を招きます。したがって、(これもマージコミットが非常に高速に実行されない限り)マージコミットを生成する方法を決定します。これを実現する方法の1つは、各コミットのベースラインを対話的に更新することです。これにより、コードの違いを簡単に確認し、コミットに署名し、そのコミットに基づいて次のコミットに進むことができます。
まず、barブランチをベースに新しいブランチ(bar-audit)を作成し、ベースライン更新を実行します(方法2で作成したbarブランチを参照)。次に、masterにマージされた各コミットを1つずつ実行するために、masterを親ブランチとして扱うベースライン更新を実行する必要があります。これにより、bar-audit(実際にはbar)に存在するもののmasterには存在しないすべてのコミットが表示されます。これらのコミットを任意のエディタで開きます。
これらのコミットを修正し、上記のようにすべての「pick」を「e」(または「edit」)に置き換えます。(Vimを使用している場合は、「ex」コマンド「%s/^pick/e/;」も使用できます。正規表現の形式はお使いのエディタに合わせて調整してください。)保存して閉じます。最初の(そして最も古い)コミットが表示されます。
ログを閲覧すると、コミットが書き換えられ、署名が含まれていることがわかります(ただし、SHA-1ハッシュ値は一致しません)。
次に、通常通りコードをマスターにマージします。次の問題は、方法2のようにマージコミットに署名するかどうかです。この例では、マージは高速なプロセスなので、マージコミットは必要ありません(マージ対象のコミットは既に署名されているため、署名のためだけに `--no-ff` オプションを使用してマージコミットを作成する必要はありません)。しかし、あなたが個人的にチェックを行い、別の人が実際のマージを行うというシナリオを考えてみましょう。プロジェクトは次のように運営されているとします。プロジェクトマネージャーがコードのレビューと署名を行い、他の開発者がマージと競合管理を担当します。この場合、誰が変更をマージしたかを明確に記録する必要があるかもしれません。 #p# 信頼を強化する 特定のプロジェクト/リポジトリに適したセキュリティ戦略を決定した(少なくともそう仮定している)ので、次のステップは署名戦略を強化する方法を見つけることです。手作業による強化は可能ですが、人為的なミスが発生しやすく、(「合格するためだけに」)ピアレビューが必要になり、不必要な時間の浪費になる可能性もあります。幸いなことに、一つのアプローチがあります。スクリプトを作成し、あとはのんびりと座って、安心して実行するだけです。 首先我们看看自动任务中较简单的---检查并确认每个提交都既签了名,又得到(信任站点的)信任。这种实现还满足了方法3里合并方面的要求。然而,也许并不是所有的提交都考虑进来。不过,如果你有一个具有相当可观数量提交的代码库,那么你就可以做到所有的提交都既签了名又得到信任。如果你想进行回溯,并对所有这些提交进行签名,那么你就彻底地更改了整个代码库中的历史信息,这会让其他用户头痛不已。相反,你可以考虑在某个提交之后开始进行安全检查。 简说提交的历史信息 Git中每一个提交的SHA-1哈希值是根据每一个提交的增量和头信息来生成的。头信息里包含着这一提交的父提交的头信息,父提交的头信息里又包含它的父提交的头信息----以此类推。另外,Git根据代码库里的整个历史信息来生成请求修改的提交信息。这也就意味着历史信息在没有通知某个人的情况下不得更改(实际上,不全是这样的;我们稍后讨论这个问题)。例如,看看下面代码分支:
如上,H表示的是当前的头信息,标识为A的提交是B提交的父提交。为了讨论方便,我们假设提交A是由SHA-1哈希值a1b2c3来确认的。我们再假设攻击者决定用另一个提交替换A提交。要进行替换的话,这个提交的SHA-1哈希值就会发生变更,以匹配头中新的增量和内容信息。新提交标识为X:
现在我们会遇到问题;当对提交B运行git时(记住:Git一定会用产生H的所有历史信息来构造H的。),Git将会检查SHA-1哈希值,然后就会注意到这个哈希值已经与其父提交的哈希值不一致了。攻击者是无法更改提交B里的哈希值的,因为用来生成某个提交SHA-1的头信息是针对某一提交的,这也就意味着B已经有一个完全不同的SHA-1哈希值了(技术上来说,这个提交已经不在是B提交了---它已经是另一个完全不同的提交了;为了方便说明,我们忍让使用B标识)。这将会使得任何B的子提交无效,以此类推。因此要对某个提交的历史信息重写,那么位于这个提交后的所有提交都必须进行重写(通过git rebase来完成)。要想这么做的话,H的SHA-1哈希值也必须得到更改。否则,H的历史信息将是无效的,而且在在你试图对代码进行检出的时候,Git会立即抛出错误信息。 这里有一个非常重要的结论——对于任何的提交。我们可以放心,如果它在本地存储器上存在,Git总是会重构,使之提交的能与被创建(包括所有之前的历史创建及提交)时一致,除非不这样做。的确,Linus提及了在Google的一次展示, 他只需要记住SHA-1散列上的一个提交,放心吧,它会把它发送到其他的存储器上,倘若我们的东西丢失,之前的提交会发送一份完全一致的提交到其他人的存储器上。这对我们意味着什么?是的,这意味着我们不需要强制重写历史记录到每个单一的提交上,因为我们其他的历史提交是被保证的。唯一的缺点是,提交历史本身可能已经被利用起来,类似于我们开头讲的故事,但是许多过去的提交记录是被自动签名的,这样对于一个给定的作者将不能抓住类似的事情。 这就是说,明白存储的完整性保障是重要的,尽管哈希碰撞不会发生——就是说,如果攻击者能对不同的数据创建一样的SHA-1哈希,那么子提交将仍然是有效的,而存储库就已经成功地被破解了。从2005年开始,可用的哈希计算速度快得超过了强力破解,这样,在SHA-1上的缺陷就变得众所周知了,尽管利用这一点并不廉价。基于这样一个事实,为了你的储存库的安全,将来的某个时候,SHA-1将会瘫痪,就像现在的MD5一样。在那个时间点上,Git可能会提供一个安全的迁移方案类似SHA-256算法或者更好的算法。的确,SHA-1哈希不能保证Git的密码安全。 正是如此,大部分人可能会不再去看他/她的历史记录,我们将会在这个假设下实现我们的操作,这提供了能去忽略所有之前确认提交的能力。如果某人希望去验证所有的提交,只是参考提交可能就会遗漏。 自动进行签名验证 验证某些提交是可信任的想法非常简单: 假定要用到的提交是r(可为空),C为所有提交的集合,此时C=r..Head(范围说明),同时K是给定GPG密钥链中所有公钥 的集合。我们断言:对C中的每个提交c,密钥链K中一定存在一个密钥k可信任,同时可用来对c的签名进行验证。这个断言 是由函数g(GPG)来表示的,如下表达式:∀c∈Cg(c)。 很幸运,就像我们在前一节在git log上使用--show-signature选项后看到的,Git帮助我们验证了签名;这样就把我们的验证签名实现简化为一个简单的shell脚本。不过我们得到的输出不是很适合于解析。如果我们可以让每个提交的提交和签名信息出现在单行上就很适合解析了。这可以通过--pretty选项完成,不过还有这样一个问题--在编写(Git 1.7.10)文档的时候,GPG --pretty选项没有写入文档中。 format_commit_one() in pretty.c 有三个不同的格式:
我们感兴趣的是使用最精确和最小限度的表达---¥G?。因为这个占位符只是匹配GPG输出的内容,字符窜“gpg: Can't check signature: public key not found”不能对应insignature_check, 不能识别的字符讲会输出空字符窜,不是"B".这点并不明显,所以我不确信是否这个在以后的版本会改变。幸运的是,我们只是对”G"感兴趣,所以这个细节对于我们的实施来说不关键。记住这点,我们能够做提交一次输出某个有用的一行。下面是基于演示上面的merge option #3 的输出的结果:
注意每一行的后缀"G",它表明签名有效(这可以理解,因为是我们自己的签名)。再增添一个提交,我们看看进行未签名提交时会出现什么情况:
注意:就像前面提到那样,在进行未签名提交时,%G?被替换为空字符串。那签了名但不可信任(即不在站点的信任内)的提交会出现什么情况?
哦,哦,Git似乎没有核查签名是否可信。我们看一看完整的GPG输出:
我们可以看到GPG给出了明确的警告信息。不幸的是pretty.c中的parse_signature_lines()引用了struct signature_check结构里的一个简单映射,并忘乎所以地忽略了警告信息,只匹配了"Good signatrue from",生成了"G"。为不信任密钥提供单独的符号,这样的补丁程序很简单,但目前我们暂时使用的是两个不同的实现方法---一个方法是对忽略是否可信任的单行输出进行解析,另一个是上面提到的对GPG输出进行解析的非简洁化实现方法。[假若采纳了这个补丁,那么这篇文档就会立即更新,使用新的符号。] 没有可信任验证的签名验证脚本 上面已经提到过,由于目前%G?实现的限制,我们无法从单行输出确定所提供的签名是可信任的。这不一定是问题所在。考虑一下运行这个脚本的一般情形---由持续集成(CI)系统运行。要让CI系统明确什么样的签名才是可信任的,你可能要为知名的提交者提供密钥,这样就不需要站点的信任了(把公钥放在服务器上就标兵你信任这些密钥)。因此,如果可识别到提交的签名而且正确,那么这次提交就值得信任。 另外一个要考虑的是不对提交的所有祖先进行签名,旧的代码库就是这么做的,其中旧的提交都是未签名的(关于为什么不需要对旧的提交进行签名的信息可参考提交信息简说一节,而且对旧提交进行签名是非常糟糕的事情)。因此,这个脚本将接收参数,而且只对该参数的子提交进行签名验证。 这个脚本假定每个提交都签了名,同时它会输出未签名或者错误提交的SHA-1哈希值,除此之外还显示其他可用的信息,信息之间以制表符间隔。
上面就是全部脚本代码;Git已经做了大部分工作!如果传入参数,那么这个参数将被转换为范围格式,也就是在其后增加".."(例如,a1b2c3就会转换为a1b2c3..),如果没有参数传入,我们将会以不带范围格式的HEAD结束,它只是简单地罗列出每个提交(空串将使Git抛出错误,因此我们必须对该字符串两边加上引号,这样用户就可以执行类似于获取"master@{5 days ago"}这样的任务了)。我们给git log加上--pretty选项,这样就会输出带有%G?的GPG签名,以及其他可用的信息,通过这些信息我们就可以看到哪些提交没有通过验证。接下来,我们对所有用密钥签名的提交进行过滤,删除所有以"G"结尾的行----依据%G?得到的输出说明这样的提交通过了签名验证。 我们看一看实际中脚本运行情况(假设脚本存储为文件signchk):
如果没有参数传入,那么这个脚本就会对整个代码库里的每个提交进行检查,查找一个没有签名的提交。此时,我们要么通过查看脚本的自己的输出,要么查看脚本退出时的状态来确定是否失败。如果脚本是由CI系统来运行的,那么此时最好是退出构建过程,同时立刻通知项目管理者潜在的安全入口所在(或者更可能是某个人只是忘记对自己的提交签名)。 如果在失败之后我们检查提交,此时假设子提交都已经签了名,那么我们就会看到下面结果:
从代码库里直接运行脚本的时候要特别小心,尤其是通过CI系统运行的时候要格外小心----你一定要做到:要么把脚本拷贝到代码库之外,要么从历史提交中一个可信任的提交处运行。举个例子,如果你所使用的CI系统只是从代码库中下拉代码,然后运行这个脚本,那么攻击者只要修改一下这个脚本就可以完全绕过这样的签名验证。 #p# 可信任签名验证 信任网络可用在具有许多贡献者的情况;此时,CI系统在需要密钥的时候就会试图从预先配置好的密钥服务器中下载公钥(如果需要可信任签名,就必须更新密钥)。依据由CI系统直接信任的公钥组建的信任网络,你就可自动确定提交是否可信任,即便提交所对应的公钥没有存放在密钥服务器上也可以确定是否可信任。 为了完成这样的工作,我们把脚本分割成两个部分---获取或者更新给定范围内的所有密钥,接着是真正的签名验证部分。我们先看看密钥收集部分,这个工作实际上微不足道:
上面的命令字符串只是通过grep命令从git log的输出(即使用--show-signature选项后生成的GPG输出)中提交密钥ID,然后向给定的密钥服务器只请求不重复的密钥。通篇文章里我们使用的代码库只有一个签名---即我自己的签名。而针对大型的代码库,所有不重复的密钥都会被罗列出来。注意:上面的例子没有指定提交的范围;你可以按照自己的意愿把它嵌入到signchk脚本,这样就可以使用同样的范围了,不过严格的来说,并不需要这么做(这么做也许在性能上有些许提高,而且这种性能上的提高还取决于你所忽略的提交的数量)。 有了这些更新的密钥,我们就可以根据信任网络对提交进行验证了。某个密钥是否可信任取决于你个人的设置。理念是信任你所配置的信任网络里用户的哪些用户(例如Linus的“助理们”)就是可信任的,即便你个人不信任他们也如此。同样的理念也适用于CI服务器,此时你可以用CI服务器的密钥链替换你自己的密钥链(这样,你就不需要运行CI服务器,可以自己运行这个脚本)。 很不幸,由于受到目前Git的%G?实现限制, 我们不能够通过检查单行输出给出结果。相反,我们必须解析每个关联提交的--show-signature的输出( 如上所示)。把现在的输出和不带可信任验证的脚本结合起来,我们就会得到以下输出,这才是我们要解析的:
看看上面部分运行结果,你应当注意到第一个提交(f7292)是未签名的,而第二个(afb1e)是签了名的。因此,GPG输出应在提交行之前。现在看看我们的目标:
前面的脚本很好地执行了第1个目标,因此我们只需要对这个脚本进行代码提交,使得它能够实现第2个目标。实际上---如果提交行前面的GPG输出表明这个签名是不可信任的,那么我们希望转换其中以"G"结尾的行为其他别的东西。 达到第2个目标有许多方法,我们选择的是在前面脚本上增家几个非常简洁的命令的方法。为了不使输出过滤掉以"G"结尾的行(这样的提交都是不可信任的),我们给这样的不信任行后增加了"U"。看-看下面输出:
在这儿,我们发现如果我们过滤前天提及的以"G"结尾的行,我们将得到的就像%G?所表示的那样:是不信任的提交,以及错误提交("B")或者未签名提交(结尾是空白)。要做到这些,我们首先使用--show-signature选项,给日志输出中增加GPG输出,为了更容易地进行过滤,我们给所有的提交行前加上控制符(^),后面我们会删除这个符号。然后我们过滤所有以控制符开头的行或者包含有"not certified"字符串的行(GPG输出中存在这样的行)。如果提交时不可信任的,那么这个就会在提交行之前出现一个"gpg:"行。 接着我们把得到的结果传递给awk命令,它将删除所有以"gpg:"作为前缀的行,然后给下一行(也就是提交行)添加上"U"。最后,我们将删除在处理开始时添加的前导控制符(^),得到最终的输出结果。 请注意,通常使用的PGP/GPG(我声明我知道这个人就是他们宣称的那个样子“)信托和信任某人提交代码之间有巨大差别。同样地,可能你最大的兴趣是,维护一个完整的独立的信誉网页,给你的CI服务商或者使用的任一个用户进行签名验证。 自动合并签名验证 如果你希望检验每个提交的有效与否,前文提到的脚本非常棒,但是并非每个人都希望做那么多努力。反之,维护的人可能更喜欢只需要标记合并的提交(上面提到的选项2),而不是每次合并引入的提交。我们来考虑下对这种情况我们要采用的方法。 假定要用到的提交时r(可为空),C'为所有第一级父提交的集合,此时C'=r..Head(范围说明),同时K是给定GPG密钥链中所有公钥 的集合。我们断言:对C'中的每个提交c,密钥链K中一定存在一个密钥k可信任,同时可用来对c的签名进行验证。这个断言 是由函数g(GPG)来表示的,如下表达式:∀c∈C' g(c)。 这个脚本与只对单个提交进行签名验证的脚本的唯一不同是这个脚本只对特定代码分支(比如master分支)下的提交进行验证。这一点非常重要---如果我们直接在master上提交,那么我们需要确保这个提交是签了名的(因为,不会存在合并提交)。如果我们需要合并到master分支上,那么就会创建合并提交,这时我们可以 对合并提交签名,同时不对合并的所涉及的提交进行签名。如果合并运行的非常快,我们就使用--no-ff选项强制创建合并提交,以避免给每个所涉及到的提交进行签名。 为了模拟能验证此种提交的脚本,咱们先做一些修改来触发合并功能:
上述操作,确保master和分支都有相应的合并提交以避免快进(也可以通过用--no-ff 选项实现). 结果如下(你本机的哈希结果可能不同):
从上图中可以看出,只有两处需要签名: 3cbc6d2, 在master直接创建, 9307dc5---合并提交后生成. 另外两处提交(996cf32 和cfe7389) 不需要签名,在合并时就确保了其有效性(假设提交者是谨慎的). 但怎么忽略这些提交呢?
上述例子简单的添加了--first-parent选项, 这样在遇到有合并提交时只会显示最初的提交记录. 重点就是, 这就只剩下master上的提交记录(或是你需要参照的分支).这些是需要验证的. 现在的验证工作仅仅需要微调原来的脚本即可:
如果你在刚建好的分支上运行上述脚本, 你会发现分支中不会包含相应的历史提交记录.由于合并提交的自带标记,结果中也不会显示相应的记录(剩下的就是那些未做标记的提交记录).要展示未标记的合并提交, 可以使用以下命令(忽略-S选项):
合并提交将被列出来,需要签名验证. [如果需要验证签名的有效性,可以参照以下文章 the section on verifying commits within a web of trust.] 要約
译文出自:http://www.oschina.net/translate/git-horror-story?lang=chs&page=1# |