blog/content/posts/2023-12-24/index.md

165 lines
11 KiB
Markdown

---
author: usbharu
draft: false
categories:
- 技術
date: 2023-12-23T10:47:05+09:00
tags:
- ActivityPub
keywords:
- ActivityPub
title: ActivityPub実装SNSを作っている。その感想
relpermalink: posts/2023-12-24/
url: posts/2023-12-24/
decription:
---
{{< alert icon="circle-info" >}}
この記事は [Fediverse (3) Advent Calender 2023](https://adventar.org/calendars/8730) 24 日目の記事です。
{{< /alert >}}
私は、去年の 11 月ごろに自分の Mastodon と Misskey サーバーを建てて、 Fediverse に移住してきました。
なんだかんだあって Kotlin+Spring Framework で ActivityPub 実装 SNS を作って遊んでいます。こういう文章を書くのは苦手なので、本題に移ります。
## JSON のシリアライズ/デシリアライズが難しい
初っ端から何を言っているんだ感がありますが、ActivityPub で送られてくる JSON は[Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/)と言ってかなり(Json デシリアライザー から見て)複雑な JSON が送られてきます。この内`type`と`object`というプロパティが鬼門で、ここさえなんとかなれば後はなんとかなります。
### type
この`type`というプロパティ、配列だったり単純な文字列だったりします。最初は`type`を読んで Jackson の`@JsonTypeInfo`などを使ってデシリアライズすればいいやなどと考えていましたが、この甘い考えはすぐに打ち砕かれました。`type`の種類がものすごく多い上に配列だったときに問答無用でエラーが起きます。実際に配列が送られてくることがあるのかは知りませんが、ともかく`@JsonTypeInfo`は使えませんでした。
結局`type`だけ読み取り、`type`に応じて処理する対象を変えることで対処[^1]しました。`type`が配列だった場合は要素で最初の有効なものを使用します。[^2]
[^1]: https://github.com/usbharu/Hideout/blob/4ceb42728d139fb4306713e5d49468e24509a8bc/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/ActivityPubProcessor.kt
[^2]: https://github.com/usbharu/Hideout/blob/4ceb42728d139fb4306713e5d49468e24509a8bc/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APService.kt#L204-L218
### object
`object`はアクティビティの対象を示すプロパティで、文字列だったり、JSON のオブジェクトだったりします。これも悩みの種で、例えば`Undo`アクティビティの`object`には`Like`や`Follow`、`Accept`、`Block`などが入ります。これらを安全にデシリアライズすることに苦労しました。結局想定されるすべてのオブジェクトをベタ書きしたカスタムデシリアライザーを作成することで解決しました。[^3]
[^3]: https://github.com/usbharu/Hideout/blob/b0f3ae4d8d319f39e7127064319bd62dc7a6f672/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt#L12
## HTTP Signature に悩ませられ続ける
Mastodon や Misskey、最近連合できるようになった Threads などのほとんどの ActivityPub 実装は HTTP Signature でアクティビティを署名することでサーバー間での通信をある程度安全にしていますが、これも悩みの種でした。
### そもそも情報がない
ActivityPub 実装を初めた頃、今ほど情報がなく HTTP Signature は IETF の文書を読むか、既存の実装を解読するしかありませんでした。そもそも署名に必要な知識が不足していたのもあり、かなり苦労しましたがとりあえず Misskey で署名を検証できる様になりました。
HTTP Signature の署名に関する解説だけで 1 記事書けてしまうのでこの記事をを読んでください。なお、Misskey は割と頻繁に破壊的変更をするので書いてある内容だと連合できないことがあります。その時は教えてくれたら直します。
{{< article link="/posts/2023-07-23/" >}}
### GET リクエストへの署名
最初期の実装では GET リクエストに対して署名することを考慮しておらず、 GET リクエストにも署名が必要な部分があることに気付いたときには主要部分がほぼ完成していました。そのためかなり雑な対応をすることになり、現在でも無茶苦茶な実装として技術的負債になっています。
なのでもし ActivityPub 実装 SNS を作ることがあるなら、最初から GET リクエストへの署名について考えておく必要があります。
### 不安定すぎる連合
#### Misskey にあった脆弱性と破壊的変更
脆弱性があったことはともかく、その後の対応で連合に関して破壊的変更が入りました。具体的には`Digest`ヘッダのアルゴリズム名が大文字のみ受け付けられるようになりました。
`Digest: sha-256=hogehoge`
でも受け付けられていたものが
`Digest: SHA-256=hogehoge`
でしか受け付けられなくなった。
私はかなり焦りました。昨日まで連合できていたインスタンスと急に連合できなくなるのです。署名や連合に関する全てのテストをパスし、自分でも色々確認しましたが何もおかしいところはありません。そもそも自分は署名に関して一切手を加えていませんでした。あれやこれやと 1 時間ほど悩んだところで、そういえば Misskey の署名周り、脆弱性の修正で変更が入っていたよな…と気づくことができたのでした。
#### HTTP Signature が複雑すぎる
1 つのバージョンの規格自体は理解すれば簡単です。しかし 10 年も色々いじくり回されているドラフト段階の規格だと話は違います。初期の規格と最新の規格があまりにも違いすぎました。私は完全に諦めて、Misskey と Mastodon と連合できれば OK ということにしています。
### 連合に必要なヘッダー
いまのところ自分の実装では
GET リクエストで
- (request-target)
- date
- host
- accept
POST リクエストで
- (request-target)
- date
- host
- digest
をヘッダーに含めて署名しています。
## SharedInbox を作らなかったら重複まみれになった
最初めんどくさがって SharedInbox を作らなかったのですが、現在進行系でエラい目にあっています。SharedInbox がない場合フォロワーの数だけそれぞれの actor の inbox にアクティビティが飛んでくるため重複まみれになります。自分のサーバーとしか連合していないので対症療法的な対処は可能ですが、sharedInbox は最初から作っておきましょう。
![actor-inboxに来る大量のリクエストのログ](image.png "1アクティビティにつきこれだけのリクエストが飛んでくるようになってしまった")
尤も、ユニーク制約やアプリケーションでの重複排除をしているため最終的には重複は発生しませんが、無駄ですし追加でリソースを取得する必要が出たとき逆にリモートサーバーに DoS じみたリクエストを送ってしまいます。
## 常に性悪説をとる必要がある
ActivityPub って結構ガバガバなので色々攻撃できちゃいます。なので常に性悪説をとって防衛する必要があります。自分の AP 実装では全く実現できておらず、基本機能実装完了後の課題となっています。
### 無限再帰、無限ループアクティビティ
大抵の実装ではメンションなどがあった際 actor を取得し、固定 Note を取得すると思います。
例えばアクティビティを辿るたびに違う actor や Object のリンクを返された場合、無限ループに陥ります。制限をかけるなどで対処する必要があります。
### めちゃくちゃ遅いがタイムアウトしないリモートのリソース
リモートのリソースを取得する際、意図的に遅くすることで簡単に DoS が出来てしまいます。
### 純粋に大量のアカウントでフォロー
上に書いた通り sharedInbox を実装せず、大量のアカウントを作ってフォローするだけで配送先が増えます。配送先が増えるということは単純に負荷が増えるため、ドメインブロックなどの機能を実装する必要があります。
## アカウント削除
ActivityPub では同じドメイン、同じ ID でアカウントを 2 回作ることが出来ません。作っても連合出来ないことが多いです。あまり有名ではないのか、Misskey の GitHub には時々連合に失敗する旨の Issue が建てられ、「仕様です」と速攻で Close されている光景を見ることが出来ます。
逆にリモートからアカウント削除のアクティビティが来た際にどうするかが問題になります。アカウント削除アクティビティを受診したあと、(エラーのリトライなどで)アクティビティがきた場合もう一度 actor を取得し、アカウント削除がなかったことになるようでは全く意味がありません。
そこで削除済み actor だけ保持するテーブルを作り、アカウント削除アクティビティを受信した actor を移動します。actor 取得時、削除済み actor にある actor は取得せず破棄することでこれを実現しています。
## actor を取得するまでわからない不正なアクティビティ
Spring Security で HTTP Signature を使って認可制御していたとき
1. 未知の actor のアクティビティを受信
2. DB から鍵を探す
3. 鍵がないので不正!!
ということが起こりました。これを解決するには
1. 未知の actor のアクティビティを受信
2. DB から鍵を探す
3. なければ actor を取得
4. 検証
というフェーズを踏む必要があり、
1. 不正なアクティビティを受信
2. actor を取得
3. 検証
4. 不正発覚!!
というように Spring Security でアクティビティ受信時にチェックしようとすると actor を取得する必要があるという罠にハマりました。
actor の取得は失敗する可能性があり、ジョブキューなどでリトライの制御をする必要があります。結局 Spring Security での HTTP Signature の検証は限定的な状況でしか行えませんでした。
## ActivityPub 実装は楽しいぞ
いい感じの締めが思いつかなかったのでこんな感じで終わりです。下手くそな文章ここまで読んでいただいて嬉しいです。本当は ActivityPub の実装ハンズオン的な記事を書きたかったのですが、試験勉強が忙しすぎてそれどころではありませんでした。どうも合格してそうなのでそのうち書きたいと思います。