背景
PR #54 のレビュー指摘(コメント)と、その後の設計議論で明らかになった read 経路の完全性ギャップをまとめる。
PR #54 で対処済みなのは以下の2点:
- credential 漏洩の悪用: 読み取り署名を
read:{content_id}:{timestamp} に content_id バインド済み。漏れても単一コンテンツ・5分間に限定。
- 偽データ注入 (本文): SDK の暗号化を AES-256-GCM (AEAD) に移行済み。改ざん・偽造された暗号文本文はクライアントの復号で必ず失敗する。
本 issue が扱うのは、GCM でも content_id バインドでも塞がらない残りの完全性問題。
問題の構造 — 2つのレイヤー
read 経路には独立した2つの信頼問題がある。両方を埋めないと防御にならない。
レイヤー1: 「誰に聞くか」(メンバーシップ)
resolve_members (state_node_service.rs) は、ローカルに ContentNetwork レコードがない場合、Kademlia DHT の近接ピア (find_closest_peers) をそのまま relay 先の「メンバー」として扱う。メンバーであることの暗号学的検証はない。攻撃者は自分の PeerID を対象コンテンツの DHT キー近傍に置くだけで relay 先候補に潜り込める。content network の正規メンバーである必要はない。
さらに、ローカルの ContentNetwork レコード自体も gossip イベント (ContentNetworkManagerAdded / ContentNetworkManagerRemoved) のペイロードを無検証で保存・上書きしている。発行者の正当性検証がない。
libp2p では埋まらない: libp2p (Noise) が保証するのは各ホップの相手 PeerID が本物であること (トランスポート認証) だけ。「その PeerID が対象コンテンツの正規メンバーか」はアプリ層の概念であり、libp2p は何も語らない。多段 relay の場合、libp2p が認証するのは隣接ホップ間のみで、中間ノードは中身を差し替え放題。
レイヤー2: 「返ってきた答えが正しいか」(応答の完全性)
relay 先が返す (data, version) および履歴 (relay_read_history の版 CID リスト) には署名も系列検証もない。GCM が守るのは暗号文本文の完全性のみで、以下は素通りする:
- 偽履歴の注入:
get_history / get_latest_version は relay 先が返す版 CID の文字列リストをそのまま信頼する。攻撃者は本物のチェーンに存在しない偽の版 ID を混ぜられる。正規メンバーの sync 遅延 (正常な stale) では決して発生しない状態。
- ロールバック攻撃: 攻撃者が過去の本物の暗号文 (v3) を「最新」として返す。中身は正規の CEK で暗号化された本物なので GCM 認証は成功し、クライアントは正常に復号して「最新」と信じる。訂正・取り消し系の更新 (アクセス権削除、宛先修正等) を「なかったこと」にできる。
正常な stale read との区別: 正規メンバーが sync 遅延で一時的に古い版を返すのは結果整合性として正常な仕様であり、守るべき挙動。攻撃との違いは「時間が経てば sync で自己修復するラグ」か「攻撃者が特定の相手に対して古い版/偽履歴を意図的に固定・注入し、収束しない」か。検出の軸はメンバー/非メンバーでも新旧でもなく、自己修復するラグか、収束しない改ざんか。
提案する方向性 — 署名付き応答 (E2E 検証)
レイヤー1・2を同時に埋める本命は、member が返すデータ/版の系列に署名し、その署名を最終読み手 (SDK/クライアント) まで運んで E2E で検証すること。これにより:
- 署名者の鍵で「本当に member か」を判定できる (レイヤー1)
- 署名検証で改ざんの有無が分かる (レイヤー2)
- 多段 relay でも中間 relay がデータに触れなくなる (member 本人の署名なので中間では作れない)
加えて、系列の完全性・単調性:
- 系列チェーン検証: version は crsl-lib の Node CID (SHA-256)。各版が genesis からの親子チェーンに繋がっているかをクライアントが辿れれば、偽履歴 (存在しない版) を弾ける。
- 単調性チェック (ロールバック検出): クライアントが「前回見た版」をローカルに記録し、返ってきた版がその祖先に巻き戻っていないか (monotonic / TOFU 的) を検証する。member/非 member 問わず効く。
やること (設計段階で詰める)
優先度メモ
すべて「攻撃者が read relay の経路に入れること」が前提。クローズドな 4 ノード構成の現状では成立しない。オープン参加型ネットワークにする前までに対応が必要 (Kademlia への Sybil/eclipse 攻撃が現実的になるため)。
背景
PR #54 のレビュー指摘(コメント)と、その後の設計議論で明らかになった read 経路の完全性ギャップをまとめる。
PR #54 で対処済みなのは以下の2点:
read:{content_id}:{timestamp}に content_id バインド済み。漏れても単一コンテンツ・5分間に限定。本 issue が扱うのは、GCM でも content_id バインドでも塞がらない残りの完全性問題。
問題の構造 — 2つのレイヤー
read 経路には独立した2つの信頼問題がある。両方を埋めないと防御にならない。
レイヤー1: 「誰に聞くか」(メンバーシップ)
resolve_members(state_node_service.rs) は、ローカルにContentNetworkレコードがない場合、Kademlia DHT の近接ピア (find_closest_peers) をそのまま relay 先の「メンバー」として扱う。メンバーであることの暗号学的検証はない。攻撃者は自分の PeerID を対象コンテンツの DHT キー近傍に置くだけで relay 先候補に潜り込める。content network の正規メンバーである必要はない。さらに、ローカルの
ContentNetworkレコード自体も gossip イベント (ContentNetworkManagerAdded/ContentNetworkManagerRemoved) のペイロードを無検証で保存・上書きしている。発行者の正当性検証がない。libp2p では埋まらない: libp2p (Noise) が保証するのは各ホップの相手 PeerID が本物であること (トランスポート認証) だけ。「その PeerID が対象コンテンツの正規メンバーか」はアプリ層の概念であり、libp2p は何も語らない。多段 relay の場合、libp2p が認証するのは隣接ホップ間のみで、中間ノードは中身を差し替え放題。
レイヤー2: 「返ってきた答えが正しいか」(応答の完全性)
relay 先が返す
(data, version)および履歴 (relay_read_historyの版 CID リスト) には署名も系列検証もない。GCM が守るのは暗号文本文の完全性のみで、以下は素通りする:get_history/get_latest_versionは relay 先が返す版 CID の文字列リストをそのまま信頼する。攻撃者は本物のチェーンに存在しない偽の版 ID を混ぜられる。正規メンバーの sync 遅延 (正常な stale) では決して発生しない状態。正常な stale read との区別: 正規メンバーが sync 遅延で一時的に古い版を返すのは結果整合性として正常な仕様であり、守るべき挙動。攻撃との違いは「時間が経てば sync で自己修復するラグ」か「攻撃者が特定の相手に対して古い版/偽履歴を意図的に固定・注入し、収束しない」か。検出の軸はメンバー/非メンバーでも新旧でもなく、自己修復するラグか、収束しない改ざんか。
提案する方向性 — 署名付き応答 (E2E 検証)
レイヤー1・2を同時に埋める本命は、member が返すデータ/版の系列に署名し、その署名を最終読み手 (SDK/クライアント) まで運んで E2E で検証すること。これにより:
加えて、系列の完全性・単調性:
やること (設計段階で詰める)
ContentNetworkレコードへの owner/genesis authority 署名と、gossip・DHT フォールバック両経路での検証 (レイヤー1 のルーティング側)優先度メモ
すべて「攻撃者が read relay の経路に入れること」が前提。クローズドな 4 ノード構成の現状では成立しない。オープン参加型ネットワークにする前までに対応が必要 (Kademlia への Sybil/eclipse 攻撃が現実的になるため)。