彷徨えるフジワラ

年がら年中さまよってます

Mercurial の検閲 (censor) 機能

Mercurial 3.4 から、エンドユーザレベルで "censor" 機能を利用するための、censor エクステンションが同梱されるようになりました。

"censor" 機能とは、一言で言うなら「履歴に記録されたファイル内容の検閲」の事です。ここで言う「検閲」は、「内容の事前確認」よりも「不適切なデータの消去」の意味と考えてください。

censor の仕組み自体は、随分前からコア機能として取り込まれていたのですが、そろそろエンドユーザ向けに機能を公開しても良かろう、という判断が下されたものと思われます。

もっとも、公開されはしたものの、censor エクステンションのヘルプでは、基本的な事柄しか説明されていません。"File Censorship Plan" と題した Wiki ページが用意されてはいますが、これはどちらかというと、開発方針や実装上のポイント等の、内部寄りな話がメインで、使い方に関する話はあまり書かれていません。

そこで、このエントリでは、現時点で私の把握している範囲で censor 機能について説明しようと思います。

想定されるユースケース

組織の内部用途や、個人的な目的で記録していた履歴を、組織外部/自分以外に公開しなければならなくたったとしましょう。

もしも、以下の様な情報を保持するファイルが存在する場合:

  • パスワード、秘密鍵、暗号化情報
  • 有効期限の切れたライセンスにひも付いた、データ/プログラム/ライブラリ
  • 個人識別情報、あるいはそれに類する個人情報

当該ファイルを "hg remove" したり、該当部分を編集で削除した結果をコミットしたとしても、履歴中には当該情報が残っていますから、情報秘匿の視点から見ると、これらの操作は意味がありません。

この場合、考えられる手段は以下の様なものでしょうか?

  • 当該情報を取り除いた状態の最新版の内容を、リビジョン 0 として公開
  • mq や histedit, rebase 等を駆使して、当該情報を履歴から消去してから公開

前者の場合、作業自体は容易ですが、履歴の連続性が失われることから、ソフトウェア開発の様なケースでは、あまり望ましくないと言えます。

後者の場合、履歴の量次第ではありますが、かなり面倒な作業になる筈です。

「当該ファイルの履歴は丸々破棄して構わない」というケースであれば、convert 機能による hg ⇒ hg リポジトリ変換の過程で、filemap の exclude 機能を使い、当該ファイルの履歴を丸ごと破棄する、という方法でも良いかもしれませんね (作業的には、こちらの方が簡単)。

いずれにしても、こららの「履歴改変を伴う」方法の場合、履歴改変後も作業を継続するのであれば、それまでに複製された既存のリポジトリ=秘匿したい情報を含むリポジトリは、全て破棄しなければなりません。

Mercurial との対比で「履歴改変が可能」と言われる Git も、容易に参照できない&GC契機で将来的に破棄されるだけで、実は履歴自体は書き換えていないので、対象データを含む履歴をうっかり push する危険を回避するためには、複製済みの履歴情報を破棄する必要があるでしょう。

Mercurial の censor 機能による「検閲」は、このようなケースにおいて、履歴中の特定リビジョンにおける指定ファイルの内容を消去できます。

単に「履歴の一部を消去」と言うと、上記の履歴改変と変わらないように思われるかもしれません。

censor 機能による検閲が、これらの履歴改変機能と異なるのは、ファイル内容消去の際に「対象リビジョンのハッシュ値を変えない」という点にあります。

検閲前後で対象リビジョンのハッシュ値が同一になることから:

  • 複製済み(= 検閲未実施)のリポジトリを、そのまま使い続けられる
  • 未検閲リポジトリから検閲済みリポジトリに push しても、検閲対象データは「連携先に既に存在する」扱いになるため、 push されない

まぁ、コーナーケースを考え出すと、「外部の空リポジトリに、内部から push されたら、秘匿情報が漏れる」みたいな話もあるので、厳密な安全性を考えたら、未検閲なリポジトリは破棄した方が良いんですけどね(笑)

とりあえず、「最新版のみの公開」や「事前の履歴改変」よりは、安全性と利便性のバランスが取れている、というところでしょうか > censor

