Fediverseに参加するための最低条件2
ActivityPubの約束どおりリクエストのキャッチボールができること
公開鍵・秘密鍵を使って署名の生成と認証
- :投げる
- 秘密鍵を使って署名したHTTPリクエストをFediverseに投げこむ
- 正しく署名されているか確認のため、自分のアカウント情報のJSONに記載された公開鍵を要求される
- :受けとる
- 投げこまれた署名付きリクエストを受けとる
- 正しく署名されているか確認のため、お相手のアカウント情報のJSONに記載された公開鍵を要求する
【秘密鍵と公開鍵の作成】
ActivityPubのActivityリクエストには署名が必須。そのために「秘密鍵」「公開鍵」を作っておく必要がある。この署名というのは、このリクエストは確かにわたしが投げたものです、というのを証明するためのもの。
暗号化の方法はいろいろあるらしいけど、RSAという方式?で暗号化すればいいらしい。
ここ、だいじなんだけど、伏魔殿でわたしのような素人の理解のおよぶところではない。とてもじゃないけど無理。
- 結果オーライの理解
- 「秘密鍵」を使って「対象とする文字列」で暗号化した署名を作成する
- 「秘密鍵」で暗号化されたものは「公開鍵」と「対象とする文字列」を使って署名が正しいかどうかを認証できる
- 「秘密鍵」は絶対に漏らしてはいけない
てことで、まずは「秘密鍵」「公開鍵」を作る。
それには、opensslを使うのが手っとり早い。わたしの場合はchromeOSのlinux開発環境。
「秘密鍵」(private.key)の生成
$ openssl genrsa -out private.key 2048
2048はbit長というか、言ってみれば強度という理解で大丈夫そう。2048ぐらいで生成するのが良いらしい。
できる秘密鍵は
「-----BEGIN RSA PRIVATE KEY-----XXXXXXX----END RSA PRIVATE KEY-----」
改行されて整形されている。そのまま署名の暗号化に使う。
「公開鍵」(public.key)の生成
最初に作った「秘密鍵」(private.key)を使う。
$ openssl rsa -in private.key -pubout -out public.key
できる公開鍵は
「-----BEGIN PUBLIC KEY-----XXXXXXX-----END PUBLIC KEY-----」
改行されて整形されている。
この公開鍵をtype:PersonのJSONの「publicKey」に記載する(FediverseからActivityPubを喋るアカウントとして認識されること)。
"publicKey": {
"id": "https://YOUR-DOMAIN/USER-NAME#main-key",
"owner": "https://YOUR-DOMAIN/USER-NAME",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n+Y51TMAWw8+uuuZeru6KyQRgcno1tYGi8ZC+mG3B5OaknRO7mw41qA70DC7r3Xqr\noguRlTc2R2Xes6iPs0/wfPCs7PmUI8NMFEzV+sg4MOcgLQvaJ2mnNBgcNCQshVqo\n-----END PUBLIC KEY-----\n"
}
このJSONに記載する時に、公開鍵の「改行」は「\」(バックスラッシュ)「n」(アルファベットの「n」)の2文字に置換して一行にする。
こちらが送った、署名したリクエストが正しいものかどうか、お相手が認証するために、このtype:PersonのJSONの「公開鍵」を取得するためにアクセスしてくる。
以上で下準備の完了となります。
【署名(Signature)を作る】
署名はHTTPリクエストにつける
投稿やFollowのActivityのリクエスト(POST)に必要で、サーバーによってはPersonのJSON取得のためのリクエスト(GET)にも必要だったりで「このリクエストはわたしが送りました」の証明。
今日確認時点(2026-01-16)で2種類のSignatureが使われている。
2020年からdraft版がずっと使われていたが、2024年2月に「RFC9421」として仕様が決まった。
「https://datatracker.ietf.org/doc/html/rfc9421」
Fediverseで広く利用されているMastodonでも4.5.0からRFC9421を使えるようになった。
【HTTPリクエストのsignature(署名)】:draft版
keyId="https://expample.com/users/name#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="Nf5TA/8fQP61tUFyyhbEGtrZ309tyFUAhWAotnh1SxSKkhYbFSU8b+074URyonFqpmvSrQmckkD+o2dgRzBgFOt+8jmE1amYc+7BqAYiEHkXmwIl2DUjcodF6Bx0boaIlWq5+cPtQtr2qKndofljxhkdHdIDe/pPeg5dvWky4jhJbdNQXvjLpeopz7SkUuOgaTJokoe/4hh5OyqFwKPuVhknES4AWuPhonS1Tfqqv8fv1F9a4darcOmb630Nrmwb+gr4QbafpWn7Jj8DxerqBfKoYis9yqAbGfar3he66Vm2j2ClADsyBQ8DXcVd6vYdottHkMhXw+gSdoFF0WoOag=="
「keyId」「algorithm」「headers」「signature」の4つで構成されている。
- keyId
signatureの認証に必要な「公開鍵」が記載されているtype:PersonのJSON(アカウント情報)のurl。リクエストする時にurlの「#main-key」は不要 - algorithm
rsa-sha256。暗号化方式。RSAで暗号化してshaでハッシュ化(既存のモジュールをブラックボックスとして使えるので理解不要。呪文) - headers
署名の対象 - signature
署名
【署名する対象】:draft版
署名の対象はheadersで指定する。
- (request-target)
- host
- date
- digest
- content-type
ActivityPubの仕様より多いけど、今日時点で飛んでるPOSTリクエストはだいたいこの5つ。
signatureを作る時はこの5つで作れば大丈夫だろう。
1)(request-target)
リクエストメソッド(POST GET) [半角空白] URI
URIはurlから「https://exmple.com」を除いた部分
2)host
ホスト名。urlから「https://」を除いたドメイン部分
3)date
曜日、日月年、時間
4)digest
content(本文)のdigest(ハッシュ)
5)content-type
application/activity+json(決め打ち)
署名の対象
| (request-target) | post /inbox |
|---|---|
| host | example.com |
| date | Wed, 14 Dec 2022 07:28:00 GMT |
| digest | SHA-256=Z2EHR3+Srb66MmKASfnRcbk83F8SBrW1LIiwx15/g80= |
| content-type | application/activity+json |
4)digestというのは
送信する内容、本文(content)をsha256でハッシュ化して、それをbase64でエンコードしたもの
perlだと以下
my $digest = 'SHA-256=';
$digest .= encode_base64(sha256($content),"");
署名に必要な(署名の対象にする)ものが揃ったらそれを順番通りに
key[: ](←半角のコロンと半角空白)value[改行]
で繋げる。
(※(request-target)やhostはアルファベット小文字にする)
(request-target): post /inbox
host: example.com
date: Wed, 14 Dec 2022 07:28:00 GMT
digest: SHA-256=Z2EHR3+Srb66MmKASfnRcbk83F8SBrW1LIiwx15/g80=
content-type: application/activity+json
この文字列を対象にしてPOSTリクエストにつける署名(Signature)を作る。
本文(contents)がないGETリクエストなんかは
(request-target) / host / date
の3つを対象に署名すればOK。
【署名(Signature)を認証する】:draft版
飛んでくるリクエストのSignatureで指定されているkeyIdのurlにアクセス、GETリクエストを投げて、お相手のアカウント情報、type:PersonのJSONを取得。
JSONに記載されている公開鍵を使って認証する(署名が正しいかどうか確認する)。
認証の対象にする文字列は、同じくSignatureのheadersで指定された要素を順番そのままで使う。
(署名を作る時に例にあげた5つとは限らない)
headersで指定されているのが
- (request-target)
- host
- date
- digest
- content-type
だったらリクエストから該当するものをそのまま署名対象とする。
(request-target)だけは、REQUEST_METHODとREQUEST_URIの2つ必要。
飛んでくるリクエスト
| REQUEST_METHOD | POST |
|---|---|
| REQUEST_URI | /inbox |
| HTTP_HOST | example.com |
| HTTP_DATE | Sun, 20 Oct 2024 21:30:16 GMT |
| HTTP_DIGEST | SHA-256=ETw1NwooStIkE4vUyRfYU8udaBeNl4xR8237uCj2pEc= |
| CONTENT_TYPE | application/activity+json |
【HTTPリクエストのsignature(署名)】:RFC9421版
RFC9421ではHTTPヘッダに「Signature」と「Signature-Input」の2つが必要となる。
【Signatue】署名:RFC9421版
sig1=:k27Yayjly+veq7UfOrcbKAC9auTzTzVMYHNEYqeIP5JwV64cpg46igAd2uGuag7kJkbambpjB363ppmy8SChFcQgBYU2huMj9eVmsfJDARLV3PvhTztBys1KgwTcmXaTJ8W8AcmBLviMzgH7gBoT6gsOXVSIjVw73I5Qqtg4uhRmno/3NY3dhYYsf8NRJZatC1Y4LuXrNGSfd2Fkv5Gc8ei2adsnPeKbqW4ikWys0GneTIIxZSDJrsWW7N48aLVIcoNvus3Iy1fawHMmMot+NSsFcfrouqj+kj1XizPmaJPIlhK3GntugZ/+fbgu43g2GAlj1YHXtFiB4NPcZWbqGQ==:
RFC9421では複数のSignatureを扱うことができるようになった。「sig1」はその識別子。
「識別子」「=:」「Signature」「:」となる。
【Signatue-Input】署名する対象:RFC9421版
sig1=("@method" "@target-uri" "content-digest");alg="rsa-v1_5-sha256";keyid="https://example.com/username#main-key";created=1768601574
Signatureと同じ識別子(「sig1」)。
署名対象を指定するということで、draft版の「headers」に相当する。
「;」で区切られて、「(対象リスト)」「alg(暗号化アルゴリズム)」「keyid」「created」の4つで構成されている。
- (対象リスト)
署名の対象 - alg
rsa-v1_5-sha256。暗号化方式。RSAで暗号化してshaでハッシュ化(draft版の「rsa-sha256」と表記は違うけど同じもの、だと思う) - keyid
signatureの認証に必要な「公開鍵」が記載されているtype:PersonのJSON(アカウント情報)のurl。リクエストする時にurlの「#main-key」は不要 - created
作成時間(秒)unixtime
署名の対象リストは「("@method" "@target-uri" "content-digest")」
必要なものが半角空白区切りでリストされている。
- @method
- @target-uri
- content-digest
Mastodon 4.5.0では上記の3つが署名の対象となる。
「https://docs.joinmastodon.org/spec/security/#http-message-signatures」
(サーバーごとで違いがあって、holloでは「sig1=("@method" "@target-uri" "@authority" "host" "date" "content-digest")」と6つ必要)
1)@method
リクエストメソッド(POST GET) ※draft版とは違って大文字のまま使用する
2)@target-uri
リクエストするURLをそのまんま、https://から全部。
3)content-digest
content(本文)のdigest(ハッシュ)
署名の対象
| @method | POST |
|---|---|
| @target-uri | https://example.com/user/inbox |
| content-digest | sha-256=:wkS96q13Uuy7sRhhA6g6sK0EgkzwQzBoWGCoawL6VeI=: |
4)content-digestというのは
送信する内容、本文(content)をsha256でハッシュ化して、それをbase64でエンコードしたもの。draft版と同じ。
draft版では「SHA-256=」「digest」
RFC9421版では「sha-256=:」「digest」「:」
と、表記が少し違う。
最後に「Signature-Input」の識別子を除いたものを「@signature-params」として追加。順番通りに
key[: ](←半角のコロンと半角空白)value[改行]
で繋げる。
"@method": POST
"@target-uri": example.com
"content-digest": sha-256=:wkS96q13Uuy7sRhhA6g6sK0EgkzwQzBoWGCoawL6VeI=:
"@signature-params": ("@method" "@target-uri" "content-digest");alg="rsa-v1_5-sha256";keyid="https://example.com/username#main-key";created=1768606383
この文字列を対象にしてPOSTリクエストにつける署名(Signature)を作る。
本文(contents)がないGETリクエストは
@target-uri / host / date
…決まりはない?これでリクエストして特に問題はなかった。
【署名(Signature)を認証する】:RFC9421版
飛んでくるリクエストのSignature-Inputで指定されているkeyidのurlにアクセス、GETリクエストを投げて、お相手のアカウント情報、type:PersonのJSONを取得。
JSONに記載されている公開鍵を使って認証する(署名が正しいかどうか確認する)。
認証の対象にする文字列は、同じくSignature-Inputの対象リストで指定された要素を順番通りで使う。
- @method
- @target-uri
- content-digest
飛んでくるリクエスト
| REQUEST_METHOD | POST |
|---|---|
| REQUEST_SCHEME | https |
| HTTP_HOST | example.com |
| REQUEST_URI | /inbox |
| HTTP_DIGEST | sha-256=:wkS96q13Uuy7sRhhA6g6sK0EgkzwQzBoWGCoawL6VeI=: |
- @method
「REQUEST_METHOD」 - @target-uri
「REQUEST_SCHEME」://「HTTP_HOST」「REQUEST_URI」 - content-digest
「HTTP_DIGEST」
「@」で始まるものは「Derived Components(派生コンポーネント)」というらしく、HTTPの環境変数で取得できるものから組み立てる必要がある(環境変数の名前そのままでは使えない)
RFC9421のページに詳細があるので、公式の一次情報を確認してください。
【Content-Digest】
直接Signatureとは関係しないと思うけど、MastodonではHTTPヘッダに「Digest」(draft版)「Content-Digest」(RFC9421版)が必須(MUST)となっている。
署名で作った本文のdigestをそのままHTTPのヘッダに設定すればOK
以上で、署名の作成・認証となります。
ここ以下は、perlの秘密鍵・公開鍵モジュールをブラックボックスで使っている例です。
使っているperlのモジュール
「Crypt-Perl-0.38」
これはpure perlで書かれているので、perlさえ動けばどのサーバーでも解凍展開するだけで使えます。
依存関係でほかのモジュールも必要になりますが、すべてpure perlなので問題はありません。
【秘密鍵を使って署名する】
my $priv = Crypt::Perl::RSA::Parse::private(PRIVATE-KEY);
my $sign = $priv->sign_RS256(SIGN-KEY);
encode_base64($sign, "");
PRIVATE-KEYは秘密鍵
SIGN-KEYは署名対象の文字列
最後の1行が返す文字列がSignature(署名)
【公開鍵を使って署名を認証する】
my $pubkey = Crypt::Perl::RSA::Parse::public(PUBLIC-KEY);
my $decoded = decode_base64(SIGNATURE);
$pubkey->verify_RS256(SIGN-KEY, $decoded);
PUBLIC-KEYは公開鍵
SIGNATUREは署名
SIGN-KEYは署名対象の文字列
最後の1行が1だったら認証OK、そうじゃなかったら認証NG。
[2026-01-17 09:19:34] v1.1.0
[2024-10-30 07:46:11] v1.0.1
[2024-10-25 08:04:03] v1.0.0

