同じファイルに同じ処理を施す場合、以前の結果を再利用すれば計算を省けます。ここでは、そのために「ファイルの内容」と「処理条件」の組を一つの目印にまとめる、説明用の仮の設計を考えます。実在サービスの事故を紹介する例ではありません。
SHA-256は、入力を一定長の値に変換するハッシュ関数です。その値(ハッシュ値)を、ここでは照合の目印に使います。開発者はこの目印を使って、保存済みの結果を探すキーを作ります。ハッシュに渡す入力には、ファイルの内容と処理条件の両方を含めます。ところが、二つの値をただつなげると、違う組なのに同じ入力になってしまうことがあります。
SequenceHashは、値を一つずつ渡すことで区切りを保ち、SHA-256などの既存のハッシュ関数と組み合わせる方式です。複数項目から目印を作る開発者が、区切り方を毎回自作する代わりに使える選択肢です。[9]
照合用の値を作るとき、項目の境界が消える
説明用に、項目1と項目2の組を考えます。項目1が「ab」、項目2が「c」の組と、項目1が「a」、項目2が「bc」の組は別のデータです。しかし、境界の情報を付けずに連結すると、どちらも「abc」になります。[1]
| 項目1 | 項目2 | 連結した結果 |
|---|---|---|
| ab | c | abc |
| a | bc | abc |
この状態でSHA-256に渡るのは、異なる二つの組ではなく、同じ「abc」という入力です。そのため出力も同じになります。これは、異なる入力から同じハッシュ値が出る現象を指す「暗号学的な衝突」ではありません。ハッシュ関数に渡す前の組み立てで区切りを失い、最初から同一のバイト列(バイト単位で並んだデータ)を入力している問題です。[2]
先ほどの仮の設計で、この目印だけを使って過去の結果を探すと、本来は別の処理なのに同じ結果を再利用してしまう可能性があります。防ぎたいのは、この取り違えです。ハッシュを計算する前に、項目の区切りを残す必要があります。
後輩ハッシュの強さを上げれば、この二つの組も区別できるんですか?
先輩入力が同じ「abc」になった後では、ハッシュ関数は元の分割を知れないよ。まず入力の作り方で境界を残す必要があるね。
区切りを保つ方法は複数あり、設計条件が異なる
境界を保つ方法は一つではありません。区切り文字を挟む方式は、値にその文字を含めない規約を設けるか、含まれる場合のエスケープ方法を決めれば、一意に読み取れる形にできます。
たとえば区切りを「|」にすると、「a|b」と「c」の組も、「a」と「b|c」の組も「a|b|c」になります。エスケープは、値の中の区切り文字を特別な表記に置き換えて、項目の区切りと区別する処理です。規約を正しく決めれば、この方式でも解決できます。
各値の長さを付ける方式も、符号化(データを決めた形式で表すこと)を一意に読み取れるなら、境界を保てます。[3]ただし、長さ自体の書き方や単位を合意する必要があります。文字数と、コンピューターが扱うデータの大きさであるバイト数は、同じとは限りません。
JSONのように項目構造を持つ形式にまとめる方法もあります。JSONの空白やキー順などの違いを同じ表記にそろえたい場合は、JCS(JSONの書き方を一定の規則にそろえる仕様)を先に使う選択肢があります。[4]
ここでは、境界を保つ課題と、同じ意味のデータを同じ表記にする課題を分けてください。JSONを正規化して表記をそろえても、どの値を一つの要素として扱うかという設計は残ります。反対に、要素の境界が保たれていても、JSONの表記差を自動で解消するわけではありません。[4]
| 方法 | 解けること | 設計者に残ること |
|---|---|---|
| 区切り文字 | 規約が一意なら項目の境界を表せます | 区切り文字の禁止やエスケープ方法を決めます |
| 長さを付ける符号化 | 一意に解釈できる長さ情報で境界を表せます | 長さの表し方を合意します |
| 構造化JSONと必要に応じたJCS | 項目構造を持たせ、必要ならJSONの表記を同じ規則でそろえられます | 要素の単位や、JSONをどう表記するかを決めます |
| HMAC | 一意な入力に、共有した秘密の鍵で改ざんの検出機能を付けられます | 複数値を曖昧に連結した場合の境界は復元しません |
| TupleHash | 複数入力の境界を扱ってハッシュできます | SHA-3(ハッシュ関数の規格)系の方式を使います |
| SequenceHash | 複数の値を境界付きで扱い、既存のハッシュ関数と組み合わせられます | 値の順序やバイト表現などを決めます |
HMACは、送信側と受信側が共有する秘密の鍵を使い、メッセージの改ざんを確かめる仕組みです。すでに一意に符号化された一つのデータを認証する用途に使えますが、複数の値を単純連結した入力から、失われた境界を自動で復元するものではありません。[5]
TupleHashは、複数の入力文字列を境界が曖昧にならない形で扱うNIST(米国国立標準技術研究所)が定めた方式です。SHA-3系の関数であるcSHAKEを使い、SequenceHashとは基礎にするハッシュの選択肢が異なります。どちらかが常に優れているという比較ではなく、求める入力形式や使いたい方式に合わせて選びます。[6]
SequenceHashは境界の符号化を仕様に任せる
SequenceHashは、値を一つずつ要素として渡し、各値の長さを符号化してからハッシュ計算に含めます。要素の順序も対象になり、外側の計算には要素数も含まれます。そのため、値の境界や列の個数を保つ符号化を、利用側が毎回独自に組み立てる代わりに、仕様や対応ライブラリに任せる選択肢になります。[7]
概念としては、各値の後ろにその長さを添えた形で内側のハッシュを計算し、その結果を外側でもう一度ハッシュします。空の値も一つの要素として扱われます。
細かな手順は、C2SPで公開されている仕様(開発版)に定められています。実装時は、Go版などの実装も確認してください。[8]
SequenceHashの特徴は、SHA-256やSHA-512など、既存のハッシュ関数を基礎に選べることです。TupleHashがSHA-3系であるのに対し、既存のハッシュを使いたい設計では比較対象になります。ただし、境界の符号化がハッシュ自体を強くするわけではありません。安全性は基礎となるハッシュ関数にも依存します。[9]
値の意味とバイト表現は、利用側が決める
SequenceHashが扱うのは、渡されたバイト列です。文字列、数値、日時などをどのバイト列に変換するかまでは決めません。したがって、同じ意味の値を常に同じバイト列にしたい場合は、その表現方法を利用側が定めてから要素として渡します。[10]
JSONを要素にする場合も、SequenceHash自体は空白やオブジェクトキーの順序などを正規化しません。空白やキー順などのJSONの表記差をそろえたい場合は、JCSで書き出したバイト列を渡します。JCSはUnicode(文字を扱う規格)の文字列そのものの正規化は行いません。文字の表し方も同一視したい場合は、その規則を別途決めます。正規化は表記をそろえる処理であり、項目の境界符号化とは別の役割です。[4]
実務では、項目の順番、どの値を一つの要素とするか、各値をどのバイト列に変換するかを決めます。Go版の追加操作 Add は、1回につき1要素を受け取ります。「ab」と「c」を2回で追加する場合と、「abc」を1回で追加する場合は、異なる要素列です。
通信で届く断片は、項目の区切りとは別物です。Go版で大きな1要素を渡す場合は、断片を一つのバイト列にまとめてから Add に渡します。断片ごとに Add を呼ぶと、要素の分割まで変わります。
共有鍵で要素列そのものを認証する必要がある場合は、関連する構成としてSequenceMACがあります。鍵は32バイト以上が必要です。選ぶハッシュと出力長にも安全性の条件があるため、実装時はC2SPの要件を確認します。[11]
既存の符号化で足りるなら、切り替えは不要
複数の項目を別々に扱いながら、SHA-256など既存のハッシュを使いたい設計では、SequenceHashを比較対象にできます。境界の符号化を仕様に任せられるため、区切り文字や長さ表現の規約を利用側で個別に組み立てる負担を減らせます。[3]
一方、処理条件を含めず、単一ファイルの内容だけを照合する用途では、複数項目の境界を符号化する必要はありません。冒頭の例のように処理条件まで照合対象にする場合は、内容と条件の両方を含む入力が必要です。既存のJSON正規化や、一意に符号化した入力へのハッシュで足りるなら、切り替えは不要です。共有鍵による認証には、正しく符号化した入力とHMACで足りる場合があります。TupleHashで要件を満たす場合も、急いで切り替える理由はありません。[5, 6]
判断の軸は、値を複数の要素として渡す必要があるか、既存のハッシュ関数を使いたいか、境界の符号化を誰が担うかです。どの方式でも、値の順序、要素の単位、バイト表現の規約は利用側に残ります。SequenceHashは、それらを決めたうえで境界の符号化を任せたい場面に向く選択肢です。