なお、Git との対比で「履歴を改変しない」と言われる Mercurial ですが、censor 機能によるファイル内容の消去は、履歴情報を思いっきり書き換えるので、一度検閲を実施すると、そのリポジトリ単独では消去前の状態に戻すことができません

元々の目的が「個人情報等の消去」なので、復旧できてしまうと、それはそれで問題ですからね。

censor エクステンションを無効にしたり、censor 機能が提供されていない頃の Mercurial を使っても、「検閲済みデータを参照できない」状況は変わりませんから、検閲操作の際には十分ご注意ください。。

検閲前の事前準備

例えば、検閲対象データを含むファイルを FILE とした場合、hg censor コマンドでの検閲実施に先立って、以下の事前準備をしてください。

  • FILE の内容が丸々検閲対象 → "hg remove FILE" + "hg commit" でファイルを登録除外
  • FILE の内容の一部が検閲対象 → 検閲対象部位を削除し、変更を "hg commit"

前者の場合、ファイルを空にする編集 + 変更内容の "hg commit" という、後者と同様の扱いでも構いません。

もしも、「最新の FILE は、既に検閲対象データを保持しない (= 検閲対象データは、履歴中の FILE だけが保持)」状態であるのなら、単に "hg update により、作業領域を最新リビジョンで更新してください。

肝心なのは、「最新リビジョン/作業領域の親リビジョンにおいて、検閲対象データが保持されていない」ようにすることです。

例えば、赤丸は「検閲対象を含むファイルが存在するリビジョン」、二重丸は作業領域の親リビジョンを表すものとした場合、現時点で以下の様な履歴であるならば:

検閲対象データを含むファイルに対して、登録除外 ("hg remove") なり、検閲対象データの削除編集なりの結果をコミットすることで:

上記のような履歴にする必要があります。

また、履歴上で FILE が検閲対象データを保持しているリビジョンの範囲の特定も必要です。

例えば、以下の様な履歴の場合、リビジョン 1 および 2 の両方に対して、censor 機能による検閲を実施する必要があります。

既に結構な量の履歴がある場合 (= まさに censor が想定しているユースケース!)、現状の Mercurial の機能だと、 "hg grep --all PATTERN FILE" やら "hg bisect を組み合わせるなど、ちょっと手間のかかる作業になるかもしれませんね。

この辺の作業を省力化できる機能拡張の私案がある (というか、このエントリを書いていて思い付いた) ので、暫くは grep や bisect で頑張ってみてください (笑)

検閲の実施

まずは censor エクステンションを有効化します。個人毎設定ファイルなり、リポジトリ毎設定ファイルなりに、以下の記述を追加してください。

[extensions]
censor=

hg help -e censor で、censor エクステンションのヘルプが表示されれば、エクステンションは正しく有効化されています。

それでは、検閲対象データを消去してみましょう。

以降の実行例では、以下の状況を仮定します。

  • ファイル FILE の追加/変更を行う、リビジョン番号 0 〜 3 の4つのリビジョンが存在
  • リビジョン 2 の時点でのみ、ファイル FILE が公開したくないデータ=検閲対照データを保持
  • 作業領域の親リビジョンは 3

この状況は、「リビジョン 2 の時点で混入した検閲対象データを、リビジョン 3 で削除」した状況とも言えます。

検閲実施に先立って、対象リビジョンのハッシュ値等を確認しておきましょう。

$ hg log -r 2 --debug
リビジョン:   2:8df8f1f5b377b7c7359fd4a155e5fe66dbb4573a
フェーズ:     draft
親リビジョン: 1:4f79a1541eda9573025bb0c5d5ae670268631159
親リビジョン: -1:0000000000000000000000000000000000000000
manifest参照: 2:9bf508b8aff71c8a6aef237ae1f171476ae05180
ユーザ:       FUJIWARA Katsunori 
日付:         Fri May 01 20:13:41 2015 +0900
ファイル:     FILE
その他:       branch=default
説明:
危険なリビジョン

$

--debug フラグ指定により、普段表示される 12 桁の短縮ハッシュ値ではなく、40 桁の完全長ハッシュ値が表示されます。

また、検閲対象データを含むファイル内容が、この時点では "hg cat -r 2 FILE" などにより参照できることも確認しておいてください。

