信頼と NAT 越えの設計
設計書 docs/trust-and-nat.md の要約。細部はそちらが正。
1. 目的と脅威モデル
| 脅威 | 本家 | この設計での扱い |
|---|---|---|
| 検索結果の捏造(どのクエリにも広告・マルウェアの URL を返す) | 形式検査のみ。検証に失敗しても remote の結果は残る | 結果の文書に作者(crawl したピア)の署名を求め、既定では作者がコーディネータの一覧に載っていることを求める |
| 他ピアの名を騙る、seed の改ざん | ピア ID は乱数。seed に署名なし | ピア ID = Ed25519 の鍵の hash。seed の中核に本人が署名し、hello のチャレンジで鍵の所有を確かめる |
| 偽ピアの大量生成(Sybil) | 3 日待てば DHT 検索先になる | 検索の信頼は一覧(入場管理)で決める。一覧に無いピアの文書は既定で使わない |
| DHT の保存ピアによる改ざん | 検出できない | 保存ピアを信頼しなくてよい。作者の署名で改ざんが分かるので、できるのは返さないことだけ |
| 信頼されたピアが広告を混ぜる、外部エンジンを中継する | ― | 禁止しないが、一覧に宣言タグとして書かせ、利用者の方針で使うか決める |
守らないもの: 保存ピアが結果を返さないこと、信頼された作者自身の虚偽(タグと監査で扱う)、検索語の秘匿。
2. 身元
- ピアは起動時に
DATA/SETTINGS/peer.key(Ed25519、PKCS#8 PEM)を読み、無ければ作る。 - ピア hash(12 文字)= SHA-256(公開鍵) の URL-safe base64 の先頭 12 文字。DHT の隙間に合わせて ID を選ぶ本家の処理はやめ、鍵で決める。
- 同じ鍵を NAT 越えの sidecar の libp2p の鍵として使う。libp2p のピア ID(
12D3KooW…)は公開鍵から計算できるので seed には載せない。
2.1 seed の署名
seed は受け取った側が IP・種別・フラグ・最終確認時刻などを書き換えて中継する。そこで本人しか決めない項目だけに署名する。
| 署名する中核 | 署名しない(観測値・受け手が書き換える) |
|---|---|
Hash, PK, Name, Port, PortSSL, BDate, SigT(署名時刻), Tags(自己申告タグ), Reach, P2PA, RDS | IP, IP6, PeerType, Flags, 各種カウンタ, LastSeen, news, TV |
Sig= Ed25519("yacy-seed-v1\n" + "Hash=<hash>\n"の後に、値のある残りの中核項目をキーの辞書順(BDate, Name, P2PA, PK, Port, PortSSL, RDS, Reach, SigT, Tags)にk=v\nで連結したもの)。PKのある seed はHash == H(PK)とSigが正しくなければ必ず拒否する。PKの無い seed はtrust.seed.acceptUnsigned=trueでない限り拒否する。- 中核が変わったか前の署名から 7 日経ったら署名し直す。まだ知らないピアの seed は、
SigTが 30 日より古いか 1 時間以上未来なら拒否する。署名付きで知っているピアの seed を、より古い seed や署名の無い seed で置き換えない。
2.2 hello のチャレンジ
IP は署名の対象外なので、その住所で本当に鍵の持ち主が応答しているかを hello で確かめる。
- 呼び出し側は乱数の
challengeを送る。応答側は、要求が届いた相手のアドレス(challengeFor、sidecar 経由ならp2p)とSign("yacy-hello-v2|" + challenge + "|" + 自分の hash + "|" + challengeFor)を返す。 - 呼び出し側は、返ってきた seed の
PKで署名を検証し、challengeForが自分のアドレスであることを確かめる。アドレスを含めるのは、攻撃者がチャレンジを本物のピアへ転送してその答えを返す攻撃を防ぐため。本物のピアは「攻撃者から来た要求」に署名するので見分けられる。 - 応答側が呼び出し元へ接続し直して到達性を確かめる back-ping でも同じチャレンジを使う。他人の IP を名乗った seed はこれで弾かれる。
- 自分の公開アドレスを知らないピア(junior、NAT の内側のピア)はアドレスの照合を省く。転送攻撃が残るのはこの場合だけ。
3. 信頼の委譲と一覧
コーディネータの鍵(利用者が設定。複数可、先頭ほど優先)
└─ 委任書: 「このオペレータの鍵に、この網の一覧を作らせる」(版付き。失効も新しい版で出す)
└─ ピアの一覧: 「これらのピアを、この優先度・このタグで信頼する」(オペレータが署名。版付き)- 委譲は 1 段だけ。コーディネータ自身がオペレータを兼ねてもよい。
- 有効期限は持たず、版で管理する。運営者がいなくなっても最後の版で動き続ける。
- すべて同じ形の封筒で運ぶ。署名は payload のバイト列そのものに対するので、JSON の正規化は要らない。
{"payload": "<base64url(JSON のバイト列)>", "signer": "<base64url 公開鍵>", "sig": "<base64url 署名>"}
{"type": "yacy-delegation-v1", "network": "freeworld", "operator": "<pk>", "version": 3, "revoked": false}
{"type": "yacy-peerlist-v1", "network": "freeworld", "version": 12,
"peers": [{"pk": "<pk>", "priority": 100, "tags": ["ads"]}]}
- 委任書はコーディネータの署名が要る。(コーディネータ, オペレータ)ごとに最大の版を採り、
revoked:trueならそのオペレータの一覧は無効になる。一覧は有効な委任を持つオペレータの署名が要り、オペレータごとに最大の版を採る。networkはnetwork.unit.nameと一致するか*。 - 実効の信頼集合は、コーディネータの順に一覧を重ねたもの。同じピアは、先にそのピアを載せた一覧の優先度とタグを使う。照合は 72 bit のピア hash だけでなく公開鍵そのもので行う。
priorityは 0〜100。結果の点数に0.5 + 0.5 × priority / 100を掛ける。- コーディネータが無いピアは自分の文書だけを信頼する(閉じる側に倒す)。全員を知っている閉じた網では
trust.signedOnly=trueで署名されたピアをすべて信頼できる。配布物は自分のコーディネータの鍵を既定値に入れて配る想定で、このフォーク自体は鍵を同梱しない。
3.1 宣言タグ
共通の最小語彙を決め、コーディネータは x-<name>: の接頭辞で独自のタグを足せる。
| タグ | 意味 |
|---|---|
ads | 結果に広告を混ぜる |
proxy:<engine> | 外部の検索エンジンの結果を中継する |
curated | 人手で選別した索引 |
unfiltered | 選別しない |
adult | 成人向けの内容を含む |
一覧のタグが正。seed のタグは自己申告で、表示にだけ使う。trust.policy.excludeTags に当たる作者の文書は捨て、結果には作者のタグを付ける。
4. 一覧の配り方
- 各ピアは受け取った封筒を検証して
DATA/SETTINGS/trust-bundle.jsonに保存し、/yacy/trust.jsonで公開する。誰が中継してもよい。 - seed に
TVを載せる。コーディネータごとに、持っている封筒の版の合計(委任書と一覧。失効したオペレータの分も数える)。新しい封筒はどれも合計を増やし、失効でも合計は減らない。 - peer ping のたびに、接続中のピアの
TVが自分より大きければ、そのピアの/yacy/trust.jsonを取って統合する。TVは署名されていないので、問い合わせ先は乱数順に選び、新しいものを返さなかったピアには 10 分間問い合わせない。 trust.bundle.urlsからも、起動時・10 分ごと・設定が変わったときに取る。新しい版の存在を複数の経路で知ることで、古い版に留め置く攻撃をしにくくする。
5. 文書の出所(作者の署名)
- ピアは crawl した頁を索引するときに署名する。値は Solr の
provenance_sに入り、DHT の転送や単語索引の結果ではプロパティprovで運ばれる:1|<作者の鍵>|<署名>|<単語の Bloom フィルタ>。 - 署名の対象:
"yacy-doc-v1\n" + 正規化した URL + "\n" + タイトル + "\n" + Bloom。Bloom フィルタには、単語索引に入るのと同じ、その文書の単語の hash を入れる。大きさは 1 語あたり 12 bit 以上の最小の 2 の冪(512〜32768 bit)、ハッシュ関数は 4 個。
| 判定 | 条件 | 既定の扱い |
|---|---|---|
SELF | 作者が自ピア | 使う |
TRUSTED | 署名が正しく、作者が信頼集合にいる | 使う。作者の優先度とタグを付ける |
SIGNED | 署名は正しいが、作者が信頼集合にいない | 捨てる(開放モードでは「未検証」) |
UNSIGNED | 出所の署名が無い | 捨てる(開放モードでは「未検証」) |
INVALID | 署名が合わない、鍵と作者が合わない、URL かタイトルが変わっている | 必ず捨てる |
- 単語索引の結果は、クエリのすべての語が Bloom フィルタに入っていることも求める。保存ピアが信頼された文書を無関係な語に結び付けるのを防ぐため。
- 要約(snippet)・本文・説明・見出し・画像の代替テキストは署名の対象外。応答したピアが作者か、住所を確かめた信頼ピアのときだけ残す。他ピアの結果を自ピアの索引に保存する(
remotesearch.result.store)のは、応答したピアが信頼された作者のときだけ。 - DHT の保存(
transferURL)はINVALIDを拒否し、UNSIGNEDは開放モードでだけ受け入れる。保存ピアは作者を信頼しなくてよい。
6. NAT 越え
NAT の内側 公開側
┌──────────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ YaCy ── sidecar ─────┼── 外向き ─▶ relay(sidecar) ◀─ 外向き ─┼── sidecar ── YaCy │
│ :8090 :8095 /:4001│ 予約 │ :4001 │ 回線 │ :8095 :8090 │
└──────────────────────┘ └──────────────────┘ └──────────────────────┘
◀──────────────── DCUtR(穴あけ)で直結に切り替え(成功すれば)────────────────▶- sidecar(
sidecar/、Go、go-libp2p): YaCy と同じ鍵で libp2p のホストを立てる。外から届かなければ、設定された circuit relay v2 に予約を取り、DCUtR を試みる。 - トンネル。YaCy が制御 API に
GET /tunnel/<libp2p ピア ID>?addrs=…を送ると、ローカルのポートが返る。127.0.0.1:portへの普通の HTTP は、libp2p のストリーム(/yacy/http/1.0.0)で相手の sidecar へ運ばれる。普通のホスト:ポートなので、YaCy のプロトコルや Solr のクライアントはそのまま動く。 - 受け側の sidecar は
/yacy/*と Solr の select だけを YaCy の専用の接続口(127.0.0.1:8096)へ渡す。管理画面は通さず、送信元を名乗るヘッダーは取り除く。接続口は送信元アドレスを libp2p のピア ID から作った2001:db8::/32のアドレスに置き換える。トンネル経由の要求にループバックの特権を与えず、負荷制限をピアごとに効かせるため。ピア ID のヘッダーは、sidecar のトークンが一緒に来たときだけ信じる。 - 制御 API は、YaCy が
DATA/SETTINGS/sidecar.tokenに書いたトークン、ループバックの Host、Originが無いことを求める(他のローカルのプロセスや DNS rebinding への対策)。上限はトンネル 256 本、15 分使われなければ閉じる、相手ごとの同時要求 8 本、1 要求 60 秒・32 MiB。 - YaCy は、AutoNAT が「private」と判定したとき、または判定が出ないまま 3 回続けて他ピアから junior と報告されたとき、seed を
Reach=relayにして回線のアドレスをP2PAに載せる。AutoNAT が「public」と判定するまでその状態を保つ。p2p.mode=leecherはReach=none。 - リレー経由のピアは既定で検索に応答するだけ。
p2p.relay.dhtStorage=true(seed のRDS=1)を宣言しない限り、DHT の保存先にも DHT 検索先にもならない。 - sidecar 経由の hello は、seed の鍵の libp2p の身元から来ていなければならない。向きはどちらでもよい。NAT の内側のピアがリレー経由で外へ呼ぶ場合も、公開側のピアがトンネルで NAT の内側のピアを呼ぶ場合もある。
7. 設定値
| キー | 既定 | 意味 |
|---|---|---|
trust.seed.acceptUnsigned | false | 署名の無い(旧実装の)seed を受け入れる |
trust.coordinators | 空 | 信頼するコーディネータの鍵(, 区切り、先頭ほど優先) |
trust.bundle.urls | 空 | 信頼の一覧の束を取りに行く URL |
trust.search.acceptUnverified | false | 開放モード: 信頼集合外・署名なしの文書も「未検証」として検証済みの後ろに出す |
trust.policy.excludeTags | 空 | このタグを持つ作者の文書を捨てる |
trust.selfTags | 空 | 自分の seed に載せる自己申告のタグ |
trust.signedOnly | false | コーディネータが無いとき、署名されたピアをすべて信頼する(閉じた網向け) |
p2p.sidecar.url | 空 | sidecar の制御 API(空なら NAT 越えは無効) |
p2p.sidecar.yacyPort | 8096 | sidecar が運んだ要求を受けるループバックの接続口 |
p2p.mode | auto | auto / direct / leecher |
p2p.relay.dhtStorage | false | リレー経由でも DHT の保存先になる |
remotesearch.maxtime | 5000 | 他ピアの応答を待つ時間(ms、最大 10000)。結果は届いた順に表示する |
鍵・委任書・一覧・束はコマンドラインの TrustTool で作る。FORK.md を参照。
8. 既知の制約
- 日本語・中国語の単語 hash は旧網と異なる(bigram のため)。開放モードでも旧網の CJK の索引とは突き合わない。
- とても長い文書(約 2700 語超)の Bloom フィルタは偽陽性が増え、単語索引の結果の語の確認が甘くなる。
- 第三者から中継された seed の IP は、自分で hello するまで確かめられていない。
- 応答そのものには署名が無い。信頼ピアになりすませた相手は、署名の対象外の部分(snippet)を差し替えられる。アドレスを含めたチャレンジで防いでいるが、自分の公開アドレスを知らないピアは例外。
- 信頼された作者が虚偽の内容に署名すること、保存ピアが結果を返さないことは防げない。