2 ポイント 投稿者 GN⁺ 5 시간 전 | 1件のコメント | WhatsAppで共有
  • ActivityPub サーバーを自作すると、最初の Follow リクエストから説明のない 401 Unauthorized に阻まれがちで、Fedify は 署名・JSON-LD・配信・セキュリティ の負担をアプリケーションコードの外へ移す TypeScript フレームワークである
  • フェディバース認証では、失効した草案 draft-cavage-http-signatures-12 と標準 RFC 9421 が併用されており、文書署名まで含めると 4種類の署名メカニズム と RSA・Ed25519 鍵を扱う必要がある
  • 同じ ActivityPub アクティビティでも、JSON-LD では文字列、配列、インラインオブジェクト、URI 参照など複数の形で届くため、直接実装するほど 防御コード がコードベース全体に広がる
  • 分散配信では、DeleteCreate より先に届く「ゾンビ投稿」のような問題が生じ、キュー・リトライ・冪等性・順序保証・サーキットブレーカーが必要になる
  • Fedify は 13 の Web フレームワーク統合、KV・メッセージキューアダプター、CLI・リンター・デバッガー・OpenTelemetry を提供し、ActivityPub の細かな知識がなくても 連合アプリ開発を始められるようにする

ActivityPub を直接実装するときにぶつかる問題

  • 最初の Follow アクティビティを Mastodon に送るには、JSON の作成、HTTP リクエスト署名、POST まで処理する必要があるが、失敗すると 401 Unauthorized の一行だけが返ってくることがある
    • 原因は Date ヘッダーの時計ずれ、Digest ハッシュの誤り、(request-target) の大文字小文字、公開鍵の表現方式などである可能性がある
    • リモートサーバーが理由を教えてくれなければ、他のサーバー実装のコードを読みながらデバッグするしかない
  • Fedify は、単一ユーザーのマイクロブログサーバー Hollo を作る過程から始まった
    • ActivityPub 実装の負担が製品開発を飲み込んだため、アプリより先に必要なフレームワークとして作られた
  • 難しさは主に 署名標準、JSON-LD 文書の形、分散配信、実装ごとの慣習、基本的なセキュリティ設定に集中している

署名標準が一つではない

  • サーバー間認証には HTTP 署名を使うが、実際のフェディバースでは、失効した草案 draft-cavage-http-signatures-12 と標準 RFC 9421 が併存している
  • どのサーバーがどの署名を受け付けるかは試すまで分からないため、ある方式で署名して拒否されたら別方式で再署名し、成功した方式をサーバーごとに記憶しておく必要がある
  • HTTP 署名はリクエスト送信者しか証明しないため、受け取ったアクティビティを第三者へ転送する inbox forwarding のような場面では、文書自体に付く署名も必要になる

JSON-LD 文書の形が変わり続ける

  • ActivityPub の転送形式は JSON-LD であり、同じ意味の Create アクティビティでも複数の形で表現されうる
    • actor は URI 文字列の場合もあれば、インラインの Person オブジェクトの場合もある
    • to は文字列 1 つの場合も、配列の場合もある
    • object はインラインオブジェクトにも URI にもなりうる
  • 公開対象を表すアドレスも https://www.w3.org/ns/activitystreams#Publicas:PublicPublic の 3 つの表現がすべて有効である
  • 仕様どおりに処理するには、JSON-LD プロセッサーで expansion を行った後、compaction して正規化する必要がある
    • 多くの実装はこれを「ただの JSON」のように扱い、特定サーバーが出力した形式で静かに壊れる
  • 直接実装すると、値が文字列か、配列か、オブジェクトか、取得すべき URI かを確認する 防御コード があちこちに生まれる

分散配信と「ゾンビ投稿」

  • ユーザーが投稿した直後に誤字を見つけて削除すると、サーバーは Create の次に Delete を送るが、ネットワーク状況によっては受信サーバーが Delete を先に受け取ることがある
    • まだ存在しない投稿の削除を無視した後で Create を処理すると、投稿者は削除済みだと信じている投稿がそのサーバーに残り続ける
  • フォロワーが 5000 人いれば、投稿 1 件で数千件の HTTP 配信が発生し、リクエストハンドラー内で処理すると投稿ボタンへの応答が遅くなったり、サーバーが耐えられなくなったりする
  • キューを使うとしても、失敗した配信の再試行スケジュール、指数バックオフ、再試行回数、500 Internal Server Error410 Gone の違い、消えたサーバーのフォロワー整理、長期障害ホストの扱いまで決めなければならない
  • この領域は単なるプロトコル実装より 分散システムエンジニアリング に近い

