👋

ActivityPubの実装についてのメモ

2023/01/09に公開

ActivityPubの実装をしているのでそれの話をまとめます。
特にActivityPubは期待していたより情報がなく、規格をちゃんと読んでなくてハマったりしたところがあるのでそれについてのメモ的なやつです

はじめに: ActivityPubとは

ActivityPub(以下AP)とはW3Cが定める分散SNS向けの規格です。これをプロトコルとして採用したSNSはmastodonとかGNU Socialとかmisskeyなんかがあります。

https://www.w3.org/TR/activitypub/

詳しいことは規格を読んでくださいなんですが、よしなに実装をしてあげるとSNSが作れて、しかもmastodonやらGNU Socialやらmisskeyやらのサーバーと連合してアカウントをフォローしあったりツイートを流し合ったりできます。

サーバーの用意とactorの宣言

最初にやるべきことはactor(ユーザーみたいなもの)の宣言をして、各種サーバーからここにユーザーがいるということを認識してもらうことです。

基本的には以下のチュートリアルの「The actor」と「Webfinger」の2つのセクションの実装を行えば良いです。

https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/

  • HTTPSのサーバーを立てて、AP用のドメインを用意する
  • /.well-known/webfinger から、actor情報のURLを返せるようにする
  • webfingerで宣言したURLから、actorの情報をJSONで返せるようにする

返すべきJSONはactivitystreamsの Person です。
規格上は色々定義されてますが、基本的には他のサービスに合わせて空気を読めばいいと思います。

また、mastodonなど他のサービスのAP#Personを取得する場合はAcceptヘッダー Accept:application/activity+json (単なるJSONでもOKなのもある) をつけてrequestを送る必要があります。

publicKeyはあとでinboxに投げたりする時に必要になるやつですが、一旦すっ飛ばしてもアカウントは認識されるので後回しでも良いです。

例えば私の実装だと以下のようなものを返しています。

❯ http https://tl.ramda.io/u/myuon accept:application/activity+json --body
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1"
    ],
    "followers": "https://tl.ramda.io/u/myuon/followers",
    "following": "https://tl.ramda.io/u/myuon/following",
    "icon": {
        "mediaType": "image/png",
        "type": "Image",
        "url": "https://pbs.twimg.com/profile_images/1398634166523097090/QhosMWKS_400x400.jpg"
    },
    "id": "https://tl.ramda.io/u/myuon",
    "inbox": "https://tl.ramda.io/u/myuon/inbox",
    "name": "myuon",
    "outbox": "https://tl.ramda.io/u/myuon/outbox",
    "preferredUsername": "myuon",
    "publicKey": {
        "id": "https://tl.ramda.io/u/myuon#main-key",
        "owner": "https://tl.ramda.io/u/myuon",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx8F5dC8js0yM3HlpQuan\n7j9bQAPaH39loiHLssRm5vvSZSVVNODi9ch3PrKlW44aXd6puQjT8cyAkuzigloK\nU+iI2cnd/nCIvXe3qONysIMbYwV1gtoccdBOZMQ8UDW3VtcT2oWdE8cGjAeAdoaN\nM7bx3gDq1Qw9X6nlzkhL9rvLp4yaVWNmsR0fpCkZw9l3wQA441UryKMo2eZ/5zUj\n185d4JWAMXjH7Xqw/ufJPly3wphJYvN3YQaw+Ryij7ruvnL1WWwUNxxb3hihmS7x\nuAeSZcVr5Xh1A/wjGU+3OU2kg20nrjkxqK6kpnhp7yrPUBMSjF9CeDKSgBRAcBZQ\nywIDAQAB\n-----END PUBLIC KEY-----\n",
        "type": "Key"
    },
    "summary": "@myuon on tl.ramda.io",
    "type": "Person",
    "url": "https://tl.ramda.io/u/myuon"
}

インスタンスの情報をNodeInfoで実装する

多くのAP実装では、そのインスタンス自体の情報も返せるようにしてあります。例えばmisskeyではその情報がタイムラインにも表示されるようになっているため、実装しておくに越したことはないでしょう。

インスタンス情報は以下のプロトコルを使って実装しているものが多いようです。

https://nodeinfo.diaspora.software

やることは単純で、

  • .well-known/nodeinfo からNodeInfoに関する情報を返すURLを提供する
  • NodeInfo情報を返す

の2つだけです。

❯ http https://tl.ramda.io/nodeinfo/2.1 --body   
{
    "openRegistrations": false,
    "protocols": [
        "activitypub"
    ],
    "software": {
        "name": "timeline",
        "version": "0.1.0"
    },
    "usage": {
        "users": {
            "total": 1
        }
    },
    "version": "2.1"
}

outboxの実装

outboxの実装をすると、投稿した情報などが見れるようになります。
ただしこれをやっても各種のインスタンスにactivityの情報が流れるわけではないので、特にmastodonから投稿が見れるようになるわけでもないようです。

この辺は実装依存だと思いますが、いくつかの実装を見た限りではリモートアカウントのプロフィールの表示はできても、outboxのNoteまで表示してくれないものが多いので他のSNSから投稿を認識してもらうためには特定のinboxにdeliverする仕組みを実装する必要があるようです。

(投稿を表示してくれないサービスごとにcontentで受け付けるMarkdown亜種みたいなやつの形式がバラバラなのでその辺の事情かなと思っていますが、よくわかりません)

Server-to-Server Interactionsに関する実装

他のサーバーなどとやりとりする部分の実装についてかきます。

署名とHTTP Signatureについて

署名の実装については、mastodonのドキュメントに沿って実装するとできます。

https://docs.joinmastodon.org/spec/security/

HTTPの署名プロトコルについては、以下のブログ記事がまとまっているので一読しておくと良いと思います。

https://asnokaze.hatenablog.com/entry/2020/01/07/012014

結論から言えば、以下の「Signing HTTP Messages」に沿って実装をすれば良いと思われます。

https://datatracker.ietf.org/doc/html/draft-richanna-http-message-signatures-00

ただしこれ自体もまだInternet-Draftであって、何度か版を重ねた別のDraftをかき直したバージョンのようです。今後変更があるのかなどはわかりません。(雰囲気でやっている)

鍵について

規格上は鍵は選択肢がいくつかあるように見えますが、rsa-sha256を使うのが一般的なようです(というかこれしかサポートしてない実装があるっぽい)。

crypto初心者すぎて鍵の扱いが何もわからなかったんですが、秘密鍵と公開鍵のPEMの読み込みはNode.jsだと以下の実装で動きました。

import { webcrypto as crypto } from "crypto";

export const pemToBuffer = (pem: string) => {
  const lines = pem.split("\n");
  const encoded = lines
    .filter(
      (line) =>
        !line.match(/(-----(BEGIN|END) (PUBLIC|PRIVATE) KEY-----)/) &&
        Boolean(line)
    )
    .join("");

  return Buffer.from(encoded, "base64");
};

export const importSignKey = (pemString: string) =>
  crypto.subtle.importKey(
    "pkcs8",
    pemToBuffer(pemString),
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    false,
    ["sign"]
  );

export const importVerifyKey = (pemString: string) =>
  crypto.subtle.importKey(
    "spki",
    pemToBuffer(pemString),
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    false,
    ["verify"]
  );

おわりに

まだCreate, Delete, Remote Followあたりしか実装してないので、LikeとかAddとかUndoとかをやって何か知見が得られたらまた書こうと思います。

実装

https://github.com/myuon/timeline

Discussion