コンテンツとハッシュ化
Label 309 がレコードをコンテンツに結び付ける仕組み。hashes マップの構造、ダイジェストが何にコミットするのか、そして多数のアイテムを 1 つのルートにまとめてチェーン上に記録するための Merkle コミットメントを解説します。
主張の中心はコンテンツのハッシュ値です。Label 309 レコードが存在について示すすべての事柄は、コンテンツのバイト列を暗号学的にダイジェスト化し、それをメタデータラベル 309 の下でチェーン上に記録することに由来します。このページでは、そのダイジェストがどのように運ばれるのか、正確には何にコミットしているのか、そして 1 つの 32 バイトのルートが任意に大きなアイテムのまとまりをどのように代表できるのかを定義します。
hashes マップ
レコード内の各アイテムは hashes マップを持ちます。これはアルゴリズム識別子から生の 32 バイトダイジェストへの CBOR マップです。
hashes = {
"sha2-256": h'…32 bytes…', ; key = algorithm id, value = raw digest
}キーはハッシュレジストリから選ばれたテキスト文字列の識別子であり、値は生のバイト文字列で、決して 16 進数エンコードはされません。このマップには少なくとも 1 つのエントリが含まれていなければなりません(MUST)。登録済みのハッシュアルゴリズムはいずれも、ちょうど 32 バイトのダイジェストを生成します。
| 識別子 | アルゴリズム | 参照 | ダイジェスト長 |
|---|---|---|---|
sha2-256 | SHA-256 | FIPS 180-4 | 32 B |
blake2b-256 | BLAKE2b-256 | RFC 7693 | 32 B |
この 2 つの識別子はどちらも検証者が実装しなければならない必須項目です。そのため、どちらか一方だけを使った単一ハッシュのレコードであっても、あらゆる環境で検証が通ります。未知の識別子に遭遇した検証者は、そのエントリを黙って読み飛ばすのではなく、安定したエラーコードを返してレコードを拒否します。ポスト量子用に予約されたスロットを含む完全なレジストリは アルゴリズムレジストリ にあります。
並列の配列や {alg, digest} サブオブジェクトのリストではなく CBOR マップを使うことには、ワイヤーコントラクトの一部をなす 3 つの帰結があります。まず、CBOR マップのキーは一意であるため、アルゴリズムの重複は構造上ありえません。次に、正規 CBOR はキーをエンコード済みのバイト列で並べ替えるため、正規順序が自動的に決まります。これにより、同じハッシュセットを表現する 2 つのプロデューサーはバイト単位で同一のマップを出力し、それに対するレコードレベルの署名はどれも安定します。そして、この構造はエントリごとの検証を必要としません。構造バリデーターは、各キーが登録済みであること、各値がそのアルゴリズムのダイジェスト長を持つことだけを確認します。
ハッシュ値がコミットする対象
ダイジェストはコンテンツのバイト列、つまりプロデューサーがタイムスタンプを付与しようとしている、まさにそのバイト列にコミットします。hashes マップ内のすべてのエントリは、いずれもその同じバイト列を、名前付きのアルゴリズムでハッシュ化した結果でなければなりません。異なる平文を記述するエントリを含むレコードは非適合です。検証者はコンテンツのバイト列を入手できる場合、すべてのダイジェストを再計算しなければならず(MUST)、1 つでも一致しなければそのレコードを拒否します。
レコードが暗号化エンベロープ(enc)を持つ場合、ハッシュ値は暗号文ではなく平文に結び付けられます。これは意図的な設計です。存在証明(Proof of Existence, PoE)は、後日著者が平文を開示し、それが特定の時刻に存在していたことを証明できるようにするために存在します。暗号文をハッシュ化しても、何らかの暗号化されたデータが存在したことを証明できるだけで、その背後にあるコンテンツについては何も語りません。だからこそ封緘済みレコードも、どの平文にタイムスタンプが付いたのかを正確に証明します。受信者は復号して平文のダイジェストを再計算し、それをオンチェーンのコミットメントと照合します。したがって、enc を持つアイテムは少なくとも 1 つのコンテンツハッシュエントリを必ず持たなければなりません(MUST)。それがなければ、照合の基準となる平文の主張が存在しないことになります。
封緘されていても平文に結び付けられます
封緘済みレコードのオンチェーンダイジェストは、平文(クリアテキスト)のダイジェストです。暗号文そのものはコンテンツアドレス型の
ar:// または ipfs:// URI
に置かれるため、ストレージゲートウェイが返すバイト列は、ゲートウェイを信頼せずともそのアドレスに照らして改ざんを検知できます。受信者はそれを復号し、平文のハッシュ値を再計算することで、オンチェーンの主張まで一貫して結び付けます。
ハッシュは 1 つでも複数でも
単一のコンテンツハッシュで完全に適合します。レジストリ内のすべての 256 ビットハッシュについて、現在知られている最良の第 2 原像攻撃は古典的計算で 2^256 付近に位置します。単一の健全な 256 ビットハッシュは、レコードのアーカイブ期間中の現実的な脅威モデルをすでにカバーしており、構造バリデーターは単一エントリのレコードに対して警告を発しません。
プロデューサーは、任意の多層防御として、独立した設計系統に属する 2 番目のエントリを追加できます(MAY)。たとえば sha2-256(SHA-2: Merkle–Damgård 構造)と blake2b-256(BLAKE2: ChaCha 由来のパーミュテーションを用いた HAIFA 構造)を組み合わせる場合です。この 2 つの系統は構造上の系譜をまったく共有しないため、両方を持つレコードが弱体化するのは、両系統が暗号解析によって同時に破られたときに限られます。その代償は、アイテムあたり 32 バイトのダイジェスト 1 つと、その短い識別子だけです。採用するかどうかはプロデューサーの判断であり、必須とされることはありません。
Merkle バッチコミットメント
1 つのコンテンツハッシュは 1 つのコンテンツをチェーン上に記録します。任意に大きなコレクション、たとえば 500 ファイルの CI アーティファクトセット、IoT イベントのストリーム、監査ログのバッチなどをチェーン上に記録するために、Label 309 はトップレベルの merkle[] 配列を定義します。各エントリは、順序付けられた 32 バイトのリーフのリストにコミットし、対応する 32 バイトのルートを 1 つチェーン上に発行します。順序付けられたリーフ自体はオフチェーンに保存されます。
merkle = [
{
"alg": "rfc9162-sha256",
"root": h'…32 bytes…', ; canonical root over the ordered leaves
"leaf_count": 4, ; binds the on-chain root to the leaf-list size
"uris": [ … ], ; OPTIONAL — where the off-chain leaves list lives
},
]登録済みのコミットメントアルゴリズムは rfc9162-sha256 です。これは RFC 9162 §2.1.1 の Merkle Tree Hash を、基となるハッシュ関数として SHA-256 を用いて構成したものです。これはリストへのコミットメント構造であり、コンテンツハッシュレジストリとは区別されます。Merkle ルートはリーフリストの構造にコミットするのに対し、sha2-256 ダイジェストは平文のバイト列にコミットします。両者は性質が異なるため、Merkle ルートは hashes の内部ではなく独自の配列に収まります。オンチェーンの leaf_count はルートをオフチェーンのリストのサイズに結び付け、同じルートを共有しつつ別のリーフ位置で異なるサイズのツリーを再構築するような置換を未然に防ぎます。
ツリーの構築
この構造は、リーフと内部ノードを 1 バイトのドメイン分離プレフィックスで区別します。リーフには 0x00、内部ノードには 0x01 が使われます。これにより、攻撃者がリーフと衝突する内部ノードを細工することを防ぎます。n ≥ 1 の 32 バイト値の順序付きリスト L = (d_0, …, d_{n-1}) に対する Merkle Tree Hash は再帰的に定義されます。
MTH(L) = SHA-256(0x00 || d_0) when n == 1
MTH(L) = SHA-256(0x01 || MTH(L[0:k]) || MTH(L[k:n])) when n > 1
where k is the largest power of 2 strictly less than nここで重要な帰結があります。単一のリーフは、リーフそのものではなく SHA-256(0x00 || d_0) としてハッシュ化されます。したがって、リーフが 1 つだけのツリーのルートが、そのリーフ自体と等しくなることは決してありません。1 つのコンテンツにタイムスタンプを付けたいプロデューサーは、リーフが 1 つだけの Merkle ツリーではなく、sha2-256 または blake2b-256 エントリを直接使用しなければなりません(MUST)。空のツリー(n == 0)は禁止されています。
この構造は順序に依存します。リーフを並べ替えると異なるルートが得られるため、プロデューサーはリーフリストを順序付きのシーケンスとして扱い、発行、アーカイブ、そしてその後の証明生成のいずれにおいても、その順序を保持しなければなりません(MUST)。
オフチェーンのリーフリスト
ルートはリーフリストがなければ役に立たないため、プロデューサーは順序付けられたリーフをオフチェーンに永続化します。その正規のアーティファクトが cardano-poe-merkle-leaves-v1 ドキュメントであり、正規 CBOR(RFC 8949)でエンコードされます。内容は、32 バイトのルート、順序付けられた 32 バイトのリーフの配列、そしてリーフ数です。
leaves-list = {
"format": "cardano-poe-merkle-leaves-v1",
"tree_alg": tstr, ; registered list-commitment algorithm id
"root": bytes .size 32, ; raw 32 bytes, not hex
"leaves": [ + bytes .size 32 ], ; ordered raw 32-byte leaves
"leaf_count": 1..4294967295, ; 1 .. 2^32-1; MUST equal the length of `leaves`
? "leaf_alg": tstr, ; informative; no verification semantics
}検証者はオフチェーンのリストを取得し、上記の構造に従って leaves からルートを再計算し、オンチェーンの merkle[i].root とバイト単位で照合します。ファイル内の leaf_count は、オンチェーンの leaf_count と len(leaves) のどちらとも一致しなければなりません(MUST)。この正規 CBOR コンテナが、リーフリストの唯一の規範形式です。JSON 表現も代替のシリアライズ形式も存在しないため、リーフリストをやり取りする 2 つの実装は、つねにバイト単位で比較可能なドキュメントをやり取りすることになります。
包含証明
バッチ化の狙いは選択的開示にあります。コミット済みのリストに特定のアイテムが含まれていたことを、残りのアイテムを再公開することなく、いやそもそも開示することすらなく証明できるのです。あるリーフの包含証明とは、そのリーフからルートに至るパスに沿って並ぶ兄弟ノードのハッシュ値の順序付きリスト、すなわち O(log n) の兄弟パスです。検証者は RFC 9162 に従ってリーフと兄弟ノードをツリーの上方に向かって畳み込み、再構築されたルートが公開済みのルートとバイト単位で一致する場合に限り、その証明を受理します。
RFC 9162 のツリーは 2 の累乗にパディングされないため、不均衡なツリーでは右端のリーフが、完全な側にあるリーフよりも短いパスを持つことがあります。したがって正しい判定は、あくまでアルゴリズムによる判定です。つまり、畳み込みがルートを再現するかどうかであって、証明の長さの比較ではありません。
バッチ化が重要な理由
1 つのトランザクションと 1 つの 32 バイトのルートが、数千、数百万のリーフを代表できます。O(log n)
の証明を持つ人は誰でも、後から「このアイテムは自分のリストに含まれていた」と示せる一方で、開示されていないリーフはすべて非公開のままです。ルートは、それがコミットするリーフについて何も明かしません。