仕様だけでは相互運用は終わらない

  • 仕様を完璧に守っても、実際のフェディバース実装との相互運用問題は残る
  • Mastodon の secure mode は、GET リクエストにも HTTP 署名を要求する authorized fetch を使う
    • 両方のサーバーが secure mode だと、相手の公開鍵を取得するには署名が必要で、署名を検証するには相手が先に自分の公開鍵を取得していなければならないというデッドロックが生じる
    • コミュニティはサーバー自体を表す instance actor で署名して回避しているが、これは仕様にはない
  • Threads は actor がインラインオブジェクトとして入ったアクティビティをパースできないため、Threads に送るときは actor を URI として送る必要がある
  • Lemmy は、Mastodon が要求しない Group actor フィールドがないと黙って拒否する
    • 例としては、attributedTo でつながる moderators collection と featured collection がある
  • Misskey は独自の語彙拡張を持っており、quote post だけでも実装ごとに 3 つのプロパティ名が使われる
  • 相互運用は一度合わせて終わる作業ではなく、継続して維持しなければならない領域である

直接実装の初期状態は安全ではない

  • 受信したアクティビティの署名検証を省くと、誰でも偽の FollowDelete を注入できる
  • ドキュメントローダーを制限しないと、悪意あるアクティビティが http://169.254.169.254/ や内部ネットワークを指し、サーバーを SSRF プロキシにしてしまう可能性がある
  • 埋め込みオブジェクトの出所検証を省略すると、どのサーバーでも特定の人物が話したように見える文書を出力できてしまう
  • こうした落とし穴はすぐには目立たず、悪用されるまではすべてが正常に動いているように見えることがある

Fedifyが代わりに処理する領域

  • Fedifyは、ActivityPub と関連標準を用いて連合サーバーアプリを構築するための TypeScript ライブラリ
  • Deno、Node.js、Bun で動作し、Cloudflare Workers のような edge ランタイムもサポート
  • 設計目標は、署名、JSON-LD、配送、実装ごとの差異、セキュリティの詳細をアプリケーションコードから取り除くこと
  • 署名処理

    • actor dispatcher と key pair dispatcher を登録すると、actor を 1 つフェディバースに公開できる
    • 送信するすべてのリクエストには署名が付く
    • RSA キーでは HTTP Signatures と Linked Data Signatures を生成する
    • Ed25519 キーを追加すると Object Integrity Proofs も付与される
    • 4 つのメカニズムが 1 つのアクティビティに共存し、受信側は自分が理解できる最も強力な方式で検証する
    • Fedify は double-knocking を直接処理する
      • 最初の接触は RFC 9421 で送信され、拒否されると draft-cavage で再試行する
      • 成功した方式はサーバーごとにキャッシュされる
      • 拒否レスポンスに Accept-Signature challenge があれば、サーバーが要求したコンポーネントで再署名する
    • 受信署名 はアプリケーションコードが見る前に検証され、検証に失敗したアクティビティはリスナーに到達しない
    • actor dispatcher を登録するだけで WebFinger RFC 7033 サーバーも作成され、Mastodon の検索欄で @alice@example.com 形式で actor を見つけられる
  • JSON-LD の代わりに型を扱う

    • Fedify は Activity Vocabulary 全体と主要ベンダー拡張をカバーする 約 80 のクラス を提供する
    • クラスは型付きで不変であり、アクセサは JSON-LD が許容する文書形式の違いを吸収する
    • lookupObject() はハンドルを受け取り、WebFinger discovery まで含む完全な検索手順を実行する
    • getFollowers() のようなアクセサ は、値が URI 参照でもインラインオブジェクトでも同じように動作し、取得した値はキャッシュされる
    • ベンダーごとの差異も API の裏側に隠される
      • quoteUri_misskey_quotequoteUrl の 3 つの quote 属性は、新たに登場した FEP-044fquote とともに 1 つの API の裏に統合される
      • Misskey の isCat 属性も型として存在し、型安全に扱える
  • 配送インフラと順序保証

    • createFederation() にメッセージキューを接続すると配送はバックグラウンドに移り、失敗時にはデフォルトで最大 10 回まで指数バックオフで自動再試行する
    • 投稿 1 件が数千のフォロワーに配送されるときは、two-stage fan-out が動作する
      • 1 つの統合メッセージがキューに入る
      • バックグラウンドワーカーがサーバーごとの配送ジョブに分割する
      • 投稿ボタンは即座に応答する
    • 再試行によって同じアクティビティが 2 回届くことがあるため、Fedify は処理済みアクティビティを 24 時間保持する 冪等性キャッシュ によって、重複をハンドラーの前でスキップする
    • sendActivity() 呼び出しに { orderingKey: post.id } を指定すると、同じ orderingKey を共有するアクティビティは、各受信サーバーに対して送信した順序どおりに配送される
      • DeleteCreate を追い越すことはできない
      • 異なるキーのアクティビティは並列送信され、スループットを維持する
    • 404 Not Found410 Gone では再試行を停止し、登録された 恒久的配送失敗ハンドラー を呼び出す
    • shared inbox に送信した場合、その背後にあるフォロワー一覧も受け取って、消えたアカウントを整理できる
    • 繰り返し失敗するホストに対しては、デフォルトで有効な サーキットブレーカー が配送を保留し、定期的に復旧を確認する