$ hg cat -r 2 FILE
問題のない情報
危険な情報
$

検閲は以下のコマンドで実施されます。検閲処理自体は、特に何もメッセージ等を表示しません。

$ hg censor -r 2 FILE
$

検閲実施後も、対象リビジョンのハッシュ値が変わらないことを確認してみましょう。

$ hg log -r 2 --debug
リビジョン:   2:8df8f1f5b377b7c7359fd4a155e5fe66dbb4573a
フェーズ:     draft
親リビジョン: 1:4f79a1541eda9573025bb0c5d5ae670268631159
親リビジョン: -1:0000000000000000000000000000000000000000
manifest参照: 2:9bf508b8aff71c8a6aef237ae1f171476ae05180
ユーザ:       FUJIWARA Katsunori 
日付:         Fri May 01 20:13:41 2015 +0900
ファイル:     FILE
その他:       branch=default
説明:
危険なリビジョン

$

リビジョン番号もハッシュ値も、"hg censor" 実施前後で変わっていないことが確認できたと思います。

さて、それでは試しに、検閲対象データを含むファイルの内容を参照してみましょう。

$ hg cat -r 2 FILE
中止: 検閲対象リビジョンです: filelog 6ffd5083a9b9
(エラーを無視する場合は censor.policy 設定を ignore に)
$

上記のようにエラー中断してしまうため、検閲対象データは参照できません。

なお、"filelog 6ffd5083a9b9" 部分は内部的な情報なので、気にしないでください (笑)。この辺の出力は誤解を招きかねないので、もうちょっと改善提案したいところ……

閑話休題

エラー時に表示されるヒントに従って、"censor.policy=ignore" 設定ありで実行してみると:

$ hg --config censor.policy=ignore cat -r 2 FILE
$

コマンド実行自体は正常終了しますが、ファイルの内容は空データ扱いになります。ポリシー設定に関わらず、検閲対象データが参照できない点は一緒です。

試しに作業領域を当該リビジョンで更新してみると:

$ hg update -C 2
中止: 検閲対象リビジョンです: filelog 6ffd5083a9b9
(エラーを無視する場合は censor.policy 設定を ignore に)
$

こちらもエラー中断となります。 "censor.policy=ignore" 設定があれば、処理自体は継続されますが、作業領域に取り出される FILE はやはり空データとなります。

これらの他にも、 exportdiff といった、ファイルの内容を扱う全ての処理において、リビジョン 1 時点における FILE の内容は、空 (あるいは「参照禁止」でエラー終了) 扱いになります。

検閲機能の制限

最も注意すべき点は、「検閲の実施」に関する情報は、「他のリポジトリに対して事後伝播しない」という点でしょう。

先述したように、censor による検閲が実施されても、当該リビジョンのハッシュ値は変化しません。

この特性は、一見すると便利に見えるかもしれませんが、情報伝播の観点で見ると「伝播すべき情報の有無を検出できない」という面もあります。

つまり、検閲前に他のリポジトリに伝播してしまった履歴に含まれるデータは、検閲実施の影響を受けません。伝播済みリポジトリを起点に、他のリポジトリへと検閲対象データを含む履歴が拡散する危険性は、そのまま残り続けるわけです。

この点で、事後の廃止指示の情報が伝播する obsolete とは異なります (とは言え、こちらも対象リビジョンの破棄が保証されているわけではないので、セキュリティ的な観点で言えば、どっこいどっこいとも言えますが……)

そのため、現状で censor 機能が利用できるシチュエーションは、冒頭の想定ユースケースで例示したようなケースや、「内部的に集約した履歴を外部公開する直前に検閲」みたいなケースに限定されてしまうでしょうね。

なお、履歴記録とは別の、メタデータ的な情報の伝播に関しては、現在絶賛実装作業中である、次期履歴伝播プロトコル bundle2 の導入で、大きく改善される予定です。

bundle2 の正式採用により、検閲情報が伝播可能になれば、もう少し使用可能なケースが増えるかもしれませんね (改造可能な OSS の場合、プロトコル仕様上の伝播可否と、安全性保証の有無は、必ずしも一致しないところがアレですが……)