Objective

Git を SHA-1 から、より強力なハッシュ関数に移行します。

Background

Git バージョン管理システムの中核は、コンテンツ アドレス指定可能(addressable)なファイルシステムです。 それは SHA-1 ハッシュ関数を使用してコンテンツに名前を付けます。 たとえば、ファイルやディレクトリやリビジョンは、ファイルまたはバージョンが連番によって参照される他の従来のバージョン管理システムとは異なり、ハッシュ値によって参照されます。 ハッシュ関数を使用してそのコンテンツを扱うことは、以下のようないくつかの利点があります:

  • 整合性チェックが簡単です。 たとえば、破損したコンテンツのハッシュはその名前と一致しないため、ビット反転(bit flips)は簡単に検出されます。

  • オブジェクトのルックアップは高速です。

暗号的に安全なハッシュ関数を使用すると、以下の追加の利点がもたらされます:

  • オブジェクト名は署名され、第三者は署名されたオブジェクトとそれが参照するすべてのオブジェクトのアドレスを表すハッシュを信頼することができます。

  • Git プロトコルを使用した通信と帯域外通信方法には、格納されたコンテンツを確実にアドレス指定するために使用できる信頼性の高い短い文字列があります。

時間の経過とともに、SHA-1 のいくつかの欠陥がセキュリティ研究者によって発見されました。 2017年2月23日の SHAttered 攻撃 (https://shattered.io) は、実際の SHA-1 ハッシュ衝突を示して見せました。

Git v2.13.0 以降では、デフォルトで強化された SHA-1 実装に移行しました。これは SHAttered 攻撃に対しては脆弱ではありませんが、SHA-1 は依然として脆弱です。

したがって、SHA-1 の派生を超えて新しいハッシュに移行することが賢明であると考えられます。 SHA-1 に対する攻撃が将来公開されないという保証はなく、それらの攻撃には有効な軽減策がない可能性があります。

SHA-1 とその派生が完全に破られた場合、Git のハッシュ関数は暗号学的に安全であるとは見なされなくなります。 特定のハッシュ値が、話者が意図したコンテンツの既知の適切なバージョンを表すことを信頼できなくなったために、ハッシュ値の通信に影響を与える可能性があります。

SHA-1 は、オブジェクトの高速検索や安全なエラーチェックなどの他の特性を引き続き備えていますが、暗号的に安全であると考えられている他のハッシュ関数も同様に適しています。

Choice of Hash

強化された、 SHA-1 を置き換えるハッシュは、SHA-1 よりも強力である必要があります。少なくとも 10 年間は、信頼性が高く、実際に役立つものであることが望まれます。

その他関連する特性:

  1. 256 ビットのハッシュ (一般的なセキュリティ実態にマッチするのに十分な長さ。しかし、パフォーマンスとディスク使用量を損なうほど長くはないこと)。

  2. 高品質の実装が広く利用できるようにする必要があります (たとえば、OpenSSL や Apple CommonCrypto など)。

  3. ハッシュ関数の特性は、Git のニーズと一致する必要があります (たとえば、Git は 衝突(collision)耐性と2番目のプリイメージ(2nd preimage)耐性が必須で、長さ拡張(length extension)耐性は必須ではりません)。

  4. タイブレーク(tiebreaker)のためにハッシュは計算が高速である必要があります (幸いなことに、多くの候補は SHA-1 よりも高速です)。

SHA-1 の後継ハッシュには、SHA-256、SHA-512/256、SHA-256x16、K12、BLAKE2bp-256 など、いくつかの候補がありました。

2018 年後半、プロジェクトではその後継ハッシュとして SHA-256 を選択しました。

詳細については 0ed8d8da374 (doc hash-function-transition: pick SHA-256 as NewHash, 2018-08-04) と、当時の多数のメーリングリスト、スレッド、 特に https://lore.kernel.org/git/20180609224913.GC38834@genre.crustytoothpaste.net/ で始まるスレッドを参照してください。

Goals

  1. SHA-256 への移行は、一度に 1 つのローカル リポジトリで実行できること。

    1. 他の当事者によるアクションを必要としないこと。

    2. SHA-256 リポジトリは、SHA-1 Git サーバーと通信できること(プッシュ/フェッチ)。

    3. ユーザーは、オブジェクトの SHA-1 識別子と SHA-256 識別子を交換可能(interchangeably)に使用できること(下記「Object names on the command line」(コマンド ラインでのオブジェクト名)を参照)。

    4. 新しい署名付きオブジェクトは、セキュリティを保証するために SHA-1 よりも強力なハッシュ関数を使用すること。

  2. SHA-1 から完全に移行できるようにすること。

    1. SHA-1 との互換性が不要になった場合は、SHA-1 互換性のローカルメタデータをリポジトリから削除できるようにすること。

  3. プロセス全体に渡る保守性が担保されていること。

    1. オブジェクトのフォーマットはシンプルで一貫性が保たれていること。

    2. 一般化されたリポジトリ変換ツールを作成すること。

Non-Goals

  1. Git プロトコルに SHA-256 サポートを追加すること。 これは価値があり、論理的な次のステップですが、この初期設計では範囲外とします。

  2. 既存の SHA-1 署名付きオブジェクトのセキュリティを透過的に改善すること。

  3. 単一のリポジトリで複数のハッシュ関数を使用してオブジェクトを混在させること。

  4. この機会に、Git のフォーマットとプロトコルの他のバグを修正すること。

  5. SHA-256 リポジトリから浅い(shallow)クローンしたりフェッチしたりすること。 (これは、Git プロトコルに SHA-256 サポートを追加すると変更されます。)

  6. プロジェクトの一部のサブモジュールの SHA-256 リポジトリへのフェッチをスキップすること。 (これは、Git プロトコルの SHA-256 サポートにも依存します。)

Overview

私達は新しい、リポジトリ形式拡張機能を導入します。 この拡張機能が有効になっているリポジトリは、SHA-1 の代わりに SHA-256 を使用してオブジェクトに名前を付けます。 これは、オブジェクト名とオブジェクトの内容の両方に影響します — つまり、オブジェクトの名前と、オブジェクト内の他のオブジェクトへのすべての参照の両方が、新しいハッシュ関数に切り替えられます。

古いバージョンの Git では SHA-256 リポジトリを読み取ることができません。

パックファイルと並んで SHA-256 リポジトリは SHA-256 と SHA-1 オブジェクト名の間の双方向マッピングを格納します。 マッピングはローカルで生成され、「git fsck」を使用して検証できます。 オブジェクトルックアップでは、このマッピングを使用して、SHA-1 名と SHA-256 名のいずれかを交換可能に使用してオブジェクトに名前を付けることができます。

「git cat-file」と「git hash-object」には、オブジェクトを SHA-1 形式で表示し、指定された SHA-1 形式でオブジェクトを書き込むオプションが追加されました。 このため、そのオブジェクトから参照されるすべてのオブジェクトがオブジェクトデータベースに存在し、適切な名前を使用(双方向ハッシュマッピングを使用)できるようにする必要があります。

SHA-1 ベースのサーバーからフェッチすると、フェッチされたオブジェクトが SHA-256 形式に変換され、マッピングが双方向マッピング表に記録されます(詳細下記)。 SHA-1 ベースのサーバーへのプッシュは、プッシュされるオブジェクトを SHA-1 形式に変換するため、クライアントが使用しているハッシュ関数をサーバーが認識する必要はありません。

Detailed Design

Repository format extension

SHA-256 リポジトリは、リポジトリ フォーマット バージョン(repository format version) 1 (Documentation/technical/repository-version.txt 参照) を拡張機能(extensions) objectFormat および compatObjectFormat とともに使用します:

[core]
        repositoryFormatVersion = 1
[extensions]
        objectFormat = sha256
        compatObjectFormat = sha1

core.repositoryFormatVersion=1` の設定と extensions.* の設定の組み合わせにより、v0.99.9l より後のバージョンの Git は SHA-256 リポジトリで操作しようとせずに死んでしまい、代わりにエラーメッセージを出すようになります。

# Between v0.99.9l and v2.7.0
$ git status
fatal: Expected git repo version <= 0, found 1
# After v2.7.0
$ git status
fatal: unknown repository extensions found:
        objectformat
        compatobjectformat

これらのリポジトリ拡張機能の詳細については、以下の「Transition plan」(移行計画)セクションを参照してください。

Object names

オブジェクトは、40 桁の 16 進数の SHA-1 名または 64 桁の 16 進数の SHA-256 名、およびそれらから派生した名前で名前を付けることができます (gitrevisions(7) 参照)。

オブジェクトの SHA-1 名は、そのタイプ、長さ、ヌルバイト、およびオブジェクトの SHA-1 コンテンツを連結した the SHA-1 です。 これは、Git でオブジェクトに名前を付けるために使用される従来の <sha1> です。

オブジェクトの SHA-256 名は、そのタイプ、長さ、ヌルバイト、およびオブジェクトの SHA-256 コンテンツを連結した the SHA-256 です。

Object format

SHA-256名で名付けられたオブジェクトはSHA-256名で他のオブジェクトを参照し、SHA-1名で名付けられたオブジェクトはSHA-1名で他のオブジェクトを参照するので、SHA-1とSHA-256で名付けられた、タグオブジェクトまたはコミットオブジェクトまたはツリーオブジェクトのバイト列としてのコンテンツは異なります。

あるオブジェクトの SHA-256 コンテンツは、あるオブジェクトによって参照されるオブジェクト達が、 SHA-1 名の代わりに SHA-256 名を使用して命名されることを除いて、オブジェクトの SHA-1 コンテンツと同じです。 ブロブオブジェクトは他のオブジェクトを参照しないため、その SHA-1 コンテンツと SHA-256 コンテンツは同一です。

この形式では、SHA-256 コンテンツと SHA-1 コンテンツ間の双方向変換(round-trip conversion)が可能です。

Object storage

緩いオブジェクト(loose objects)は zlib 圧縮を使用し、パックオブジェクトは gitformat-pack(5) に記載されているパック形式を使用します。 圧縮されて保存されるコンテンツは、SHA-1 コンテンツの代わりに SHA-256 コンテンツを使用します。

Pack index

パックインデックスファイル(.idx) は、複数のハッシュ関数をサポートする新しい v3 形式を使用します。 その形式は以下のとおりです(すべての整数はネットワークバイトオーダーです):

  • ヘッダーは先頭に表れ、以下のもので構成されます:

    • 4バイト。パックインデックス シグネチャ: "\377t0c"(訳注: 0xff, 0x74 , 0x30, 0x63)

    • 4バイト。バージョン番号: 3

    • 4バイト。署名とバージョン番号を含むヘッダーセクションの長さ

    • 4バイト。パックに含まれるオブジェクトの数

    • 4バイト。このパックインデックス内のオブジェクト形式の数: 2

    • 以下、オブジェクト形式ごとに、:

      • 4バイト。フォーマットID(例えば、 sha1 なら SHA-1)

      • 4バイト。短縮オブジェクト名(shortened object names)のバイト単位の長さ。 これは、短縮オブジェクト名表内の名前を明確(unambiguous)にするために必要な最短の長さです(訳注:パックファイル内でそのオブジェクト名ユニークにするのに最低限必要な長さ)

      • 4バイト。このインデックスファイル内で、この形式に関連する表が格納されている箇所を、先頭からのオフセットとして記録する整数。

    • 4バイト。このファイルの先頭からトレーラまでのオフセット。

    • 0個以上の追加の キー/値 (key/value) のペア(4バイトのキー(key)、4バイトの値(value))。 サポートされているキーは 1 つだけです: PSRC 。 サポートされている値とその使用方法については、「Loose objects and unreachable objects」(緩いオブジェクトと到達不能オブジェクト)セクションを参照してください。 他のすべてのキーは予約されています。 読み込みプログラム(readers)は、認識されないキーを無視する必要があります。

  • 0 個以上の NUL バイト。 これはオプションで、下記の完全なオブジェクト名表(the full object name table)の位置合わせを改善するために使用できます。

  • 最初のオブジェクト形式の表達(tables):

    • 短縮オブジェクト名の並べ替えられた表。これらは、このパックファイル内のすべてのオブジェクトの名前のプレフィックスであり、特定のオブジェクト名のバイナリ検索のキャッシュ・フットプリントを減らすために、オフセット値なしで一緒にパックされます。

    • パック順の完全なオブジェクト名(full object names)の表。 これにより、「パックファイル内の n 番目のオブジェクト」への参照(到達可能性ビットマップまたは、別のオブジェクト形式の次の表ルから)を、そのオブジェクト名に解決できます。

    • オブジェクト名の順序をパックの順序にマッピングする4バイト値の表。 並べ替えられた短縮オブジェクト名表内のオブジェクトの場合、この表内の対応するインデックスの値は、同一オブジェクトの前の表内のインデックスです。 これを使用して、到達可能性ビットマップでオブジェクトを検索したり、別のオブジェクト形式でその名前を検索したりできます。

    • オブジェクトがパックファイルに表示される順序での、パックされたオブジェクトデータの4バイトのCRC32値の表。 これは、圧縮されたデータが再パック中、パックからパックに直接コピーされるとき、データの破損が検出されないようにするためです。

    • 4バイトのオフセット値の表。 並べ替えのられた短縮オブジェクト名の表内のオブジェクトの場合、この表内の対応するインデックスの値は、そのオブジェクトがパックファイル内のどこにあるかを示します。 これらは通常 31 ビットのパック ファイル オフセットですが、大きなオフセットは、最上位ビットが設定され、次の表へのインデックスとしてエンコードされます。

    • 8バイトのオフセットエントリの表(2GiB未満のパックファイルの場合は空)。 パックファイルは、使用頻度の高いオブジェクトを前に並べて編成されているため、ほとんどのオブジェクト参照ではこの表を参照する必要はありません。

  • ゼロ個以上の NUL バイト。

  • 2番目のオブジェクト形式の表。CRC32 値の表までは含まれませんが、上記と同じレイアウトです。

  • ゼロ個以上の NUL バイト。

  • トレーラーは以下の内容で構成されています:

    • 対応するパックファイルの末尾にある 20 バイトの SHA-256 チェックサムのコピー。

    • 20バイト 上記全てのSHA-256チェックサム

Loose object index

新しいファイル $GIT_OBJECT_DIR/loose-object-idx には、すべての緩いオブジェクト(loose objects)に関する情報が含まれています。その形式は以下のとおり

# loose-object-idx
(sha256-name SP sha1-name LF)*

ここで、オブジェクト名は 16 進形式です。 ファイルはソートされません。

緩いオブジェクト・インデックスは、ロック・ファイル $GIT_OBJECT_DIR/loose-object-idx.lock によって並列書き込みから保護されます。 新しい緩いオブジェクト(loose objects)を追加するには:

  1. まずは、緩いオブジェクト(loose object)を一時ファイルに書き込みます。

  2. ロックを取得するためにファイル loose-object-idx.lock を O_CREAT | O_EXCL でオープンします。

  3. 緩いオブジェクト(loose object)の名前をこれに変更します。

  4. Loose-object-idx を O_APPEND でオープンし、新しいオブジェクトを書き込みます

  5. Loose-object-idx.lock を削除(unlink)して、ロックを開放します。

エントリを削除するには(「git pack-refs」内や「git-prune」内などで):

  1. ロックを取得するためにファイル loose-object-idx.lock を O_CREAT | O_EXCL でオープンします。

  2. 新しいコンテンツを Loose-object-idx.lock に書き込みます。

  3. 削除しようとしている緩いオブジェクト(loose object)を削除(unlink)します。

  4. 名前変更して Loose-object-idx を置き換え、ロックを開放します。

Translation table

インデックスファイルは、SHA-1 名と SHA-256 名の間の双方向マッピングをサポートします。 ルックアップは、通常のオブジェクトルックアップと同様に進行します。 たとえば、SHA-1 名を SHA-256 名に変換(convert)するには、以下のようにします:

  1. idx ファイルでオブジェクトを探します。 切り捨てられた SHA-1 名(truncated SHA-1 names)の idx の並べ替えられたリストに一致が存在する場合:

    1. SHA-1 名前順の対応するエントリを読み取り、名前順マッピングをパックします。

    2. 完全な SHA-1 名表の対応するエントリを読んで、正しいオブジェクトが見つかったことを確認します。 もしそうなら、

    3. 完全な SHA-256 名前表(the full SHA-256 name table)の対応するエントリを読み取ります。これがそのオブジェクトの SHA-256 名です。

  2. 緩いオブジェクト(loose object)がないか確認してください。 一致するものが見つかるまで Loose-object-idx から行を読み取ります。

ステップ(1)は、通常のオブジェクト検索(lookup)と同じ時間がかかります: O(パック数 * log(パックあたりのオブジェクト数)) です。 ステップ(2)は O(緩いオブジェクトの数) 時間かかります。 良好なパフォーマンスを維持するには、緩いオブジェクトの数を低く抑える必要があります。 詳細については、以下の「Loose objects and unreachable objects」(緩いオブジェクトと到達不能オブジェクト)セクションを参照してください。

新しいオブジェクトを作成するすべての操作(「git commit」など) は、新しいオブジェクトを対応するインデックスに追加するため、このマッピングはオブジェクトストア内のすべてのオブジェクトに対して可能です。

Reading an object’s SHA-1 content

オブジェクトのSHA-1コンテンツは、そのSHA-256コンテンツ参照のすべてのSHA-256名を、変換表(translation table)を使用してSHA-1名に変換することで読み取ることができます。

Fetch

SHA-1ベースのサーバーから取得するには、 SHA-1ベースの表現と SHA-256 ベースの表現をその場で変換する必要があります。

クライアントに存在する ref 広告(advertisement)で指定された SHA-1 は、SHA-256 に変換でき、変換表(translation table)を使用してローカルオブジェクトとして検索(look up)できます。

ネゴシエーションは次のように進行します。 ローカルで生成されたすべての「have」は、サーバーに送信される前に SHA-1 に変換され、サーバーによって言及された SHA-1 は、ローカルで検索(look up)するときに SHA-256 に変換されます。

ネゴシエーション後、サーバーは要求されたオブジェクトを含むパックファイルを送信します。 以下の手順を使用して、パックファイルを SHA-256 形式に変換します:

  1. インデックスパック(index-pack): パックファイル内の各オブジェクトを解凍(inflate)し、その SHA-1 を計算します。 オブジェクトには、クライアントがローカルに持っているオブジェクトに対する OBJ_REF_DELTA 形式のデルタを含めることができます。 これらのオブジェクトは、変換表(translation table)を使用して検索(look up)でき、そのSHA-1コンテンツを上記のように読み取ることでデルタを解決することができる。

  2. トポロジー的ソート(topological sort): ネゴシエーションフェーズの「want」達から始めて、パック内のオブジェクトをウォークスルーし、ブロブを除くそれらのリストをトポロジー的に逆順に並べ替えて出力します。各オブジェクトは、それが参照するすべてのオブジェクトよりも後になります。 (このリストには、「wants」から到達可能なオブジェクトのみが含まれます。サーバーからのパックに余分なオブジェクトが含まれていた場合、それらは破棄されます。)

  3. SHA-256 に変換(convert): 新しい SHA-256 パックファイルを開きます。 生成されたばかりの、トポロジ的(topologically)に並べ替えられたリストを読み取ります。 オブジェクトごとに、その SHA-1 コンテンツを解凍し、SHA-256 コンテンツに変換して、SHA-256 パックに書き込みます。 idx で使用するために、新しい SHA-1⇔SHA-256 マッピング エントリを記録します。

  4. ソート: サーバーが生成したパック内のオブジェクトの順序と一致するように新しいパック内のエントリを並べ替え、ブロブを含めます。 SHA-256 idx ファイルへ書き込みます。

  5. クリーンアップ: SHA-1 ベースのパックファイルと、インデックスと、手順 1 と 2 でサーバーから取得したトポロジ的(topologically)に並べ替えられたリストを削除します。

ステップ 3 では、新しいオブジェクトによって参照されるすべてのオブジェクトが変換表(translation table)にある必要があります。 これが、トポロジ的並べ替えステップ(the topological sort step)が必要な理由です。

最適化として、ステップ 1 で、各オブジェクトがパックファイル参照から解凍させた非ブロブオブジェクトを記述したファイルを作成できます。 これにより、パックファイル内のオブジェクトを再度解凍させることなく、ステップ 2 のトポロジ的並べ替えが可能になります。 オブジェクトは、ステップ 3 で再度解凍する必要があり、合計で 2 回膨らませます。

ステップ 4 は、読み取り時のパフォーマンスを向上させるためにたぶん必要です。 サーバー上の git pack-objects は、パックファイルを最適化してデータの局所性を高めます(good data locality)(Documentation/technical/pack-heuristics.txt 参照)。

このプロセスの詳細は変更される可能性があります。 これをうまく機能させるには、ある程度実地で試してみる事が必要です。

Push

プッシュされたオブジェクトによって参照されるオブジェクトはすでに変換表(translation table)にあるため、プッシュはフェッチよりも簡単です。 プッシュされる各オブジェクトの SHA-1 コンテンツは、「Reading an object’s SHA-1 content」(オブジェクトの SHA-1 コンテンツの読み取り)セクションで説明した方法で説明した方法で読み込むと、git send-pack で書かれたパックを生成することができるようになります。

Signed Commits

新しいフィールド「gpgsig-sha256」をコミットオブジェクト形式に追加して、 SHA-1 に依存せずにコミットに署名できるようにします。 既存の「gpgsig」フィールドに似ています。 その署名付きペイロードは、「gpgsig」および「gpgsig-sha256」フィールドが削除されたコミットオブジェクトの SHA-256 コンテンツです。

これは、コミットに署名できることを意味します

  1. 既存の署名付きコミットオブジェクトと同様に、SHA-1 のみを使用する

  2. gpgsig-sha256 と gpgsig フィールドの両方を使用して、SHA-1 と SHA-256 の両方を使用します。

  3. gpgsig-sha256 フィールドのみを使用して、SHA-256 のみを使用します。

「git verify-commit」の古いバージョンでは、ケース (1) と (2) の gpgsig 署名を変更せずに検証でき、ケース (3) を通常の署名なしコミットと見なすことができます。

Signed Tags

新しいフィールド「gpgsig-sha256」をタグオブジェクト形式に追加して、SHA-1 に依存せずにタグに署名できるようにします。 署名されたペイロードは、gpgsig-sha256 フィールドと -----BEGIN PGP SIGNATURE----- で区切られた本文内の署名が削除されたタグの SHA-256 コンテンツです。

これは、タグに署名できることを意味します

  1. 既存の署名付きタグ オブジェクトと同様に、SHA-1 のみを使用する

  2. gpgsig-sha256 と in-body 署名を使用して、SHA-1 と SHA-256 の両方を使用します。

  3. gpgsig-sha256 フィールドのみを使用して、SHA-256 のみを使用します。

Mergetag embedding

コミットの SHA-1 コンテンツの mergetag フィールドには、そのコミットによってマージされたタグの SHA-1 コンテンツが含まれます。

同じコミットの SHA-256 コンテンツの mergetag フィールドには、同じタグの SHA-256 コンテンツが含まれています。

Submodules

記録されたサブモジュールポインターを変換(convert)するには、変換されたサブモジュールリポジトリを配置する必要があります。 サブモジュールの変換表(translation table)を使用して、新しいハッシュを検索(look up)できます。

Loose objects and unreachable objects

Loose-object-idx の高速検索(fast lookup)では、緩いオブジェクト(loose objects)の数が増えすぎないようにする必要があります。

「git gc --auto」は現在、6700 個の緩いオブジェクトが存在するようになるまで待ってから、パックファイルに統合します。 私達はこれを使用するためのより適切なしきい値を見つけるために測定を行う必要があります。

「git gc --auto」は現在、50 個のパックが存在するようになるまで待ってから、パックファイルを結合します。 緩いオブジェクトを積極的にパッキングすると、パックファイルの数が急激に増加する可能性があります。 これは、Martin Fick の指数ローリング ガベージ コレクション スクリプト(exponential rolling garbage collection script) (https://gerrit-review.googlesource.com/c/gerrit/+/35215) に似た戦略を使用することで軽減できます。

「git gc」は現在、刈り込み(prune)時の競合(race)を防ぐために。パックファイル内で遭遇(encounter)した到達不可能なオブジェクトを緩いオブジェクト(loose objects)に追い出し(expel)ます(別のプロセスが、削除しようとしているオブジェクト(the about-to-be-deleted object)を参照する新しいオブジェクトを同時に書き込んでいる場合)。 このため、存在する緩いオブジェクトの数が爆発的に増加し、そして、デルタ形式のオブジェクトが独立した緩いオブジェクトに置き換えられるためにディスク領域の使用量が爆発的に増加します。 さらに悪いことに、緩いオブジェクトの競合は依然として存在します。

代わりに、「git gc」は到達不能オブジェクトを UNREACHABLE_GARBAGE としてマークされた新しいパックファイルに移動する必要があります(PSRC フィールドを使用。下記参照)。 削除されようとしている(about-to-be-deleted)オブジェクトを参照する新しいオブジェクトを作成するときの競合(race)を避けるために、新しいオブジェクトを作成するプログラムコードは、新しい非 UNREACHABLE_GARBAGE パック (または緩いオブジェクト) を参照する UNREACHABLE_GARBAGE パックからすべてのオブジェクトをコピーする必要があります。 UNREACHABLE_GARBAGE は、その作成時刻 (ファイルの mtime で示される) が十分前であれば安全に削除できます。

UNREACHABLE_GARBAGE パックの急増を避けるために、特定の状況下でそれらを組み合わせることができます。 gc.garbageTtl が 1 日(one day)よりも長く設定されている場合は、単一暦日(single calendar day)(UTC)以内に作成されたパック達をまとめて結合できます。 結果のパックファイルには、その日の午前 0 時(midnight)より前の mtime が含まれるため、有効な最大 ttl は、garbageTtl + 1 日になります。 gc.garbageTtl が 1 日未満の場合、暦日(calendar day)をその ttl の 3 分の 1 の間隔に分割します。 同じ間隔内で作成されたパック達は、一緒に結合できます。 結果のパックファイルには、間隔の終了前の mtime が含まれるため、有効な最大 ttl は、garbageTtl * 4/3 に等しくなります。

このルールは、Thirumala Reddy Mutchukota の JGit の変更 https://git.eclipse.org/r/90465 に由来します。

UNREACHABLE_GARBAGE 設定は、パックインデックスの PSRC フィールドに入ります。 より一般的には、そのフィールドはパックがどこから来たかを示します。

  • 1 (PACK_SOURCE_RECEIVE) ネットワーク経由で受信したパックの場合

  • 2 (PACK_SOURCE_AUTO) 軽量の gc --auto 操作で作成されたパックの場合

  • 3 (PACK_SOURCE_GC) 完全(full)な GC によって作成されたパックの場合

  • 4 (PACK_SOURCE_UNREACHABLE_GARBAGE) gc によって検出された潜在的なゴミ(garbage)

  • 5 (PACK_SOURCE_INSERT) 例えば git add . で、パックファイルに直接書き込まれた、ローカルで作成されたオブジェクトの場合

この情報は、デバッグや、「gc --auto」でどのパックを合体させるかを適切に選択するのに役立ちます。

Caveats

Invalid objects

SHA-1 コンテンツから SHA-256 コンテンツへの変換(conversion)では、元のオブジェクトの破損(brokenness)が保持されます(例えば: 先行 0 でエンコードされたツリーエントリモードや、パスが正しくソートされていないツリーオブジェクトや、作成者またはコミッターなしでオブジェクトをコミットする)。 これは、双方向(round-trip)変換を可能にする設計上の意図的な機能です。

より深刻に壊れたオブジェクト (たとえば、切り捨てられた「ツリー」ヘッダー行を含むコミット) は変換できませんが、なにしろそんなのは現在の Git でも使用できません。

Shallow clone and submodules

ローカルで生成された変換表(translation table)ですべての参照オブジェクトを使用できるようにする必要があるため、この設計では浅い(shallow)クローンまたはフェッチされていないサブモジュールはサポートされていません。 プロトコルの改善により、この制限を解除できる可能性はあります。

Alternates

上記と同一の理由により、SHA-256 リポジトリは、objects/info/alternates または $GIT_ALTERNATE_OBJECT_REPOSITORIES を使用して SHA-1 リポジトリからオブジェクトを借用(borrow)することはできません。

git notes

「git notes」ツールは、SHA-1 名をキーとしてオブジェクトに注釈を付けます。 この設計では、ノートツリーを移行(migrate)して SHA-256 名を使用する方法については説明していません。 その移行は個別に行われるものと思われます(例えばノートツリーのルートにあるファイルを使用して、どのハッシュを使用するかを記述するなど)。

Server-side cost

Git プロトコルが SHA-256 をサポートするまでは、公開されている Git サーバーで SHA-256 ベースのストレージを使用することはかなり非推奨です。 Git プロトコルが SHA-256 のサポートを取得したら、SHA-256 ベースのサーバーは SHA-1 互換性をサポートしない可能性が高くなります。これは、複製中に非常にコストのかかるハッシュの再エンコードを回避し、ピアの最新化を促進するためです。

ここで説明する設計では、個人の SHA-256 リポジトリの SHA-1 クライアントによるフェッチを許可します。これは、そのリポジトリからのプッシュを許可するよりもそれほど難しくないためです。 このサポートは、構成オプションによって保護する必要があります — 多数のクライアントにサービスを提供する git.kernel.org のようなサーバーは、そのコストを負担しないと予想されます。

Meaning of signatures

署名されたコミットとタグの署名されたペイロードは、オブジェクトの識別に使用されるハッシュを明示的に指定しません。 いつか Git が現在の SHA-1 (16 進数 40 桁) または SHA-256 (16 進数 64 桁) オブジェクトと同じ長さの新しいハッシュ関数を採用した場合、オブジェクト署名の PGP 署名ペイロードの背後にある意図は不明瞭になります:

object e7e07d5a4fcc2a203d9873968ad3e6bd4d7419d7
type commit
tag v2.12.0
tagger Junio C Hamano <gitster@pobox.com> 1487962205 -0800
Git 2.12

Does this mean Git v2.12.0 is the commit with SHA-1 name e7e07d5a4fcc2a203d9873968ad3e6bd4d7419d7 or the commit with new-40-digit-hash-name e7e07d5a4fcc2a203d9873968ad3e6bd4d7419d7?

幸い、SHA-256 と SHA-1 では長さが異なります。 Git がオブジェクトに名前を付けるために同じ長さの別のハッシュを使用し始めた場合、この問題に対処するには、そのハッシュを使用する署名付きペイロードの形式を変更する必要があります。

Object names on the command line

移行をサポートするために (下記「Transition plan」(移行計画)参照)、この設計では 4 つの異なる操作モードがサポートされています:

  1. ("dark launch") ユーザーが入力したオブジェクト名を SHA-1 として扱い、出力に書き込まれたオブジェクト名を SHA-1 に変換(convert)しますが、SHA-256 を使用してオブジェクトを格納します。 これにより、ユーザーは、パフォーマンス以外の目に見える動作の変更なしでコードをテストできます。 これにより、SHA-1 ハッシュ関数を想定したテストを実行して、新しいモードの動作を健全性チェックすることができます。

  2. ("early transition") 入力で SHA-1 と SHA-256 の両方のオブジェクト名を許可します。 出力に書き込まれるオブジェクト名は、SHA-1 を使用します。 これにより、ユーザーは引き続き SHA-1 を使用して、まだ移行されておらず、モード 3 の準備をしているピアと (電子メールなどで) 通信できます。

  3. ("late transition") 入力で SHA-1 と SHA-256 の両方のオブジェクト名を許可します。 出力に書き込まれるオブジェクト名は、SHA-256 を使用します。 このモードでは、ユーザーはデフォルトでより安全なオブジェクト命名方法を使用しています。 ほとんどのピアがモード 2 またはモード 3 である限り、中断は最小限です。

  4. ("post-transition") ユーザーが入力したオブジェクト名を SHA-256 として扱い、SHA-256 を使用して出力を書き込みます。 これは、間違ったハッシュ関数を使用して入力が誤って解釈されるリスクが少ないため、モード 3 よりも安全です。

モードは構成で指定されます。

ユーザーは、モードをオーバーライドして、特定のリビジョン指定子(specifier)と出力に使用する形式を明示的に指定することもできます。 例えば:

git --output-format=sha1 log abac87a^{sha1}..f787cac^{sha256}

Transition plan

いくつかの初期ステップは、互いに独立して実装できます:

  • ハッシュ関数 API (vtable) の追加

  • fsck に gpgsig-sha256 フィールドを許容するように教える(teaching)

  • git commit --amend でコピーされたフィールドから gpgsig-* を除外する

  • SHA-1 テスト前提条件で SHA-1 値に依存するテストに注釈を付ける

  • unsigned char * とハードコードされた定数 20 や 40 の代わりに、一貫して struct object_id と GIT_MAX_RAWSZ や GIT_MAX_HEXSZ を使用します。

  • インデックス v3 を導入

  • PSRCフィールドのサポートの追加と、より安全なオブジェクト刈り込み(pruning)の追加

ユーザーが目にする最初の変更は、objectFormat 拡張機能 (compatObjectFormat なし) の導入です。 これには以下が必要です:

  • この操作モードについて fsck に教える(teaching)

  • オブジェクト名の計算時にハッシュ関数 API (vtable) を使用

  • オブジェクトへの署名と署名の検証(verify)

  • 互換性のないリポジトリからのフェッチまたはプッシュの試行を拒否

お次は compatObjectFormat の紹介です:

  • Loose-object-idx の実装

  • オブジェクト形式間でのオブジェクト名の変換(translate)

  • オブジェクト形式間でのオブジェクトコンテンツの変換(translate)

  • compat 形式での署名の生成と検証(verify)

  • 新しいオブジェクトをオブジェクトストアに追加するときに、適切なインデックスエントリを追加する

  • --output-format オプション

  • デフォルトの入力形式および出力形式を指定する構成(上記「Object names on the command line」(コマンド ライン上のオブジェクト名)参照)

次のステップは、 SHA-1 リポジトリへのフェッチとプッシュのサポートです:

  • compat 形式を使用したリポジトリへのプッシュを許可する

  • フェッチされたオブジェクトの SHA-1 名のトポロジ的(topologically)にソートされたリストを生成する

  • 取得したパックファイルを SHA-256 形式に変換(convert)し、idx ファイルを生成します

  • 取得したパックファイル内のオブジェクトの順序に一致するように再ソートします

フェッチをサポートするインフラストラクチャにより、既存のリポジトリを変換(convert)することもできます。 変換されたリポジトリと新しいクローンでは、エンドユーザーは、動作に目に見える変更を加えることなく、新しいハッシュ関数のサポートを得ることができます(「Object names on the command line」セクションの「dark launch」を参照してください)。 特に、これにより、ユーザーはリポジトリ内のオブジェクトの SHA-256 署名を検証(verify)できます。また、より広く使用する準備として、移行コードが本番環境で安定していることを確認しなければなりません。

時間が経つにつれて、プロジェクトはユーザーに「early transition」(早期移行)モードを採用し、次に「late transition」(後期移行)モードを採用して、新しい、より将来性の高い SHA-256 オブジェクト名を利用するよう促します。

objectFormat と compatObjectFormat の両方が設定されている場合、署名を生成するコマンドはデフォルトで SHA-1 と SHA-256 の両方の署名を生成し、新しいユーザーと古いユーザーの両方をサポートします。

SHA-256 を多用するプロジェクトでは、SHA-1 オブジェクト名を誤って暗黙的に使用することを避けるために、ユーザーは「post-transition」(移行後)モードを採用するよう奨励される可能性があります。

クリティカル・マスのユーザーが SHA-256 署名を検証(verify)できる Git のバージョンにアップグレードし、既存のリポジトリを変換(convert)して検証をサポートすると、SHA-256 署名のみを生成する設定のサポートを追加できます。 これは、少なくとも 1 年後になると予想されます。

これは、リポジトリを変換して SHA-256 のみを使用し、SHA-1 関連のメタデータをすべて取り除く機能を宣伝する良い機会でもあります。 これにより、変換のオーバーヘッドをなくすことで性能を向上させ、誤ってSHA-1の安全性に頼ってしまう可能性をなくすことで安全性を向上させています。

サーバーがサポートするハッシュ関数を指定できるように Git のプロトコルを更新することも、この移行の重要な部分です。 このドキュメントでは詳しく説明しませんが、この移行計画ではそれが発生することを前提としています。 :)

検討された代替案

指定日に特定のプロジェクトに取り組んでいる全員をアップグレード

Linux カーネルのようなプロジェクトは大規模で複雑なため、リポジトリに基づくすべてのプロジェクトのスイッチを一度に切り替えることは不可能です。

すべての開発者と開発者をサポートするサーバーオペレーターが同じ指定日に切り替えなければならないだけでなく、サポートツール (継続的インテグレーション、コードレビュー、バグトラッカーなど) も同様に調整する必要があります。 これにより、多数の人が移行に踏み切る時期が来る前に、一部のプロジェクト参加者のテストから早期にフィードバックを得るというのは難しくなります。

Using hash functions in parallel

(例: https://lore.kernel.org/git/22708.8913.864049.452252@chiark.greenend.org.uk/ ) 新しく作成されたオブジェクトは新しいハッシュによってアドレス指定されますが、そのようなオブジェクト内(例: コミットオブジェクト)では、まだ古いハッシュ関数を使用してオブジェクトをアドレス指定できます。

  • あなたは、将来、さらなる作業なしにその履歴(二分探索可能性(bisectability)に必要)を信頼することはできません

  • サポートされているハッシュ関数の数が増えると、保守の負担が大きくなります (決してなくなることはないため、累積されます)。 この提案では、比較すると、変換されたオブジェクトは SHA-1 へのすべての参照を失います。

Signed objects with multiple hashes

SHA-256 コンテンツ ベースの署名のコミットおよびタグオブジェクトに gpgsig-sha256 フィールドを導入する代わりに、この設計の以前のバージョンでは、既存の SHA-1 コンテンツ ベースの署名を強化するために「hash sha256 <SHA-256 name>」フィールドが追加されました。

つまり、単一の署名が、両方のハッシュ関数を使用してオブジェクトのコンテンツを証明するために使用されました。 これにはいくつかの利点がありました:

  • 2 つではなく 1 つの署名を使用すると、署名プロセスが高速化されます。

  • 両方のハッシュを持つ 1 つの署名済みペイロードを持つことで、署名者は同じオブジェクトを参照する SHA-1 名と SHA-256 名を証明できます。

  • すべてのユーザーが同じ署名を使用します。 破損した署名は、現在のバージョンの git を使用して迅速に検出(detect)される可能性があります。

ただし、欠点もありました:

  • 署名されたオブジェクトを検証(verify)するには、移行が完了し、他の目的で変換表が(translation table)不要になった後でも、それが参照するすべてのオブジェクトの SHA-1 名にアクセスする必要があります。 これをサポートするために、署名付きコミットの SHA-256 コンテンツに「hash sha1 tree <SHA-1 name>」や「hash sha1 parent <SHA-1 name>」などのフィールドが追加され、変換プロセスが複雑になりました。

  • SHA-1 なしで署名付きオブジェクトを許可すると、(移行完了後、)設計がさらに複雑になり、SHA-256 コンテンツと署名付きペイロードに「hash sha1」フィールドを含めることを抑制するために「nohash sha1」フィールドが必要になりました。

Lazily populated translation table

変換表(translation table)を構築する作業の一部は、プッシュする時まで延期できますが、それはプッシュを非常に複雑にし、遅くします。 オブジェクトの作成時に SHA-1 名を計算すると同時に、オブジェクトがディスクにストリーミングされ、その SHA-256 名が計算されることは、許容できるコストです。

Document History

2017-03-03 jrnieder@gmail.com jonathantanmy と sbeller からの提案を受け入れました:

  • 各ハッシュタイプで署名済みオブジェクトの目的を説明

  • 最初のハッシュ関数の下でオブジェクトコンテンツを使用して、署名付きオブジェクトの検証(verification)を再定義

  • SHA2 の代わりに SHA3-256 を使用します(Linus と brian m. carlson に感謝します)。 [1][2]

  • SHA3ベースの署名を別のフィールドにして、「hash」フィールドと「nohash」フィールドの必要性を回避します(peff[3] に感謝)。

  • フェッチするための並べ替えフェーズを追加します (この必要性に気付いた Junio に感謝します)。

  • フェッチ中にトポロジ的(topological)ソートからブロブを省略します (peff に感謝)。

  • 代替案 や git note や git サーバーについては、「caveats」セクションで扱います (Junio Hamano、brian m. carlson[4]、Shawn Pearce に感謝します)。

  • 全体を通して言葉を明確にします (さまざまなコメンテーター、特に Junio に感謝します)。

  • SHA3-256 の代わりにプレースホルダー NewHash を使用する

  • ハッシュ関数をピックする基準を説明。

  • 移行計画を含める (特に、これらのアイデアを具体化してくれた Brandon Williams に感謝します)

  • 変換(translation)表を定義 (Shawn Pearce[5] 、Jonathan Tan 、Masaya Suzuki に感謝します)。

  • git gc --auto でより積極的にパッキングすることで、緩いオブジェクト(loose object)のオーバーヘッドを回避。

その後の履歴

  • その後の編集の履歴については、 git.git でこのファイルの履歴を参照してください。 このドキュメントの履歴は、コミットログには不要になるため、維持されなくなりました

References

[1] https://lore.kernel.org/git/CA+55aFzJtejiCjV0e43+9oR3QuJK2PiFiLQemytoLpyJWe6P9w@mail.gmail.com/
[2] https://lore.kernel.org/git/CA+55aFz+gkAsDZ24zmePQuEs1XPS9BP_s8O7Q4wQ7LV7X5-oDA@mail.gmail.com/
[3] https://lore.kernel.org/git/20170306084353.nrns455dvkdsfgo5@sigill.intra.peff.net/
[4] https://lore.kernel.org/git/20170304224936.rqqtkdvfjgyezsht@genre.crustytoothpaste.net
[5] https://lore.kernel.org/git/CAJo=hJtoX9=AyLHHpUJS7fueV9ciZ_MNpnEPHUz8Whui6g9F0A@mail.gmail.com/