実装ごとの慣行とセキュリティのデフォルト

  • Fedify は authorized fetch において、.authorize() を dispatcher に接続し、検証済みのリクエスターの身元をコールバックに渡す
    • ブロックリストや非公開コレクションのような処理はアプリケーションロジックとして記述できる
    • instance actor のデッドロック問題にも対応パターンがある
  • Threads のインライン actor 問題は、デフォルトで有効な activity transformer が送信アクティビティ内のインライン actor を URI に変換して処理する
  • Lemmy が要求する moderators collection は custom collection API で数行で公開でき、Lemmy の JSON-LD context はあらかじめ含まれている
  • 新しい相互運用性の問題が見つかれば、修正は各アプリケーションではなく Fedify に入る
  • セキュリティのデフォルトは安全側に寄っている
    • 署名検証は有効化する機能ではなく、テスト用に無効化する機能である
    • ドキュメントローダーは private address range と loopback をデフォルトでブロックし、DNS rebinding も考慮する
    • SSRF にさらされるには、テスト用であることを示す名前のオプションを明示的に有効化しなければならない
    • 埋め込みオブジェクトの出所が親ドキュメントと異なる場合、アクセサはそれを信頼せずオリジンから再取得する
    • このオリジンベースのセキュリティモデルは FEP-fe34 に基づく

既存スタックと開発ツール

  • Fedify は既存の Web スタックに合うように設計されており、13 の Web フレームワーク統合を提供する
    • Express, Hono, Fastify, Koa, NestJS, Elysia
    • Next.js, Nuxt, SvelteKit, Astro, SolidStart, Fresh
  • ミドルウェアがコンテンツネゴシエーションを処理するため、同じ URL でブラウザには HTML を、Fediverse には JSON-LD を提供できる
  • Fedify 自体のストレージは、単一のキー・バリューインターフェースだけを要求する
    • Redis, PostgreSQL, MySQL/MariaDB, SQLite, Deno KV, Cloudflare Workers KV, インメモリアダプターがある
  • メッセージキューは PostgreSQL, Redis, AMQP/RabbitMQ などを含む 8 種類が提供されており、合うものがなければインターフェースを自分で実装できる
  • ドメインデータは既存のデータベースと ORM にそのまま置いておける
  • ほかのライブラリですでに federation を運用している場合は、マイグレーションガイドとデータ移行スクリプトにより、activitypub-express などから既存のフォロワーを失わずに移行できる
  • 上位パッケージも提供される
    • @fedify/relayは 1 回の関数呼び出しで完全な ActivityPub relay サーバーを提供する
    • @fedify/backfillは Fediverse をたどって不完全な会話スレッドを復元する
  • 開発ループのためのツール

    • fedify initは 1 行でプロジェクトをスキャフォールドする
    • fedify tunnelはローカルサーバーを HTTPS で公開し、実際の Mastodon とテストできるようにする
    • fedify inboxはサーバーが送るアクティビティを受け取る一時的な inbox サーバーを立ち上げる
    • fedify lookupは他のサーバーが投稿したオブジェクトを検査できるようにする
    • fedify lookup --authorized-fetchは使い捨ての鍵ペアを作成し、一時的な ActivityPub サーバーを立てて、secure mode の背後にあるオブジェクトへ署名付きリクエストを送る
    • @fedify/lintは、actor に inbox がない場合のような 20 種類の相互運用バグを検出する ActivityPub 専用リンターである
    • @fedify/testingの mock により、ネットワークなしでテストを実行できる
    • @fedify/debuggerは 1 行でデバッグダッシュボードを追加でき、ブラウザでアクティビティと署名検証結果をリアルタイムに確認できるようにする
    • 本番環境には OpenTelemetry 計測が組み込まれており、28 種類の span type と 37 種類の metric を提供する
    • 監視ガイドと、ActivityPub 向けの負荷テストツール fedify benchも提供される
    • 公式ドキュメントは 30 章のマニュアルと 5 本のチュートリアルで構成されており、キューの backlog を確認するための PromQL クエリやアラートルール、Mastodon でアバターを表示させるための属性といった実践的な知見を扱っている

すでに使われている事例と始め方

  • Fedify は実際のサービスで使われている
    • Ghost の ActivityPub サービス
    • ORCID の研究者記録を Fediverse につなぐ Encyclia
    • Cloudflare Workers 上で serverless に動作する SiliconBeest
    • 韓国のブログプラットフォーム Typo Blue
    • 単一ユーザー向けマイクロブログプラットフォーム Hollo
    • コミュニティ運営の Hackers' Pub
  • チュートリアルは規模別の例を提供する
  • Fedify の目標は、より多くの ActivityPub 専門家を作ることではなく、開発者が ActivityPub の詳細を知らなくても連合アプリを作れるようにすることである
  • 開始コマンドは npm init @fedify である
  • 助けが必要なら Matrix room または GitHub Discussions を利用できる

1件のコメント

 
GN⁺ 5 시간 전
Lobste.rsのコメント
  • ActivityPubプロジェクトに互いのフォークが多い理由はこれだ。全部を自前で実装するより、他人のアプローチを把握するほうが簡単だからだ
    筆者の提案も、実際によく見かけるMisskeyやPleromaのフォークと大きくは違わないように見える。ライブラリにもそれなりの視点やアプローチがあり、あまり大きな制御権は与えてくれないようだ。それでも、サーバ全体をフォークするときのようにUIまで強制されないという利点はある
    APを実装している立場からすると、いちばん難しいのはJSON-LDをきちんと扱うための良い方法がないことだ。オブジェクトを標準的な表現に簡単に変換できるなら、相互作用は自然についてくるはずだが、本当にリンクドドキュメントのように使うには効率が悪すぎるし、生のJSONドキュメントのように使うと無数の例外ケースで死ぬ。これまでは後者のアプローチを選んで死んだ

    • 特に署名を考えると、「オブジェクトの標準的な表現」という問題はさらに重要になる。昔のXML正規化は、まさにこの署名の問題、つまり受信側のバイト列シリアライズが送信側のものと一致することを保証するためにあった
      JSON-LDの世界と完全に同じ問題ではないが、まったく無関係というわけでもない
      ただ、JSON周辺技術のかなりの部分が似た問題を抱えていると思う。同じ論理スキーマを表現するJSON Schemaのやり方が多すぎて、そのせいでJSON Schema周辺技術と相互作用するのが笑えるほどひどいことになっている。特にOpenAPIスキーマは似ているが同一ではないホラーで、スキーマのドラフト版の違いまで考えなくても十分に悪い
    • APサーバ実装については考えてきたが、まだ始めてはいないのでかなり割り引いて聞いてほしい。役に立つかもしれない一つの方法は、アプリケーションをより小さなサービス群に分け、アクターモデルにもっと依存して、それらを「統合された」インターフェースのように見せることだ。たとえばメールサーバのMTAとMUAの分離から学べる
      APの「MTA」サービスは、送信箱からメッセージを送り、受信箱でメッセージを受け取る役割を担う。このサービスから見るとJSON-LD文書は、ほとんど塊データに近い。送信者と受信者を見つけるための多少のパースは必要だが、それ以上はあまりない。ストレージもファイルベースでよく、記憶が正しければgo-apはそういう方式を使っている
      APの「MUA」は実際のアプリケーションだ。JSON-LDの意味を理解しなければならない側だ。PostgreSQLのようなものを使って文書をjsonbで保存し、生成列やビューでSQLフレンドリーな形を提供できそうだ。そうすれば、オブジェクト型に応じて文書を最も適切に表現する方法を決められる
      別の例として、検索サービスもアクターとしてモデル化し、結果を一時的な送信箱に返すようにできる
  • 複数の実装の癖のある挙動とその緩和策を整理した、とても貴重な一覧だ
    残念ながらGoActivityPubでは、その半分もまだ実装できていない

  • 文章が最初は技術的な内容から始まったのでありがたかったが、途中から自分のフレームワークの宣伝に方向転換したようで、読む面白さが落ちた
    TypeScriptを使う一部の世界では、こうした実装上の癖を再発見しなくて済むのはありがたい。だが、頭の中のモデルとして「この条件と状況ではこういう結果が出て、こういう修正が必要になる」という記録があるなら、TypeScriptではない状況、たとえば兄弟プロジェクトであるGoActivityPubの作者もその苦労の成果を得られる。ここではそうしたこともいくつか扱ってはいるが、記事は特定時点のスナップショットでしかなく、プロジェクトは時間とともにあらゆる相互運用性バグを蓄積していくように見える
    現状の代替手段は、私が見る限り、人間が書いたものではないコミットメッセージを全部読みながら、Fedify自身のバグと相互運用性バグを見分けることしかない
    特に、リポジトリがAIに「全振り」しているように見えるのに、そうした台帳整理をしていないのは皮肉だ。LLMについて聞いてきた売り文句は、反復的な雑務を自動化することだった。ならClaudeにGitHub Issueを作らせるか、もっとよいのはリポジトリ内の.mdファイルに観測結果とFedifyがそれをどう直すかを文書化させればいい。独自のデバッガもあり、何を意味しているのかわからない「ベストプラクティス」もあるのだから、まさにうってつけの仕事だろう

    • 本当に些細な問題を大げさに扱って、ActivityPubの失敗であるかのように見せている。たとえばフォロワーが5,000人いれば、投稿1件が数千件のHTTP配送になり、これをリクエストハンドラ内で直接やると投稿ボタンの応答に30秒かかるかサーバが落ちるのでキューを使え、という話だ
      なぜサードパーティサービスへのリクエストをインラインで実行するのか。こんなのはWebアプリケーションの基本だ。サードパーティサービスと通信しなければならないなら、バックグラウンドジョブに回すべきだ。リクエストに応答するのに不要な情報なら、バックグラウンドジョブに回すべきだ。リクエストハンドラ内でこうしたリクエストをして起きる問題は、熊手を踏んで顔をぶつけるレベルの自業自得であって、ActivityPubとは関係ない
      配送が失敗したら再試行すべきだし、スケジュールをどうするか、指数バックオフを使うか、何回やるか、500 Internal Server Errorと410 Goneを同じ失敗として扱うか、といったこともただの一般的なWebアプリケーション開発の問題だ。ジョブキューからサードパーティサービスへリクエストするときに発生する問題であり、ActivityPubとは無関係だ。たいていのWebフレームワークには妥当なデフォルトがある。どんなエラーだったかによって再試行するかを決めなければならない場面でだけ判断が必要になる。410を再試行するのは無駄だが、緊急に解決すべき問題ではない。ジョブキューのメモリ圧迫は増えるだろうが、数時間のうちにアプリケーションを落とす可能性は低い
  • 「拒否されるか見て、別の方法で再署名し、サーバごとにどの方式が通ったか覚えておけ」って、一体何を読まされているんだという気分になる。これがMastodon開発が遅い理由なのか?
    「投稿1件が数千件のHTTP配送」だなんて、ネットワークシステムプログラミングとキューイングに強い言語として知られるRubyで、だ
    信じがたいし、これをライブラリで包んであるのは良いとしても、それでもちょっとひどい

  • JavaでActivityPubを実装してみたあと、こういうサーバ間プロトコルは単にgitの上に作るほうがいい、という結論に達した
    複雑さのかなりの部分は、gitがすでによりうまく解決している問題をもう一度解くために存在している。これをgitリポジトリ内のJSON文書としてモデル化すれば、ページネーションを扱う必要もなくなる。プロトコルはすでに存在しないデータだけを送ることを保証してくれるし、コミット署名も得られるし、イベント順序の保証も得られるし、この記事で触れられている問題も解決され、履歴まで無料で手に入る。こういうプロトコルには、バグが多くて遅いgitの半端な実装が含まれている、というGreenspunの第十法則みたいなことが言えそうだ

    • gitは親コミットに履歴が依存するので、素晴らしい選択肢とは言えない。ただし、似たようなネゴシエーション戦略で動くマークルツリーのゴシッププロトコルはうまく合うかもしれない
  • この記事はAIが作った低品質な文章のように読める
    もっと具体的に言うと、なぜ物語形式で書いたのかわからない。ここで伝えている事実は、もっと簡潔に、もっと偏りなく書けたはずで、物語としても説得力がない。特にAI特有の言い回しが多いのでそう感じる
    楽しく読めるものではなかった。それでも、問題点を指摘してくれたことには感謝しているし、別の形でそれらの問題を読み、修正できることを願う